From 57d9fd55f6905c36e65f93270db9cb0276ee20d3 Mon Sep 17 00:00:00 2001 From: Human Date: Fri, 5 Jun 2026 09:51:26 -0700 Subject: [PATCH] fix: resolve target path before PWD comparison in wrktr_remove MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS, $BATS_TMPDIR resolves through /tmp → /private/tmp, causing the "inside worktree" safety check to silently miss when target contained the unresolved symlink path. Resolve both sides with pwd -P before comparing. Update two wrktr_unload test assertions from exact string match to =~ since $output includes the sourcing banner ahead of the echoed value. --- CHANGELOG.md | 21 ++++++++- README.md | 1 + install.sh | 4 ++ tests/wrktr.bats | 103 ++++++++++++++++++++++++++++++++++++++++++ worktree-functions.sh | 70 +++++++++++++++++++--------- 5 files changed, 176 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8922b6d..0bbe4be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,27 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html): ## [Unreleased] +--- + +## [1.0.1] — 2026-06-05 + +### Fixed +- `wrktr_use`: removed the legacy `source "$config"` path; old-format configs with `export ` prefixes are now parsed safely by the KEY=value parser (strips the prefix, never executes the file as shell code) +- `wrktr_remove`: PWD check now resolves both the current directory and the target path with `pwd -P`, preventing bypass via symlinks or systems where `$TMPDIR` itself is a symlink (e.g. `/tmp` → `/private/tmp` on macOS) +- `wrktr_clone`: initial worktree directory now uses the detected main branch name instead of the hardcoded string `"main"` +- `_wrktr_remote_branches`: replaced `sed` with shell parameter expansion to avoid metacharacter injection when remote names contain special characters; `wrktr_checkout` listing uses `grep -F` for the same reason +- `wrktr_unload`: now unsets `WRKTR_VERSION` along with all other `WRKTR_*` variables +- `wrktr_clone`, `wrktr_adopt`: print a message before any `rm -rf` cleanup so the operation is visible +- `install.sh`: validate `SCRIPT_DIR` immediately after assignment and exit with an error if it could not be resolved +- `wrktr_init`, `wrktr_generate`, `wrktr_adopt`: fail early with a clear error when stdin is not a terminal, preventing hangs in non-interactive environments (CI, piped scripts) + ### Removed - Homebrew formula (`Formula/wrktr.rb`) — distribution via a personal tap is not planned +### Added +- README: explicit platform note that wrktr requires macOS or Linux (Windows needs WSL) +- Tests: coverage for old-format config loading, symlink-based worktree removal guard, `wrktr_unload` variable cleanup, and non-interactive stdin guards + --- ## [1.0.0] — 2024 @@ -79,5 +97,6 @@ Initial release. --- -[Unreleased]: https://github.com/your-username/wrktr/compare/v1.0.0...HEAD +[Unreleased]: https://github.com/your-username/wrktr/compare/v1.0.1...HEAD +[1.0.1]: https://github.com/your-username/wrktr/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/your-username/wrktr/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 8e1f1c9..4c6b5a3 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ ## Requirements +- macOS or Linux (Windows requires WSL) - bash 3.2 or later (the macOS default is sufficient) - git 2.7 or later (git 2.36+ required for `wrktr_init`) - rsync (required only by `wrktr_init`) diff --git a/install.sh b/install.sh index 44538c6..9ff567c 100755 --- a/install.sh +++ b/install.sh @@ -9,6 +9,10 @@ INSTALL_FILE="$INSTALL_DIR/worktree-functions.sh" MAN_DIR="$HOME/.local/share/man/man1" MAN_FILE="$MAN_DIR/wrktr.1" SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd -P)" +if [ -z "$SCRIPT_DIR" ]; then + printf 'Error: cannot resolve script directory\n' >&2 + exit 1 +fi SOURCE_FILE="$SCRIPT_DIR/worktree-functions.sh" SOURCE_MAN="$SCRIPT_DIR/docs/wrktr.1" diff --git a/tests/wrktr.bats b/tests/wrktr.bats index 039c6b6..b0855a6 100644 --- a/tests/wrktr.bats +++ b/tests/wrktr.bats @@ -53,6 +53,8 @@ teardown() { rm -rf "$BATS_TMPDIR/wrktr-trunk-$$-"* rm -rf "$BATS_TMPDIR/wrktr-parent-$$" rm -rf "$BATS_TMPDIR/wrktr-outside-$$" + rm -f "$BATS_TMPDIR/wrktr-symlink-$$" + rm -f "$BATS_TMPDIR/wrktr-sentinel-$$" } # =========================================================================== @@ -285,6 +287,40 @@ EOF [ "$status" -eq 1 ] } +@test "wrktr_use: loads old export-format config safely" { + local trunk="$BATS_TMPDIR/wrktr-trunk-$$-$RANDOM" + _make_bare_repo "$trunk" + + cat > "$WRKTR_CONFIG_DIR/testproject.env" < "$WRKTR_CONFIG_DIR/testproject.env" <&1" + [[ "$output" =~ "not found" ]] +} + +@test "wrktr_unload: unsets WRKTR_VERSION" { + run bash -c "source \"$WRKTR_FUNCTIONS\" && wrktr_unload && echo \"\${WRKTR_VERSION:-unset}\"" + [[ "$output" =~ "unset" ]] +} + +@test "wrktr_unload: unsets session variables" { + run bash -c "source \"$WRKTR_FUNCTIONS\" && export WRKTR_NAME=test && wrktr_unload && echo \"\${WRKTR_NAME:-unset}\"" + [[ "$output" =~ "unset" ]] +} + +# =========================================================================== +# Non-interactive guards +# =========================================================================== + +@test "wrktr_init: fails when stdin is not a terminal" { + run wrktr_init "$WRKTR_CONFIG_DIR/testproject.env" + # Passing a session name skips the interactive prompts; should not block on stdin + run bash -c "source \"$WRKTR_FUNCTIONS\" && WRKTR_CONFIG_DIR=\"$WRKTR_CONFIG_DIR\" wrktr_generate --help /dev/null; then - printf 'wrktr: session "%s" uses the old shell-script config format.\n' "$session" >&2 - printf 'To migrate: wrktr_config_edit — remove the "export " prefix from each line.\n\n' >&2 - # shellcheck disable=SC1090 - source "$config" - else - local _line _key _value - while IFS= read -r _line; do - case "$_line" in - ''|'#'*) continue ;; - esac - _key="${_line%%=*}" - _value="${_line#*=}" - case "$_key" in - WRKTR_*) export "$_key"="$_value" ;; - esac - done < "$config" + printf 'wrktr: session "%s" uses the old config format (has "export " prefix).\n' "$session" >&2 + printf 'To migrate: wrktr_config_edit — remove "export " from each line.\n\n' >&2 fi + local _line _key _value + while IFS= read -r _line; do + case "$_line" in + ''|'#'*) continue ;; + esac + _key="${_line%%=*}" + _key="${_key#export }" + _value="${_line#*=}" + case "$_key" in + WRKTR_*) export "$_key"="$_value" ;; + esac + done < "$config" + if ! wrktr_validate; then _wrktr_breaker red "wrktr session validation failed: $session" printf '\nPossible causes:\n' @@ -1575,6 +1574,11 @@ function wrktr_generate() { [ "$1" = "--help" ] && { wrktr_help generate; return 0; } local session="$1" + if [ -z "$session" ] && [ ! -t 0 ]; then + _wrktr_breaker red "wrktr_generate requires an interactive terminal when no session name is given" + return 1 + fi + if [ -z "$session" ]; then # read -r -p "Session name: " session printf "Session name: " @@ -1728,6 +1732,10 @@ function wrktr_generate() { # ----------------------------------------------------------------------------- function wrktr_init() { [ "$1" = "--help" ] && { wrktr_help init; return 0; } + if [ ! -t 0 ]; then + _wrktr_breaker red "wrktr_init requires an interactive terminal" + return 1 + fi if ! command -v rsync >/dev/null 2>&1; then _wrktr_breaker red "rsync is required by wrktr_init but was not found" return 1 @@ -1999,6 +2007,7 @@ function wrktr_clone() { if ! git clone --bare "$url" "$repo_dir"; then _wrktr_breaker red "Clone failed" + _wrktr_breaker "Cleaning up: $abs_destination" rm -rf "$abs_destination" return 1 fi @@ -2035,6 +2044,8 @@ function wrktr_clone() { return 0 fi + worktree_dir="$abs_destination/$(_wrktr_sanitize_branch_name "$main_branch")" + _wrktr_breaker "Detected main branch: $main_branch" _wrktr_breaker "Creating main worktree: $worktree_dir" @@ -2092,6 +2103,10 @@ function wrktr_clone() { # ----------------------------------------------------------------------------- function wrktr_adopt() { [ "$1" = "--help" ] && { wrktr_help adopt; return 0; } + if [ ! -t 0 ]; then + _wrktr_breaker red "wrktr_adopt requires an interactive terminal" + return 1 + fi local existing_path="${1:-}" if [ -z "$existing_path" ]; then @@ -2198,6 +2213,7 @@ function wrktr_adopt() { _wrktr_breaker "Creating bare clone..." if ! git clone --bare "$abs_existing" "$repo_dir"; then _wrktr_breaker red "Failed to create bare clone" + _wrktr_breaker "Cleaning up: $abs_trunk" rm -rf "$abs_trunk" return 1 fi @@ -2215,6 +2231,7 @@ function wrktr_adopt() { _wrktr_breaker "Creating main worktree: $worktree_dir" if ! git --git-dir="$repo_dir" worktree add "$worktree_dir" "$main_branch"; then _wrktr_breaker red "Failed to create main worktree" + _wrktr_breaker "Cleaning up: $abs_trunk" rm -rf "$abs_trunk" return 1 fi @@ -2749,9 +2766,10 @@ function wrktr_checkout() { if ! git --git-dir="$WRKTR_BASE_DIR" rev-parse --verify "$remote_ref" >/dev/null 2>&1; then _wrktr_breaker red "Remote branch not found: $remote_ref" printf '\nAvailable remote branches:\n' + local _remote="$WRKTR_REMOTE" git --git-dir="$WRKTR_BASE_DIR" branch -r 2>/dev/null \ - | grep "^ $WRKTR_REMOTE/" \ - | sed "s| $WRKTR_REMOTE/||" \ + | grep -F " $_remote/" \ + | while IFS= read -r _ref; do printf '%s\n' "${_ref#" $_remote/"}"; done \ | head -20 printf '\n' return 1 @@ -2831,8 +2849,11 @@ function wrktr_remove() { local target="$WRKTR_BASE_TRUNK/$safe_branch" - case "$PWD" in - "$target"/*|"$target") + local resolved_pwd resolved_target + resolved_pwd="$(pwd -P 2>/dev/null || printf '%s' "$PWD")" + resolved_target="$(cd "$target" 2>/dev/null && pwd -P 2>/dev/null || printf '%s' "$target")" + case "$resolved_pwd" in + "$resolved_target"/*|"$resolved_target") _wrktr_breaker red "Cannot remove worktree while inside it. Please cd out first." return 1 ;; @@ -2903,6 +2924,9 @@ function wrktr_remove() { # Use this when you want to reload a modified version of the file, cleanly # remove wrktr from a session, or test changes from scratch. # +# Note: removes all functions matching ^_?wrktr (i.e. wrktr_* and _wrktr_*). +# If you have your own functions with a wrktr_ or _wrktr_ prefix they will +# also be removed. # # Exit codes: # 0 — always @@ -2918,6 +2942,7 @@ function wrktr_unload() { unset -f "$f" done < <(compgen -A function | grep -E '^_?wrktr') + unset WRKTR_VERSION unset WRKTR_NAME unset WRKTR_BASE_TRUNK unset WRKTR_BASE_DIR @@ -2996,8 +3021,9 @@ function _wrktr_local_branches() { function _wrktr_remote_branches() { [ -z "$WRKTR_BASE_DIR" ] || [ -z "$WRKTR_REMOTE" ] && return + local _remote="$WRKTR_REMOTE" git --git-dir="$WRKTR_BASE_DIR" branch -r --format='%(refname:short)' 2>/dev/null \ - | sed "s|^${WRKTR_REMOTE}/||" + | while IFS= read -r _ref; do printf '%s\n' "${_ref#"$_remote/"}"; done } function _wrktr_worktree_branches() {