From 4995c614e484a5a4636ffcea6e949dd7bd80bdac Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Fri, 7 Oct 2022 16:35:38 +0200 Subject: [PATCH 01/10] Move implementation to standalone forgit Previously we had two different forgit implementations: one for zsh/bash and one for fish. In this commit we move the bash implementation of the forgit functions to the git-forgit script in order to have them in the form of an executable script. This makes using forgit possible without any shell plugin at all and furthermore removes the need to maintain a separate implementation for the fish shell. The simplest way of using forgit from now on is to put git-forgit somewhere in your PATH and call it via "git forgit", without the need for any other files. We keep the shell plugins, though, and maintain backwards compatibility by retaining the plugin functions as simple wrappers around the git-forgit script, making the plugins mainly a collection of aliases only. --- .github/workflows/ci.yaml | 2 +- bin/git-forgit | 516 ++++++++++++++++++++++++++++++++- conf.d/bin/git-forgit | 1 + conf.d/forgit.plugin.fish | 595 ++------------------------------------ forgit.plugin.zsh | 505 +++----------------------------- 5 files changed, 579 insertions(+), 1040 deletions(-) create mode 120000 conf.d/bin/git-forgit diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 89e2795..bf3f8aa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,7 +41,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 diff --git a/bin/git-forgit b/bin/git-forgit index 2bf5820..ff216d6 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -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,512 @@ # 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" +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" +} cmd="$1" shift -forgit::${cmd//_/::} "$@" +_forgit_"${cmd}" "$@" diff --git a/conf.d/bin/git-forgit b/conf.d/bin/git-forgit new file mode 120000 index 0000000..b25cefc --- /dev/null +++ b/conf.d/bin/git-forgit @@ -0,0 +1 @@ +../../bin/git-forgit \ No newline at end of file diff --git a/conf.d/forgit.plugin.fish b/conf.d/forgit.plugin.fish index 58ced23..4f082ba 100644 --- a/conf.d/forgit.plugin.fish +++ b/conf.d/forgit.plugin.fish @@ -1,640 +1,87 @@ # MIT (c) Chris Apple -function forgit::warn - printf "%b[Warn]%b %s\n" '\e[0;33m' '\e[0m' "$argv" >&2; -end +set -g FORGIT_INSTALL_DIR (dirname (dirname (status -f))) -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 - 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_INSTALL_DIR/conf.d/bin/git-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_INSTALL_DIR/conf.d/bin/git-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_INSTALL_DIR/conf.d/bin/git-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_INSTALL_DIR/conf.d/bin/git-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.' + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_file "$argv" 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 % -- + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_commit "$argv" 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 + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" branch_delete "$argv" 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 + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_branch "$argv" 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_INSTALL_DIR/conf.d/bin/git-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_INSTALL_DIR/conf.d/bin/git-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_INSTALL_DIR/conf.d/bin/git-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 - - 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 + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" cherry_pick_from_branch "$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_INSTALL_DIR/conf.d/bin/git-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 -end - -# git ignore generator -if test -z "$FORGIT_GI_REPO_REMOTE" - set -g FORGIT_GI_REPO_REMOTE https://github.com/dvcs/gitignore -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 -end - -if test -z "$FORGIT_GI_TEMPLATES" - set -g FORGIT_GI_TEMPLATES $FORGIT_GI_REPO_LOCAL/templates + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" rebase "$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 + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" ignore "$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 - - 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 - " - - 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 + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" revert_commit "$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_INSTALL_DIR/conf.d/bin/git-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_INSTALL_DIR/conf.d/bin/git-forgit" ignore_get "$argv" end function forgit::ignore::list - find "$FORGIT_GI_TEMPLATES" -print | sed -e 's#.gitignore$##' -e 's#.*/##' | sort -fu + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" ignore_list "$argv" end function forgit::ignore::clean - if test -d "$FORGIT_GI_REPO_LOCAL" - rm -rf "$FORGIT_GI_REPO_LOCAL" - end + "$FORGIT_INSTALL_DIR/conf.d/bin/git-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" diff --git a/forgit.plugin.zsh b/forgit.plugin.zsh index b4fbb15..10e9828 100755 --- a/forgit.plugin.zsh +++ b/forgit.plugin.zsh @@ -1,510 +1,107 @@ #!/usr/bin/env bash # MIT (c) Wenxuan Zhang + 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 -} +# set installation path +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 -# 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 + "$FORGIT_INSTALL_DIR/bin/git-forgit" log "$@" } -# 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 + "$FORGIT_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-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_INSTALL_DIR/bin/git-forgit" ignore_update "$@" +} + +forgit::ignore::get() { + "$FORGIT_INSTALL_DIR/bin/git-forgit" ignore_get "$@" +} + +forgit::ignore::list() { + "$FORGIT_INSTALL_DIR/bin/git-forgit" ignore_list "$@" +} + +forgit::ignore::clean() { + "$FORGIT_INSTALL_DIR/bin/git-forgit" ignore_clean "$@" +} # register aliases # shellcheck disable=SC2139 @@ -527,15 +124,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 From 9471f292bb4369233b308f25209ea210e0e73ec5 Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Fri, 14 Oct 2022 14:41:01 +0200 Subject: [PATCH 02/10] zsh plugin: use standardized $0 handling See https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html#zero-handling --- forgit.plugin.zsh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/forgit.plugin.zsh b/forgit.plugin.zsh index 10e9828..4c81c7d 100755 --- a/forgit.plugin.zsh +++ b/forgit.plugin.zsh @@ -5,7 +5,11 @@ forgit::warn() { printf "%b[Warn]%b %s\n" '\e[0;33m' '\e[0m' "$@" >&2; } # set installation path if [[ -n "$ZSH_VERSION" ]]; then - FORGIT_INSTALL_DIR="$( dirname -- "$0")" + # shellcheck disable=2277,2296,2299 + 0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}" + # shellcheck disable=2277,2296,2298 + 0="${${(M)0:#/*}:-$PWD/$0}" + FORGIT_INSTALL_DIR="${0:h}" elif [[ -n "$BASH_VERSION" ]]; then FORGIT_INSTALL_DIR="$(dirname -- "${BASH_SOURCE[0]}")" else From 58882fd6ba0098804faf992d40ab1d70b75f45d8 Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Fri, 14 Oct 2022 14:19:38 +0200 Subject: [PATCH 03/10] fish plugin: move function order to match zsh version Also add checkout::tag and blame, which were missing. --- conf.d/forgit.plugin.fish | 52 ++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/conf.d/forgit.plugin.fish b/conf.d/forgit.plugin.fish index 4f082ba..f3dd8ab 100644 --- a/conf.d/forgit.plugin.fish +++ b/conf.d/forgit.plugin.fish @@ -18,22 +18,6 @@ function forgit::reset::head -d "git reset HEAD (unstage) selector" "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" reset_head "$argv" end -function forgit::checkout::file -d "git checkout-file selector" --argument-names 'file_name' --wraps "git checkout --" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_file "$argv" -end - -function forgit::checkout::commit -d "git checkout commit selector" --argument-names 'commit_id' --wraps "git checkout" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_commit "$argv" -end - -function forgit::branch::delete -d "git checkout branch deleter" --wraps "git branch --delete" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" branch_delete "$argv" -end - -function forgit::checkout::branch -d "git checkout branch selector" --argument-names 'input_branch_name' --wraps "git branch" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_branch "$argv" -end - function forgit::stash::show -d "git stash viewer" "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" stash_show "$argv" end @@ -50,22 +34,46 @@ function forgit::cherry::pick::from::branch -d "git cherry-picking with interact "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" cherry_pick_from_branch "$argv" end -function forgit::fixup -d "git fixup" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" fixup "$argv" -end - function forgit::rebase -d "git rebase" "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" rebase "$argv" end -function forgit::ignore -d "git ignore generator" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" ignore "$argv" +function forgit::fixup -d "git fixup" + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" fixup "$argv" +end + +function forgit::checkout::file -d "git checkout-file selector" --argument-names 'file_name' --wraps "git checkout --" + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_file "$argv" +end + +function forgit::checkout::branch -d "git checkout branch selector" --argument-names 'input_branch_name' --wraps "git branch" + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_branch "$argv" +end + +function forgit::checkout::tag -d "git checkout tag selector" --argument-names 'tag_name' --wraps "git checkout" + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_tag "$argv" +end + +function forgit::checkout::commit -d "git checkout commit selector" --argument-names 'commit_id' --wraps "git checkout" + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_commit "$argv" +end + +function forgit::branch::delete -d "git checkout branch deleter" --wraps "git branch --delete" + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" branch_delete "$argv" end function forgit::revert::commit --argument-names 'commit_hash' --wraps "git revert --" "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" revert_commit "$argv" end +function forgit::blame + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" blame "$argv" +end + +function forgit::ignore -d "git ignore generator" + "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" ignore "$argv" +end + function forgit::ignore::update "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" ignore_update "$argv" end From b2d24138df0a4c618d18aed3ffbace042d4bd57d Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Mon, 24 Oct 2022 14:08:11 +0200 Subject: [PATCH 04/10] zsh plugin: exit with an error if bin path cannot be determined --- forgit.plugin.zsh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/forgit.plugin.zsh b/forgit.plugin.zsh index 4c81c7d..7c90fae 100755 --- a/forgit.plugin.zsh +++ b/forgit.plugin.zsh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # MIT (c) Wenxuan Zhang -forgit::warn() { printf "%b[Warn]%b %s\n" '\e[0;33m' '\e[0m' "$@" >&2; } +forgit::error() { printf "%b[Error]%b %s\n" '\e[0;31m' '\e[0m' "$@" >&2; return 1; } # set installation path if [[ -n "$ZSH_VERSION" ]]; then @@ -13,12 +13,11 @@ if [[ -n "$ZSH_VERSION" ]]; then 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 + forgit::error "Only zsh and bash are supported" fi +export FORGIT_INSTALL_DIR +# register shell functions forgit::log() { "$FORGIT_INSTALL_DIR/bin/git-forgit" log "$@" } From 0820811862a2fa7697cc9a0178baa5627204c82f Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Mon, 24 Oct 2022 14:13:00 +0200 Subject: [PATCH 05/10] Add variable for forgit binary --- conf.d/forgit.plugin.fish | 45 ++++++++++++++++++------------------ forgit.plugin.zsh | 48 +++++++++++++++++++-------------------- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/conf.d/forgit.plugin.fish b/conf.d/forgit.plugin.fish index f3dd8ab..b1d38f8 100644 --- a/conf.d/forgit.plugin.fish +++ b/conf.d/forgit.plugin.fish @@ -1,93 +1,94 @@ # MIT (c) Chris Apple set -g FORGIT_INSTALL_DIR (dirname (dirname (status -f))) +set -g FORGIT "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" function forgit::log -d "git commit viewer" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" log "$argv" + "$FORGIT" log "$argv" end function forgit::diff -d "git diff viewer" --argument-names arg1 arg2 - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" diff "$argv" + "$FORGIT" diff "$argv" end function forgit::add -d "git add selector" --wraps "git add" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" add "$argv" + "$FORGIT" add "$argv" end function forgit::reset::head -d "git reset HEAD (unstage) selector" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" reset_head "$argv" + "$FORGIT" reset_head "$argv" end function forgit::stash::show -d "git stash viewer" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" stash_show "$argv" + "$FORGIT" stash_show "$argv" end function forgit::clean -d "git clean selector" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" clean "$argv" + "$FORGIT" clean "$argv" end function forgit::cherry::pick -d "git cherry-picking" --argument-names 'target' --wraps "git cherry-pick" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" cherry_pick "$argv" + "$FORGIT" cherry_pick "$argv" end function forgit::cherry::pick::from::branch -d "git cherry-picking with interactive branch selection" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" cherry_pick_from_branch "$argv" + "$FORGIT" cherry_pick_from_branch "$argv" end function forgit::rebase -d "git rebase" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" rebase "$argv" + "$FORGIT" rebase "$argv" end function forgit::fixup -d "git fixup" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" fixup "$argv" + "$FORGIT" fixup "$argv" end function forgit::checkout::file -d "git checkout-file selector" --argument-names 'file_name' --wraps "git checkout --" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_file "$argv" + "$FORGIT" checkout_file "$argv" end function forgit::checkout::branch -d "git checkout branch selector" --argument-names 'input_branch_name' --wraps "git branch" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_branch "$argv" + "$FORGIT" checkout_branch "$argv" end function forgit::checkout::tag -d "git checkout tag selector" --argument-names 'tag_name' --wraps "git checkout" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_tag "$argv" + "$FORGIT" checkout_tag "$argv" end function forgit::checkout::commit -d "git checkout commit selector" --argument-names 'commit_id' --wraps "git checkout" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" checkout_commit "$argv" + "$FORGIT" checkout_commit "$argv" end function forgit::branch::delete -d "git checkout branch deleter" --wraps "git branch --delete" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" branch_delete "$argv" + "$FORGIT" branch_delete "$argv" end function forgit::revert::commit --argument-names 'commit_hash' --wraps "git revert --" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" revert_commit "$argv" + "$FORGIT" revert_commit "$argv" end function forgit::blame - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" blame "$argv" + "$FORGIT" blame "$argv" end function forgit::ignore -d "git ignore generator" - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" ignore "$argv" + "$FORGIT" ignore "$argv" end function forgit::ignore::update - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" ignore_update "$argv" + "$FORGIT" ignore_update "$argv" end function forgit::ignore::get - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" ignore_get "$argv" + "$FORGIT" ignore_get "$argv" end function forgit::ignore::list - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" ignore_list "$argv" + "$FORGIT" ignore_list "$argv" end function forgit::ignore::clean - "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" ignore_clean "$argv" + "$FORGIT" ignore_clean "$argv" end # register aliases diff --git a/forgit.plugin.zsh b/forgit.plugin.zsh index 7c90fae..decd5ea 100755 --- a/forgit.plugin.zsh +++ b/forgit.plugin.zsh @@ -3,7 +3,7 @@ forgit::error() { printf "%b[Error]%b %s\n" '\e[0;31m' '\e[0m' "$@" >&2; return 1; } -# set installation path +# determine installation path if [[ -n "$ZSH_VERSION" ]]; then # shellcheck disable=2277,2296,2299 0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}" @@ -15,95 +15,95 @@ elif [[ -n "$BASH_VERSION" ]]; then else forgit::error "Only zsh and bash are supported" fi -export FORGIT_INSTALL_DIR +export FORGIT="$FORGIT_INSTALL_DIR/bin/git-forgit" # register shell functions forgit::log() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" log "$@" + "$FORGIT" log "$@" } forgit::diff() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" diff "$@" + "$FORGIT" diff "$@" } forgit::add() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" add "$@" + "$FORGIT" add "$@" } forgit::reset::head() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" reset_head "$@" + "$FORGIT" reset_head "$@" } forgit::stash::show() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" stash_show "$@" + "$FORGIT" stash_show "$@" } forgit::clean() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" clean "$@" + "$FORGIT" clean "$@" } forgit::cherry::pick() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" cherry_pick "$@" + "$FORGIT" cherry_pick "$@" } forgit::cherry::pick::from::branch() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" cherry_pick_from_branch "$@" + "$FORGIT" cherry_pick_from_branch "$@" } forgit::rebase() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" rebase "$@" + "$FORGIT" rebase "$@" } forgit::fixup() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" fixup "$@" + "$FORGIT" fixup "$@" } forgit::checkout::file() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" checkout_file "$@" + "$FORGIT" checkout_file "$@" } forgit::checkout::branch() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" checkout_branch "$@" + "$FORGIT" checkout_branch "$@" } forgit::checkout::tag() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" checkout_tag "$@" + "$FORGIT" checkout_tag "$@" } forgit::checkout::commit() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" checkout_commit "$@" + "$FORGIT" checkout_commit "$@" } forgit::branch::delete() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" branch_delete "$@" + "$FORGIT" branch_delete "$@" } forgit::revert::commit() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" revert_commit "$@" + "$FORGIT" revert_commit "$@" } forgit::blame() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" blame "$@" + "$FORGIT" blame "$@" } forgit::ignore() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" ignore "$@" + "$FORGIT" ignore "$@" } forgit::ignore::update() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" ignore_update "$@" + "$FORGIT" ignore_update "$@" } forgit::ignore::get() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" ignore_get "$@" + "$FORGIT" ignore_get "$@" } forgit::ignore::list() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" ignore_list "$@" + "$FORGIT" ignore_list "$@" } forgit::ignore::clean() { - "$FORGIT_INSTALL_DIR/bin/git-forgit" ignore_clean "$@" + "$FORGIT" ignore_clean "$@" } # register aliases From 26f0f5c1e1156a286d2e0d8bdde96a114559db1b Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Fri, 11 Nov 2022 09:40:30 +0100 Subject: [PATCH 06/10] Ensure that fzf preview commands are executed in bash All preview commands are written in bash now. fzf, however, uses the shell defined in $SHELL to run the preview commands. We have to force this to bash within forgit in order to not get any errors on different default shells. --- bin/git-forgit | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/git-forgit b/bin/git-forgit index ff216d6..e88734b 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -12,6 +12,9 @@ # This gives users the choice to set aliases inside of their git config instead # of their shell config if they prefer. +# Set shell for fzf preview commands +SHELL=/bin/bash + FORGIT_FZF_DEFAULT_OPTS=" $FZF_DEFAULT_OPTS --ansi From 49def1fe91d925d4a1c07772467e655b3c44e235 Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Mon, 14 Nov 2022 13:36:42 +0100 Subject: [PATCH 07/10] Fish plugin: do not quote passing of $argv Fish stores command line arguments in $argv, which is an array. Unlike in bash, when passing this array to a function, we must not enclose it in double-quotes. This made an empty argument list (i.e. no arguments passed) to an actual empty string argument (i.e. one argument passed), which is not what we want. See https://stackoverflow.com/a/42379014/3018229 --- conf.d/forgit.plugin.fish | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/conf.d/forgit.plugin.fish b/conf.d/forgit.plugin.fish index b1d38f8..3f9cc2d 100644 --- a/conf.d/forgit.plugin.fish +++ b/conf.d/forgit.plugin.fish @@ -4,91 +4,91 @@ set -g FORGIT_INSTALL_DIR (dirname (dirname (status -f))) set -g FORGIT "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" function forgit::log -d "git commit viewer" - "$FORGIT" log "$argv" + "$FORGIT" log $argv end function forgit::diff -d "git diff viewer" --argument-names arg1 arg2 - "$FORGIT" diff "$argv" + "$FORGIT" diff $argv end function forgit::add -d "git add selector" --wraps "git add" - "$FORGIT" add "$argv" + "$FORGIT" add $argv end function forgit::reset::head -d "git reset HEAD (unstage) selector" - "$FORGIT" reset_head "$argv" + "$FORGIT" reset_head $argv end function forgit::stash::show -d "git stash viewer" - "$FORGIT" stash_show "$argv" + "$FORGIT" stash_show $argv end function forgit::clean -d "git clean selector" - "$FORGIT" clean "$argv" + "$FORGIT" clean $argv end function forgit::cherry::pick -d "git cherry-picking" --argument-names 'target' --wraps "git cherry-pick" - "$FORGIT" cherry_pick "$argv" + "$FORGIT" cherry_pick $argv end function forgit::cherry::pick::from::branch -d "git cherry-picking with interactive branch selection" - "$FORGIT" cherry_pick_from_branch "$argv" + "$FORGIT" cherry_pick_from_branch $argv end function forgit::rebase -d "git rebase" - "$FORGIT" rebase "$argv" + "$FORGIT" rebase $argv end function forgit::fixup -d "git fixup" - "$FORGIT" fixup "$argv" + "$FORGIT" fixup $argv end function forgit::checkout::file -d "git checkout-file selector" --argument-names 'file_name' --wraps "git checkout --" - "$FORGIT" checkout_file "$argv" + "$FORGIT" checkout_file $argv end function forgit::checkout::branch -d "git checkout branch selector" --argument-names 'input_branch_name' --wraps "git branch" - "$FORGIT" checkout_branch "$argv" + "$FORGIT" checkout_branch $argv end function forgit::checkout::tag -d "git checkout tag selector" --argument-names 'tag_name' --wraps "git checkout" - "$FORGIT" checkout_tag "$argv" + "$FORGIT" checkout_tag $argv end function forgit::checkout::commit -d "git checkout commit selector" --argument-names 'commit_id' --wraps "git checkout" - "$FORGIT" checkout_commit "$argv" + "$FORGIT" checkout_commit $argv end function forgit::branch::delete -d "git checkout branch deleter" --wraps "git branch --delete" - "$FORGIT" branch_delete "$argv" + "$FORGIT" branch_delete $argv end function forgit::revert::commit --argument-names 'commit_hash' --wraps "git revert --" - "$FORGIT" revert_commit "$argv" + "$FORGIT" revert_commit $argv end function forgit::blame - "$FORGIT" blame "$argv" + "$FORGIT" blame $argv end function forgit::ignore -d "git ignore generator" - "$FORGIT" ignore "$argv" + "$FORGIT" ignore $argv end function forgit::ignore::update - "$FORGIT" ignore_update "$argv" + "$FORGIT" ignore_update $argv end function forgit::ignore::get - "$FORGIT" ignore_get "$argv" + "$FORGIT" ignore_get $argv end function forgit::ignore::list - "$FORGIT" ignore_list "$argv" + "$FORGIT" ignore_list $argv end function forgit::ignore::clean - "$FORGIT" ignore_clean "$argv" + "$FORGIT" ignore_clean $argv end # register aliases From 372239fd37dbf9e3a0c012614d99a1e4e8b6eaff Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Mon, 14 Nov 2022 17:42:44 +0100 Subject: [PATCH 08/10] Export user-defined FORGIT variables for backwards compatibility Before forgit became a standalone script, users defined their FORGIT variables (e.g. FORGIT_FZF_DEFAULT_OPTS) in the shell environment before sourcing the forgit shell plugin. This worked because the forgit code was executed within the currently running shell. With forgit being an executable script, these variables are only available to forgit when being exported. This patch adds an automatism for this so that users do not have to change their configuration. A warning is issued in this case so that users get notified to update their configuration. --- conf.d/forgit.plugin.fish | 23 +++++++++++++++++++++-- forgit.plugin.zsh | 23 ++++++++++++++++++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/conf.d/forgit.plugin.fish b/conf.d/forgit.plugin.fish index 3f9cc2d..03843ca 100644 --- a/conf.d/forgit.plugin.fish +++ b/conf.d/forgit.plugin.fish @@ -1,7 +1,26 @@ # MIT (c) Chris Apple -set -g FORGIT_INSTALL_DIR (dirname (dirname (status -f))) -set -g FORGIT "$FORGIT_INSTALL_DIR/conf.d/bin/git-forgit" +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 + +# 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::log -d "git commit viewer" "$FORGIT" log $argv diff --git a/forgit.plugin.zsh b/forgit.plugin.zsh index decd5ea..21bf56c 100755 --- a/forgit.plugin.zsh +++ b/forgit.plugin.zsh @@ -2,6 +2,7 @@ # 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; } # determine installation path if [[ -n "$ZSH_VERSION" ]]; then @@ -9,13 +10,29 @@ if [[ -n "$ZSH_VERSION" ]]; then 0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}" # shellcheck disable=2277,2296,2298 0="${${(M)0:#/*}:-$PWD/$0}" - FORGIT_INSTALL_DIR="${0:h}" + INSTALL_DIR="${0:h}" elif [[ -n "$BASH_VERSION" ]]; then - FORGIT_INSTALL_DIR="$(dirname -- "${BASH_SOURCE[0]}")" + INSTALL_DIR="$(dirname -- "${BASH_SOURCE[0]}")" else forgit::error "Only zsh and bash are supported" fi -export FORGIT="$FORGIT_INSTALL_DIR/bin/git-forgit" +FORGIT="$INSTALL_DIR/bin/git-forgit" + +# 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 + forgit::warn " export $var" + unexported_vars=$((unexported_vars + 1)) + # shellcheck disable=SC2163 + export "$var" + fi +done # register shell functions forgit::log() { From da9210e97245edbd6fa6aaa0450cd5606c1f0553 Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Thu, 17 Nov 2022 10:13:17 +0100 Subject: [PATCH 09/10] Meta: update documentation --- README.md | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index c261e54..50d1b2e 100644 --- a/README.md +++ b/README.md @@ -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. - +[![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 | From 785c3f9774fcc363957f5d63ebe6b1ab99e16b33 Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Mon, 21 Nov 2022 10:47:46 +0100 Subject: [PATCH 10/10] Display useful error message if no or wrong command given --- bin/git-forgit | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/bin/git-forgit b/bin/git-forgit index e88734b..5174548 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -521,6 +521,39 @@ _forgit_ignore_clean() { [[ -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 + +# 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}" "$@"