Skip to content

Feat: add interactive git worktree operations#402

Open
suft wants to merge 11 commits intowfxr:mainfrom
suft:worktrees
Open

Feat: add interactive git worktree operations#402
suft wants to merge 11 commits intowfxr:mainfrom
suft:worktrees

Conversation

@suft
Copy link
Contributor

@suft suft commented Nov 3, 2024

Check list

  • I have performed a self-review of my code
  • I have commented my code in hard-to-understand areas
  • I have made corresponding changes to the documentation

Description

I recently adopted using multiple fixed worktrees as part of my workflow to help with productivity.
Each worktree is used for a different type of concurrent activity:

  • main for looking at the pristine code
  • work for looking at my code
  • review for looking at someone else’s code
  • background for my computer to look at my code
    • operates in the detached head state
  • scratch for everything else
. (bare repo)
├── background/ (* worktree)
├── config
├── description
├── HEAD
├── hooks/
├── info/
├── logs/
├── main/ (* worktree)
├── objects/
├── packed-refs
├── refs/
├── review/ (* worktree)
├── rr-cache/
├── scratch/ (*worktree)
├── work/(* worktree)
└── worktrees/

Example of worktrees in a bare clone
Screenshot 2024-11-13 at 7 29 51 PM

Another approach is to use worktrees as a replacement of, or a supplement to git branches. Instead of switching branches, you just change directories. So that would involve creating a new worktree and branch, then delete the worktree upon merging.

Worktree Operations (should we stick with these abbreviations?)

  • locking a worktree - gwl
  • unlocking a worktree - gwu
  • removing a worktree - gwr
  • jumping to a worktree - gwj
    • similar to switching branches
    • involves a combination of selecting a result of git worktree list and cd into that result (not a native git operation)

Screenshots

bash (repo with short history) - gwj
FORGIT_WORKTREE_PREVIEW_GIT_OPTS='--oneline --graph --decorate --color'
Screenshot 2024-11-04 at 8 24 20 PM

fish (repo with mid-length history) - gwj
FORGIT_WORKTREE_PREVIEW_GIT_OPTS='--oneline --graph --decorate --color'
Screenshot 2024-11-10 at 11 41 59 AM

bash (repo with mid-length history) - gwj
FORGIT_WORKTREE_PREVIEW_GIT_OPTS='--oneline --graph --decorate --color --max-count=100'
Screenshot 2024-11-10 at 12 46 05 PM

Closes #399.

Type of change

  • Bug fix
  • New feature
  • Refactor
  • Breaking change
  • Documentation change

Test environment

  • Shell
    • bash
    • zsh
    • fish
  • OS
    • Linux
    • Mac OS X
    • Windows
    • Others:

@carlfriedrich
Copy link
Collaborator

@suft Thanks for your contribution! Is this ready for review or did you make it a draft on purpose?

@suft
Copy link
Contributor Author

suft commented Nov 4, 2024

@carlfriedrich I made it draft on purpose. Still have a few things to adjust.

bin/git-forgit Outdated
[[ "$sha" == "(bare)" ]] && return
# the trailing '--' ensures that this works for branches that have a name
# that is identical to a file
git log "$sha" "${_forgit_log_preview_options[@]}" --
Copy link
Contributor Author

@suft suft Nov 10, 2024

