Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: add interactive git worktree operations #402

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft

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.

[[ "$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

bin/git-forgit Outdated Show resolved Hide resolved
@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.

bin/git-forgit Outdated Show resolved Hide resolved
bin/git-forgit Show resolved Hide resolved
bin/git-forgit Outdated Show resolved Hide resolved
bin/git-forgit Outdated Show resolved Hide resolved
bin/git-forgit Show resolved Hide resolved

tree=$(git worktree list | awk '{print $1}' | FZF_DEFAULT_OPTS="$opts" fzf)
[[ -z "$tree" ]] && return 1
echo "$tree"
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it would be cleaner to cd into the directory here instead of doing so in forgit.plugin.zsh and forgit.plugin.fish.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • Using cd directly in the function only changes the current directory in the script child process, not in the terminal process where the script is called
  • Any preference between cd and pushd?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is tricky, because the actual change of the directory is currently implemented in the shell plugin. Some users (e.g. me) do not use forgit with the plugin but in the form of a subcommand of git. Calling git forgit worktree_jump won't work at the moment, though. IMO we should find a way to make this work correctly in both usecases, or otherwise rename the function (because right now it actually does not jump).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could rename the function to worktree_select - gws
(but also offer the gwj alias which combines cd and worktree_select)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Given that there is no way to manipulate the directory of the parent process, I think this is the best compromise we can achieve. @carlfriedrich WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems resonable to me. We should add an explaining line on this to the documentation, though.

bin/git-forgit Outdated
$FORGIT_WORKTREE_LOCK_FZF_OPTS
"

tree=$(git worktree list | awk '{print $1}' | grep -v "(bare)" | FZF_DEFAULT_OPTS="$opts" fzf)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What exactly is grep -v "(bare)" filtering out? I'm new to the whole worktree thing, could you please elaborate?
There are also more things we need to filter out here:

  • The root worktree (which can not be locked)
  • Worktrees that are already locked

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When using worktrees with a bare clone, you automatically get the git-dir as an entry in the
git worktree list output (even though its not really a worktree).
So for certain operations we want to filter it out (like locking, unlocking, removing, ...).
Good catch on filtering already locked (totally slipped my mind).

bin/git-forgit Show resolved Hide resolved
bin/git-forgit Outdated

opts="
$FORGIT_FZF_DEFAULT_OPTS
+s -m --tiebreak=index --header-lines=1
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not a fan of the header line here. I would prefer to have no header line and simply not display the root worktree with this command. This also applies to the other functions using --header-lines=1. Would be interested to know what other maintainers think @cjappl @carlfriedrich @wfxr.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is the way we do it with all other forgit functions as well, so I would approve it like this. @sandr01d are you questioning this principle in general for all forgit functions?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is the way we do it with all other forgit functions as well

Weird, I've never noticed. In this case I'm fine with keeping it in this PR as well.

bin/git-forgit Outdated Show resolved Hide resolved
bin/git-forgit Outdated Show resolved Hide resolved
bin/git-forgit Outdated

tree=$(git worktree list | awk '{print $1}' | grep -v "(bare)" | FZF_DEFAULT_OPTS="$opts" fzf)
[[ -z "$tree" ]] && return 1
_forgit_git_worktree_lock "$tree"
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be nice to allow passing in additional arguments as we do with other functions by propagating the function arguments, so that gwl --reason <STRING> works as expected.
This also applies to the other functions.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Agree.

@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:

bin/git-forgit Outdated Show resolved Hide resolved
@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
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 again for your work @suft. There are a few more things that need to be addressed, but I think we're heading in the right direction 🙂 I think there might have been a misunderstanding regarding argument parsing, sorry if I didn't make it clear enough what I meant previously. I provided an example this time. In case any questions come up, please don't hesitate to ping me.

README.md Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
Comment on lines +1054 to +1085
local tree opts reason
if [[ $# -ne 0 ]]; then
if [[ "$1" == "--reason" ]]; then
if [[ -z "$2" ]]; then
_forgit_warn "Option \`--reason' requires a value"
return 1
fi
reason="$2"
shift 2
fi

if [[ $# -eq 0 ]] || _forgit_contains_non_flags "$@"; then
git worktree lock "$@" --reason "$reason"
return $?
fi
fi

opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--preview=\"$FORGIT worktree_preview {1}\"
$FORGIT_WORKTREE_LOCK_FZF_OPTS
"

tree=$(git worktree list | grep -vE "\(bare\)|locked" | awk '{print $1}' | FZF_DEFAULT_OPTS="$opts" fzf)
[[ -z "$tree" ]] && return 1

if [[ -z "$reason" ]]; then
_forgit_git_worktree_lock "$tree"
else
_forgit_git_worktree_lock "$tree" --reason "$reason"
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 think we could simplify this by not parsing the arguments and instead simply propagating them. This also allows us to stay up-to-date without any changes in case git introduces new arguments in the future.

Suggested change
local tree opts reason
if [[ $# -ne 0 ]]; then
if [[ "$1" == "--reason" ]]; then
if [[ -z "$2" ]]; then
_forgit_warn "Option \`--reason' requires a value"
return 1
fi
reason="$2"
shift 2
fi
if [[ $# -eq 0 ]] || _forgit_contains_non_flags "$@"; then
git worktree lock "$@" --reason "$reason"
return $?
fi
fi
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--preview=\"$FORGIT worktree_preview {1}\"
$FORGIT_WORKTREE_LOCK_FZF_OPTS
"
tree=$(git worktree list | grep -vE "\(bare\)|locked" | awk '{print $1}' | FZF_DEFAULT_OPTS="$opts" fzf)
[[ -z "$tree" ]] && return 1
if [[ -z "$reason" ]]; then
_forgit_git_worktree_lock "$tree"
else
_forgit_git_worktree_lock "$tree" --reason "$reason"
fi
local tree opts
if [[ $# -ne 0 ]] && _forgit_contains_non_flags "$@"; then
git worktree lock "$@"
return $?
fi
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--preview=\"$FORGIT worktree_preview {1}\"
$FORGIT_WORKTREE_LOCK_FZF_OPTS
"
tree=$(git worktree list | grep -vE "\(bare\)|locked" | awk '{print $1}' | FZF_DEFAULT_OPTS="$opts" fzf)
[[ -z "$tree" ]] && return 1
_forgit_git_worktree_lock "$tree" "$@"

_forgit_inside_work_tree || _forgit_inside_git_dir || return 1
local worktree_list tree opts force_flags

force_flags=()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here, I would prefer simply propagating the arguments as we do everywhere else.

Comment on lines +21 to +23
_git-worktrees() {
_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.

This always suggests me refs/heads/worktrees which is not a worktree. Also consider using -z together with --porcelain. From the git man page:

--porcelain
With list, output in an easy-to-parse format for scripts. This format will remain stable
across Git versions and regardless of user configuration. It is recommended to combine this
with -z. See below for
-z
Terminate each line with a NUL rather than a newline when --porcelain is specified with
list. This makes it possible to parse the output when a worktree path contains a newline
character.

The same applies to the bash completions.

@@ -42,6 +42,10 @@ complete -c git-forgit -n __fish_forgit_needs_subcommand -a reset_head -d 'git r
complete -c git-forgit -n __fish_forgit_needs_subcommand -a revert_commit -d 'git revert commit selector'
complete -c git-forgit -n __fish_forgit_needs_subcommand -a stash_show -d 'git stash viewer'
complete -c git-forgit -n __fish_forgit_needs_subcommand -a stash_push -d 'git stash push selector'
complete -c git-forgit -n __fish_forgit_needs_subcommand -a worktree_select -d 'git worktree selector'
Copy link
Collaborator

Choose a reason for hiding this comment

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

I get error messages that look like this when trying to execute one of the commands in fish when inside a worktree:

forgit: 'worktree_lock' is not a valid forgit command.

The following commands are supported:
	add
	blame
	branch_delete
	checkout_branch
	checkout_commit
	checkout_file
	checkout_tag
	cherry_pick
	cherry_pick_from_branch
	clean
	diff
	fixup
	ignore
	log
	rebase
	reset_head
	revert_commit
	stash_show
	stash_push

@cjappl is our fish expert in case you need some help with this.

@sandr01d
Copy link
Collaborator

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

I'm not exactly sure what you mean. git worktree list always seems to give me the root worktree as the first line and git worktree list | fzf --header-lines 1 does not allow me to select the root worktree as I'd expect. Could you please provide an example on how to reproduce this?

@suft suft marked this pull request as draft December 24, 2024 20:44
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
4 participants