Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/wt-add
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ else
echo "Error: Cannot find wt-common" >&2
exit 1
fi
wt_require_valid_config || exit 1

usage() {
echo "Usage: $(basename "$0") [git worktree add arguments...]"
Expand Down
1 change: 1 addition & 0 deletions bin/wt-cd
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ else
echo "Error: Cannot find wt-common" >&2
exit 1
fi
wt_require_valid_config || exit 1

# Source wt-choose using the helper
wt_source wt-choose
Expand Down
1 change: 1 addition & 0 deletions bin/wt-list
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ else
echo "Error: Cannot find wt-common" >&2
exit 1
fi
wt_require_valid_config || exit 1

usage() {
cat <<EOF
Expand Down
1 change: 1 addition & 0 deletions bin/wt-metadata-export
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ else
echo "Error: Cannot find wt-common" >&2
exit 1
fi
wt_require_valid_config || exit 1

usage() {
cat >&2 <<EOF
Expand Down
1 change: 1 addition & 0 deletions bin/wt-metadata-import
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ else
echo "Error: Cannot find wt-common" >&2
exit 1
fi
wt_require_valid_config || exit 1

# Source wt-choose using the helper
wt_source wt-choose
Expand Down
1 change: 1 addition & 0 deletions bin/wt-remove
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ else
echo "Error: Cannot find wt-common" >&2
exit 1
fi
wt_require_valid_config || exit 1

# Source wt-choose using the helper
wt_source wt-choose
Expand Down
1 change: 1 addition & 0 deletions bin/wt-switch
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ else
echo "Error: Cannot find wt-common" >&2
exit 1
fi
wt_require_valid_config || exit 1

# Source wt-choose using the helper
wt_source wt-choose
Expand Down
54 changes: 54 additions & 0 deletions lib/wt-common
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,60 @@ _wt_expand_path() {
echo "${path/#\~\//$HOME/}"
}

# Check if a path value is valid for a directory config variable.
# Valid means: absolute path (starts with /) and contains no glob characters.
# Args: $1 = path value
# Returns: 0 if valid, 1 if invalid
_wt_is_valid_path_config() {
local path="$1"

# Must be absolute
[[ "$path" != /* ]] && return 1

# Must not contain glob metacharacters
# shellcheck disable=SC2254 # intentional glob check via case pattern
case "$path" in
*\**|*\?*|*\[*) return 1 ;;
esac

return 0
}

# Validate that all directory config variables are absolute paths without globs.
# Prints actionable error messages to stderr on failure.
# Returns: 0 if all valid, 1 if any invalid
#
# This is called by bin/wt-* scripts after bootstrap. It is NOT called
# during shell startup (wt.sh sourcing) to avoid breaking the user's shell.
wt_require_valid_config() {
local rc=0
local var val

for var in WT_MAIN_REPO_ROOT WT_WORKTREES_BASE WT_IDEA_FILES_BASE; do
eval "val=\"\${$var:-}\""
if [[ -z "$val" ]]; then
continue # empty/unset — fallback defaults handle this
fi
if ! _wt_is_valid_path_config "$val"; then
error "$var must be an absolute path (no globs): $val"
rc=1
fi
done

if [[ $rc -ne 0 ]]; then
local config_file=""
if [[ -n "${WT_CONTEXT_NAME:-}" ]]; then
config_file="$HOME/.wt/repos/${WT_CONTEXT_NAME}.conf"
fi
if [[ -n "$config_file" && -f "$config_file" ]]; then
printf " Fix the value(s) in: %s\n" "$config_file" >&2
else
printf " Check your context config or git local config (wt.* keys)\n" >&2
fi
fi

return $rc
}

# ─────────────────────────────────────────────────────────────────────────────
# Color support (only if output is a TTY)
Expand Down
89 changes: 89 additions & 0 deletions test/integration/wt-path-validation.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bats

# Integration tests for path validation in bin/wt-* scripts

setup() {
load '../test_helper/common'
setup_test_env

# Create mock repo
REPO=$(create_mock_repo "$BATS_TEST_TMPDIR/repo")

# Create test context with valid config (don't load into env —
# we want the subprocess to read from .conf, not inherited env)
create_test_context "test" "$REPO"
}

teardown() {
teardown_test_env
}

# Helper: unset all WT_* config vars so subprocess reads from .conf
_unset_wt_vars() {
unset WT_MAIN_REPO_ROOT WT_WORKTREES_BASE WT_IDEA_FILES_BASE
unset WT_ACTIVE_WORKTREE WT_BASE_BRANCH WT_METADATA_PATTERNS
unset WT_CONTEXT_NAME
}

# =============================================================================
# bin scripts abort on invalid config
# =============================================================================

@test "wt-add aborts with relative WT_WORKTREES_BASE" {
local conf="$TEST_HOME/.wt/repos/test.conf"
sed -i.bak 's|^WT_WORKTREES_BASE=.*|WT_WORKTREES_BASE="relative/worktrees"|' "$conf"
_unset_wt_vars

run "$TEST_HOME/.wt/bin/wt-add" -b some-branch
assert_failure
assert_output --partial "WT_WORKTREES_BASE"
assert_output --partial "must be an absolute path"
}

@test "wt-list aborts with glob in WT_MAIN_REPO_ROOT" {
local conf="$TEST_HOME/.wt/repos/test.conf"
sed -i.bak 's|^WT_MAIN_REPO_ROOT=.*|WT_MAIN_REPO_ROOT="/tmp/repo-*"|' "$conf"
_unset_wt_vars

run "$TEST_HOME/.wt/bin/wt-list"
assert_failure
assert_output --partial "WT_MAIN_REPO_ROOT"
assert_output --partial "must be an absolute path"
}

@test "wt-switch aborts with relative WT_IDEA_FILES_BASE" {
local conf="$TEST_HOME/.wt/repos/test.conf"
sed -i.bak 's|^WT_IDEA_FILES_BASE=.*|WT_IDEA_FILES_BASE="idea-files"|' "$conf"
_unset_wt_vars

run "$TEST_HOME/.wt/bin/wt-switch" "$REPO"
assert_failure
assert_output --partial "WT_IDEA_FILES_BASE"
assert_output --partial "must be an absolute path"
}

@test "wt-context is not guarded and works with invalid config" {
local conf="$TEST_HOME/.wt/repos/test.conf"
sed -i.bak 's|^WT_WORKTREES_BASE=.*|WT_WORKTREES_BASE="relative/worktrees"|' "$conf"
_unset_wt_vars

run "$TEST_HOME/.wt/bin/wt-context" --list
assert_success
}

@test "error message includes config file path" {
local conf="$TEST_HOME/.wt/repos/test.conf"
sed -i.bak 's|^WT_WORKTREES_BASE=.*|WT_WORKTREES_BASE="relative/worktrees"|' "$conf"
_unset_wt_vars

run "$TEST_HOME/.wt/bin/wt-list"
assert_failure
assert_output --partial "test.conf"
}

@test "valid absolute paths pass validation normally" {
_unset_wt_vars

run "$TEST_HOME/.wt/bin/wt-list"
assert_success
}
119 changes: 119 additions & 0 deletions test/unit/wt-common.bats
Original file line number Diff line number Diff line change
Expand Up @@ -749,3 +749,122 @@ teardown() {
assert_equal "$WT_BASE_BRANCH" "pre-existing"
}

# =============================================================================
# Tests for _wt_is_valid_path_config()
# =============================================================================

@test "_wt_is_valid_path_config accepts absolute path" {
run _wt_is_valid_path_config "/home/user/worktrees"
assert_success
}

@test "_wt_is_valid_path_config accepts absolute path with spaces" {
run _wt_is_valid_path_config "/tmp/my worktrees"
assert_success
}

@test "_wt_is_valid_path_config rejects relative path" {
run _wt_is_valid_path_config "worktrees/foo"
assert_failure
}

@test "_wt_is_valid_path_config rejects dot-relative path" {
run _wt_is_valid_path_config "./worktrees"
assert_failure
}

@test "_wt_is_valid_path_config rejects parent-relative path" {
run _wt_is_valid_path_config "../worktrees"
assert_failure
}

@test "_wt_is_valid_path_config rejects glob with asterisk" {
run _wt_is_valid_path_config "/tmp/wt-*"
assert_failure
}

@test "_wt_is_valid_path_config rejects glob with question mark" {
run _wt_is_valid_path_config "/tmp/wt-?"
assert_failure
}

@test "_wt_is_valid_path_config rejects glob with bracket" {
run _wt_is_valid_path_config "/tmp/wt-[0-9]"
assert_failure
}

@test "_wt_is_valid_path_config rejects empty string" {
run _wt_is_valid_path_config ""
assert_failure
}

# =============================================================================
# Tests for wt_require_valid_config()
# =============================================================================

@test "wt_require_valid_config passes when all paths are absolute" {
export WT_MAIN_REPO_ROOT="/home/user/repo"
export WT_WORKTREES_BASE="/home/user/worktrees"
export WT_IDEA_FILES_BASE="/home/user/idea-files"

run --separate-stderr wt_require_valid_config
assert_success
assert_equal "$stderr" ""
}

@test "wt_require_valid_config fails for relative WT_MAIN_REPO_ROOT" {
export WT_MAIN_REPO_ROOT="relative/repo"
export WT_WORKTREES_BASE="/home/user/worktrees"
export WT_IDEA_FILES_BASE="/home/user/idea-files"

run --separate-stderr wt_require_valid_config
assert_failure
[[ "$stderr" == *"WT_MAIN_REPO_ROOT"* ]]
[[ "$stderr" == *"relative/repo"* ]]
}

@test "wt_require_valid_config fails for glob in WT_WORKTREES_BASE" {
export WT_MAIN_REPO_ROOT="/home/user/repo"
export WT_WORKTREES_BASE="/home/user/wt-*"
export WT_IDEA_FILES_BASE="/home/user/idea-files"

run --separate-stderr wt_require_valid_config
assert_failure
[[ "$stderr" == *"WT_WORKTREES_BASE"* ]]
}

@test "wt_require_valid_config reports all invalid vars" {
export WT_MAIN_REPO_ROOT="relative/repo"
export WT_WORKTREES_BASE="../worktrees"
export WT_IDEA_FILES_BASE="/valid/path"

run --separate-stderr wt_require_valid_config
assert_failure
[[ "$stderr" == *"WT_MAIN_REPO_ROOT"* ]]
[[ "$stderr" == *"WT_WORKTREES_BASE"* ]]
}

@test "wt_require_valid_config shows config file path when context is set" {
export WT_MAIN_REPO_ROOT="relative/repo"
export WT_WORKTREES_BASE="/valid/path"
export WT_IDEA_FILES_BASE="/valid/path"
export WT_CONTEXT_NAME="mycontext"

# Create the config file so the message references it
mkdir -p "$HOME/.wt/repos"
echo 'WT_MAIN_REPO_ROOT="relative/repo"' > "$HOME/.wt/repos/mycontext.conf"

run --separate-stderr wt_require_valid_config
assert_failure
[[ "$stderr" == *"mycontext.conf"* ]]
}

@test "wt_require_valid_config skips empty variables" {
unset WT_MAIN_REPO_ROOT
unset WT_WORKTREES_BASE
unset WT_IDEA_FILES_BASE

run --separate-stderr wt_require_valid_config
assert_success
}