Choose a reason for hiding this comment

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

  • I should probably add preview options FORGIT_WORKTREE_PREVIEW_GIT_OPTS (like Add *_PREVIEW_GIT_OPTS variables #396)
  • I've noticed this can run a bit slow (on some computers) if you have a repo with a long history because it attempts to get the entire history for the branch checked out in that worktree
    • It would be good to see what others think when they test this out
  • Would be quicker if I limit the log entries in the worktree preview options

@suft suft force-pushed the worktrees branch 3 times, most recently from cae816c to 525441f Compare November 10, 2024 19:44
@suft suft marked this pull request as ready for review November 10, 2024 19:45
Copy link
Collaborator

@sandr01d sandr01d left a comment

Choose a reason for hiding this comment

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

Thanks for you work @suft! I think this is going to be a nice improvement. There are a few things that need to be adjusted. In some of my comments I've pinged the other maintainers for their opinions. Please hold back on implementing those, as those comments are opinionated and I'd like to have feedback from the others first before deciding in which direction to go.

@suft suft requested a review from sandr01d November 14, 2024 00:32
@carlfriedrich
Copy link
Collaborator

@suft Great implementation so far, @sandr01d great review. This will be a good addition to forgit.

Copy link
Collaborator

@sandr01d sandr01d left a comment

Choose a reason for hiding this comment

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

Thanks for the changes @suft, looks good to me so far. Let me summarize the things that still need to be addressed:

  • The default preview should look the same as for other commands that use git log (#402 (comment))
  • _forgit_worktree_jump does not change the directory when forgit is used as a git subcommand (#402 (comment))
  • The root worktree should not be selectable with gwl. We can make it non-interactable with --header-lines=1 and also bring this back in the other functions that you used this with. Don't mind my earlier comment regarding this. (#402 (comment))
  • We should add completions for the new commands (#402 (comment))
  • We should allow passing additional arguments to the underlying git commands (#402 (comment)). The logic for detecting whether the git command should be executed immediately without using fzf needs to be adjusted to do so. Instead of testing if any arguments were passed, we need to test whether non flag arguments were passed. You can use _forgit_contains_non_flags to do so.

@cjappl
Copy link
Collaborator

cjappl commented Nov 15, 2024

(it looks like there is very thorough reviewing going on here, please ping me if you need another pair of eyes, otherwise I completely defer to @sandr01d and @carlfriedrich )

@suft suft marked this pull request as draft November 18, 2024 00:09
@suft suft marked this pull request as ready for review December 18, 2024 19:08
@suft suft requested a review from sandr01d December 18, 2024 19:08
@suft suft marked this pull request as draft December 18, 2024 19:24
@suft suft marked this pull request as ready for review December 18, 2024 19:50
@suft
Copy link
Contributor Author

suft commented Dec 18, 2024

Thanks for the changes @suft, looks good to me so far. Let me summarize the things that still need to be addressed:

The root worktree doesn't show up as the first selection, so --header-lines=1 will affect a different worktree (which I'm guessing is not the behaviour we want), unless we reverse the fzf item list.

Screenshot 2024-12-18 at 3 02 14 PM

@suft suft changed the title feat: add interactive git worktree operations Feat: add interactive git worktree operations Dec 18, 2024
README.md Outdated
Comment on lines 179 to 182
- **Interactive `git commit --squash && git rebase -i --autosquash` selector** (`gsq`)

- **Interactive `git commit --fixup=reword && git rebase -i --autosquash` selector** (`grw`)

Copy link
Collaborator

Choose a reason for hiding this comment

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

This does not belong here

README.md Outdated

- **Interactive `git worktree list` selector** (`gws`/`gwj`)

+ `gwj` jumps to the worktree using `cd` and can only be used via the alias, no equivalent behavior using forgit as a git subcommand
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
+ `gwj` jumps to the worktree using `cd` and can only be used via the alias, no equivalent behavior using forgit as a git subcommand
+ `gwj` jumps to the worktree using `cd` and can only be used via the alias, there is no equivalent behavior when using forgit as a git subcommand.

README.md Outdated
Comment on lines 319 to 322
| `gws`/`gwj` | `FORGIT_WORKTREE_PREVIEW_GIT_OPTS` |
| `gwl` | `FORGIT_WORKTREE_LOCK_GIT_OPTS`, `FORGIT_WORKTREE_PREVIEW_GIT_OPTS` |
| `gwr` | `FORGIT_WORKTREE_REMOVE_GIT_OPTS`, `FORGIT_WORKTREE_PREVIEW_GIT_OPTS` |
| `gwu` | `FORGIT_WORKTREE_PREVIEW_GIT_OPTS` |
Copy link
Collaborator

Choose a reason for hiding this comment

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

The *_PREVIEW_GIT_OPTS do not exist and don't belong here.

bin/git-forgit Outdated
local worktree_list tree opts

worktree_list=$(git worktree list | grep -v "(bare)" | grep -E "locked$")
count=$(echo "$worktree_list" | wc -l)
Copy link
Collaborator

@sandr01d sandr01d Aug 3, 2025

Choose a reason for hiding this comment

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

$count is 1 when there are actually no locked workspaces, because echo adds a newline (which is counted by wc). We can work around it using grep instead:

Suggested change
count=$(echo "$worktree_list" | wc -l)
count=$(echo "$worktree_list" | grep -c .)

This applies to multiple functions.

bin/git-forgit Outdated
Comment on lines 1241 to 1245
_forgit_git_worktree_lock() {
_forgit_worktree_lock_git_opts=()
_forgit_parse_array _forgit_worktree_lock_git_opts "$FORGIT_WORKTREE_LOCK_GIT_OPTS"
git worktree lock "${_forgit_worktree_lock_git_opts[@]}" "$@"
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be nice to allow operating on multiple worktrees at the same time. Should be easy to implement by allowing multiselect in fzf and modifiying the _forgit_git_worktree_* functions like this:

Suggested change
_forgit_git_worktree_lock() {
_forgit_worktree_lock_git_opts=()
_forgit_parse_array _forgit_worktree_lock_git_opts "$FORGIT_WORKTREE_LOCK_GIT_OPTS"
git worktree lock "${_forgit_worktree_lock_git_opts[@]}" "$@"
}
_forgit_git_worktree_lock() {
local trees
trees=$1
shift
_forgit_worktree_lock_git_opts=()
_forgit_parse_array _forgit_worktree_lock_git_opts "$FORGIT_WORKTREE_LOCK_GIT_OPTS"
echo "$trees" | xargs -I% git worktree lock "${_forgit_worktree_lock_git_opts[@]}" %
}

@carlfriedrich
Copy link
Collaborator

Hey @suft. Are you going to continue working on this? Otherwise somebody else might adopt this PR in order to make it merge-ready.

@wfxr
Copy link
Owner

wfxr commented Feb 5, 2026

Hey @suft, thank you so much for all the work you've put into this PR — the worktree integration is a feature more and more users have been looking forward to.

I noticed it's been a while since the last update. Just wanted to check in: are you still interested in continuing to work on this? No pressure at all — I completely understand that life gets busy!

If you'd prefer to hand it off or if I don't hear back by this weekend, I'd be happy to pick it up from here and get it across the finish line. Either way, your contribution is greatly appreciated and will be properly credited.

Thanks again!

@suft
Copy link
Contributor Author

suft commented Feb 5, 2026

Hey @suft, thank you so much for all the work you've put into this PR — the worktree integration is a feature more and more users have been looking forward to.

I noticed it's been a while since the last update. Just wanted to check in: are you still interested in continuing to work on this? No pressure at all — I completely understand that life gets busy!

If you'd prefer to hand it off or if I don't hear back by this weekend, I'd be happy to pick it up from here and get it across the finish line. Either way, your contribution is greatly appreciated and will be properly credited.

Thanks again!

Thanks for checking in! Life has indeed gotten busy on my end, so I'd really appreciate you taking this over. Feel free to use whatever parts are helpful from my work. Looking forward to seeing this feature land!

@iloveitaly
Copy link
Contributor

https://mikebian.co/using-git-worktrees-for-parallel-ai-agent-development/

this zsh script is very forgit-like and has been working well for me.

wfxr and others added 3 commits February 6, 2026 00:53
Add six new git worktree commands with fzf integration:
- worktree (gw): Browse/list worktrees with preview
- worktree_jump (gwj): Select and cd into worktree
- worktree_delete (gwd): Interactive multi-select deletion
- worktree_move (gwm): Interactive move with path input
- worktree_lock (gwl): Interactive multi-select locking
- worktree_unlock (gwu): Interactive multi-select unlocking

Features:
- Support for bare repositories
- Preview showing git status and recent commits
- Ctrl-Y to copy worktree path to clipboard
- Multi-select support for delete/lock/unlock
- Dirty worktree warning in delete preview
- Shell integration for zsh, bash, and fish
- Tab completions for all three shells
- Full README documentation

Co-Authored-By: Sufien Tout <sufientout@gmail.com>
- Add _forgit_branch_list to list branches with current branch first
- Add _forgit_extract_branch_name to remove 2-char prefix from git branch output
- Replace 5 occurrences of `cut -c3-` and `awk '{print $1}'` with the helper
- Fix preview commands to use {} instead of {1} for full line matching
- Display header line (current branch) in gray for visual distinction

The previous `LC_ALL=C sort -k1.1,1.1 -rs` approach didn't work reliably
because git branch has three prefixes: '*' (current), '+' (worktree), ' '
(other), and their ASCII order conflicts with the desired display order.
@wfxr wfxr marked this pull request as draft February 5, 2026 16:54
wfxr added 4 commits February 6, 2026 15:49
Redesign worktree list format to show colored branch name (cyan),
relative commit time (gray), and lock status (yellow). Simplify
preview to show only colorized git status and commit log.
Deduplicate the ANSI-stripping sed pattern into _forgit_strip_ansi
and adopt the [XY] bracket prefix style (consistent with ga) for
worktree list output. The Y position now encodes both lock (L) and
prunable (P) states, with L taking precedence when both present.
The separate `gw` (browse) and `gwj` (jump) worktree commands
were redundant — `gwj` was a strict superset that called `gw`
internally then `cd`'d into the result. Users almost always
want to jump into the selected worktree, not just print its
path.

Merge both into a single `gwt` alias that selects a worktree
and `cd`s into it. The raw path output remains available via
`git forgit worktree` for scripting use cases.
The interactive worktree move adds little value over plain
`git worktree move` — unlike browse/delete, there is no
multi-select or preview benefit that justifies the wrapper.
wfxr added 2 commits February 6, 2026 16:31
- Use `_forgit_branch_list` and `_forgit_extract_branch_name` in
  `_forgit_switch_branch` so it stays consistent with
  `_forgit_checkout_branch` and other branch selectors
- Remove duplicate `_forgit_worktree_delete_preview` since it is
  identical to `_forgit_worktree_preview`
- Add `ctrl-y` (yank path) binding to worktree delete selector
  for parity with the worktree browser
- Fix detached HEAD causing fzf to swallow first branch
  candidate: always emit a header line, showing the
  detached SHA when no current branch exists

- Fix `_forgit_strip_ansi` breaking on macOS: replace
  GNU-only `\x1b` with portable ANSI-C quoting

- Fix `gwt` wrapper returning error when arguments are
  passed (e.g. `gwt add`): short-circuit to passthrough
  in both zsh/bash and fish plugins

- Fix `_forgit_extract_branch_name` failing on ANSI-coded
  and symbolic ref (`->`) lines from `git branch --all`

- Filter out symbolic ref lines (`remotes/origin/HEAD ->`)
  from branch list to prevent them as fzf candidates

- Add bashunit tests for `_forgit_extract_branch_name`,
  `_forgit_strip_ansi`, and `_forgit_extract_worktree_path`
Deduplicate the ANSI-stripping sed pattern into _forgit_strip_ansi
and adopt the [XY] bracket prefix style (consistent with ga) for
worktree list output. The Y position now encodes both lock (L) and
prunable (P) states, with L taking precedence when both present.
@wfxr
Copy link
Owner

wfxr commented Feb 6, 2026

Thanks again to @suft for the original work and idea on this PR! I've taken over and done a reimplementation based on the original proposal and the review feedback from @sandr01d and @carlfriedrich. Also thanks to @iloveitaly for providing the link, which helped improve the display format of the worktree list.

First — my sincere apologies, @suft. I accidentally force-pushed to your branch, which overwrote your original commits. I'm really sorry about that. Hopefully you still have a local copy of your original implementation. Again, very sorry for the mishap.

Summary of the current implementation

Two new commands:

Alias Command Description
gwt forgit worktree Interactive worktree browser — select a worktree and cd into it
gwd forgit worktree_delete Interactive git worktree remove selector (multi-select)

Worktree browser features

  • Rich display format: [XY] /path/to/worktree (branch) 3 hours ago
    • X: * = current worktree, = other
    • Y: L (yellow) = locked, P (yellow) = prunable, = normal
  • Preview pane: git status -s + git log --oneline for the selected worktree
  • Keybindings:
    • Ctrl-Y — copy worktree path to clipboard
    • Alt-L — toggle lock/unlock (with live reload)
  • Bare repo support
  • Passthrough: gwt <args> forwards arguments directly to git worktree

Also included

  • Shell completions for bash, zsh, and fish
  • Refactored _forgit_branch_list / _forgit_extract_branch_name helpers, replacing the fragile LC_ALL=C sort -k1.1,1.1 -rs pattern in checkout_branch, switch_branch, branch_delete, and cherry_pick_from_branch
image

Differences from the original proposal

Aspect Original proposal Current implementation
Jump to worktree gwj — dedicated jump command gwt — browse + jump as a single command
Lock worktree gwl — standalone command Alt-L keybinding inside the worktree browser (toggle)
Unlock worktree gwu — standalone command Same Alt-L toggle (no separate command)
Remove worktree gwr gwd (renamed to worktree_delete for consistency with branch_delete)
Preview git log with FORGIT_WORKTREE_PREVIEW_GIT_OPTS git status -s + git log --oneline; customizable via FORGIT_WORKTREE_FZF_OPTS
Main worktree Reviewers requested --header-lines=1 to make it non-selectable Current worktree listed first, remains selectable (needed for lock/unlock); delete excludes main worktree

The main design shift: instead of multiple standalone commands (lock/unlock/jump/remove), the current approach uses a single browser with keybindings for in-place operations + a separate delete command, keeping the alias namespace smaller.

I've done a thorough self-review already, but given the scope of this PR — new feature + refactoring of existing branch-related code — I'd appreciate another round of review. @cjappl @carlfriedrich @sandr01d Would you mind taking a look when you have time? And @suft, your input would also be very valuable given all the thought you put into the original design!

This comment was marked as resolved.

@wfxr wfxr marked this pull request as ready for review February 6, 2026 10:57
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.

Feature Request: Add git worktree switcher like gcb

6 participants