feat(nvim): git worktree picker that follows open buffers #1

Open
unkin-agent wants to merge 3 commits from unkin-agent/dotfiles:benvin/worktree-picker into master
First-time contributor

Jumping between agentws worktrees of the same repo meant quitting nvim or hand-reopening every buffer at the new path.

  • Add lua/func/worktree.lua, parsing git worktree list --porcelain for the current buffer's repo.
  • Selecting an entry cds to that worktree root and re-points each file buffer at the same relative path, restoring cursor position.
  • Leave buffers with no counterpart open on the old path, flagged b:worktree_orphan; refuse the switch if a modified buffer would be replaced.
  • Expose as a telescope extension, falling back to vim.ui.select.
  • Bind <Leader>gw, add :Worktree [path], add tests/worktree_spec.lua.
Jumping between agentws worktrees of the same repo meant quitting nvim or hand-reopening every buffer at the new path. - Add `lua/func/worktree.lua`, parsing `git worktree list --porcelain` for the current buffer's repo. - Selecting an entry `cd`s to that worktree root and re-points each file buffer at the same relative path, restoring cursor position. - Leave buffers with no counterpart open on the old path, flagged `b:worktree_orphan`; refuse the switch if a modified buffer would be replaced. - Expose as a telescope extension, falling back to `vim.ui.select`. - Bind `<Leader>gw`, add `:Worktree [path]`, add `tests/worktree_spec.lua`.
unkin-agent added 1 commit 2026-09-05 11:20:17 +10:00
Adds a telescope picker over 'git worktree list --porcelain'. Selecting an
entry cds to that worktree and re-points every open buffer at the same
relative path there, keeping cursor position and refusing to clobber
unsaved work.

Signed-off-by: unkin-agent <unkin-agent@unkin.net>
Author
First-time contributor

Review of the worktree picker (correctness only; PR is atomic and the body follows conventions — no split or format issues).

func/worktree.lua

  1. M.switch's old_root (lines ~151-153) is derived from detect_root(context_dir()), i.e. the repo of whatever buffer/cwd is currently active. context_dir()'s own comment says it's deliberately meant to keep working "after wandering into another repo" — but switch() reuses that same context to decide which buffers to carry over. If the active buffer/cwd isn't in the same worktree as your other open file buffers (e.g. you're browsing a different repo's worktree list, or sitting in a help/scratch buffer whose cwd resolved elsewhere), old_root is wrong, contains(old_root, ...) matches nothing, and the switch silently reports "0 buffer(s) moved" with no warning that anything's off.

  2. Cursor capture only reads wins[1] (line ~190) when a buffer is open in multiple split windows, then reapplies that single cursor to every window showing it after the move (lines ~208-212) — per-window cursor divergence is lost.

  3. restore_cursor (lines ~127-131): for an empty destination file, row becomes 0, producing an invalid nvim_win_set_cursor(win, {0, ...}). It's wrapped in pcall so it won't error, but it silently no-ops instead of e.g. placing the cursor at line 1 — worth a guard rather than relying on the swallow.

None of these are blocking (the tested/common paths work), but especially #1 is worth a fix or at least a documented caveat before this becomes muscle memory.

Review of the worktree picker (correctness only; PR is atomic and the body follows conventions — no split or format issues). **func/worktree.lua** 1. `M.switch`'s `old_root` (lines ~151-153) is derived from `detect_root(context_dir())`, i.e. the repo of whatever buffer/cwd is currently active. `context_dir()`'s own comment says it's deliberately meant to keep working "after wandering into another repo" — but `switch()` reuses that same context to decide *which buffers to carry over*. If the active buffer/cwd isn't in the same worktree as your other open file buffers (e.g. you're browsing a different repo's worktree list, or sitting in a help/scratch buffer whose cwd resolved elsewhere), `old_root` is wrong, `contains(old_root, ...)` matches nothing, and the switch silently reports "0 buffer(s) moved" with no warning that anything's off. 2. Cursor capture only reads `wins[1]` (line ~190) when a buffer is open in multiple split windows, then reapplies that single cursor to *every* window showing it after the move (lines ~208-212) — per-window cursor divergence is lost. 3. `restore_cursor` (lines ~127-131): for an empty destination file, `row` becomes `0`, producing an invalid `nvim_win_set_cursor(win, {0, ...})`. It's wrapped in `pcall` so it won't error, but it silently no-ops instead of e.g. placing the cursor at line 1 — worth a guard rather than relying on the swallow. None of these are blocking (the tested/common paths work), but especially #1 is worth a fix or at least a documented caveat before this becomes muscle memory.
unkin-agent added 1 commit 2026-09-08 23:18:07 +10:00
Buffer migration keyed off the active buffer's repo, so a switch made from
an unrelated buffer moved nothing and said nothing.

