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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ Then add the following to your shell's config file:

- **Interactive `git commit --fixup && git rebase -i --autosquash` selector** (`gfu`)

- **Interactive `git worktree list` selector** (`gwj`)
suft marked this conversation as resolved.
Show resolved Hide resolved

- **Interactive `git worktree lock <worktree>` selector** (`gwl`)

- **Interactive `git worktree remove <worktree>` selector** (`gwr`)

- **Interactive `git worktree unlock <worktree>` selector** (`gwu`)

# ⌨ Keybindings

| Key | Action |
Expand Down Expand Up @@ -188,6 +196,10 @@ forgit_cherry_pick=gcp
forgit_rebase=grb
forgit_blame=gbl
forgit_fixup=gfu
forgit_worktree_jump=gwj
forgit_worktree_lock=gwl
forgit_worktree_remove=gwr
forgit_worktree_unlock=gwu
suft marked this conversation as resolved.
Show resolved Hide resolved
```

## git integration
Expand Down Expand Up @@ -246,6 +258,10 @@ These are passed to the according `git` calls.
| `gbl` | `FORGIT_BLAME_GIT_OPTS` |
| `gfu` | `FORGIT_FIXUP_GIT_OPTS` |
| `gcp` | `FORGIT_CHERRY_PICK_GIT_OPTS` |
| `gwj` | `FORGIT_WORKTREE_PREVIEW_GIT_OPTS` |
suft marked this conversation as resolved.
Show resolved Hide resolved
| `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` |

## pagers

Expand Down Expand Up @@ -300,6 +316,10 @@ Customizing fzf options for each command individually is also supported:
| `gbl` | `FORGIT_BLAME_FZF_OPTS` |
| `gfu` | `FORGIT_FIXUP_FZF_OPTS` |
| `gcp` | `FORGIT_CHERRY_PICK_FZF_OPTS` |
| `gwj` | `FORGIT_WORKTREE_JUMP_FZF_OPTS` |
| `gwl` | `FORGIT_WORKTREE_LOCK_FZF_OPTS` |
| `gwr` | `FORGIT_WORKTREE_REMOVE_FZF_OPTS` |
| `gwu` | `FORGIT_WORKTREE_UNLOCK_FZF_OPTS` |

Complete loading order of fzf options is:

Expand Down
98 changes: 98 additions & 0 deletions bin/git-forgit
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ $FORGIT_FZF_DEFAULT_OPTS

_forgit_warn() { printf "%b[Warn]%b %s\n" '\e[0;33m' '\e[0m' "$@" >&2; }
_forgit_info() { printf "%b[Info]%b %s\n" '\e[0;32m' '\e[0m' "$@" >&2; }
_forgit_inside_git_dir() { git rev-parse --is_inside-git-dir >/dev/null; }
suft marked this conversation as resolved.
Show resolved Hide resolved
_forgit_inside_work_tree() { git rev-parse --is-inside-work-tree >/dev/null; }
# tac is not available on OSX, tail -r is not available on Linux, so we use either of them
_forgit_reverse_lines() { tac 2> /dev/null || tail -r; }
Expand Down Expand Up @@ -1002,6 +1003,98 @@ _forgit_ignore_clean() {
[[ -d "$FORGIT_GI_REPO_LOCAL" ]] && rm -rf "$FORGIT_GI_REPO_LOCAL"
}

_forgit_worktree_preview() {
local sha
# trailing space in grep to avoid matching worktrees with a common path
suft marked this conversation as resolved.
Show resolved Hide resolved
sha=$(git worktree list | grep "$1 " | awk '{print $2}')
# bare git-dir has no history
[[ "$sha" == "(bare)" ]] && return
suft marked this conversation as resolved.
Show resolved Hide resolved
suft marked this conversation as resolved.
Show resolved Hide resolved
_forgit_worktree_preview_git_opts=()
_forgit_parse_array _forgit_worktree_preview_git_opts "$FORGIT_WORKTREE_PREVIEW_GIT_OPTS"
# the trailing '--' ensures that this works for branches that have a name
# that is identical to a file
git log "$sha" "${_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.

I think the default preview looks a bit dull. This should have the same options as in $_forgit_log_preview_options by default. It might even be possible to simply call _forgit_branch_preview from here. This would of course make it impossible to specify different options for _forgit_branch_preview and _forgit_worktree_preview, but that would be fine from my side as they show the same thing (a log). I think it would be best to implement FORGIT_WORKTREE_PREVIEW_GIT_OPTS with #396 instead. Would be interested to hear 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.

I agree.

}

_forgit_worktree_jump() {
_forgit_inside_work_tree || _forgit_inside_git_dir || return 1
sandr01d marked this conversation as resolved.
Show resolved Hide resolved
local count tree opts
count=$(git worktree list | wc -l)

[[ $count -eq 1 ]] && return 1

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

tree=$(git worktree list | awk '{print $1}' | FZF_DEFAULT_OPTS="$opts" fzf)
suft marked this conversation as resolved.
Show resolved Hide resolved
[[ -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.

}

_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_worktree_lock() {
_forgit_inside_work_tree || _forgit_inside_git_dir || return 1
sandr01d marked this conversation as resolved.
Show resolved Hide resolved
local tree opts

opts="
$FORGIT_FZF_DEFAULT_OPTS
+s -m --tiebreak=index
suft marked this conversation as resolved.
Show resolved Hide resolved
--preview=\"$FORGIT worktree_preview {1}\"
$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).

[[ -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.

}

_forgit_git_worktree_remove() {
_forgit_worktree_remove_git_opts=()
_forgit_parse_array _forgit_worktree_remove_git_opts "$FORGIT_WORKTREE_REMOVE_GIT_OPTS"
git worktree remove "${_forgit_worktree_remove_git_opts[@]}" "$@"
}

_forgit_worktree_remove() {
_forgit_inside_work_tree || _forgit_inside_git_dir || return 1
local tree opts

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.

--preview=\"$FORGIT worktree_preview {1}\"
$FORGIT_WORKTREE_REMOVE_FZF_OPTS
"

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

_forgit_worktree_unlock() {
_forgit_inside_work_tree || _forgit_inside_git_dir || return 1
local tree opts

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

tree=$(git worktree list | awk '{print $1}' | grep -v "(bare)" | FZF_DEFAULT_OPTS="$opts" fzf)
suft marked this conversation as resolved.
Show resolved Hide resolved
[[ -z "$tree" ]] && return 1
git worktree unlock "$tree"
suft marked this conversation as resolved.
Show resolved Hide resolved
}

public_commands=(
"add"
"blame"
Expand All @@ -1023,12 +1116,17 @@ public_commands=(
"revert_commit"
"stash_show"
"stash_push"
"worktree_jump"
"worktree_lock"
"worktree_remove"
"worktree_unlock"
)

private_commands=(
"add_preview"
"blame_preview"
"branch_preview"
"worktree_preview"
"checkout_commit_preview"
"checkout_file_preview"
"cherry_pick_from_branch_preview"
Expand Down
4 changes: 4 additions & 0 deletions conf.d/forgit.plugin.fish
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,8 @@ if test -z "$FORGIT_NO_ALIASES"
abbr -a -- (string collect $forgit_revert_commit; or string collect "grc") git-forgit revert_commit
abbr -a -- (string collect $forgit_blame; or string collect "gbl") git-forgit blame
abbr -a -- (string collect $forgit_checkout_tag; or string collect "gct") git-forgit checkout_tag
abbr -a -- (string collect $forgit_worktree_jump; or string collect "gwj") 'set tree (git-forgit worktree_jump); test -n "$tree"; and cd "$tree"'
abbr -a -- (string collect $forgit_worktree_lock; or string collect "gwl") git-forgit worktree_lock
abbr -a -- (string collect $forgit_worktree_remove; or string collect "gwr") git-forgit worktree_remove
abbr -a -- (string collect $forgit_worktree_unlock; or string collect "gwu") git-forgit worktree_unlock
end
24 changes: 24 additions & 0 deletions forgit.plugin.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ forgit::ignore::clean() {
"$FORGIT" ignore_clean "$@"
}

forgit::worktree::jump() {
cd "$("$FORGIT" worktree_jump "$@")" || exit
}

forgit::worktree::lock() {
"$FORGIT" worktree_lock "$@"
}

forgit::worktree::remove() {
"$FORGIT" worktree_remove "$@"
}

forgit::worktree::unlock() {
"$FORGIT" worktree_unlock "$@"
}

# register aliases
# shellcheck disable=SC2139
if [[ -z "$FORGIT_NO_ALIASES" ]]; then
Expand All @@ -160,6 +176,10 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then
export forgit_rebase="${forgit_rebase:-grb}"
export forgit_fixup="${forgit_fixup:-gfu}"
export forgit_blame="${forgit_blame:-gbl}"
export forgit_worktree_jump="${forgit_worktree_jump:-gwj}"
export forgit_worktree_lock="${forgit_worktree_lock:-gwl}"
export forgit_worktree_remove="${forgit_worktree_remove:-gwr}"
export forgit_worktree_unlock="${forgit_worktree_unlock:-gwu}"

alias "${forgit_add}"='forgit::add'
alias "${forgit_reset_head}"='forgit::reset::head'
Expand All @@ -180,5 +200,9 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then
alias "${forgit_rebase}"='forgit::rebase'
alias "${forgit_fixup}"='forgit::fixup'
alias "${forgit_blame}"='forgit::blame'
alias "${forgit_worktree_jump}"='forgit::worktree::jump'
alias "${forgit_worktree_lock}"='forgit::worktree::lock'
alias "${forgit_worktree_remove}"='forgit::worktree::remove'
alias "${forgit_worktree_unlock}"='forgit::worktree::unlock'

fi