Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 74 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,20 @@ If you're having issues after updating, and commands such as `forgit::add` or al

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

- **Interactive `git commit --squash && git rebase -i --autosquash` selector** (`gsq`)

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

Comment on lines +179 to +182
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

- **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

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.


- **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 @@ -237,6 +251,10 @@ forgit_blame=gbl
forgit_fixup=gfu
forgit_squash=gsq
forgit_reword=grw
forgit_worktree_select=gws/gwj
forgit_worktree_lock=gwl
forgit_worktree_remove=gwr
forgit_worktree_unlock=gwu
```

## git integration
Expand Down Expand Up @@ -275,29 +293,33 @@ git cf
If you want to customize `git`'s behavior within forgit there is a dedicated variable for each forgit command.
These are passed to the according `git` calls.

| Command | Option |
| -------- | --------------------------------------------------------------------------- |
| `ga` | `FORGIT_ADD_GIT_OPTS` |
| `glo` | `FORGIT_LOG_GIT_OPTS` |
| `grl` | `FORGIT_REFLOG_GIT_OPTS` |
| `gd` | `FORGIT_DIFF_GIT_OPTS` |
| `gso` | `FORGIT_SHOW_GIT_OPTS` |
| `grh` | `FORGIT_RESET_HEAD_GIT_OPTS` |
| `gcf` | `FORGIT_CHECKOUT_FILE_GIT_OPTS` |
| `gcb` | `FORGIT_CHECKOUT_BRANCH_GIT_OPTS`, `FORGIT_CHECKOUT_BRANCH_BRANCH_GIT_OPTS` |
| `gbd` | `FORGIT_BRANCH_DELETE_GIT_OPTS` |
| `gct` | `FORGIT_CHECKOUT_TAG_GIT_OPTS` |
| `gco` | `FORGIT_CHECKOUT_COMMIT_GIT_OPTS` |
| `grc` | `FORGIT_REVERT_COMMIT_GIT_OPTS` |
| `gss` | `FORGIT_STASH_SHOW_GIT_OPTS` |
| `gsp` | `FORGIT_STASH_PUSH_GIT_OPTS` |
| `gclean` | `FORGIT_CLEAN_GIT_OPTS` |
| `grb` | `FORGIT_REBASE_GIT_OPTS` |
| `gbl` | `FORGIT_BLAME_GIT_OPTS` |
| `gfu` | `FORGIT_FIXUP_GIT_OPTS` |
| `gsq` | `FORGIT_SQUASH_GIT_OPTS` |
| `grw` | `FORGIT_REWORD_GIT_OPTS` |
| `gcp` | `FORGIT_CHERRY_PICK_GIT_OPTS` |
| Command | Option |
| ----------- | --------------------------------------------------------------------------- |
| `ga` | `FORGIT_ADD_GIT_OPTS` |
| `glo` | `FORGIT_LOG_GIT_OPTS` |
| `grl` | `FORGIT_REFLOG_GIT_OPTS` |
| `gd` | `FORGIT_DIFF_GIT_OPTS` |
| `gso` | `FORGIT_SHOW_GIT_OPTS` |
| `grh` | `FORGIT_RESET_HEAD_GIT_OPTS` |
| `gcf` | `FORGIT_CHECKOUT_FILE_GIT_OPTS` |
| `gcb` | `FORGIT_CHECKOUT_BRANCH_GIT_OPTS`, `FORGIT_CHECKOUT_BRANCH_BRANCH_GIT_OPTS` |
| `gbd` | `FORGIT_BRANCH_DELETE_GIT_OPTS` |
| `gct` | `FORGIT_CHECKOUT_TAG_GIT_OPTS` |
| `gco` | `FORGIT_CHECKOUT_COMMIT_GIT_OPTS` |
| `grc` | `FORGIT_REVERT_COMMIT_GIT_OPTS` |
| `gss` | `FORGIT_STASH_SHOW_GIT_OPTS` |
| `gsp` | `FORGIT_STASH_PUSH_GIT_OPTS` |
| `gclean` | `FORGIT_CLEAN_GIT_OPTS` |
| `grb` | `FORGIT_REBASE_GIT_OPTS` |
| `gbl` | `FORGIT_BLAME_GIT_OPTS` |
| `gfu` | `FORGIT_FIXUP_GIT_OPTS` |
| `gsq` | `FORGIT_SQUASH_GIT_OPTS` |
| `grw` | `FORGIT_REWORD_GIT_OPTS` |
| `gcp` | `FORGIT_CHERRY_PICK_GIT_OPTS` |
| `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` |
Comment on lines +319 to +322
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.


## pagers

Expand Down Expand Up @@ -332,31 +354,35 @@ export FORGIT_FZF_DEFAULT_OPTS="

Customizing fzf options for each command individually is also supported:

| Command | Option |
|----------|-----------------------------------|
| `ga` | `FORGIT_ADD_FZF_OPTS` |
| `glo` | `FORGIT_LOG_FZF_OPTS` |
| `grl` | `FORGIT_REFLOG_FZF_OPTS` |
| `gi` | `FORGIT_IGNORE_FZF_OPTS` |
| `gat` | `FORGIT_ATTRIBUTES_FZF_OPTS` |
| `gd` | `FORGIT_DIFF_FZF_OPTS` |
| `gso` | `FORGIT_SHOW_FZF_OPTS` |
| `grh` | `FORGIT_RESET_HEAD_FZF_OPTS` |
| `gcf` | `FORGIT_CHECKOUT_FILE_FZF_OPTS` |
| `gcb` | `FORGIT_CHECKOUT_BRANCH_FZF_OPTS` |
| `gbd` | `FORGIT_BRANCH_DELETE_FZF_OPTS` |
| `gct` | `FORGIT_CHECKOUT_TAG_FZF_OPTS` |
| `gco` | `FORGIT_CHECKOUT_COMMIT_FZF_OPTS` |
| `grc` | `FORGIT_REVERT_COMMIT_FZF_OPTS` |
| `gss` | `FORGIT_STASH_FZF_OPTS` |
| `gsp` | `FORGIT_STASH_PUSH_FZF_OPTS` |
| `gclean` | `FORGIT_CLEAN_FZF_OPTS` |
| `grb` | `FORGIT_REBASE_FZF_OPTS` |
| `gbl` | `FORGIT_BLAME_FZF_OPTS` |
| `gfu` | `FORGIT_FIXUP_FZF_OPTS` |
| `gsq` | `FORGIT_SQUASH_FZF_OPTS` |
| `grw` | `FORGIT_REWORD_FZF_OPTS` |
| `gcp` | `FORGIT_CHERRY_PICK_FZF_OPTS` |
| Command | Option |
|-------------|-----------------------------------|
| `ga` | `FORGIT_ADD_FZF_OPTS` |
| `glo` | `FORGIT_LOG_FZF_OPTS` |
| `grl` | `FORGIT_REFLOG_FZF_OPTS` |
| `gi` | `FORGIT_IGNORE_FZF_OPTS` |
| `gat` | `FORGIT_ATTRIBUTES_FZF_OPTS` |
| `gd` | `FORGIT_DIFF_FZF_OPTS` |
| `gso` | `FORGIT_SHOW_FZF_OPTS` |
| `grh` | `FORGIT_RESET_HEAD_FZF_OPTS` |
| `gcf` | `FORGIT_CHECKOUT_FILE_FZF_OPTS` |
| `gcb` | `FORGIT_CHECKOUT_BRANCH_FZF_OPTS` |
| `gbd` | `FORGIT_BRANCH_DELETE_FZF_OPTS` |
| `gct` | `FORGIT_CHECKOUT_TAG_FZF_OPTS` |
| `gco` | `FORGIT_CHECKOUT_COMMIT_FZF_OPTS` |
| `grc` | `FORGIT_REVERT_COMMIT_FZF_OPTS` |
| `gss` | `FORGIT_STASH_FZF_OPTS` |
| `gsp` | `FORGIT_STASH_PUSH_FZF_OPTS` |
| `gclean` | `FORGIT_CLEAN_FZF_OPTS` |
| `grb` | `FORGIT_REBASE_FZF_OPTS` |
| `gbl` | `FORGIT_BLAME_FZF_OPTS` |
| `gfu` | `FORGIT_FIXUP_FZF_OPTS` |
| `gsq` | `FORGIT_SQUASH_FZF_OPTS` |
| `grw` | `FORGIT_REWORD_FZF_OPTS` |
| `gcp` | `FORGIT_CHERRY_PICK_FZF_OPTS` |
| `gws`/`gwj` | `FORGIT_WORKTREE_SELECT_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
155 changes: 154 additions & 1 deletion bin/git-forgit
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,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; }
_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 All @@ -64,6 +65,18 @@ _forgit_previous_commit() {
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() {

while (("$#")); do
case "$1" in
-*)
return 1
;;
esac
shift
done
return 0
}

