Skip to content

Feat: add interactive git worktree operations#402

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

Feat: add interactive git worktree operations#402
suft wants to merge 2 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
@suft suft marked this pull request as ready for review July 1, 2025 16:10
@sandr01d
Copy link
Collaborator

Sorry for the late reply @suft. Just wanted to let you know that I saw your changes and will give you a review. Unfortunately I don't have much free time right now, so it might take a while until I get to it.

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.

There are a few more things we need to resolve. I'll do another functional review once done. The issue with the main worktree being present in the list of gwl is also not resoved.
Please do not close my comments, just let me know once you're done and I will review and close them. Closing them from your side makes it difficult for me to know where we last left off and what needs to be addressed.

bin/git-forgit Outdated
Comment on lines 1250 to 1256
local tree opts max_args
max_args=$(_forgit_worktree_lock_max_args)

if [[ $# -ge "$max_args" ]] || { [[ $# -ne 0 ]] && _forgit_all_non_flags "$@"; }; then
git worktree lock "$@"
return $?
fi
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't like the idea of checking for the number of allowed arguments for the command using --help. If the user provides invalid arguments it is fine to just pass them to git like we do everywhere else. Git will print a descriptive error message. Please try to keep things as simple as possible. Depending on the output of --help is not a good idea in general either, as this could easily change between different git version.

bin/git-forgit Outdated
usage=$(git worktree lock --help 2>/dev/null | grep -E 'usage: git worktree lock' | head -n1)
usage=${usage#*:}
rest=$(echo "$usage" | awk '{for(i=4;i<=NF;++i)printf "%s ",$i; print ""}')
# shellcheck disable=SC2206
Copy link
Collaborator

Choose a reason for hiding this comment

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

This function should be removed anyway, but as a general note, please don't just disable shellchecks. If you're having issues with something, feel free to ask, but don't just turn them off.

bin/git-forgit Outdated
Comment on lines 1304 to 1308
if [[ $# -ne 0 ]]; then
git worktree unlock "$@"
worktree_unlock_status=$?
return $worktree_unlock_status
fi
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can be simplified

Suggested change
if [[ $# -ne 0 ]]; then
git worktree unlock "$@"
worktree_unlock_status=$?
return $worktree_unlock_status
fi
[[ $# -ne 0 ]] && {
git worktree unlock "$@"
return $?
}

bin/git-forgit Outdated

_forgit_worktree_select() {
_forgit_inside_work_tree || _forgit_inside_git_dir || return 1
local worktree_list count tree opts
Copy link
Collaborator

Choose a reason for hiding this comment

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

To keep things consistent with the rest of the code, I would prefer to name this variable to worktrees. This applies to all functions.

Suggested change
local worktree_list count tree opts
local worktrees count tree opts

bin/git-forgit Outdated
worktree_list=$(git worktree list | grep -vE "prunable$" | awk '{print $1}' | _forgit_filter_existing_paths)

count=$(echo "$worktree_list" | wc -l)
[[ $count -eq 1 ]] && return 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should let the user know why we refuse to run a function, e.g. echo "No worktrees to select" in this specific case. This applies to all your functions.

bin/git-forgit Outdated
fi
}

_forgit_all_non_flags() {
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
_forgit_all_non_flags() {
_forgit_only_non_flags() {

Comment on lines 22 to 23
# --porcelain usually paired with -z but not needed since we use awk
_alternative "worktrees:worktree:($(git worktree list --porcelain | awk '/worktree/ {print $2}'))"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please use -z here, as this allows handling worktrees that contain a newline character.

Suggested change
# --porcelain usually paired with -z but not needed since we use awk
_alternative "worktrees:worktree:($(git worktree list --porcelain | awk '/worktree/ {print $2}'))"
__gitcomp_nl "$(git worktree list --porcelain -z | awk 'BEGIN {RS="\0"} /worktree/ {print $2}')"

The same applies to the zsh completions for this command.

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, no equivalent behavior using forgit as a git subcommand

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 wfxr marked this pull request as draft February 5, 2026 16:54
@wfxr wfxr force-pushed the worktrees branch 3 times, most recently from f5c8d6c to 3bfa1d2 Compare February 6, 2026 10:17
@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
@sandr01d
Copy link
Collaborator

Hey @wfxr, could you rearrange the commits in this MR, so we have one commit for each refactoring and one for the new workspace commands? This would make reviewing a lot easier for me.

@carlfriedrich
Copy link
Collaborator

@wfxr Thanks a lot for pushing this forward and your work on this! I like that you combined multiple commands into one and make use of keybindings instead (that goes into the direction I proposed in #259 some time ago).

I did a quick test of your branch and it seems to works as expected.

For a useful code review I am going with @sandr01d: given the huge size of the PR it would be really helpful to have a clean history of atomic commits on this branch.

- Add _forgit_strip_ansi for removing ANSI escape codes
- Add _forgit_branch_list to list branches with current branch first
- Add _forgit_extract_branch_name to parse branch names from git output
- Update checkout_branch, switch_branch, branch_delete, cherry_pick_from_branch,
  and branch_preview to use the new helpers
- Fix _forgit_inside_work_tree to suppress stderr
Add worktree browser (gwt) and worktree delete selector (gwd) with:
- Interactive worktree list with lock status, branch info, and age
- Preview showing working tree status and recent commits
- Ctrl-Y to copy worktree path, Alt-L to toggle lock/unlock
- Shell integration for zsh (with cd) and fish (with cd)
- Tab completions for zsh, bash, and fish
- Documentation in README with keybindings and options

Co-Authored-By: Sufien Tout <sufientout@gmail.com>
@wfxr
Copy link
Owner

wfxr commented Feb 27, 2026

Thanks @sandr01d @carlfriedrich for the review feedback! I've reorganized the branch into two atomic commits:

This should make reviewing much easier. PTAL when you get a chance!

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

7 participants