Skip to content

Give linked worktrees their own row - #2

Open
HugoHeneault wants to merge 4 commits into
mainfrom
fix/worktree-rows
Open

HugoHeneault wants to merge 4 commits into
mainfrom
fix/worktree-rows

Conversation

@HugoHeneault

@HugoHeneault HugoHeneault commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Généré par Claude Opus 5.5

A repo with worktrees under .claude/worktrees or next to it showed them wrong. It now keeps a single row, and its worktrees are listed with its branches.

REPO                     CHANGES              UNPUSHED
ownweb/claude            3 files  +435 -9     OW-553-seeders-setup-commun [2 files], +2 more
ownweb/laravel-starter   -                    OW-553-seeders-setup-commun ahead 7 (new) [17 files]
ownweb/flutter-starter   -                    OW-701-durcissement-securite [11 files]

Worktrees were never scanned. The walk skips dotted directories and stops at the first .git. A repo's worktrees are now read from its .git/worktrees/*/gitdir, a filesystem read rather than an extra git call, so they are found wherever they are checked out: inside the repo, next to it, in an ignored directory or outside DIR. A worktree the walk reaches on its own is mapped to its main repo through its .git file and commondir, and the root is canonicalized so both paths compare. Stale entries (worktree deleted without git worktree remove), worktree.useRelativePaths, detached worktrees and bare main repos are handled.

One row per repo. CHANGES counts the main checkout. A worktree holding changes joins UNPUSHED as branch [n files], merged with that branch's unpushed commits when it has some, before the other branches. Inside a repo each worktree is a row: space selects it, d shows its diff, a commit goes through every checkout showing changes, or only the selected files and worktrees, each asking for its own message, and a push covers every unpushed branch, or only the selected branches and the ones the selected worktrees have checked out.

The parent listed them as files. ls-files --others reports a nested repo as dir/, which showed up as an untracked +0 -0 file. A trailing slash now marks a nested repo, left out of the parent's files.

add --all staged them as embedded gitlinks. Committing with nothing selected would have recorded the worktree as a gitlink in its own parent (reproduced in a throwaway repo: A .claude/worktrees/feat). add --all now excludes each nested repo with :(exclude,literal)<path>.

Limits

  • A worktree is committed as a whole (git add --all): files inside it cannot be selected one by one.
  • UNPUSHED keeps its 44 column cap, so a repo with several worktrees shows the first ones and +n more; the repo view lists them all.
  • A nested clone that is not a worktree no longer appears in its parent's untracked files. It is still not scanned on its own, as before.

Test
Four tests added (add --all argument vector, UNPUSHED labels, commit scope, push scope), 20 pass. Checked against ~/dev: one row per repo, 87 scanned instead of 96. Checked in throwaway repos with a nested, a sibling, a relative path, a detached, a bare repo and an outside DIR worktree, and drove the repo view in a pty: worktree row, selection, and a preview that lists only the selected worktree's branch. README, CHANGELOG and CLAUDE.md updated.

A worktree checked out under .claude/worktrees was never scanned: the walk
skips dotted directories and stops at the first .git. Its parent listed it as
an untracked +0 -0 file instead, and a commit with nothing selected ran
git add --all, which records it as an embedded gitlink.

- Worktrees are read from the repo's .git/worktrees, wherever they are checked
  out, and deduplicated against the walk.
- A worktree row carries its uncommitted changes only: branches are shared and
  stay on the main repo's row.
- A nested repo is left out of its parent's untracked files, and add --all
  excludes it.
A worktree row was labelled by its directory, so claude-OW-701 or
.claude/worktrees/tech-writer did not say which repo it belonged to, and its
branch sat on the main repo's row, away from the changes made on it.

- The row is named after the main repo and the checked out branch:
  ownweb/claude [OW-701-skill-owasp-mobile]. A detached worktree falls back to
  its directory name, a bare repo to its own directory.
- A worktree row lists the unpushed commits of its own branch. The main repo
  keeps every branch no live worktree has checked out.
- Both are read from the .git files and .git/worktrees, still three git calls
  per row.
One row per worktree still read as several repos: ownweb/claude showed up
three times. A repo now keeps a single row, and its worktrees are listed
with its branches, which is what they are.

- A linked worktree the walk reaches is mapped to its main repo, which scans
  every worktree listed in .git/worktrees. A worktree outside DIR is found
  through its repo, so the worktree without main repo case is gone.
- UNPUSHED lists worktrees holding changes first, as branch [n files], merged
  with that branch's unpushed commits when it has some, then the other
  branches. CHANGES stays the main checkout's.
- Inside a repo a worktree is a row: space selects it, d shows its diff, and
  commit goes through every checkout showing changes, or the selected files
  and worktrees only, each asking for its own message.
Selecting a worktree narrowed the commit to it but not the push: with no
branch selected, p went on to push every unpushed branch of the repo. A
selected worktree now narrows the push to the branch it has checked out, next
to any selected branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant