Skip to content

Fix clean path parsing, cwd guard, and remove/branch error handling#9

Merged
leogdion merged 1 commit into
mainfrom
paths-and-clean
Jul 24, 2026
Merged

Fix clean path parsing, cwd guard, and remove/branch error handling#9
leogdion merged 1 commit into
mainfrom
paths-and-clean

Conversation

@leogdion

@leogdion leogdion commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Parse full worktree paths after the worktree prefix so spaces are preserved in list/clean/path lookup
  • Canonicalize cwd vs worktree paths so --older-than --apply never removes the tree you are in (or under)
  • On remove failure, skip branch delete and report clearly; only suggest git branch -D for a true unmerged tip after the worktree is gone

Fixes #3
Fixes #2
Fixes #6

Test plan

  • bash -n git-trees
  • Create a trees project under a path with a space; git trees list --json shows full paths
  • From inside a stale worktree, git trees clean --older-than 1 --apply skips that worktree
  • Dirty gone/merged worktree: remove fails with a clear message and does not suggest git branch -D

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes
    • Improved branch and worktree path detection for more reliable listing and cleanup.
    • Prevented branch deletion when its associated worktree cannot be removed.
    • Improved stale worktree detection by recognizing equivalent and nested paths.
    • Cleanup now safely skips the current project directory and shared repository storage.

Paths with spaces were truncated by awk $2; --older-than could delete the
active worktree via string equality; remove failures were misreported as
unmerged so branch -D was wrongly suggested.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

git-trees updates worktree path parsing, prevents branch deletion after failed worktree removal, and canonicalizes stale-worktree path checks to protect the current worktree and related directories.

Changes

Worktree cleanup