_forgit_contains_non_flags() {
while (("$#")); do
case "$1" in
Expand Down Expand Up @@ -1106,7 +1119,7 @@ _forgit_ignore() {
args=()
while IFS='' read -r arg; do
args+=("$arg")
done < <(_forgit_paths_list "$FORGIT_GI_TEMPLATES" .gitignore |
done < <(_forgit_paths_list "$FORGIT_GI_TEMPLATES" .gitignore |
nl -w4 -s' ' |
FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $2}')
fi
Expand Down Expand Up @@ -1176,6 +1189,141 @@ _forgit_paths_list() {
find "$path" -name "*$ext" -print |sed -e "s#$ext\$##" -e 's#.*/##' -e '/^$/d' | sort -fu
}

_forgit_filter_existing_paths() {
while read -r path; do
[[ -d "$path" ]] && echo "$path"
done
}

_forgit_worktree_preview() {
local sha
# trailing space in grep to avoid matching worktrees with a common path
sha=$(git worktree list | grep "$1 " | awk '{print $2}')
if [[ "$sha" == "(bare)" ]]; then
printf "%b(bare)%b %s\n" '\e[0;33m' '\e[0m' 'No history for git dir'
return
fi
# 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

}

_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

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.


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

tree=$(echo "$worktree_list" | FZF_DEFAULT_OPTS="$opts" fzf)
[[ -z "$tree" ]] && return 1
echo "$tree"
}

_forgit_worktree_lock_max_args() {
local usage
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.

local args=($rest)
echo "${#args[@]}"
}

_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[@]}" "$@"
}
Comment on lines +1241 to +1245
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[@]}" %
}


_forgit_worktree_lock() {
_forgit_inside_work_tree || _forgit_inside_git_dir || return 1

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
Comment on lines +1250 to +1256
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.


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_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 worktree_list tree opts

if [[ $# -ne 0 ]] && _forgit_contains_non_flags "$@"; then
git worktree remove "$@"
return $?
fi

worktree_list=$(git worktree list | grep -v "(bare)")

count=$(echo "$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_REMOVE_FZF_OPTS
"

tree=$(echo "$worktree_list" | awk '{print $1}' | 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
if [[ $# -ne 0 ]]; then
git worktree unlock "$@"
worktree_unlock_status=$?
return $worktree_unlock_status
fi
Comment on lines +1304 to +1308
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 $?
}

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.

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

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

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

public_commands=(
"add"
"attributes"
Expand All @@ -1201,12 +1349,17 @@ public_commands=(
"show"
"stash_show"
"stash_push"
"worktree_select"
"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
17 changes: 17 additions & 0 deletions completions/_git-forgit
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ _git-stash-show() {
_alternative "files:filename:($(git stash list | sed -n -e 's/:.*//p'))"
}

_git-worktrees() {
# --porcelain usually paired with -z but not needed since we use awk
_alternative "worktrees:worktree:($(git worktree list --porcelain | awk '/worktree/ {print $2}'))"
Comment on lines +22 to +23
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.

}

# The completions for git already define a _git-diff completion function, but
# it provides the wrong results when called from _git-forgit because it heavily
# depends on the context it's been called from (usage of $curcontext and
Expand Down Expand Up @@ -79,6 +84,10 @@ _git-forgit() {
'squash:git squash'
'stash_show:git stash viewer'
'stash_push:git stash push selector'
'worktree_select:git worktree selector'
'worktree_lock:git worktree lock selector'
'worktree_remove:git worktree remove selector'
'worktree_unlock:git worktree unlock selector'
)
_describe -t commands 'git forgit' subcommands
;;
Expand All @@ -102,6 +111,10 @@ _git-forgit() {
squash) __git_branch_names ;;
stash_show) _git-stash-show ;;
show) _git-show ;;
worktree_select) _git-worktrees ;;
worktree_lock) _git-worktrees ;;
worktree_remove) _git-worktrees ;;
worktree_unlock) _git-worktrees ;;
esac
}

Expand Down Expand Up @@ -130,6 +143,10 @@ compdef __git_branch_names forgit::reword
compdef __git_branch_names forgit::squash
compdef _git-stash-show forgit::stash::show
compdef _git-show forgit::show
compdef _git-worktrees forgit::worktree::select
compdef _git-worktrees forgit::worktree::lock
compdef _git-worktrees forgit::worktree::remove
compdef _git-worktrees forgit::worktree::unlock

# this is the case of calling the command and pressing tab
# the very first time of a shell session, we have to manually
Expand Down
Loading