- Match each buffer against the target's own worktree list, longest root first.
- Warn when open buffers all live outside the repository.
- Capture and restore the cursor per window, not once from the first one.
- Clamp restored cursors to a line the destination buffer has.

Signed-off-by: unkin-agent <unkin-agent@unkin.net>
Author
First-time contributor

Review: neovim git-worktree picker

Looked closely at buffer-to-worktree resolution, per-window cursor capture/restore, and cursor clamping.

  • Correctness: no bugs found. contains() correctly boundary-checks with a trailing / (avoids dotfiles matching dotfiles__branch), source_roots() sorts candidate roots longest-first so a worktree nested inside another wins the prefix match (worktree.lua:157-181, root_of at 183-190), per-window cursor capture/restore (worktree.lua:246-281) and clamp_cursor (worktree.lua:146-150) are both correct and match test expectations.
  • Test coverage gap: every fixture worktree in worktree_spec.lua (main/other/outside) is a sibling directory — none is actually nested inside another worktrees path. The longest-first sort in source_roots exists specifically to resolve that case correctly, but no test constructs a real nested worktree (e.g. other living at main/.worktrees/other) to prove root_of picks the inner root over the outer one. Worth a follow-up test since this is the one piece of "ownership resolution" logic that is unexercised.
  • Minor (by design, not a bug): a single modified+conflicting buffer blocks the entire switch, including otherwise-clean buffers in moved. Matches the stated "refuse the switch" behavior in the PR body, just flagging so its all-or-nothing semantics are a deliberate choice.
  • Atomicity: change is a single cohesive feature (picker + telescope extension + keymap/command + tests); no unrelated concerns mixed in.
  • PR body: within limits (7 lines, 644 chars), why + present-tense how bullets, no narrative/test section/person references. Compliant.

Nothing here blocks merge; the nested-worktree test gap is the only actionable follow-up.

**Review: neovim git-worktree picker** Looked closely at buffer-to-worktree resolution, per-window cursor capture/restore, and cursor clamping. - Correctness: no bugs found. `contains()` correctly boundary-checks with a trailing `/` (avoids `dotfiles` matching `dotfiles__branch`), `source_roots()` sorts candidate roots longest-first so a worktree nested inside another wins the prefix match (worktree.lua:157-181, root_of at 183-190), per-window cursor capture/restore (worktree.lua:246-281) and `clamp_cursor` (worktree.lua:146-150) are both correct and match test expectations. - Test coverage gap: every fixture worktree in worktree_spec.lua (main/other/outside) is a *sibling* directory — none is actually nested inside another worktrees path. The longest-first sort in `source_roots` exists specifically to resolve that case correctly, but no test constructs a real nested worktree (e.g. `other` living at `main/.worktrees/other`) to prove `root_of` picks the inner root over the outer one. Worth a follow-up test since this is the one piece of "ownership resolution" logic that is unexercised. - Minor (by design, not a bug): a single modified+conflicting buffer blocks the *entire* switch, including otherwise-clean buffers in `moved`. Matches the stated "refuse the switch" behavior in the PR body, just flagging so its all-or-nothing semantics are a deliberate choice. - Atomicity: change is a single cohesive feature (picker + telescope extension + keymap/command + tests); no unrelated concerns mixed in. - PR body: within limits (7 lines, 644 chars), why + present-tense how bullets, no narrative/test section/person references. Compliant. Nothing here blocks merge; the nested-worktree test gap is the only actionable follow-up.
unkin-agent added 1 commit 2026-09-08 23:24:36 +10:00
First-time contributor

This should be managed via yadm

This should be managed via yadm
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u https://git.unkin.net/unkin-agent/dotfiles benvin/worktree-picker:unkin-agent-benvin/worktree-picker
git checkout unkin-agent-benvin/worktree-picker
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unkinben/dotfiles#1