Feat: add interactive git worktree operations#402
Conversation
|
@suft Thanks for your contribution! Is this ready for review or did you make it a draft on purpose? |
|
@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[@]}" -- |
There was a problem hiding this comment.
- 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
cae816c to
525441f
Compare
sandr01d
left a comment
There was a problem hiding this comment.
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.
sandr01d
left a comment
There was a problem hiding this comment.
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_jumpdoes 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=1and 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_flagsto do so.
|
(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 ) |
The root worktree doesn't show up as the first selection, so
|
|
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. |
sandr01d
left a comment
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
| if [[ $# -ne 0 ]]; then | ||
| git worktree unlock "$@" | ||
| worktree_unlock_status=$? | ||
| return $worktree_unlock_status | ||
| fi |
There was a problem hiding this comment.
This can be simplified
| 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 |
There was a problem hiding this comment.
To keep things consistent with the rest of the code, I would prefer to name this variable to worktrees. This applies to all functions.
| 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 |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
| _forgit_all_non_flags() { | |
| _forgit_only_non_flags() { |
completions/_git-forgit
Outdated
| # --porcelain usually paired with -z but not needed since we use awk | ||
| _alternative "worktrees:worktree:($(git worktree list --porcelain | awk '/worktree/ {print $2}'))" |
There was a problem hiding this comment.
Please use -z here, as this allows handling worktrees that contain a newline character.
| # --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 |
There was a problem hiding this comment.
| + `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
| - **Interactive `git commit --squash && git rebase -i --autosquash` selector** (`gsq`) | ||
|
|
||
| - **Interactive `git commit --fixup=reword && git rebase -i --autosquash` selector** (`grw`) | ||
|
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
| + `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
| | `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` | |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
$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:
| count=$(echo "$worktree_list" | wc -l) | |
| count=$(echo "$worktree_list" | grep -c .) |
This applies to multiple functions.
bin/git-forgit
Outdated
| _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[@]}" "$@" | ||
| } |
There was a problem hiding this comment.
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:
| _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[@]}" % | |
| } |
|
Hey @suft. Are you going to continue working on this? Otherwise somebody else might adopt this PR in order to make it merge-ready. |
|
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! |
|
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. |
f5c8d6c to
3bfa1d2
Compare
|
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 implementationTwo new commands:
Worktree browser features
Also included
Differences from the original proposal
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! |
|
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. |
|
@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>
|
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! |


Check list
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:
mainfor looking at the pristine codeworkfor looking at my codereviewfor looking at someone else’s codebackgroundfor my computer to look at my codescratchfor everything elseExample of worktrees in a bare clone

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?)
gwlgwugwrgwjgit worktree listandcdinto that result (not a native git operation)Screenshots
bash (repo with short history) -

gwjFORGIT_WORKTREE_PREVIEW_GIT_OPTS='--oneline --graph --decorate --color'fish (repo with mid-length history) -

gwjFORGIT_WORKTREE_PREVIEW_GIT_OPTS='--oneline --graph --decorate --color'bash (repo with mid-length history) -

gwjFORGIT_WORKTREE_PREVIEW_GIT_OPTS='--oneline --graph --decorate --color --max-count=100'Closes #399.
Type of change
Test environment