Merge pull request #241 from carlfriedrich/move-implementation-to-standalone-forgit

Move implementation to standalone forgit
This commit is contained in:
Tim 2022-11-26 13:10:26 +01:00 committed by GitHub
commit 43f5f1c58e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 685 additions and 1067 deletions

View file

@ -47,7 +47,7 @@ jobs:
fish --version; echo
- name: Shellcheck
run: shellcheck forgit.plugin.sh
run: shellcheck forgit.plugin.sh bin/git-forgit
- name: Test bash
run: bash forgit.plugin.sh

View file

@ -41,7 +41,7 @@ zgen load 'wfxr/forgit'
# for antigen
antigen bundle 'wfxr/forgit'
# for fisher
# for fisher (requires fisher v4.4.3 or higher)
fisher install wfxr/forgit
# for omf
@ -51,16 +51,7 @@ omf install https://github.com/wfxr/forgit
zinit load wfxr/forgit
# manually
# Clone the repository and source it in your shell's rc file.
```
You can run the following command to try `forgit` without installing:
``` bash
# for bash / zsh
source <(curl -sSL git.io/forgit)
# for fish
source (curl -sSL git.io/forgit-fish | psub)
# Clone the repository and source it in your shell's rc file or put bin/git-forgit into your $PATH
```
### Fig
@ -69,7 +60,7 @@ source (curl -sSL git.io/forgit-fish | psub)
Install `forgit` in just one click.
<a href="https://fig.io/plugins/other/forgit" target="_blank"><img src="https://fig.io/badges/install-with-fig.svg" /></a>
[![Install with Fig](https://fig.io/badges/install-with-fig.svg)](https://fig.io/plugins/other/forgit)
### 📝 Features
@ -137,9 +128,10 @@ For linux users `FORGIT_COPY_CMD` should be set to make copy work. Example: `FOR
### ⚙ Options
#### aliases
Options can be set via environment variables. They have to be **exported** in
order to be recognized by `forgit`.
##### shell
#### shell aliases
You can change the default aliases by defining these variables below.
(To disable all aliases, Set the `FORGIT_NO_ALIASES` flag.)
@ -166,7 +158,7 @@ forgit_fixup=gfu
#### git
You can use git aliases by making `git-forgit` available in `$PATH`:
You can use forgit as a subcommand of git by making `git-forgit` available in `$PATH`:
```sh
# after `forgit` was loaded
@ -177,10 +169,10 @@ export PATH="$PATH:$FORGIT_INSTALL_DIR/bin"
Then any forgit command will be a subcommand of git:
```
$ git forgit log
$ git forgit add
$ git forgit diff
```cmd
git forgit log
git forgit add
git forgit diff
```
Optionally you can add [aliases in git](https://git-scm.com/book/en/v2/Git-Basics-Git-Aliases):
@ -189,7 +181,7 @@ Optionally you can add [aliases in git](https://git-scm.com/book/en/v2/Git-Basic
git config --global alias.cf 'forgit checkout_file'
```
And use the alias in git:
And use forgit functions via a git alias:
```sh
git cf
@ -216,7 +208,7 @@ You can add default fzf options for `forgit`, including keybinds, layout, etc.
(No need to repeat the options already defined in `FZF_DEFAULT_OPTS`)
``` bash
FORGIT_FZF_DEFAULT_OPTS="
export FORGIT_FZF_DEFAULT_OPTS="
--exact
--border
--cycle
@ -256,18 +248,21 @@ Complete loading order of fzf options is:
Examples:
- `ctrl-d` to drop the selected stash but do not quit fzf (`gss` specific).
```
FORGIT_STASH_FZF_OPTS='
```sh
export FORGIT_STASH_FZF_OPTS='
--bind="ctrl-d:reload(git stash drop $(cut -d: -f1 <<<{}) 1>/dev/null && git stash list)"
'
```
- `ctrl-e` to view the logs in a vim buffer (`glo` specific).
```
FORGIT_LOG_FZF_OPTS='
```sh
export FORGIT_LOG_FZF_OPTS='
--bind="ctrl-e:execute(echo {} |grep -Eo [a-f0-9]+ |head -1 |xargs git show |vim -)"
'
```
#### other options
| Option | Description | Default |

View file

@ -1,7 +1,9 @@
#!/usr/bin/env bash
# This file is a bridge between forgit and git itself. This makes forgit a
# subcommand of git, and forwards all arguments on to forgit. So, all of these
# commands will work as expected:
# MIT (c) Wenxuan Zhang
# This file is meant to be executed directly. If it's available on the PATH,
# it can also be used as a subcommand of git, which then forwards all arguments
# on to forgit. So, all of these commands will work as expected:
#
# `git forgit log`
# `git forgit checkout_file`
@ -10,8 +12,548 @@
# This gives users the choice to set aliases inside of their git config instead
# of their shell config if they prefer.
source "$FORGIT_INSTALL_DIR/forgit.plugin.zsh"
# Set shell for fzf preview commands
SHELL=/bin/bash
FORGIT_FZF_DEFAULT_OPTS="
$FZF_DEFAULT_OPTS
--ansi
--height='80%'
--bind='alt-k:preview-up,alt-p:preview-up'
--bind='alt-j:preview-down,alt-n:preview-down'
--bind='ctrl-r:toggle-all'
--bind='ctrl-s:toggle-sort'
--bind='?:toggle-preview'
--bind='alt-w:toggle-preview-wrap'
--preview-window='right:60%'
+1
$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_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; }
_forgit_previous_commit() {
# "SHA~" is invalid when the commit is the first commit, but we can use "--root" instead
if [[ "$(git rev-parse "$1")" == "$(git rev-list --max-parents=0 HEAD)" ]]; then
echo "--root"
else
echo "$1~"
fi
}
# optional render emoji characters (https://github.com/wfxr/emoji-cli)
hash emojify &>/dev/null && _forgit_emojify='|emojify'
# extract the first git sha occurring in the input and strip trailing newline
_forgit_extract_sha="grep -Eo '[a-f0-9]+' | head -1 | tr -d '[:space:]'"
_forgit_pager=${FORGIT_PAGER:-$(git config core.pager || echo 'cat')}
_forgit_show_pager=${FORGIT_SHOW_PAGER:-$(git config pager.show || echo "$_forgit_pager")}
_forgit_diff_pager=${FORGIT_DIFF_PAGER:-$(git config pager.diff || echo "$_forgit_pager")}
_forgit_ignore_pager=${FORGIT_IGNORE_PAGER:-$(hash bat &>/dev/null && echo 'bat -l gitignore --color=always' || echo 'cat')}
_forgit_blame_pager=${FORGIT_BLAME_PAGER:-$(git config pager.blame || echo "$_forgit_pager")}
_forgit_enter_pager=${FORGIT_ENTER_PAGER:-"LESS='-r' less"}
_forgit_log_format=${FORGIT_LOG_FORMAT:-%C(auto)%h%d %s %C(black)%C(bold)%cr%Creset}
_forgit_log_preview_options="--graph --pretty=format:'$_forgit_log_format' --color=always --abbrev-commit --date=relative"
_forgit_fullscreen_context=${FORGIT_FULLSCREEN_CONTEXT:-10}
_forgit_preview_context=${FORGIT_PREVIEW_CONTEXT:-3}
# git commit viewer
_forgit_log() {
_forgit_inside_work_tree || return 1
local opts graph files log_format preview_cmd enter_cmd
files=$(sed -nE 's/.* -- (.*)/\1/p' <<< "$*") # extract files parameters for `git show` command
preview_cmd="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always -U$_forgit_preview_context % -- $files | $_forgit_show_pager"
enter_cmd="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always -U$_forgit_fullscreen_context % -- $files | $_forgit_show_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"enter:execute($enter_cmd | $_forgit_enter_pager)\"
--bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
--preview=\"$preview_cmd\"
$FORGIT_LOG_FZF_OPTS
"
graph=--graph
[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
log_format=${FORGIT_GLO_FORMAT:-$_forgit_log_format}
eval "git log $graph --color=always --format='$log_format' $* $_forgit_emojify" |
FZF_DEFAULT_OPTS="$opts" fzf
fzf_exit_code=$?
# exit successfully on 130 (ctrl-c/esc)
[[ $fzf_exit_code == 130 ]] && return 0
return $fzf_exit_code
}
# git diff viewer
_forgit_diff() {
_forgit_inside_work_tree || return 1
local files opts commits repo get_files preview_cmd enter_cmd
[[ $# -ne 0 ]] && {
if git rev-parse "$1" -- &>/dev/null ; then
if [[ $# -gt 1 ]] && git rev-parse "$2" -- &>/dev/null; then
commits="$1 $2" && files=("${@:3}")
else
commits="$1" && files=("${@:2}")
fi
else
files=("$@")
fi
}
repo="$(git rev-parse --show-toplevel)"
get_files="cd '$repo' && echo {} | sed 's/.*] *//' | sed 's/ -> / /'"
# Git stashes are named "stash@{x}", which contains the fzf placeholder "{x}".
# In order to support passing stashes as arguments to _forgit_diff, we have to
# prevent fzf from interpreting this substring by escaping the opening bracket.
# The string is evaluated a few subsequent times, so we need multiple escapes.
escaped_commits=${commits//\{/\\\\\{}
preview_cmd="$get_files | xargs -I% git diff --color=always -U$_forgit_preview_context $escaped_commits -- % | $_forgit_diff_pager"
enter_cmd="$get_files | xargs -I% git diff --color=always -U$_forgit_fullscreen_context $escaped_commits -- % | $_forgit_diff_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+m -0 --bind=\"enter:execute($enter_cmd | $_forgit_enter_pager)\"
--preview=\"$preview_cmd\"
$FORGIT_DIFF_FZF_OPTS
--prompt=\"$commits > \"
"
eval "git diff --name-status $commits -- ${files[*]} | sed -E 's/^([[:alnum:]]+)[[:space:]]+(.*)$/[\1]\t\2/'" |
sed 's/\t/ -> /2' | expand -t 8 |
FZF_DEFAULT_OPTS="$opts" fzf
fzf_exit_code=$?
# exit successfully on 130 (ctrl-c/esc)
[[ $fzf_exit_code == 130 ]] && return 0
return $fzf_exit_code
}
# git add selector
_forgit_add() {
_forgit_inside_work_tree || return 1
# Add files if passed as arguments
[[ $# -ne 0 ]] && git add "$@" && git status -su && return
local changed unmerged untracked files opts preview extract
changed=$(git config --get-color color.status.changed red)
unmerged=$(git config --get-color color.status.unmerged red)
untracked=$(git config --get-color color.status.untracked red)
# NOTE: paths listed by 'git status -su' mixed with quoted and unquoted style
# remove indicators | remove original path for rename case | remove surrounding quotes
extract="
sed 's/^.*] //' |
sed 's/.* -> //' |
sed -e 's/^\\\"//' -e 's/\\\"\$//'"
preview="
file=\$(echo {} | $extract)
if (git status -s -- \$file | grep '^??') &>/dev/null; then # diff with /dev/null for untracked files
git diff --color=always --no-index -- /dev/null \$file | $_forgit_diff_pager | sed '2 s/added:/untracked:/'
else
git diff --color=always -- \$file | $_forgit_diff_pager
fi"
opts="
$FORGIT_FZF_DEFAULT_OPTS
-0 -m --nth 2..,..
--preview=\"$preview\"
$FORGIT_ADD_FZF_OPTS
"
files=$(git -c color.status=always -c status.relativePaths=true status -su |
grep -F -e "$changed" -e "$unmerged" -e "$untracked" |
sed -E 's/^(..[^[:space:]]*)[[:space:]]+(.*)$/[\1] \2/' |
FZF_DEFAULT_OPTS="$opts" fzf |
sh -c "$extract")
[[ -n "$files" ]] && echo "$files"| tr '\n' '\0' |xargs -0 -I% git add % && git status -su && return
echo 'Nothing to add.'
}
# git reset HEAD (unstage) selector
_forgit_reset_head() {
_forgit_inside_work_tree || return 1
local cmd files opts
cmd="git diff --cached --color=always -- {} | $_forgit_diff_pager "
opts="
$FORGIT_FZF_DEFAULT_OPTS
-m -0
--preview=\"$cmd\"
$FORGIT_RESET_HEAD_FZF_OPTS
"
files="$(git diff --cached --name-only --relative | FZF_DEFAULT_OPTS="$opts" fzf)"
[[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | xargs -0 -I% git reset -q HEAD % && git status --short && return
echo 'Nothing to unstage.'
}
# git stash viewer
_forgit_stash_show() {
_forgit_inside_work_tree || return 1
local cmd opts
cmd="echo {} |cut -d: -f1 |xargs -I% git stash show --color=always --ext-diff % |$_forgit_diff_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m -0 --tiebreak=index --bind=\"enter:execute($cmd | $_forgit_enter_pager)\"
--preview=\"$cmd\"
$FORGIT_STASH_FZF_OPTS
"
git stash list | FZF_DEFAULT_OPTS="$opts" fzf
fzf_exit_code=$?
# exit successfully on 130 (ctrl-c/esc)
[[ $fzf_exit_code == 130 ]] && return 0
return $fzf_exit_code
}
# git clean selector
_forgit_clean() {
_forgit_inside_work_tree || return 1
local files opts
opts="
$FORGIT_FZF_DEFAULT_OPTS
-m -0
$FORGIT_CLEAN_FZF_OPTS
"
# Note: Postfix '/' in directory path should be removed. Otherwise the directory itself will not be removed.
files=$(git clean -xdffn "$@"| sed 's/^Would remove //' | FZF_DEFAULT_OPTS="$opts" fzf |sed 's#/$##')
[[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | xargs -0 -I% git clean -xdff '%' && git status --short && return
echo 'Nothing to clean.'
}
_forgit_cherry_pick() {
local base target preview opts fzf_selection fzf_exitval
base=$(git branch --show-current)
[[ -z $1 ]] && echo "Please specify target branch" && return 1
target="$1"
preview="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always % | $_forgit_show_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
--preview=\"$preview\"
-m -0 --tiebreak=index
$FORGIT_CHERRY_PICK_FZF_OPTS
"
fzf_selection=$(git cherry "$base" "$target" --abbrev -v | _forgit_reverse_lines |
FZF_DEFAULT_OPTS="$opts" fzf)
fzf_exitval=$?
[[ $fzf_exitval != 0 ]] && return $fzf_exitval
commits=()
while IFS="" read -r line
do
commits+=("$line")
done < <(echo "$fzf_selection" | _forgit_reverse_lines | cut -d' ' -f2)
git cherry-pick "${commits[@]}"
}
_forgit_cherry_pick_from_branch() {
_forgit_inside_work_tree || return 1
[[ $# -ne 0 ]] && { git checkout -b "$@"; return $?; }
local cmd preview opts branch exitval
cmd="git branch --color=always --all | LC_ALL=C sort -k1.1,1.1 -rs"
preview="git log {1} $_forgit_log_preview_options"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index --header-lines=1
--preview=\"$preview\"
$FORGIT_CHERRY_PICK_FROM_BRANCH_FZF_OPTS
"
# loop until either the branch selector is closed or a commit to be cherry
# picked has been selected from within a branch
while true
do
branch="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')"
[[ -z "$branch" ]] && return 1
_forgit_cherry_pick "$branch"
exitval=$?
[[ $exitval != 130 ]] && return $exitval
done
}
_forgit_rebase() {
_forgit_inside_work_tree || return 1
local cmd preview opts graph files target_commit prev_commit
graph=--graph
[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
cmd="git log $graph --color=always --format='$_forgit_log_format' $* $_forgit_emojify"
files=$(sed -nE 's/.* -- (.*)/\1/p' <<< "$*") # extract files parameters for `git show` command
preview="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always % -- $files | $_forgit_show_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
--preview=\"$preview\"
$FORGIT_REBASE_FZF_OPTS
"
target_commit=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | eval "$_forgit_extract_sha")
if [[ -n "$target_commit" ]]; then
prev_commit=$(_forgit_previous_commit "$target_commit")
git rebase -i "$prev_commit"
fi
}
_forgit_fixup() {
_forgit_inside_work_tree || return 1
git diff --cached --quiet && echo 'Nothing to fixup: there are no staged changes.' && return 1
local cmd preview opts graph files target_commit prev_commit
graph=--graph
[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
cmd="git log $graph --color=always --format='$_forgit_log_format' $* $_forgit_emojify"
files=$(sed -nE 's/.* -- (.*)/\1/p' <<< "$*")
preview="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always % -- $files | $_forgit_show_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
--preview=\"$preview\"
$FORGIT_FIXUP_FZF_OPTS
"
target_commit=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | eval "$_forgit_extract_sha")
if [[ -n "$target_commit" ]] && git commit --fixup "$target_commit"; then
prev_commit=$(_forgit_previous_commit "$target_commit")
# rebase will fail if there are unstaged changes so --autostash is needed to temporarily stash them
# GIT_SEQUENCE_EDITOR=: is needed to skip the editor
GIT_SEQUENCE_EDITOR=: git rebase --autostash -i --autosquash "$prev_commit"
fi
}
# git checkout-file selector
_forgit_checkout_file() {
_forgit_inside_work_tree || return 1
[[ $# -ne 0 ]] && { git checkout -- "$@"; return $?; }
local cmd files opts
cmd="git diff --color=always -- {} | $_forgit_diff_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
-m -0
--preview=\"$cmd\"
$FORGIT_CHECKOUT_FILE_FZF_OPTS
"
files="$(git ls-files --modified "$(git rev-parse --show-toplevel)"| FZF_DEFAULT_OPTS="$opts" fzf)"
[[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | xargs -0 -I% git checkout %
}
# git checkout-branch selector
_forgit_checkout_branch() {
_forgit_inside_work_tree || return 1
# if called with arguments, check if branch exists, else create a new one
if [[ $# -ne 0 ]]; then
if git show-branch "$@" &>/dev/null; then
git switch "$@"
else
git switch -c "$@"
fi
checkout_status=$?
git status --short
return $checkout_status
fi
local cmd preview opts branch
cmd="git branch --color=always --all | LC_ALL=C sort -k1.1,1.1 -rs"
preview="git log {1} $_forgit_log_preview_options"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index --header-lines=1
--preview=\"$preview\"
$FORGIT_CHECKOUT_BRANCH_FZF_OPTS
"
branch="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')"
[[ -z "$branch" ]] && return 1
# track the remote branch if possible
if [[ "$branch" == "remotes/origin/"* ]]; then
if git branch | grep -qw "${branch#remotes/origin/}"; then
# hack to force creating a new branch which tracks the remote if a local branch already exists
git checkout -b "track/${branch#remotes/origin/}" --track "$branch"
elif ! git checkout --track "$branch" 2>/dev/null; then
git checkout "$branch"
fi
else
git checkout "$branch"
fi
}
# git checkout-tag selector
_forgit_checkout_tag() {
_forgit_inside_work_tree || return 1
[[ $# -ne 0 ]] && { git checkout "$@"; return $?; }
local cmd opts preview
cmd="git tag -l --sort=-v:refname"
preview="git log {1} $_forgit_log_preview_options"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--preview=\"$preview\"
$FORGIT_CHECKOUT_TAG_FZF_OPTS
"
tag="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf)"
[[ -z "$tag" ]] && return 1
git checkout "$tag"
}
# git checkout-commit selector
_forgit_checkout_commit() {
_forgit_inside_work_tree || return 1
[[ $# -ne 0 ]] && { git checkout "$@"; return $?; }
local cmd opts graph
cmd="echo {} | $_forgit_extract_sha |xargs -I% git show --color=always % | $_forgit_show_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
--preview=\"$cmd\"
$FORGIT_COMMIT_FZF_OPTS
"
graph=--graph
[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
eval "git log $graph --color=always --format='$_forgit_log_format' $_forgit_emojify" |
FZF_DEFAULT_OPTS="$opts" fzf | eval "$_forgit_extract_sha" | xargs -I% git checkout % --
}
_forgit_branch_delete() {
_forgit_inside_work_tree || return 1
local preview opts cmd branches
preview="git log {1} $_forgit_log_preview_options"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s --multi --tiebreak=index --header-lines=1
--preview=\"$preview\"
$FORGIT_BRANCH_DELETE_FZF_OPTS
"
cmd="git branch --color=always | LC_ALL=C sort -k1.1,1.1 -rs"
branches=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')
echo -n "$branches" | tr '\n' '\0' | xargs -I{} -0 git branch -D {}
}
# git revert-commit selector
_forgit_revert_commit() {
_forgit_inside_work_tree || return 1
[[ $# -ne 0 ]] && { git revert "$@"; return $?; }
local cmd opts files preview commits IFS
cmd="git log --graph --color=always --format='$_forgit_log_format' $* $_forgit_emojify"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s --tiebreak=index
$FORGIT_REVERT_COMMIT_OPTS
"
files=$(sed -nE 's/.* -- (.*)/\1/p' <<< "$*") # extract files parameters for `git show` command
preview="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always % -- $files | $_forgit_show_pager"
# shellcheck disable=2207
IFS=$'\n' commits=($(eval "$cmd" |
FZF_DEFAULT_OPTS="$opts" fzf --preview="$preview" -m |
sed 's/^[^a-f^0-9]*\([a-f0-9]*\).*/\1/'))
[ ${#commits[@]} -eq 0 ] && return 1
for commit in "${commits[@]}"; do
git revert "$commit"
done
}
# git blame viewer
_forgit_blame() {
_forgit_inside_work_tree || return 1
[[ $# -ne 0 ]] && git blame "$@" && return 0
local opts flags preview file
opts="
$FORGIT_FZF_DEFAULT_OPTS
$FORGIT_BLAME_FZF_OPTS
"
flags=$(git rev-parse --flags "$@")
preview="
if (git ls-files {} --error-unmatch) &>/dev/null; then
git blame {} --date=short $flags | $_forgit_blame_pager
else
echo File not tracked
fi
"
file=$(FZF_DEFAULT_OPTS="$opts" fzf --preview="$preview")
[[ -z "$file" ]] && return 1
eval git blame "$file" "$flags"
}
# git ignore generator
export FORGIT_GI_REPO_REMOTE=${FORGIT_GI_REPO_REMOTE:-https://github.com/dvcs/gitignore}
export FORGIT_GI_REPO_LOCAL="${FORGIT_GI_REPO_LOCAL:-${XDG_CACHE_HOME:-$HOME/.cache}/forgit/gi/repos/dvcs/gitignore}"
export FORGIT_GI_TEMPLATES=${FORGIT_GI_TEMPLATES:-$FORGIT_GI_REPO_LOCAL/templates}
_forgit_ignore() {
[ -d "$FORGIT_GI_REPO_LOCAL" ] || _forgit_ignore_update
local IFS cmd args opts
cmd="$_forgit_ignore_pager $FORGIT_GI_TEMPLATES/{2}{,.gitignore} 2>/dev/null"
opts="
$FORGIT_FZF_DEFAULT_OPTS
-m --preview-window='right:70%'
--preview=\"eval $cmd\"
$FORGIT_IGNORE_FZF_OPTS
"
# shellcheck disable=SC2206,2207
IFS=$'\n' args=($@) && [[ $# -eq 0 ]] && args=($(_forgit_ignore_list | nl -nrn -w4 -s' ' |
FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $2}'))
[ ${#args[@]} -eq 0 ] && return 1
# shellcheck disable=SC2068
_forgit_ignore_get ${args[@]}
}
_forgit_ignore_update() {
if [[ -d "$FORGIT_GI_REPO_LOCAL" ]]; then
_forgit_info 'Updating gitignore repo...'
(cd "$FORGIT_GI_REPO_LOCAL" && git pull --no-rebase --ff) || return 1
else
_forgit_info 'Initializing gitignore repo...'
git clone --depth=1 "$FORGIT_GI_REPO_REMOTE" "$FORGIT_GI_REPO_LOCAL"
fi
}
_forgit_ignore_get() {
local item filename header
for item in "$@"; do
if filename=$(find -L "$FORGIT_GI_TEMPLATES" -type f \( -iname "${item}.gitignore" -o -iname "${item}" \) -print -quit); then
[[ -z "$filename" ]] && _forgit_warn "No gitignore template found for '$item'." && continue
header="${filename##*/}" && header="${header%.gitignore}"
echo "### $header" && cat "$filename" && echo
fi
done
}
_forgit_ignore_list() {
find "$FORGIT_GI_TEMPLATES" -print |sed -e 's#.gitignore$##' -e 's#.*/##' | sort -fu
}
_forgit_ignore_clean() {
setopt localoptions rmstarsilent
[[ -d "$FORGIT_GI_REPO_LOCAL" ]] && rm -rf "$FORGIT_GI_REPO_LOCAL"
}
valid_commands=(
"add"
"blame"
"branch_delete"
"checkout_branch"
"checkout_commit"
"checkout_file"
"checkout_tag"
"cherry_pick"
"cherry_pick_from_branch"
"clean"
"diff"
"ignore"
"log"
"rebase fixup"
"reset_head"
"revert_commit"
"stash_show"
)
cmd="$1"
shift
forgit::${cmd//_/::} "$@"
# shellcheck disable=SC2076
if [[ ! " ${valid_commands[*]} " =~ " ${cmd} " ]]; then
if [[ -z "$cmd" ]]; then
printf "forgit: missing command\n\n"
else
printf "forgit: '%s' is not a valid forgit command.\n\n" "$cmd"
fi
printf "The following commands are supported:\n"
printf "\t%s\n" "${valid_commands[@]}"
exit 1
fi
_forgit_"${cmd}" "$@"

1
conf.d/bin/git-forgit Symbolic link
View file

@ -0,0 +1 @@
../../bin/git-forgit

View file

@ -1,640 +1,115 @@
# MIT (c) Chris Apple
set INSTALL_DIR (dirname (dirname (status -f)))
set FORGIT "$INSTALL_DIR/conf.d/bin/git-forgit"
function forgit::warn
printf "%b[Warn]%b %s\n" '\e[0;33m' '\e[0m' "$argv" >&2;
end
function forgit::info
printf "%b[Info]%b %s\n" '\e[0;32m' '\e[0m' "$argv" >&2;
end
function forgit::inside_work_tree
git rev-parse --is-inside-work-tree >/dev/null;
end
function forgit::reverse_lines
# tac is not available on OSX, tail -r is not available on Linux, so we use either of them
if command -v tac &> /dev/null
tac
else
tail -r
# backwards compatibility:
# export all user-defined FORGIT variables to make them available in git-forgit
set unexported_vars 0
set | awk -F ' ' '{ print $1 }' | grep FORGIT_ | while read var
if ! set -x | grep -q "^$var "
if test $unexported_vars = 0
forgit::warn "Config options have to be exported in future versions of forgit."
forgit::warn "Please update your config accordingly:"
end
forgit::warn " set -x $var \"$$var\""
set unexported_vars (math $unexported_vars + 1)
set -x $var $$var
end
end
function forgit::previous_commit
# "SHA~" is invalid when the commit is the first commit, but we can use "--root" instead
if test (git rev-parse $argv) = (git rev-list --max-parents=0 HEAD)
echo "--root"
else
echo "$argv~"
end
end
# extract the first git sha occurring in the input and strip trailing newline
set -g forgit_extract_sha "grep -Eo '[a-f0-9]+' | head -1 | tr -d '[:space:]'"
set -g forgit_pager "$FORGIT_PAGER"
set -g forgit_show_pager "$FORGIT_SHOW_PAGER"
set -g forgit_diff_pager "$FORGIT_DIFF_PAGER"
set -g forgit_ignore_pager "$FORGIT_IGNORE_PAGER"
set -g forgit_enter_pager "$FORGIT_ENTER_PAGER"
set -g forgit_log_format "$FORGIT_LOG_FORMAT"
set -x FORGIT_INSTALL_DIR (dirname (dirname (status -f)))
test -z "$forgit_pager"; and set -g forgit_pager (git config core.pager || echo 'cat')
test -z "$forgit_show_pager"; and set -g forgit_show_pager (git config pager.show || echo "$forgit_pager")
test -z "$forgit_diff_pager"; and set -g forgit_diff_pager (git config pager.diff || echo "$forgit_pager")
test -z "$forgit_ignore_pager"; and set -g forgit_ignore_pager (type -q bat >/dev/null 2>&1 && echo 'bat -l gitignore --color=always' || echo 'cat')
test -z "$forgit_enter_pager"; and set -g forgit_enter_pager "env LESS='-r' less"
test -z "$forgit_log_format"; and set -g forgit_log_format "-%C(auto)%h%d %s %C(black)%C(bold)%cr%Creset"
test -z "$forgit_fullscreen_context"; and set -g forgit_fullscreen_context "10"
test -z "$forgit_preview_context"; and set -g forgit_preview_context "3"
set -g forgit_log_preview_options "--graph --pretty=format:'$forgit_log_format' --color=always --abbrev-commit --date=relative"
# optional render emoji characters (https://github.com/wfxr/emoji-cli)
type -q emojify >/dev/null 2>&1 && set -g forgit_emojify '|emojify'
# git commit viewer
function forgit::log -d "git commit viewer"
forgit::inside_work_tree || return 1
set files (echo $argv | sed -nE 's/.* -- (.*)/\1/p')
set preview_cmd "echo {} | $forgit_extract_sha | xargs -I% git show --color=always -U$forgit_preview_context % -- $files | $forgit_show_pager"
set enter_cmd "echo {} | $forgit_extract_sha | xargs -I% git show --color=always -U$forgit_fullscreen_context % -- $files | $forgit_show_pager"
if test -n "$FORGIT_COPY_CMD"
set copy_cmd $FORGIT_COPY_CMD
else
set copy_cmd pbcopy
end
set opts "
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"enter:execute($enter_cmd | $forgit_enter_pager)\"
--bind=\"ctrl-y:execute-silent(echo {} | $forgit_extract_sha | $copy_cmd)\"
--preview=\"$preview_cmd\"
$FORGIT_LOG_FZF_OPTS
"
if test -n "$FORGIT_GLO_FORMAT"
set log_format "$FORGIT_GLO_FORMAT"
else
set log_format "$forgit_log_format"
end
if set -q FORGIT_LOG_GRAPH_ENABLE
set graph "--graph"
else
set graph ""
end
eval "git log $graph --color=always --format='$log_format' $argv $forgit_emojify" |
env FZF_DEFAULT_OPTS="$opts" fzf
set fzf_exit_code $status
# exit successfully on 130 (ctrl-c/esc)
[ $fzf_exit_code = 130 ] && return 0
return $fzf_exit_code
"$FORGIT" log $argv
end
function forgit::extract_file --argument-names 'path'
set no_leading_whitespace (echo $path | sed 's/^[[:space:]]*//')
set no_m_or_double_question (echo $no_leading_whitespace | cut -d ' ' -f 2-)
set if_renamed (echo $no_m_or_double_question | sed 's/.* -> //')
set no_quotes (echo $if_renamed | tr -d "\"")
set no_leading_whitespace_again (echo $no_quotes | sed 's/^[[:space:]]*//')
set no_spaces (echo $no_leading_whitespace_again | string escape --no-quoted)
echo $no_spaces
end
## git diff viewer
function forgit::diff -d "git diff viewer" --argument-names arg1 arg2
forgit::inside_work_tree || return 1
if test -n "$arg1"
# If this first arg is a commit hash
if git rev-parse "$arg1" > /dev/null 2>&1
if git rev-parse "$arg2" > /dev/null 2>&1
set commits "$arg1 $arg2" && set files "$argv[3..-1]"
else
set commits "$arg1" && set files "$argv[2..-1]"
end
else
set files "$argv"
end
end
# Git stashes are named "stash@{x}", which contains the fzf placeholder "{x}".
# In order to support passing stashes as arguments to forgit::diff, we have to
# prevent fzf from interpreting this substring by escaping the opening bracket.
# The string is evaluated a few subsequent times, so we need multiple escapes.
set escaped_commits (echo $commits | sed 's/{/\\\\\\\\{/g')
set preview_cmd "forgit::extract_file {} | xargs -I% git diff --color=always -U$forgit_preview_context $escaped_commits -- % | $forgit_diff_pager"
set enter_cmd "forgit::extract_file {} | xargs -I% git diff --color=always -U$forgit_fullscreen_context $escaped_commits -- % | $forgit_diff_pager"
set opts "
$FORGIT_FZF_DEFAULT_OPTS
+m -0 --bind=\"enter:execute($enter_cmd | $forgit_enter_pager)\"
--preview=\"$preview_cmd\"
$FORGIT_DIFF_FZF_OPTS
--prompt=\"$commits > \"
"
eval git diff --name-status $commits -- $files* |
sed -E 's/^([[:alnum:]]+)[[:space:]]+(.*)\$/[\1]\t\2/' |
sed ' s/\t/ -> /2' |
expand -t 8 |
env FZF_DEFAULT_OPTS="$opts" fzf
set fzf_exit_code $status
# exit successfully on 130 (ctrl-c/esc)
[ $fzf_exit_code = 130 ] && return 0
return $fzf_exit_code
"$FORGIT" diff $argv
end
# git add selector
function forgit::add -d "git add selector" --wraps "git add"
forgit::inside_work_tree || return 1
# Add files if passed as arguments
count $argv >/dev/null && git add "$argv" && git status --short && return
set changed (git config --get-color color.status.changed red)
set unmerged (git config --get-color color.status.unmerged red)
set untracked (git config --get-color color.status.untracked red)
set preview "
set file (forgit::extract_file {})
# exit
if test (git status -s -- \$file | grep '^??') # diff with /dev/null for untracked files
git diff --color=always --no-index -- /dev/null \$file | $forgit_diff_pager | sed '2 s/added:/untracked:/'
else
git diff --color=always -- \$file | $forgit_diff_pager
end
"
set opts "
$FORGIT_FZF_DEFAULT_OPTS
-0 -m --nth 2..,..
--preview=\"$preview\"
$FORGIT_ADD_FZF_OPTS
"
set files (git -c color.status=always -c status.relativePaths=true status -su |
grep -F -e "$changed" -e "$unmerged" -e "$untracked" |
sed -E 's/^(..[^[:space:]]*)[[:space:]]+(.*)\$/[\1] \2/' | # deal with white spaces internal to fname
env FZF_DEFAULT_OPTS="$opts" fzf
)
if test -n "$files"
for file in $files
set stripped_file (forgit::extract_file $file)
echo $stripped_file | tr '\n' '\0' | xargs -I{} -0 git add {}
end
git status --short
return
end
echo 'Nothing to add.'
"$FORGIT" add $argv
end
## git reset HEAD (unstage) selector
function forgit::reset::head -d "git reset HEAD (unstage) selector"
forgit::inside_work_tree || return 1
set preview "git diff --cached --color=always -- {} | $forgit_diff_pager"
set opts "
$FORGIT_FZF_DEFAULT_OPTS
-m -0
--preview=\"$preview\"
$FORGIT_RESET_HEAD_FZF_OPTS
"
set files (git diff --cached --name-only --relative | env FZF_DEFAULT_OPTS="$opts" fzf)
if test -n "$files"
for file in $files
echo $file | tr '\n' '\0' |xargs -I{} -0 git reset -q HEAD {}
end
git status --short
return
end
echo 'Nothing to unstage.'
"$FORGIT" reset_head $argv
end
# git checkout-restore selector
function forgit::checkout::file -d "git checkout-file selector" --argument-names 'file_name' --wraps "git checkout --"
forgit::inside_work_tree || return 1
if test -n "$file_name"
git checkout -- "$file_name"
set checkout_status $status
git status --short
return $checkout_status
end
set preview "git diff --color=always -- {} | $forgit_diff_pager"
set opts "
$FORGIT_FZF_DEFAULT_OPTS
-m -0
--preview=\"$preview\"
$FORGIT_CHECKOUT_FILE_FZF_OPTS
"
set git_rev_parse (git rev-parse --show-toplevel)
set files (git ls-files --modified "$git_rev_parse" | env FZF_DEFAULT_OPTS="$opts" fzf)
if test -n "$files"
for file in $files
echo $file | tr '\n' '\0' | xargs -I{} -0 git checkout -q {}
end
git status --short
return
end
echo 'Nothing to restore.'
end
function forgit::checkout::commit -d "git checkout commit selector" --argument-names 'commit_id' --wraps "git checkout"
forgit::inside_work_tree || return 1
if test -n "$commit_id"
git checkout "$commit_id"
set checkout_status $status
git status --short
return $checkout_status
end
if test -n "$FORGIT_COPY_CMD"
set copy_cmd $FORGIT_COPY_CMD
else
set copy_cmd pbcopy
end
set preview "echo {} | $forgit_extract_sha | xargs -I% git show --color=always % | $forgit_show_pager"
set opts "
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"ctrl-y:execute-silent(echo {} | $forgit_extract_sha | $copy_cmd)\"
--preview=\"$preview\"
$FORGIT_COMMIT_FZF_OPTS
"
if set -q FORGIT_LOG_GRAPH_ENABLE
set graph "--graph"
else
set graph ""
end
eval "git log $graph --color=always --format='$forgit_log_format' $forgit_emojify" |
FZF_DEFAULT_OPTS="$opts" fzf | eval "$forgit_extract_sha" | xargs -I% git checkout % --
end
function forgit::branch::delete -d "git checkout branch deleter" --wraps "git branch --delete"
forgit::inside_work_tree || return 1
set preview "git log {1} $forgit_log_preview_options"
set opts "
$FORGIT_FZF_DEFAULT_OPTS
+s --multi --tiebreak=index --header-lines=1
--preview=\"$preview\"
$FORGIT_BRANCH_DELETE_FZF_OPTS
"
set cmd "git branch --color=always | LC_ALL=C sort -k1.1,1.1 -rs"
set branches (eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')
if test -n "$branches"
for branch in $branches
echo $branch | tr '\n' '\0' | xargs -I{} -0 git branch -D {}
end
git status --short
return
end
end
function forgit::checkout::branch -d "git checkout branch selector" --argument-names 'input_branch_name' --wraps "git branch"
forgit::inside_work_tree || return 1
if test -n "$input_branch_name"
if git branch --list | grep "$input_branch_name" > /dev/null
git switch "$input_branch_name"
else
git switch -c "$input_branch_name"
end
set checkout_status $status
git status --short
return $checkout_status
end
set preview "git log {1} $forgit_log_preview_options"
set opts "
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index --header-lines=1
--preview=\"$preview\"
$FORGIT_CHECKOUT_BRANCH_FZF_OPTS
"
set cmd "git branch --color=always --all | LC_ALL=C sort -k1.1,1.1 -rs"
set branch (eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')
test -z "$branch" && return 1
# track the remote branch if possible
if not git checkout --track "$branch" 2>/dev/null
git checkout "$branch"
end
end
# git stash viewer
function forgit::stash::show -d "git stash viewer"
forgit::inside_work_tree || return 1
set preview "echo {} |cut -d: -f1 |xargs -I% git stash show --color=always --ext-diff % |$forgit_diff_pager"
set opts "
$FORGIT_FZF_DEFAULT_OPTS
+s +m -0 --tiebreak=index --bind=\"enter:execute($preview | $forgit_enter_pager)\"
--preview=\"$preview\"
$FORGIT_STASH_FZF_OPTS
"
git stash list | env FZF_DEFAULT_OPTS="$opts" fzf
set fzf_exit_code $status
# exit successfully on 130 (ctrl-c/esc)
[ $fzf_exit_code = 130 ] && return 0
return $fzf_exit_code
"$FORGIT" stash_show $argv
end
# git clean selector
function forgit::clean -d "git clean selector"
forgit::inside_work_tree || return 1
set opts "
$FORGIT_FZF_DEFAULT_OPTS
-m -0
$FORGIT_CLEAN_FZF_OPTS
"
set files (git clean -xdffn $argv| awk '{print $3}'| env FZF_DEFAULT_OPTS="$opts" fzf |sed 's#/$##')
if test -n "$files"
for file in $files
echo $file | tr '\n' '\0'| xargs -0 -I{} git clean -xdff {}
end
git status --short
return
end
echo 'Nothing to clean.'
"$FORGIT" clean $argv
end
function forgit::cherry::pick -d "git cherry-picking" --argument-names 'target' --wraps "git cherry-pick"
forgit::inside_work_tree || return 1
set base (git branch --show-current)
if test -z "$target"
echo "Please specify target branch"
return 1
end
set preview "echo {} | $forgit_extract_sha | xargs -I% git show --color=always % | $forgit_show_pager"
set opts "
--preview=\"$preview\"
$FORGIT_FZF_DEFAULT_OPTS
-m -0 --tiebreak=index
$FORGIT_CHERRY_PICK_FZF_OPTS
"
set fzf_selection (git cherry "$base" "$target" --abbrev -v | forgit::reverse_lines |
env FZF_DEFAULT_OPTS="$opts" fzf)
set fzf_exitval $status
test $fzf_exitval != 0 && return $fzf_exitval
set commits (echo "$fzf_selection" | forgit::reverse_lines | cut -d' ' -f2)
git cherry-pick $commits
"$FORGIT" cherry_pick $argv
end
function forgit::cherry::pick::from::branch -d "git cherry-picking with interactive branch selection"
forgit::inside_work_tree || return 1
"$FORGIT" cherry_pick_from_branch $argv
end
set preview "git log {1} $forgit_log_preview_options"
set opts "
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index --header-lines=1
--preview=\"$preview\"
$FORGIT_CHERRY_PICK_FROM_BRANCH_FZF_OPTS
"
set cmd "git branch --color=always --all | LC_ALL=C sort -k1.1,1.1 -rs"
# loop until either the branch selector is closed or a commit to be cherry
# picked has been selected from within a branch
while true
set branch (eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')
test -z "$branch" && return 1
forgit::cherry::pick "$branch"
set exitval $status
test $exitval != 130 && return $exitval
end
function forgit::rebase -d "git rebase"
"$FORGIT" rebase $argv
end
function forgit::fixup -d "git fixup"
forgit::inside_work_tree || return 1
git diff --cached --quiet && echo 'Nothing to fixup: there are no staged changes.' && return 1
if set -q FORGIT_LOG_GRAPH_ENABLE
set graph "--graph"
else
set graph ""
end
set files (echo $argv | sed -nE 's/.* -- (.*)/\1/p')
set preview "echo {} | $forgit_extract_sha | xargs -I% git show --color=always % -- $files | $forgit_show_pager"
if test -n "$FORGIT_COPY_CMD"
set copy_cmd $FORGIT_COPY_CMD
else
set copy_cmd pbcopy
end
set opts "
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"ctrl-y:execute-silent(echo {} | $forgit_extract_sha | $copy_cmd)\"
--preview=\"$preview\"
$FORGIT_FIXUP_FZF_OPTS
"
set cmd "git log $graph --color=always --format='$forgit_log_format' $argv $forgit_emojify"
set target_commit (eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | eval "$forgit_extract_sha")
if test -n "$target_commit" && git commit --fixup "$target_commit"
set prev_commit (forgit::previous_commit $target_commit)
GIT_SEQUENCE_EDITOR=: git rebase --autostash -i --autosquash "$prev_commit"
end
"$FORGIT" fixup $argv
end
function forgit::rebase -d "git rebase"
forgit::inside_work_tree || return 1
if set -q FORGIT_LOG_GRAPH_ENABLE
set graph "--graph"
else
set graph ""
end
set cmd "git log $graph --color=always --format='$forgit_log_format' $argv $forgit_emojify"
set files (echo $argv | sed -nE 's/.* -- (.*)/\1/p')
set preview "echo {} | $forgit_extract_sha | xargs -I% git show --color=always % -- $files | $forgit_show_pager"
if test -n "$FORGIT_COPY_CMD"
set copy_cmd $FORGIT_COPY_CMD
else
set copy_cmd pbcopy
end
set opts "
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"ctrl-y:execute-silent(echo {} | $forgit_extract_sha | $copy_cmd)\"
--preview=\"$preview\"
$FORGIT_REBASE_FZF_OPTS
"
set target_commit (eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | eval "$forgit_extract_sha")
if test $target_commit
set prev_commit (forgit::previous_commit $target_commit)
git rebase -i "$prev_commit"
end
function forgit::checkout::file -d "git checkout-file selector" --argument-names 'file_name' --wraps "git checkout --"
"$FORGIT" checkout_file $argv
end
# git ignore generator
if test -z "$FORGIT_GI_REPO_REMOTE"
set -g FORGIT_GI_REPO_REMOTE https://github.com/dvcs/gitignore
function forgit::checkout::branch -d "git checkout branch selector" --argument-names 'input_branch_name' --wraps "git branch"
"$FORGIT" checkout_branch $argv
end
if test -z "$FORGIT_GI_REPO_LOCAL"
if test -z "XDG_CACHE_HOME"
set -g FORGIT_GI_REPO_LOCAL $XDG_CACHE_HOME/forgit/gi/repos/dvcs/gitignore
else
set -g FORGIT_GI_REPO_LOCAL $HOME/.cache/forgit/gi/repos/dvcs/gitignore
end
function forgit::checkout::tag -d "git checkout tag selector" --argument-names 'tag_name' --wraps "git checkout"
"$FORGIT" checkout_tag $argv
end
if test -z "$FORGIT_GI_TEMPLATES"
set -g FORGIT_GI_TEMPLATES $FORGIT_GI_REPO_LOCAL/templates
function forgit::checkout::commit -d "git checkout commit selector" --argument-names 'commit_id' --wraps "git checkout"
"$FORGIT" checkout_commit $argv
end
function forgit::ignore -d "git ignore generator"
if not test -d "$FORGIT_GI_REPO_LOCAL"
forgit::ignore::update
end
set preview "$forgit_ignore_pager $FORGIT_GI_TEMPLATES/{2}{,.gitignore} 2>/dev/null"
set opts "
$FORGIT_FZF_DEFAULT_OPTS
-m --preview-window='right:70%'
--preview=\"$preview\"
$FORGIT_IGNORE_FZF_OPTS
"
set IFS '\n'
set args $argv
if not count $argv > /dev/null
set args (forgit::ignore::list | nl -nrn -w4 -s' ' |
env FZF_DEFAULT_OPTS="$opts" fzf |awk '{print $2}')
end
if not count $args > /dev/null
return 1
end
forgit::ignore::get $args
function forgit::branch::delete -d "git checkout branch deleter" --wraps "git branch --delete"
"$FORGIT" branch_delete $argv
end
function forgit::revert::commit --argument-names 'commit_hash' --wraps "git revert --"
if test -n "$commit_hash"
git revert -- "$commit_hash"
set revert_status $status
git status --short
return $revert_status
end
"$FORGIT" revert_commit $argv
end
set preview "echo {} | $forgit_extract_sha | xargs -I% git show --color=always % -- $files | $forgit_show_pager"
set opts "
$FORGIT_FZF_DEFAULT_OPTS
+s --tiebreak=index
--preview=\"$preview\"
$FORGIT_REVERT_COMMIT_OPTS
"
function forgit::blame
"$FORGIT" blame $argv
end
set files (echo $argv | sed -nE 's/.* -- (.*)/\1/p') # extract files parameters for `git show` command
set cmd "git log --graph --color=always --format='$forgit_log_format' $argv $forgit_emojify"
set commits (eval $cmd |
FZF_DEFAULT_OPTS="$opts" fzf -m |
string match -r "[a-f0-9]+")
if test -z "$commits"
return 1
end
git revert $commits
function forgit::ignore -d "git ignore generator"
"$FORGIT" ignore $argv
end
function forgit::ignore::update
if test -d "$FORGIT_GI_REPO_LOCAL"
forgit::info 'Updating gitignore repo...'
set pull_result (git -C "$FORGIT_GI_REPO_LOCAL" pull --no-rebase --ff)
test -n "$pull_result" || return 1
else
forgit::info 'Initializing gitignore repo...'
git clone --depth=1 "$FORGIT_GI_REPO_REMOTE" "$FORGIT_GI_REPO_LOCAL"
end
"$FORGIT" ignore_update $argv
end
function forgit::ignore::get
for item in $argv
set filename (find -L "$FORGIT_GI_TEMPLATES" -type f \( -iname "$item.gitignore" -o -iname "$item}" \) -print -quit)
if test -n "$filename"
set header $filename && set header (echo $filename | sed 's/.*\.//')
echo "### $header" && cat "$filename" && echo
else
forgit::warn "No gitignore template found for '$item'." && continue
end
end
"$FORGIT" ignore_get $argv
end
function forgit::ignore::list
find "$FORGIT_GI_TEMPLATES" -print | sed -e 's#.gitignore$##' -e 's#.*/##' | sort -fu
"$FORGIT" ignore_list $argv
end
function forgit::ignore::clean
if test -d "$FORGIT_GI_REPO_LOCAL"
rm -rf "$FORGIT_GI_REPO_LOCAL"
end
"$FORGIT" ignore_clean $argv
end
set -g FORGIT_FZF_DEFAULT_OPTS "
$FZF_DEFAULT_OPTS
--ansi
--height='80%'
--bind='alt-k:preview-up,alt-p:preview-up'
--bind='alt-j:preview-down,alt-n:preview-down'
--bind='ctrl-r:toggle-all'
--bind='ctrl-s:toggle-sort'
--bind='?:toggle-preview'
--bind='alt-w:toggle-preview-wrap'
--preview-window='right:60%'
+1
$FORGIT_FZF_DEFAULT_OPTS
"
# register aliases
if test -z "$FORGIT_NO_ALIASES"
if test -n "$forgit_add"

View file

@ -1,510 +1,127 @@
#!/usr/bin/env bash
# MIT (c) Wenxuan Zhang
forgit::error() { printf "%b[Error]%b %s\n" '\e[0;31m' '\e[0m' "$@" >&2; return 1; }
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_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; }
forgit::previous_commit() {
# "SHA~" is invalid when the commit is the first commit, but we can use "--root" instead
if [[ "$(git rev-parse "$1")" == "$(git rev-list --max-parents=0 HEAD)" ]]; then
echo "--root"
else
echo "$1~"
fi
}
# determine installation path
if [[ -n "$ZSH_VERSION" ]]; then
# shellcheck disable=2277,2296,2299
0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}"
# shellcheck disable=2277,2296,2298
0="${${(M)0:#/*}:-$PWD/$0}"
INSTALL_DIR="${0:h}"
elif [[ -n "$BASH_VERSION" ]]; then
INSTALL_DIR="$(dirname -- "${BASH_SOURCE[0]}")"
else
forgit::error "Only zsh and bash are supported"
fi
FORGIT="$INSTALL_DIR/bin/git-forgit"
# optional render emoji characters (https://github.com/wfxr/emoji-cli)
hash emojify &>/dev/null && forgit_emojify='|emojify'
# extract the first git sha occurring in the input and strip trailing newline
forgit_extract_sha="grep -Eo '[a-f0-9]+' | head -1 | tr -d '[:space:]'"
forgit_pager=${FORGIT_PAGER:-$(git config core.pager || echo 'cat')}
forgit_show_pager=${FORGIT_SHOW_PAGER:-$(git config pager.show || echo "$forgit_pager")}
forgit_diff_pager=${FORGIT_DIFF_PAGER:-$(git config pager.diff || echo "$forgit_pager")}
forgit_ignore_pager=${FORGIT_IGNORE_PAGER:-$(hash bat &>/dev/null && echo 'bat -l gitignore --color=always' || echo 'cat')}
forgit_blame_pager=${FORGIT_BLAME_PAGER:-$(git config pager.blame || echo "$forgit_pager")}
forgit_enter_pager=${FORGIT_ENTER_PAGER:-"LESS='-r' less"}
forgit_log_format=${FORGIT_LOG_FORMAT:-%C(auto)%h%d %s %C(black)%C(bold)%cr%Creset}
forgit_log_preview_options="--graph --pretty=format:'$forgit_log_format' --color=always --abbrev-commit --date=relative"
forgit_fullscreen_context=${FORGIT_FULLSCREEN_CONTEXT:-10}
forgit_preview_context=${FORGIT_PREVIEW_CONTEXT:-3}
# git commit viewer
forgit::log() {
forgit::inside_work_tree || return 1
local opts graph files log_format preview_cmd enter_cmd
files=$(sed -nE 's/.* -- (.*)/\1/p' <<< "$*") # extract files parameters for `git show` command
preview_cmd="echo {} | $forgit_extract_sha | xargs -I% git show --color=always -U$forgit_preview_context % -- $files | $forgit_show_pager"
enter_cmd="echo {} | $forgit_extract_sha | xargs -I% git show --color=always -U$forgit_fullscreen_context % -- $files | $forgit_show_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"enter:execute($enter_cmd | $forgit_enter_pager)\"
--bind=\"ctrl-y:execute-silent(echo {} | $forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
--preview=\"$preview_cmd\"
$FORGIT_LOG_FZF_OPTS
"
graph=--graph
[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
log_format=${FORGIT_GLO_FORMAT:-$forgit_log_format}
eval "git log $graph --color=always --format='$log_format' $* $forgit_emojify" |
FZF_DEFAULT_OPTS="$opts" fzf
fzf_exit_code=$?
# exit successfully on 130 (ctrl-c/esc)
[[ $fzf_exit_code == 130 ]] && return 0
return $fzf_exit_code
}
# git diff viewer
forgit::diff() {
forgit::inside_work_tree || return 1
local files opts commits repo get_files preview_cmd enter_cmd
[[ $# -ne 0 ]] && {
if git rev-parse "$1" -- &>/dev/null ; then
if [[ $# -gt 1 ]] && git rev-parse "$2" -- &>/dev/null; then
commits="$1 $2" && files=("${@:3}")
else
commits="$1" && files=("${@:2}")
fi
else
files=("$@")
# backwards compatibility:
# export all user-defined FORGIT variables to make them available in git-forgit
unexported_vars=0
set | awk -F '=' '{ print $1 }' | grep FORGIT_ | while read -r var; do
if ! export | grep -q "^$var="; then
if [[ $unexported_vars == 0 ]]; then
forgit::warn "Config options have to be exported in future versions of forgit."
forgit::warn "Please update your config accordingly:"
fi
}
repo="$(git rev-parse --show-toplevel)"
get_files="cd '$repo' && echo {} | sed 's/.*] *//' | sed 's/ -> / /'"
# Git stashes are named "stash@{x}", which contains the fzf placeholder "{x}".
# In order to support passing stashes as arguments to forgit::diff, we have to
# prevent fzf from interpreting this substring by escaping the opening bracket.
# The string is evaluated a few subsequent times, so we need multiple escapes.
escaped_commits=${commits//\{/\\\\\{}
preview_cmd="$get_files | xargs -I% git diff --color=always -U$forgit_preview_context $escaped_commits -- % | $forgit_diff_pager"
enter_cmd="$get_files | xargs -I% git diff --color=always -U$forgit_fullscreen_context $escaped_commits -- % | $forgit_diff_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+m -0 --bind=\"enter:execute($enter_cmd | $forgit_enter_pager)\"
--preview=\"$preview_cmd\"
$FORGIT_DIFF_FZF_OPTS
--prompt=\"$commits > \"
"
eval "git diff --name-status $commits -- ${files[*]} | sed -E 's/^([[:alnum:]]+)[[:space:]]+(.*)$/[\1]\t\2/'" |
sed 's/\t/ -> /2' | expand -t 8 |
FZF_DEFAULT_OPTS="$opts" fzf
fzf_exit_code=$?
# exit successfully on 130 (ctrl-c/esc)
[[ $fzf_exit_code == 130 ]] && return 0
return $fzf_exit_code
forgit::warn " export $var"
unexported_vars=$((unexported_vars + 1))
# shellcheck disable=SC2163
export "$var"
fi
done
# register shell functions
forgit::log() {
"$FORGIT" log "$@"
}
forgit::diff() {
"$FORGIT" diff "$@"
}
# git add selector
forgit::add() {
forgit::inside_work_tree || return 1
# Add files if passed as arguments
[[ $# -ne 0 ]] && git add "$@" && git status -su && return
local changed unmerged untracked files opts preview extract
changed=$(git config --get-color color.status.changed red)
unmerged=$(git config --get-color color.status.unmerged red)
untracked=$(git config --get-color color.status.untracked red)
# NOTE: paths listed by 'git status -su' mixed with quoted and unquoted style
# remove indicators | remove original path for rename case | remove surrounding quotes
extract="
sed 's/^.*] //' |
sed 's/.* -> //' |
sed -e 's/^\\\"//' -e 's/\\\"\$//'"
preview="
file=\$(echo {} | $extract)
if (git status -s -- \$file | grep '^??') &>/dev/null; then # diff with /dev/null for untracked files
git diff --color=always --no-index -- /dev/null \$file | $forgit_diff_pager | sed '2 s/added:/untracked:/'
else
git diff --color=always -- \$file | $forgit_diff_pager
fi"
opts="
$FORGIT_FZF_DEFAULT_OPTS
-0 -m --nth 2..,..
--preview=\"$preview\"
$FORGIT_ADD_FZF_OPTS
"
files=$(git -c color.status=always -c status.relativePaths=true status -su |
grep -F -e "$changed" -e "$unmerged" -e "$untracked" |
sed -E 's/^(..[^[:space:]]*)[[:space:]]+(.*)$/[\1] \2/' |
FZF_DEFAULT_OPTS="$opts" fzf |
sh -c "$extract")
[[ -n "$files" ]] && echo "$files"| tr '\n' '\0' |xargs -0 -I% git add % && git status -su && return
echo 'Nothing to add.'
"$FORGIT" add "$@"
}
# git reset HEAD (unstage) selector
forgit::reset::head() {
forgit::inside_work_tree || return 1
local cmd files opts
cmd="git diff --cached --color=always -- {} | $forgit_diff_pager "
opts="
$FORGIT_FZF_DEFAULT_OPTS
-m -0
--preview=\"$cmd\"
$FORGIT_RESET_HEAD_FZF_OPTS
"
files="$(git diff --cached --name-only --relative | FZF_DEFAULT_OPTS="$opts" fzf)"
[[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | xargs -0 -I% git reset -q HEAD % && git status --short && return
echo 'Nothing to unstage.'
"$FORGIT" reset_head "$@"
}
# git stash viewer
forgit::stash::show() {
forgit::inside_work_tree || return 1
local cmd opts
cmd="echo {} |cut -d: -f1 |xargs -I% git stash show --color=always --ext-diff % |$forgit_diff_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m -0 --tiebreak=index --bind=\"enter:execute($cmd | $forgit_enter_pager)\"
--preview=\"$cmd\"
$FORGIT_STASH_FZF_OPTS
"
git stash list | FZF_DEFAULT_OPTS="$opts" fzf
fzf_exit_code=$?
# exit successfully on 130 (ctrl-c/esc)
[[ $fzf_exit_code == 130 ]] && return 0
return $fzf_exit_code
"$FORGIT" stash_show "$@"
}
# git clean selector
forgit::clean() {
forgit::inside_work_tree || return 1
local files opts
opts="
$FORGIT_FZF_DEFAULT_OPTS
-m -0
$FORGIT_CLEAN_FZF_OPTS
"
# Note: Postfix '/' in directory path should be removed. Otherwise the directory itself will not be removed.
files=$(git clean -xdffn "$@"| sed 's/^Would remove //' | FZF_DEFAULT_OPTS="$opts" fzf |sed 's#/$##')
[[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | xargs -0 -I% git clean -xdff '%' && git status --short && return
echo 'Nothing to clean.'
"$FORGIT" clean "$@"
}
forgit::cherry::pick() {
local base target preview opts fzf_selection fzf_exitval
base=$(git branch --show-current)
[[ -z $1 ]] && echo "Please specify target branch" && return 1
target="$1"
preview="echo {} | $forgit_extract_sha | xargs -I% git show --color=always % | $forgit_show_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
--preview=\"$preview\"
-m -0 --tiebreak=index
$FORGIT_CHERRY_PICK_FZF_OPTS
"
fzf_selection=$(git cherry "$base" "$target" --abbrev -v | forgit::reverse_lines |
FZF_DEFAULT_OPTS="$opts" fzf)
fzf_exitval=$?
[[ $fzf_exitval != 0 ]] && return $fzf_exitval
commits=()
while IFS="" read -r line
do
commits+=("$line")
done < <(echo "$fzf_selection" | forgit::reverse_lines | cut -d' ' -f2)
git cherry-pick "${commits[@]}"
"$FORGIT" cherry_pick "$@"
}
forgit::cherry::pick::from::branch() {
forgit::inside_work_tree || return 1
[[ $# -ne 0 ]] && { git checkout -b "$@"; return $?; }
local cmd preview opts branch exitval
cmd="git branch --color=always --all | LC_ALL=C sort -k1.1,1.1 -rs"
preview="git log {1} $forgit_log_preview_options"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index --header-lines=1
--preview=\"$preview\"
$FORGIT_CHERRY_PICK_FROM_BRANCH_FZF_OPTS
"
# loop until either the branch selector is closed or a commit to be cherry
# picked has been selected from within a branch
while true
do
branch="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')"
[[ -z "$branch" ]] && return 1
forgit::cherry::pick "$branch"
exitval=$?
[[ $exitval != 130 ]] && return $exitval
done
"$FORGIT" cherry_pick_from_branch "$@"
}
forgit::rebase() {
forgit::inside_work_tree || return 1
local cmd preview opts graph files target_commit prev_commit
graph=--graph
[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
cmd="git log $graph --color=always --format='$forgit_log_format' $* $forgit_emojify"
files=$(sed -nE 's/.* -- (.*)/\1/p' <<< "$*") # extract files parameters for `git show` command
preview="echo {} | $forgit_extract_sha | xargs -I% git show --color=always % -- $files | $forgit_show_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"ctrl-y:execute-silent(echo {} | $forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
--preview=\"$preview\"
$FORGIT_REBASE_FZF_OPTS
"
target_commit=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | eval "$forgit_extract_sha")
if [[ -n "$target_commit" ]]; then
prev_commit=$(forgit::previous_commit "$target_commit")
git rebase -i "$prev_commit"
fi
"$FORGIT" rebase "$@"
}
forgit::fixup() {
forgit::inside_work_tree || return 1
git diff --cached --quiet && echo 'Nothing to fixup: there are no staged changes.' && return 1
local cmd preview opts graph files target_commit prev_commit
graph=--graph
[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
cmd="git log $graph --color=always --format='$forgit_log_format' $* $forgit_emojify"
files=$(sed -nE 's/.* -- (.*)/\1/p' <<< "$*")
preview="echo {} | $forgit_extract_sha | xargs -I% git show --color=always % -- $files | $forgit_show_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"ctrl-y:execute-silent(echo {} | $forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
--preview=\"$preview\"
$FORGIT_FIXUP_FZF_OPTS
"
target_commit=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | eval "$forgit_extract_sha")
if [[ -n "$target_commit" ]] && git commit --fixup "$target_commit"; then
prev_commit=$(forgit::previous_commit "$target_commit")
# rebase will fail if there are unstaged changes so --autostash is needed to temporarily stash them
# GIT_SEQUENCE_EDITOR=: is needed to skip the editor
GIT_SEQUENCE_EDITOR=: git rebase --autostash -i --autosquash "$prev_commit"
fi
"$FORGIT" fixup "$@"
}
# git checkout-file selector
forgit::checkout::file() {
forgit::inside_work_tree || return 1
[[ $# -ne 0 ]] && { git checkout -- "$@"; return $?; }
local cmd files opts
cmd="git diff --color=always -- {} | $forgit_diff_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
-m -0
--preview=\"$cmd\"
$FORGIT_CHECKOUT_FILE_FZF_OPTS
"
files="$(git ls-files --modified "$(git rev-parse --show-toplevel)"| FZF_DEFAULT_OPTS="$opts" fzf)"
[[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | xargs -0 -I% git checkout %
"$FORGIT" checkout_file "$@"
}
# git checkout-branch selector
forgit::checkout::branch() {
forgit::inside_work_tree || return 1
# if called with arguments, check if branch exists, else create a new one
if [[ $# -ne 0 ]]; then
if git show-branch "$@" &>/dev/null; then
git switch "$@"
else
git switch -c "$@"
fi
checkout_status=$?
git status --short
return $checkout_status
fi
local cmd preview opts branch
cmd="git branch --color=always --all | LC_ALL=C sort -k1.1,1.1 -rs"
preview="git log {1} $forgit_log_preview_options"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index --header-lines=1
--preview=\"$preview\"
$FORGIT_CHECKOUT_BRANCH_FZF_OPTS
"
branch="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')"
[[ -z "$branch" ]] && return 1
# track the remote branch if possible
if [[ "$branch" == "remotes/origin/"* ]]; then
if git branch | grep -qw "${branch#remotes/origin/}"; then
# hack to force creating a new branch which tracks the remote if a local branch already exists
git checkout -b "track/${branch#remotes/origin/}" --track "$branch"
elif ! git checkout --track "$branch" 2>/dev/null; then
git checkout "$branch"
fi
else
git checkout "$branch"
fi
"$FORGIT" checkout_branch "$@"
}
# git checkout-tag selector
forgit::checkout::tag() {
forgit::inside_work_tree || return 1
[[ $# -ne 0 ]] && { git checkout "$@"; return $?; }
local cmd opts preview
cmd="git tag -l --sort=-v:refname"
preview="git log {1} $forgit_log_preview_options"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--preview=\"$preview\"
$FORGIT_CHECKOUT_TAG_FZF_OPTS
"
tag="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf)"
[[ -z "$tag" ]] && return 1
git checkout "$tag"
"$FORGIT" checkout_tag "$@"
}
# git checkout-commit selector
forgit::checkout::commit() {
forgit::inside_work_tree || return 1
[[ $# -ne 0 ]] && { git checkout "$@"; return $?; }
local cmd opts graph
cmd="echo {} | $forgit_extract_sha |xargs -I% git show --color=always % | $forgit_show_pager"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index
--bind=\"ctrl-y:execute-silent(echo {} | $forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
--preview=\"$cmd\"
$FORGIT_COMMIT_FZF_OPTS
"
graph=--graph
[[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
eval "git log $graph --color=always --format='$forgit_log_format' $forgit_emojify" |
FZF_DEFAULT_OPTS="$opts" fzf | eval "$forgit_extract_sha" | xargs -I% git checkout % --
"$FORGIT" checkout_commit "$@"
}
forgit::branch::delete() {
forgit::inside_work_tree || return 1
local preview opts cmd branches
preview="git log {1} $forgit_log_preview_options"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s --multi --tiebreak=index --header-lines=1
--preview=\"$preview\"
$FORGIT_BRANCH_DELETE_FZF_OPTS
"
cmd="git branch --color=always | LC_ALL=C sort -k1.1,1.1 -rs"
branches=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')
echo -n "$branches" | tr '\n' '\0' | xargs -I{} -0 git branch -D {}
"$FORGIT" branch_delete "$@"
}
# git revert-commit selector
forgit::revert::commit() {
forgit::inside_work_tree || return 1
[[ $# -ne 0 ]] && { git revert "$@"; return $?; }
local cmd opts files preview commits IFS
cmd="git log --graph --color=always --format='$forgit_log_format' $* $forgit_emojify"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s --tiebreak=index
$FORGIT_REVERT_COMMIT_OPTS
"
files=$(sed -nE 's/.* -- (.*)/\1/p' <<< "$*") # extract files parameters for `git show` command
preview="echo {} | $forgit_extract_sha | xargs -I% git show --color=always % -- $files | $forgit_show_pager"
# shellcheck disable=2207
IFS=$'\n' commits=($(eval "$cmd" |
FZF_DEFAULT_OPTS="$opts" fzf --preview="$preview" -m |
sed 's/^[^a-f^0-9]*\([a-f0-9]*\).*/\1/'))
[ ${#commits[@]} -eq 0 ] && return 1
for commit in "${commits[@]}"; do
git revert "$commit"
done
"$FORGIT" revert_commit "$@"
}
# git blame viewer
forgit::blame() {
forgit::inside_work_tree || return 1
[[ $# -ne 0 ]] && git blame "$@" && return 0
local opts flags preview file
opts="
$FORGIT_FZF_DEFAULT_OPTS
$FORGIT_BLAME_FZF_OPTS
"
flags=$(git rev-parse --flags "$@")
preview="
if (git ls-files {} --error-unmatch) &>/dev/null; then
git blame {} --date=short $flags | $forgit_blame_pager
else
echo File not tracked
fi
"
file=$(FZF_DEFAULT_OPTS="$opts" fzf --preview="$preview")
[[ -z "$file" ]] && return 1
eval git blame "$file" "$flags"
"$FORGIT" blame "$@"
}
# git ignore generator
export FORGIT_GI_REPO_REMOTE=${FORGIT_GI_REPO_REMOTE:-https://github.com/dvcs/gitignore}
export FORGIT_GI_REPO_LOCAL="${FORGIT_GI_REPO_LOCAL:-${XDG_CACHE_HOME:-$HOME/.cache}/forgit/gi/repos/dvcs/gitignore}"
export FORGIT_GI_TEMPLATES=${FORGIT_GI_TEMPLATES:-$FORGIT_GI_REPO_LOCAL/templates}
forgit::ignore() {
[ -d "$FORGIT_GI_REPO_LOCAL" ] || forgit::ignore::update
local IFS cmd args opts
cmd="$forgit_ignore_pager $FORGIT_GI_TEMPLATES/{2}{,.gitignore} 2>/dev/null"
opts="
$FORGIT_FZF_DEFAULT_OPTS
-m --preview-window='right:70%'
--preview=\"eval $cmd\"
$FORGIT_IGNORE_FZF_OPTS
"
# shellcheck disable=SC2206,2207
IFS=$'\n' args=($@) && [[ $# -eq 0 ]] && args=($(forgit::ignore::list | nl -nrn -w4 -s' ' |
FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $2}'))
[ ${#args[@]} -eq 0 ] && return 1
# shellcheck disable=SC2068
forgit::ignore::get ${args[@]}
}
forgit::ignore::update() {
if [[ -d "$FORGIT_GI_REPO_LOCAL" ]]; then
forgit::info 'Updating gitignore repo...'
(cd "$FORGIT_GI_REPO_LOCAL" && git pull --no-rebase --ff) || return 1
else
forgit::info 'Initializing gitignore repo...'
git clone --depth=1 "$FORGIT_GI_REPO_REMOTE" "$FORGIT_GI_REPO_LOCAL"
fi
}
forgit::ignore::get() {
local item filename header
for item in "$@"; do
if filename=$(find -L "$FORGIT_GI_TEMPLATES" -type f \( -iname "${item}.gitignore" -o -iname "${item}" \) -print -quit); then
[[ -z "$filename" ]] && forgit::warn "No gitignore template found for '$item'." && continue
header="${filename##*/}" && header="${header%.gitignore}"
echo "### $header" && cat "$filename" && echo
fi
done
}
forgit::ignore::list() {
find "$FORGIT_GI_TEMPLATES" -print |sed -e 's#.gitignore$##' -e 's#.*/##' | sort -fu
}
forgit::ignore::clean() {
setopt localoptions rmstarsilent
[[ -d "$FORGIT_GI_REPO_LOCAL" ]] && rm -rf "$FORGIT_GI_REPO_LOCAL"
"$FORGIT" ignore "$@"
}
FORGIT_FZF_DEFAULT_OPTS="
$FZF_DEFAULT_OPTS
--ansi
--height='80%'
--bind='alt-k:preview-up,alt-p:preview-up'
--bind='alt-j:preview-down,alt-n:preview-down'
--bind='ctrl-r:toggle-all'
--bind='ctrl-s:toggle-sort'
--bind='?:toggle-preview'
--bind='alt-w:toggle-preview-wrap'
--preview-window='right:60%'
+1
$FORGIT_FZF_DEFAULT_OPTS
"
forgit::ignore::update() {
"$FORGIT" ignore_update "$@"
}
forgit::ignore::get() {
"$FORGIT" ignore_get "$@"
}
forgit::ignore::list() {
"$FORGIT" ignore_list "$@"
}
forgit::ignore::clean() {
"$FORGIT" ignore_clean "$@"
}
# register aliases
# shellcheck disable=SC2139
@ -527,15 +144,3 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then
alias "${forgit_fixup:-gfu}"='forgit::fixup'
alias "${forgit_blame:-gbl}"='forgit::blame'
fi
# set installation path (for use by `bin/git-forgit`)
if [[ -n "$ZSH_VERSION" ]]; then
FORGIT_INSTALL_DIR="$( dirname -- "$0")"
elif [[ -n "$BASH_VERSION" ]]; then
FORGIT_INSTALL_DIR="$(dirname -- "${BASH_SOURCE[0]}")"
else
forgit::warn "Only zsh and bash are fully supported"
fi
if [[ -n "$FORGIT_INSTALL_DIR" ]]; then
export FORGIT_INSTALL_DIR
fi