#!/usr/bin/env bash # 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` # `git forgit checkout_file README.md` # # 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 # Disable shellcheck for "which", because it suggests "command -v xxx" instead, # which is not a working replacement. # See https://github.com/koalaman/shellcheck/issues/1162 # shellcheck disable=2230 SHELL="$(which bash)" export SHELL # Get absolute forgit path FORGIT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)/$(basename -- "${BASH_SOURCE[0]}") 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 } _forgit_contains_non_flags() { while (("$#")); do case "$1" in -*) shift ;; *) return 0 ;; esac done return 1 } # optional render emoji characters (https://github.com/wfxr/emoji-cli) _forgit_emojify() { if hash emojify &>/dev/null; then emojify else cat fi } # 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:]' } # extract the first git sha and copy it to the clipboard _forgit_yank_sha() { echo "$1" | _forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy} } # extract the first stash name in the input _forgit_extract_stash_name() { cut -d: -f1 | tr -d '[:space:]' } # extract the first stash name and copy it to the clipboard _forgit_yank_stash_name() { echo "$1" | _forgit_extract_stash_name | ${FORGIT_COPY_CMD:-pbcopy} } # parse a space separated string into an array # arrays parsed with this function are global _forgit_parse_array() { ${IFS+"false"} && unset old_IFS || old_IFS="$IFS" # read the value of the second argument # into an array that has the name of the first argument IFS=" " read -r -a "$1" <<< "$2" ${old_IFS+"false"} && unset IFS || IFS="$old_IFS" } # parse the input arguments and print only those after the "--" # separator as a single line of quoted arguments to stdout _forgit_quote_files() { local files add files=() add=false while (( "$#" )); do case "$1" in --) add=true shift ;; *) if [ $add == true ]; then files+=("'$1'") fi shift ;; esac done echo "${files[*]}" } _forgit_log_graph_enable=${FORGIT_LOG_GRAPH_ENABLE:-"true"} _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} _forgit_dir_view=${FORGIT_DIR_VIEW:-$(hash tree &> /dev/null && echo 'tree' || echo 'find')} _forgit_pager() { local pager pager=$(_forgit_get_pager "$1") [[ -z "${pager}" ]] && exit 1 eval "${pager} ${*:2}" } _forgit_get_pager() { local pager pager=${1:-core} case "$pager" in core) echo -n "${FORGIT_PAGER:-$(git config core.pager || echo 'cat')}" ;; show) echo -n "${FORGIT_SHOW_PAGER:-$(git config pager.show || _forgit_get_pager)}" ;; diff) echo -n "${FORGIT_DIFF_PAGER:-$(git config pager.diff || _forgit_get_pager)}" ;; ignore) echo -n "${FORGIT_IGNORE_PAGER:-$(hash bat &>/dev/null && echo 'bat -l gitignore --color=always' || echo 'cat')}" ;; blame) echo -n "${FORGIT_BLAME_PAGER:-$(git config pager.blame || _forgit_get_pager)}" ;; enter) echo -n "${FORGIT_ENTER_PAGER:-"LESS='-r' less"}" ;; *) echo "pager not found: $1" >&2 ;; esac } _forgit_is_file_tracked() { git ls-files "$1" --error-unmatch &> /dev/null } _forgit_list_files() { local rootdir rootdir=$(git rev-parse --show-toplevel) # git escapes special characters in it's output when core.quotePath is # true or unset. Git always expects unquoted file paths as input. This # leads to issues when we consume output from git and use it to build # input for other git commands. Use the -z flag to ensure file paths are # unquoted. # uniq is necessary because unmerged files are printed once for each # merge conflict. # With the -z flag, git also uses \0 line termination, so we # have to replace the terminators. git ls-files -z "$@" "$rootdir" | tr '\0' '\n' | uniq } _forgit_log_preview() { local sha sha=$(echo "$1" | _forgit_extract_sha) shift echo "$sha" | xargs -I% git show --color=always -U"$_forgit_preview_context" % -- "$@" | _forgit_pager show } _forgit_log_enter() { local sha sha=$(echo "$1" | _forgit_extract_sha) shift echo "$sha" | xargs -I% "${FORGIT}" diff %^! "$@" } # git commit viewer _forgit_log() { _forgit_inside_work_tree || return 1 local opts graph quoted_files log_format quoted_files=$(_forgit_quote_files "$@") opts=" $FORGIT_FZF_DEFAULT_OPTS +s +m --tiebreak=index --bind=\"enter:execute($FORGIT log_enter {} $quoted_files)\" --bind=\"ctrl-y:execute-silent($FORGIT yank_sha {})\" --preview=\"$FORGIT log_preview {} $quoted_files\" $FORGIT_LOG_FZF_OPTS " graph=() [[ $_forgit_log_graph_enable == true ]] && graph=(--graph) log_format=${FORGIT_GLO_FORMAT:-$_forgit_log_format} _forgit_log_git_opts=() _forgit_parse_array _forgit_log_git_opts "$FORGIT_LOG_GIT_OPTS" git log "${graph[@]}" --color=always --format="$log_format" "${_forgit_log_git_opts[@]}" "$@" | _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 reflog viewer _forgit_reflog() { _forgit_inside_work_tree || return 1 _forgit_contains_non_flags "$@" && { git reflog "$@"; return $?; } local opts reflog_format opts=" $FORGIT_FZF_DEFAULT_OPTS +s +m --tiebreak=index --bind=\"enter:execute($FORGIT log_enter {})\" --bind=\"ctrl-y:execute-silent($FORGIT yank_sha {})\" --preview=\"$FORGIT log_preview {}\" $FORGIT_REFLOG_FZF_OPTS " reflog_format=${FORGIT_GRL_FORMAT:-$_forgit_log_format} _forgit_reflog_git_opts=() _forgit_parse_array _forgit_reflog_git_opts "$FORGIT_REFLOG_GIT_OPTS" git reflog show --color=always --format="$reflog_format" "${_forgit_reflog_git_opts[@]}" "$@" | _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_get_files_from_diff_line() { # Construct a null-terminated list of the filenames # The input looks like one of these lines: # [R100] file -> another file # [A] file with spaces # [D] oldfile # And we transform it to this representation for further usage with "xargs -0": # file\0another file\0 # file with spaces\0 # oldfile\0 # We have to do a two-step sed -> tr pipe because OSX's sed implementation does # not support the null-character directly. sed 's/^[[:space:]]*\[[A-Z0-9]*\][[:space:]]*//' | sed 's/ -> /\n/' | tr '\n' '\0' } _forgit_get_single_file_from_diff_line() { # Similar to the function above, but only gets a single file from a single line # Gets the new name of renamed files sed 's/^[[:space:]]*\[[A-Z0-9]*\][[:space:]]*//' | sed 's/.*-> //' } _forgit_exec_diff() { _forgit_diff_git_opts=() _forgit_parse_array _forgit_diff_git_opts "$FORGIT_DIFF_GIT_OPTS" git diff --color=always "${_forgit_diff_git_opts[@]}" "$@" } _forgit_diff_view() { local input_line=$1 local diff_context=$2 local repo local commits=() repo=$(git rev-parse --show-toplevel) cd "$repo" || return 1 if [ $# -gt 2 ]; then IFS=" " read -r -a commits <<< "${*:3}" fi echo "$input_line" | _forgit_get_files_from_diff_line | xargs -0 \ "$FORGIT" exec_diff "${commits[@]}" -U"$diff_context" -- | _forgit_pager diff } _forgit_edit_diffed_file() { local input_line rootdir input_line=$1 rootdir=$(git rev-parse --show-toplevel) filename=$(echo "$input_line" | _forgit_get_single_file_from_diff_line) $EDITOR "$rootdir/$filename" >/dev/tty /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 } # 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. for commit in "${commits[@]}"; do escaped_commits+="'${commit//\{/\\\\\{}' " done opts=" $FORGIT_FZF_DEFAULT_OPTS +m -0 --bind=\"enter:execute($FORGIT diff_enter {} $escaped_commits | $FORGIT pager enter)\" --preview=\"$FORGIT diff_view {} '$_forgit_preview_context' $escaped_commits\" --bind=\"alt-e:execute-silent($FORGIT edit_diffed_file {})+refresh-preview\" $FORGIT_DIFF_FZF_OPTS --prompt=\"${commits[*]} > \" " _forgit_diff_git_opts=() _forgit_parse_array _forgit_diff_git_opts "$FORGIT_DIFF_GIT_OPTS" git diff --name-status "${_forgit_diff_git_opts[@]}" "${commits[@]}" -- "${files[@]}" | sed -E 's/^([[:alnum:]]+)[[:space:]]+(.*)$/[\1] \2/' | sed 's/ / -> /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_add_preview() { file=$(echo "$1" | _forgit_get_single_file_from_add_line) 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_pager diff | sed '2 s/added:/untracked:/' else git diff --color=always -- "$file" | _forgit_pager diff fi } _forgit_git_add() { _forgit_add_git_opts=() _forgit_parse_array _forgit_add_git_opts "$FORGIT_ADD_GIT_OPTS" git add "${_forgit_add_git_opts[@]}" "$@" } _forgit_get_single_file_from_add_line() { # NOTE: paths listed by 'git status -su' mixed with quoted and unquoted style # remove indicators | remove original path for rename case | remove surrounding quotes sed 's/^.*] //' | sed 's/.* -> //' | sed -e 's/^\"//' -e 's/\"$//' } _forgit_edit_add_file() { local input_line=$1 filename=$(echo "$input_line" | _forgit_get_single_file_from_add_line) $EDITOR "$filename" >/dev/tty newest when you multiselect # The instances of "cut", "nl" and "sort" all serve this purpose # Please see https://github.com/wfxr/forgit/issues/253 for more details opts=" $FORGIT_FZF_DEFAULT_OPTS --preview=\"$FORGIT cherry_pick_preview {}\" --multi --ansi --with-nth 2.. -0 --tiebreak=index $FORGIT_CHERRY_PICK_FZF_OPTS " # Note: do not add any pipe after the fzf call here, otherwise the fzf_exitval is not propagated properly. # Any eventual post processing can be done afterwards when the "commits" variable is assigned below. fzf_selection=$(git log --right-only --color=always --cherry-pick --oneline "$base"..."$target" | nl | FZF_DEFAULT_OPTS="$opts" fzf) fzf_exitval=$? [[ $fzf_exitval != 0 ]] && return $fzf_exitval [[ -z "$fzf_selection" ]] && return $fzf_exitval commits=() while IFS='' read -r commit; do commits+=("$commit") done < <(echo "$fzf_selection" | sort -n -k 1 | cut -f2 | cut -d' ' -f1 | _forgit_reverse_lines) [ ${#commits[@]} -eq 0 ] && return 1 _forgit_cherry_pick_git_opts=() _forgit_parse_array _forgit_cherry_pick_git_opts "$FORGIT_CHERRY_PICK_GIT_OPTS" git cherry-pick "${_forgit_cherry_pick_git_opts[@]}" "${commits[@]}" } _forgit_cherry_pick_from_branch_preview() { git log --right-only --color=always --cherry-pick --oneline "$1"..."$2" } _forgit_cherry_pick_from_branch() { _forgit_inside_work_tree || return 1 local opts branch exitval input_branch args base base=$(git branch --show-current) [[ -z "$base" ]] && echo "Current commit is not on a branch." && return 1 args=("$@") if [[ $# -ne 0 ]]; then input_branch=${args[0]} fi opts=" $FORGIT_FZF_DEFAULT_OPTS +s +m --tiebreak=index --header-lines=1 --preview=\"$FORGIT cherry_pick_from_branch_preview '$base' {1}\" $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 if [[ -z $input_branch ]]; then branch="$(git branch --color=always --all | LC_ALL=C sort -k1.1,1.1 -rs | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')" else branch=$input_branch fi unset input_branch [[ -z "$branch" ]] && return 1 _forgit_cherry_pick "$branch" exitval=$? [[ $exitval != 130 ]] || [[ $# -ne 0 ]] && return $exitval done } _forgit_rebase() { _forgit_inside_work_tree || return 1 local opts graph quoted_files target_commit prev_commit graph=() [[ $_forgit_log_graph_enable == true ]] && graph=(--graph) _forgit_rebase_git_opts=() _forgit_parse_array _forgit_rebase_git_opts "$FORGIT_REBASE_GIT_OPTS" quoted_files=$(_forgit_quote_files "$@") opts=" $FORGIT_FZF_DEFAULT_OPTS +s +m --tiebreak=index --bind=\"ctrl-y:execute-silent($FORGIT yank_sha {})\" --preview=\"$FORGIT file_preview {} $quoted_files\" $FORGIT_REBASE_FZF_OPTS " target_commit=$( git log "${graph[@]}" --color=always --format="$_forgit_log_format" "$@" | _forgit_emojify | FZF_DEFAULT_OPTS="$opts" fzf | _forgit_extract_sha) if [[ -n "$target_commit" ]]; then prev_commit=$(_forgit_previous_commit "$target_commit") git rebase -i "${_forgit_rebase_git_opts[@]}" "$prev_commit" fi } _forgit_file_preview() { local sha sha=$(echo "$1" | _forgit_extract_sha) shift echo "$sha" | xargs -I% git show --color=always % -- "$@" | _forgit_pager show } _forgit_fixup() { _forgit_inside_work_tree || return 1 git diff --cached --quiet && echo 'Nothing to fixup: there are no staged changes.' && return 1 local opts graph quoted_files target_commit prev_commit graph=() [[ $_forgit_log_graph_enable == true ]] && graph=(--graph) _forgit_fixup_git_opts=() _forgit_parse_array _forgit_fixup_git_opts "$FORGIT_FIXUP_GIT_OPTS" quoted_files=$(_forgit_quote_files "$@") opts=" $FORGIT_FZF_DEFAULT_OPTS +s +m --tiebreak=index --bind=\"ctrl-y:execute-silent($FORGIT yank_sha {})\" --preview=\"$FORGIT file_preview {} $quoted_files\" $FORGIT_FIXUP_FZF_OPTS " target_commit=$( git log "${graph[@]}" --color=always --format="$_forgit_log_format" "$@" | _forgit_emojify | FZF_DEFAULT_OPTS="$opts" fzf | _forgit_extract_sha) if [[ -n "$target_commit" ]] && git commit "${_forgit_fixup_git_opts[@]}" --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_checkout_file_preview() { git diff --color=always -- "$1" | _forgit_pager diff } _forgit_git_checkout_file() { _forgit_checkout_file_git_opts=() _forgit_parse_array _forgit_checkout_file_git_opts "$FORGIT_CHECKOUT_FILE_GIT_OPTS" git checkout "${_forgit_checkout_file_git_opts[@]}" "$@" } # git checkout-file selector _forgit_checkout_file() { _forgit_inside_work_tree || return 1 local files opts [[ $# -ne 0 ]] && { _forgit_git_checkout_file -- "$@"; return $?; } opts=" $FORGIT_FZF_DEFAULT_OPTS -m -0 --preview=\"$FORGIT checkout_file_preview {}\" $FORGIT_CHECKOUT_FILE_FZF_OPTS " files=() while IFS='' read -r file; do files+=("$file") done < <(_forgit_list_files --modified | FZF_DEFAULT_OPTS="$opts" fzf) [[ "${#files[@]}" -gt 0 ]] && _forgit_git_checkout_file "${files[@]}" } _forgit_git_checkout_branch() { _forgit_checkout_branch_git_opts=() _forgit_parse_array _forgit_checkout_branch_git_opts "$FORGIT_CHECKOUT_BRANCH_GIT_OPTS" git checkout "${_forgit_checkout_branch_git_opts[@]}" "$@" } # 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 opts branch opts=" $FORGIT_FZF_DEFAULT_OPTS +s +m --tiebreak=index --header-lines=1 --preview=\"$FORGIT branch_preview {1}\" $FORGIT_CHECKOUT_BRANCH_FZF_OPTS " _forgit_checkout_branch_branch_git_opts=() _forgit_parse_array _forgit_checkout_branch_branch_git_opts "$FORGIT_CHECKOUT_BRANCH_BRANCH_GIT_OPTS" branch="$(git branch --color=always "${_forgit_checkout_branch_branch_git_opts[@]:---all}" | LC_ALL=C sort -k1.1,1.1 -rs | 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 _forgit_git_checkout_branch -b "track/${branch#remotes/origin/}" --track "$branch" elif ! _forgit_git_checkout_branch --track "$branch" 2>/dev/null; then _forgit_git_checkout_branch "$branch" fi else _forgit_git_checkout_branch "$branch" fi } _forgit_git_checkout_tag() { _forgit_checkout_tag_git_opts=() _forgit_parse_array _forgit_checkout_tag_git_opts "$FORGIT_CHECKOUT_TAG_GIT_OPTS" git checkout "${_forgit_checkout_tag_git_opts[@]}" "$@" } # git checkout-tag selector _forgit_checkout_tag() { _forgit_inside_work_tree || return 1 local opts [[ $# -ne 0 ]] && { _forgit_git_checkout_tag "$@"; return $?; } opts=" $FORGIT_FZF_DEFAULT_OPTS +s +m --tiebreak=index --preview=\"$FORGIT branch_preview {}\" $FORGIT_CHECKOUT_TAG_FZF_OPTS " tag="$(git tag -l --sort=-v:refname | FZF_DEFAULT_OPTS="$opts" fzf)" [[ -z "$tag" ]] && return 1 _forgit_git_checkout_tag "$tag" } _forgit_checkout_commit_preview() { echo "$1" | _forgit_extract_sha | xargs -I% git show --color=always % | _forgit_pager show } _forgit_git_checkout_commit() { _forgit_checkout_commit_git_opts=() _forgit_parse_array _forgit_checkout_commit_git_opts "$FORGIT_CHECKOUT_COMMIT_GIT_OPTS" git checkout "${_forgit_checkout_commit_git_opts[@]}" "$@" } # git checkout-commit selector _forgit_checkout_commit() { _forgit_inside_work_tree || return 1 local opts graph commit [[ $# -ne 0 ]] && { _forgit_git_checkout_commit "$@"; return $?; } opts=" $FORGIT_FZF_DEFAULT_OPTS +s +m --tiebreak=index --bind=\"ctrl-y:execute-silent($FORGIT yank_sha {})\" --preview=\"$FORGIT checkout_commit_preview {}\" $FORGIT_CHECKOUT_COMMIT_FZF_OPTS " graph=() [[ $_forgit_log_graph_enable == true ]] && graph=(--graph) commit="$(git log "${graph[@]}" --color=always --format="$_forgit_log_format" | _forgit_emojify | FZF_DEFAULT_OPTS="$opts" fzf | _forgit_extract_sha)" _forgit_git_checkout_commit "$commit" } _forgit_branch_preview() { # the trailing '--' ensures that this works for branches that have a name # that is identical to a file git log "$1" "${_forgit_log_preview_options[@]}" -- } _forgit_git_branch_delete() { _forgit_branch_delete_git_opts=() _forgit_parse_array _forgit_branch_delete_git_opts "$FORGIT_BRANCH_DELETE_GIT_OPTS" git branch "${_forgit_branch_delete_git_opts[@]}" -D "$@" } _forgit_branch_delete() { _forgit_inside_work_tree || return 1 local opts [[ $# -ne 0 ]] && { _forgit_git_branch_delete "$@"; return $?; } opts=" $FORGIT_FZF_DEFAULT_OPTS +s --multi --tiebreak=index --header-lines=1 --preview=\"$FORGIT branch_preview {1}\" $FORGIT_BRANCH_DELETE_FZF_OPTS " for branch in $(git branch --color=always | LC_ALL=C sort -k1.1,1.1 -rs | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}') do _forgit_git_branch_delete "$branch" done } _forgit_revert_preview() { echo "$1" | cut -f2- | _forgit_extract_sha | xargs -I% git show --color=always % | _forgit_pager show } _forgit_git_revert() { _forgit_revert_commit_git_opts=() _forgit_parse_array _forgit_revert_commit_git_opts "$FORGIT_REVERT_COMMIT_GIT_OPTS" git revert "${_forgit_revert_commit_git_opts[@]}" "$@" } # git revert-commit selector _forgit_revert_commit() { _forgit_inside_work_tree || return 1 local opts commits IFS [[ $# -ne 0 ]] && { _forgit_git_revert "$@"; return $?; } opts=" $FORGIT_FZF_DEFAULT_OPTS -m +s --tiebreak=index --ansi --with-nth 2.. --preview=\"$FORGIT revert_preview {}\" $FORGIT_REVERT_COMMIT_FZF_OPTS " graph=() [[ $_forgit_log_graph_enable == true ]] && graph=(--graph) # in this function, we do something interesting to maintain proper ordering as it's assumed # you generally want to revert newest->oldest when you multiselect # The instances of "cut", "nl" and "sort" all serve this purpose # Please see https://github.com/wfxr/forgit/issues/253 for more details commits=() while IFS='' read -r commit; do commits+=("$commit") done < <( git log "${graph[@]}" --color=always --format="$_forgit_log_format" | _forgit_emojify | nl | FZF_DEFAULT_OPTS="$opts" fzf | sort -n -k 1 | cut -f2- | sed 's/^[^a-f^0-9]*\([a-f0-9]*\).*/\1/') [ ${#commits[@]} -eq 0 ] && return 1 _forgit_git_revert "${commits[@]}" } _forgit_blame_preview() { if _forgit_is_file_tracked "$1"; then _forgit_blame_git_opts=() _forgit_parse_array _forgit_blame_git_opts "$FORGIT_BLAME_GIT_OPTS" git blame --date=short "${_forgit_blame_git_opts[@]}" "$@" | _forgit_pager blame else echo "File not tracked" fi } _forgit_git_blame() { _forgit_blame_git_opts=() _forgit_parse_array _forgit_blame_git_opts "$FORGIT_BLAME_GIT_OPTS" git blame "${_forgit_blame_git_opts[@]}" "$@" } # git blame viewer _forgit_blame() { _forgit_inside_work_tree || return 1 local opts flags file _forgit_contains_non_flags "$@" && { _forgit_git_blame "$@"; return $?; } flags=() while IFS='' read -r flag; do flags+=("$flag") done < <(git rev-parse --flags "$@") opts=" $FORGIT_FZF_DEFAULT_OPTS --preview=\"$FORGIT blame_preview {} ${flags[*]}\" $FORGIT_BLAME_FZF_OPTS " # flags is not quoted here, which is fine given that they are retrieved # with git rev-parse and can only contain flags file=$(FZF_DEFAULT_OPTS="$opts" fzf) [[ -z "$file" ]] && return 1 _forgit_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_preview() { quoted_files=() for file in "$FORGIT_GI_TEMPLATES/$1"{,.gitignore}; do quoted_files+=("'$file'") done _forgit_pager ignore "${quoted_files[@]}" 2>/dev/null } _forgit_ignore() { [ -d "$FORGIT_GI_REPO_LOCAL" ] || _forgit_ignore_update local IFS args opts opts=" $FORGIT_FZF_DEFAULT_OPTS -m --preview-window='right:70%' --preview=\"$FORGIT ignore_preview {2}\" $FORGIT_IGNORE_FZF_OPTS " args=("$@") if [[ $# -eq 0 ]]; then args=() while IFS='' read -r arg; do args+=("$arg") done < <(_forgit_ignore_list | nl -w4 -s' ' | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $2}') fi [ ${#args[@]} -eq 0 ] && return 1 _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" } public_commands=( "add" "blame" "branch_delete" "checkout_branch" "checkout_commit" "checkout_file" "checkout_tag" "cherry_pick" "cherry_pick_from_branch" "clean" "diff" "fixup" "ignore" "log" "reflog" "rebase" "reset_head" "revert_commit" "stash_show" "stash_push" ) private_commands=( "add_preview" "blame_preview" "branch_preview" "checkout_commit_preview" "checkout_file_preview" "cherry_pick_from_branch_preview" "cherry_pick_preview" "clean_preview" "diff_enter" "file_preview" "ignore_preview" "revert_preview" "reset_head_preview" "stash_push_preview" "stash_show_preview" "yank_sha" "yank_stash_name" "log_preview" "log_enter" "exec_diff" "diff_view" "edit_diffed_file" "edit_add_file" "pager" ) cmd="$1" shift # shellcheck disable=SC2076 if [[ ! " ${public_commands[*]} " =~ " ${cmd} " ]] && [[ ! " ${private_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" "${public_commands[@]}" exit 1 fi _forgit_"${cmd}" "$@"