Layer / File(s) Summary
Worktree path parsing
git-trees
_path_for and cmd_list use explicit porcelain parsing to associate branches with their worktree paths.
Guarded worktree removal
git-trees
--gone and --merged cleanup reports removal failures and skips branch deletion for affected entries.
Canonical stale-worktree checks
git-trees
--older-than canonicalizes paths and skips the current directory, containing worktree, project root, and common store.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: path parsing, cwd guards, and worktree removal error handling.
Linked Issues check ✅ Passed The changes address all linked issues by preserving spaces in paths, protecting the active worktree, and skipping branch deletion after remove failures.
Out of Scope Changes check ✅ Passed No obvious out-of-scope changes are present; the diff appears limited to the requested worktree parsing and clean behavior fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch paths-and-clean

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@leogdion
leogdion merged commit 0b2b1ee into main Jul 24, 2026
1 of 2 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@git-trees`:
- Around line 443-447: Update the stale-worktree cleanup loop around git
worktree remove "$wt" to capture removal failures, report the affected worktree,
and preserve a non-zero status across iterations. Replace the unconditional
final success return with the tracked cleanup status so any failed removal
causes the function to return non-zero.
- Around line 388-395: Update both branch-delete cleanup sites in git-trees
(lines 388-395 and 413-420) to capture the actual git branch -d error instead of
suppressing it. Report the captured error for general failures, and suggest git
branch -D only when the error indicates an unmerged tip; preserve the existing
cleanup flow for successful deletions and worktree-removal failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d0e88b2-f55e-4644-8278-245cf6bacebf

📥 Commits

Reviewing files that changed from the base of the PR and between 1262eaa and 1737213.

📒 Files selected for processing (1)
  • git-trees

Comment thread git-trees
Comment on lines +388 to 395
if [ -n "$p" ]; then
if ! git worktree remove "$p"; then
echo " ! worktree remove failed — skipped branch delete for $br" >&2
continue
fi
fi
git branch -d "$br" 2>/dev/null \
|| echo " ! unmerged — use: git branch -D $br"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | grep -E '(^|/)git-trees$|(^|/)git-trees' || true

echo "== relevant git-trees lines =="
if [ -f git-trees ]; then
  cat -n git-trees | sed -n '360,430p'
fi

echo "== search branch error wording =="
rg -n "unmerged|use: git branch -D|branch -d|branch -D" git-trees || true

echo "== git branch error strings in working git? =="
if command -V git >/dev/null; then
  tmp="$(mktemp -d)"
  cd "$tmp"
  git init -q
  git checkout -b test1 >/dev/null 2>&1 || true
  git branch -d test2 2>&1 >/tmp/git-output
  git branch -d test1 2>&1 | sed 's/Warning: /Warning: /' > /tmp/git-output2
  cat /tmp/git-output
  echo "---"
  cat /tmp/git-output2
fi

Repository: brightdigit/git-trees

Length of output: 3522


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate git-trees source =="
git ls-files | grep -E '(^|/)git-trees$|(^|/)git-trees[' || true

echo "== git version availability =="
git version 2>/dev/null || true

echo "== deterministic git branch -d failure modes =="
tmp="$(mktemp -d)"
cd "$tmp"
git init -q
git config user.email test@example.com
git config user.name Test
git commit --allow-empty -m base >/dev/null
git checkout -b main-branch >/dev/null
git branch -d nonexistent 2>&1 | sed 's#<path:[[:alnum:]]*/##'"# <path>/"
git branch -d main-branch 2>&1 | sed 's#<path:[[:alnum:]]*/##'"# <path>/"
(cd "$tmp" && git branch -d main-branch)

Repository: brightdigit/git-trees

Length of output: 460


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"
cd "$tmp"
git init -q
git config user.email test@example.com
git config user.name Test
git commit --allow-empty -m base >/dev/null
git checkout -b main >/dev/null
git branch feature

echo "== missing branch: =="
git branch -d missing 2>&1

echo "== unmerged branch: =="
git branch -d feature 2>&1

echo "== branch -D missing: =="
git branch -D missing 2>&1

echo "== branch -d exit status, unmerged: =="
(set -e; exit 0) 2>/dev/null || true
set +e
git branch -d feature >/tmp/branch-output 2>&1
status=$?
set -e
echo "status=$status"
cat /tmp/branch-output

Repository: brightdigit/git-trees

Length of output: 310


🏁 Script executed:

#!/bin/bash
set -u
tmp="$(mktemp -d)"
cd "$tmp"
git init -q
git config user.email test@example.com
git config user.name Test
git commit --allow-empty -m base >/tmp/commit.log 2>&1 || true
git checkout -b main >/tmp/checkout.log 2>&1 || true
git branch feature >/tmp/branch.log 2>&1 || true

echo "repo files:"
git ls-files

echo "== missing branch =="
if git branch -d missing 2>/tmp/branch-missing.log >/tmp/branch-missing.out; then
  echo "missing status=0 unexpected"
else
  echo "missing status=$?"
  cat /tmp/branch-missing.out /tmp/branch-missing.log
fi

echo "== unmerged branch =="
if git branch -d feature 2>/tmp/branch-unmerged.log >/tmp/branch-unmerged.out; then
  echo "unmerged status=0 unexpected"
else
  echo "unmerged status=$?"
  cat /tmp/branch-unmerged.out /tmp/branch-unmerged.log
fi

echo "== branch -D missing =="
if git branch -D missing 2>/tmp/branch-d-missing.log >/tmp/branch-d-missing.out; then
  echo "D status=0 unexpected"
else
  echo "D status=$?"
  cat /tmp/branch-d-missing.out /tmp/branch-d-missing.log
fi

Repository: brightdigit/git-trees

Length of output: 427


🏁 Script executed:

#!/bin/bash
set -u

tmp="$(mktemp -d)"
cd "$tmp"
git init -q
git config user.email test@example.com
git config user.name Test
git commit --allow-empty -m base >/tmp/commit.log 2>&1 || true
git checkout -b main >/tmp/checkout.log 2>&1 || true
git branch feature >/tmp/branch.log 2>&1 || true

echo "== repo files =="
git ls-files

echo "== status of unmerged delete =="
(git branch -d feature >/tmp/branch-unmerged.out 2>/tmp/branch-unmerged.log)
status=$?
echo "unmerged status=$status"
cat /tmp/branch-unmerged.out /tmp/branch-unmerged.log

echo "== status of non-existent delete =="
(git branch -d missing >/tmp/branch-missing.out 2>/tmp/branch-missing.log)
status=$?
echo "missing status=$status"
cat /tmp/branch-missing.out /tmp/branch-missing.log

echo "== status of current branch delete =="
(git branch -d main >/tmp/branch-current.out 2>/tmp/branch-current.log)
status=$?
echo "current status=$status"
cat /tmp/branch-current.out /tmp/branch-current.log

Repository: brightdigit/git-trees

Length of output: 541


🌐 Web query:

git branch -d unmerged tip error message "did not merge" "not fully merged"

💡 Result:

The error message "The branch is not fully merged" occurs when you attempt to delete a local Git branch using the command git branch -d <branch_name> while that branch contains commits that Git cannot find in your current checked-out branch [1][2]. Git uses the -d (lowercase) flag as a safety mechanism to prevent you from accidentally deleting work that has not been integrated into your current branch [1][3]. Why this happens: 1. True Unmerged Work: You may genuinely have new commits on the branch that have not been merged [4][2]. 2. Squash Merges: If you merged your branch using a "squash" strategy on a platform like GitHub, GitLab, or Bitbucket, Git may not recognize the branch as "merged" locally because the commit hashes on the target branch are different from the ones on your local branch [5][6]. 3. Outdated Local History: If you merged a pull request on a remote server but have not pulled those changes into your local repository, your local Git still views the branch as having unique, unmerged commits [7]. How to resolve it: If you are confident that the branch's work is already integrated elsewhere (or if you simply want to discard the unmerged work), you can force the deletion by using the capital -D flag [1][8]: git branch -D <branch_name> Alternatively, you can switch to the target branch (e.g., main or master) and perform a git pull to ensure your local history is up to date before attempting the standard delete again [7]. To identify which commits are causing the "not merged" status, you can run: git log <branch_name> --not <main_branch_name> [4]

Citations:


🏁 Script executed:

#!/bin/bash
set -u

tmp="$(mktemp -d)"
cd "$tmp"
git init -q
git config user.email test@example.com
git config user.name Test
git commit --allow-empty -m base >/tmp/commit.log 2>&1 || true
git checkout -b main >/tmp/checkout.log 2>&1 || true

echo "== unmergeable branch delete in fresh repo on current branch =="
(git branch feature 2>/tmp/branch-create-old.log) 2>&1
(git branch -d feature >/tmp/branch-create-old.out 2>/tmp/branch-create-old.log)
old_status=$?
echo "old unmergeable status=$old_status"
cat /tmp/branch-create-old.out /tmp/branch-create-old.log

git checkout -b branch1 >/tmp/checkout.log 2>&1
git branch feature2 >/tmp/branch.log 2>&1

echo "== unmergeable branch delete with feature2 not merged into branch1 =="
(git branch -d feature2 >/tmp/branch-unmerged.out 2>/tmp/branch-unmerged.log)
status=$?
echo "unmergeable status=$status"
cat /tmp/branch-unmerged.out /tmp/branch-unmerged.log

echo "== current checked-out branch delete from branch3 with feature2 not merged =="
git checkout -b branch3 >/tmp/checkout.log 2>&1
(git branch -d feature2 >/tmp/branch-unmerged-from-other.out 2>/tmp/branch-unmerged-from-other.log)
status=$?
echo "unmergeable from branch3 status=$status"
cat /tmp/branch-unmerged-from-other.out /tmp/branch-unmerged-from-other.log

echo "== checked-out branch delete from branch3 with feature2 merged into branch3 =="
(git branch -d branch3 >/tmp/branch-current-from-other.out 2>/tmp/branch-current-from-other.log)
status=$?
echo "checkout branch from branch3 status=$status"
cat /tmp/branch-current-from-other.out /tmp/branch-current-from-other.log

Repository: brightdigit/git-trees

Length of output: 818


🏁 Script executed:

#!/bin/bash
set -u

tmp="$(mktemp -d)"
cd "$tmp"
git init -q
git config user.email test@example.com
git config user.name Test

git checkout -b main >/tmp/checkout.log 2>&1 || true
git commit --allow-empty -m base >/tmp/commit.log 2>&1 || true

git checkout -b b1 >/tmp/checkout.log 2>&1 || true
git commit --allow-empty -m b1 >/tmp/commit.log 2>&1 || true

git checkout -b b2 >/tmp/checkout.log 2>&1 || true
git commit --allow-empty -m b2 >/tmp/commit.log 2>&1 || true

echo "== b2 with unique commits not merged into b1 =="
(git branch -d b2 >/tmp/merge-status.out 2>/tmp/merge-status.log)
status=$?
echo "status=$status"
cat /tmp/merge-status.out /tmp/merge-status.log

echo "== b2 already merged into b1 =="
git checkout b1 >/tmp/checkout.log 2>&1 || true
git merge --no-ff b2 >/tmp/merge.log 2>&1 || true
(git branch -d b2 >/tmp/merge-found.out 2>/tmp/merge-found.log)
status=$?
echo "status=$status"
cat /tmp/merge-found.out /tmp/merge-found.log

echo "== b1 merged directly into main and tried from b2 =="
git checkout main >/tmp/checkout.log 2>&1 || true
git merge --no-ff b1 >/tmp/merge.log 2>&1 || true
(git branch -d b1 >/tmp/m2-found.out 2>/tmp/m2-found.log)
status=$?
echo "status=$status"
cat /tmp/m2-found.out /tmp/m2-found.log

Repository: brightdigit/git-trees

Length of output: 517


🌐 Web query:

git branch -d unmerged commits fatal error unmerged tip error output

💡 Result:

When you attempt to delete a local branch using git branch -d, Git checks whether the branch's tip commit is an ancestor of the current HEAD or its upstream branch [1][2]. If it is not, Git prevents the deletion with the error: error: The branch '...' is not fully merged [3][2]. This message is a safety feature designed to prevent the accidental loss of commits that have not been incorporated into your current work or upstream tracking branch [1][2]. Common scenarios where this occurs include: 1. Squash merges: If you merge a pull request using a "squash" strategy on a platform like GitHub or GitLab, the resulting commit on the target branch has a new hash and does not contain the individual commits from your local branch [4][5]. Because your local history does not match the upstream history, Git considers the branch "unmerged" [5]. 2. Outdated local branches: You may have already merged the work into the remote repository, but your local repository is unaware of these updates [4]. Running git fetch or git pull may resolve this if it brings your local tracking branch up to date with the remote [4]. 3. Multiple branches: Git only checks the current HEAD and the configured upstream. If you merged the branch into a different local branch, Git will not automatically see that, and the error will persist [2]. To resolve this, you have two primary options: - Confirm you have safely integrated your changes: If you are certain the work is captured elsewhere, you can force the deletion using the uppercase -D flag [6][2]: git branch -D - Investigate unmerged work: If you want to verify what has not been merged, you can list the commits with [7]: git log --not Note: If the branch is currently checked out, you must switch to a different branch before deleting it, or you will receive a different error message indicating the branch is in use [3][8].

Citations:


Make branch-delete error handling reason-aware.

Both cleanup passes suppress git branch -d errors and unconditionally tell users the branch is unmerged, but branch -d can also fail for missing checked-out, or otherwise unprotected branches. Capture the real error and only suggest git branch -D when it is the unmerged-tip failure.

📍 Affects 1 file
  • git-trees#L388-L395 (this comment)
  • git-trees#L413-L420
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@git-trees` around lines 388 - 395, Update both branch-delete cleanup sites in
git-trees (lines 388-395 and 413-420) to capture the actual git branch -d error
instead of suppressing it. Report the captured error for general failures, and
suggest git branch -D only when the error indicates an unmerged tip; preserve
the existing cleanup flow for successful deletions and worktree-removal
failures.

Comment thread git-trees
Comment on lines 443 to +447
if [ -n "$(find "$wt" -maxdepth 0 -mtime +"$days" 2>/dev/null)" ]; then
echo " $wt"
[ "$apply" -eq 1 ] && git worktree remove "$wt"
fi
done < <(git worktree list --porcelain | awk '/^worktree /{print $2}')
done < <(git worktree list --porcelain | awk '/^worktree /{sub(/^worktree /, ""); print}')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Propagate stale-worktree removal failures.

If git worktree remove "$wt" fails, the loop continues and the function’s unconditional final return 0 reports success. Track the failure, report it, and return non-zero so automation cannot mistake an incomplete cleanup for success.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@git-trees` around lines 443 - 447, Update the stale-worktree cleanup loop
around git worktree remove "$wt" to capture removal failures, report the
affected worktree, and preserve a non-zero status across iterations. Replace the
unconditional final success return with the tracked cleanup status so any failed
removal causes the function to return non-zero.

@leogdion
leogdion deleted the paths-and-clean branch July 24, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant