diff --git a/.config/fish/completions/_git-forgit b/.config/fish/completions/_git-forgit new file mode 100644 index 0000000..7760511 --- /dev/null +++ b/.config/fish/completions/_git-forgit @@ -0,0 +1,131 @@ +#compdef git-forgit -p forgit::* +#description Utility tool for using git interactively +# +# forgit completions for zsh +# +# Place this file in your $fpath (e.g. /usr/share/zsh/site-functions) to enable +# tab completions for forgit. + +_git-branches() { + _alternative "branches:branchname:($(git branch -a --format '%(refname:short)'))" +} + +_git-checkout-file() { + _alternative "files:filename:($(git ls-files --modified))" +} + +_git-stash-show() { + _alternative "files:filename:($(git stash list | sed -n -e 's/:.*//p'))" +} + +# The completions for git already define a _git-diff completion function, but +# it provides the wrong results when called from _git-forgit because it heavily +# depends on the context it's been called from (usage of $curcontext and +# $CURRENT), so we use a simplified version here which always provides the same +# results independent of the context. +_git-forgit-diff() { + _alternative \ + 'commit-ranges::__git_commit_ranges' \ + 'blobs-and-trees-in-treeish::__git_blobs_and_trees_in_treeish' \ + 'files::__git_changed-in-working-tree_files' \ + 'blobs::__git_blobs ' +} + +_git-staged() { + _alternative "files:filename:($(git diff --name-only --staged))" +} + +_git-forgit-reflog() { + declare -a cmds + cmds=('expire:prune old reflog entries' 'delete:delete entries from reflog' 'show:show log of ref' 'exists:check whether a ref has a reflog') + _alternative 'cmds:: _describe -t cmds cmd cmds' 'refs:: __git_references' +} + +_git-forgit() { + local subcommand cmd + subcommand="${words[1]}" + if [[ "$subcommand" != "forgit"* ]]; then + # Forgit is obviously called via a git alias. Get the original + # aliased subcommand and proceed as if it was the previous word. + cmd=$(git config --get "alias.$subcommand" | cut -d ' ' -f 2) + else + # The last word is the relevant command + cmd=${words[(( ${#words} - 1 ))]} + fi + + case ${cmd} in + forgit) + local -a subcommands + subcommands=( + 'add:git add selector' + 'blame:git blame viewer' + 'branch_delete:git branch deletion selector' + 'checkout_branch:git checkout branch selector' + 'checkout_commit:git checkout commit selector' + 'checkout_file:git checkout-file selector' + 'checkout_tag:git checkout tag selector' + 'cherry_pick:git cherry-picking' + 'cherry_pick_from_branch:git cherry-picking with interactive branch selection' + 'clean:git clean selector' + 'diff:git diff viewer' + 'fixup:git fixup' + 'ignore:git ignore generator' + 'log:git commit viewer' + 'reflog:git reflog viewer' + 'rebase:git rebase' + 'reset_head:git reset HEAD (unstage) selector' + 'revert_commit:git revert commit selector' + 'stash_show:git stash viewer' + 'stash_push:git stash push selector' + ) + _describe -t commands 'git forgit' subcommands + ;; + add) _git-add ;; + branch_delete) _git-branches ;; + checkout_branch) _git-branches ;; + checkout_commit) __git_recent_commits ;; + checkout_file) _git-checkout-file ;; + checkout_tag) __git_tags ;; + cherry_pick) _git-cherry-pick ;; + cherry_pick_from_branch) _git-branches ;; + clean) _git-clean ;; + diff) _git-forgit-diff ;; + fixup) __git_branch_names ;; + log) _git-log ;; + reflog) _git-forgit-reflog ;; + rebase) _git-rebase ;; + reset_head) _git-staged ;; + revert_commit) __git_recent_commits ;; + stash_show) _git-stash-show ;; + esac +} + +# We're reusing existing completion functions, so load those first +# if not already loaded and check if completion function exists afterwards. +(( $+functions[_git-add] )) || _git +(( $+functions[_git-add] )) || return 1 +# Completions for forgit plugin shell functions (also works for aliases) +compdef _git-add forgit::add +compdef _git-branches forgit::branch::delete +compdef _git-branches forgit::checkout::branch +compdef __git_recent_commits forgit::checkout::commit +compdef _git-checkout-file forgit::checkout::file +compdef __git_tags forgit::checkout::tag +compdef _git-cherry-pick forgit::cherry::pick +compdef _git-branches forgit::cherry::pick::from::branch +compdef _git-clean forgit::clean +compdef _git-forgit-diff forgit::diff +compdef __git_branch_names forgit::fixup +compdef _git-log forgit::log +compdef _git-reflog forgit::reflog +compdef _git-rebase forgit::rebase +compdef _git-staged forgit::reset::head +compdef __git_recent_commits forgit::revert::commit +compdef _git-stash-show forgit::stash::show + +# this is the case of calling the command and pressing tab +# the very first time of a shell session, we have to manually +# call the dispatch function +if [[ $funcstack[1] == "_git-forgit" ]]; then + _git-forgit "$@" +fi diff --git a/.config/fish/completions/git-forgit.bash b/.config/fish/completions/git-forgit.bash new file mode 100755 index 0000000..9f500a8 --- /dev/null +++ b/.config/fish/completions/git-forgit.bash @@ -0,0 +1,159 @@ +# forgit completions for bash + +# When using forgit as a subcommand of git, put this file in one of the +# following places and it will be loaded automatically on tab completion of +# 'git forgit' or any configured git aliases of it: +# +# /usr/share/bash-completion/completions +# ~/.local/share/bash-completion/completions +# +# When using forgit via the shell plugin, source this file explicitly after +# forgit.plugin.zsh to enable tab completion for shell functions and aliases. + +_git_branch_delete() +{ + __gitcomp_nl "$(__git_heads)" +} + +_git_checkout_branch() +{ + __gitcomp_nl "$(__git branch -a --format '%(refname:short)')" +} + +_git_checkout_file() +{ + __gitcomp_nl "$(__git ls-files --modified)" +} + +_git_checkout_tag() +{ + __gitcomp_nl "$(__git_tags)" +} + +_git_stash_show() +{ + __gitcomp_nl "$(__git stash list | sed -n -e 's/:.*//p')" +} + +# Completion for git-forgit +# This includes git aliases, e.g. "alias.cb=forgit checkout_branch" will +# correctly complete available branches on "git cb". +_git_forgit() +{ + local subcommand cword cur prev cmds + + subcommand="${COMP_WORDS[1]}" + if [[ "$subcommand" != "forgit" ]] + then + # Forgit is obviously called via a git alias. Get the original + # aliased subcommand and proceed as if it was the previous word. + prev=$(git config --get "alias.$subcommand" | cut -d' ' -f 2) + cword=$((${COMP_CWORD} + 1)) + else + cword=${COMP_CWORD} + prev=${COMP_WORDS[COMP_CWORD-1]} + fi + + cur=${COMP_WORDS[COMP_CWORD]} + + cmds=" + 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 + " + + case ${cword} in + 2) + COMPREPLY=($(compgen -W "${cmds}" -- ${cur})) + ;; + 3) + case ${prev} in + add) _git_add ;; + branch_delete) _git_branch_delete ;; + checkout_branch) _git_checkout_branch ;; + checkout_commit) _git_checkout ;; + checkout_file) _git_checkout_file ;; + checkout_tag) _git_checkout_tag ;; + cherry_pick) _git_cherry_pick ;; + cherry_pick_from_branch) _git_checkout_branch ;; + clean) _git_clean ;; + diff) _git_diff ;; + fixup) _git_branch ;; + log) _git_log ;; + reflog) _git_reflog ;; + rebase) _git_rebase ;; + reset_head) _git_reset ;; + revert_commit) _git_revert ;; + stash_show) _git_stash_show ;; + esac + ;; + *) + COMPREPLY=() + ;; + esac +} + +# Check if forgit plugin is loaded +if [[ $(type -t forgit::add) == function ]] +then + # We're reusing existing git completion functions, so load those first + # and check if completion function exists afterwards. + _completion_loader git + [[ $(type -t __git_complete) == function ]] || return 1 + + # Completion for forgit plugin shell functions + __git_complete forgit::add _git_add + __git_complete forgit::branch::delete _git_branch_delete + __git_complete forgit::checkout::branch _git_checkout_branch + __git_complete forgit::checkout::commit _git_checkout + __git_complete forgit::checkout::file _git_checkout_file + __git_complete forgit::checkout::tag _git_checkout_tag + __git_complete forgit::cherry::pick _git_cherry_pick + __git_complete forgit::cherry::pick::from::branch _git_checkout_branch + __git_complete forgit::clean _git_clean + __git_complete forgit::diff _git_diff + __git_complete forgit::fixup _git_branch + __git_complete forgit::log _git_log + __git_complete forgit::reflog _git_reflog + __git_complete forgit::rebase _git_rebase + __git_complete forgit::reset::head _git_reset + __git_complete forgit::revert::commit _git_revert + __git_complete forgit::stash::show _git_stash_show + + # Completion for forgit plugin shell aliases + if [[ -z "$FORGIT_NO_ALIASES" ]]; then + __git_complete "${forgit_add}" _git_add + __git_complete "${forgit_branch_delete}" _git_branch_delete + __git_complete "${forgit_checkout_branch}" _git_checkout_branch + __git_complete "${forgit_checkout_commit}" _git_checkout + __git_complete "${forgit_checkout_file}" _git_checkout_file + __git_complete "${forgit_checkout_tag}" _git_checkout_tag + __git_complete "${forgit_cherry_pick}" _git_checkout_branch + __git_complete "${forgit_clean}" _git_clean + __git_complete "${forgit_diff}" _git_diff + __git_complete "${forgit_fixup}" _git_branch + __git_complete "${forgit_log}" _git_log + __git_complete "${forgit_reflog}" _git_reflog + __git_complete "${forgit_rebase}" _git_rebase + __git_complete "${forgit_reset_head}" _git_reset + __git_complete "${forgit_revert_commit}" _git_revert + __git_complete "${forgit_stash_show}" _git_stash_show + fi +fi diff --git a/.config/fish/completions/git-forgit.fish b/.config/fish/completions/git-forgit.fish new file mode 100644 index 0000000..72c896b --- /dev/null +++ b/.config/fish/completions/git-forgit.fish @@ -0,0 +1,61 @@ +# +# forgit completions for fish plugin +# +# Place this file inside your /completions/ directory. +# It's usually located at ~/.config/fish/completions/. The file is lazily +# sourced when git-forgit command or forgit subcommand of git is invoked. + +function __fish_forgit_needs_subcommand + for subcmd in 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 + if contains -- $subcmd (commandline -opc) + return 1 + end + end + return 0 +end + +# Load helper functions in git completion file +not functions -q __fish_git && source $__fish_data_dir/completions/git.fish + +# No file completion by default +complete -c git-forgit -x + +complete -c git-forgit -n __fish_forgit_needs_subcommand -a add -d 'git add selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a blame -d 'git blame viewer' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a branch_delete -d 'git branch deletion selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a checkout_branch -d 'git checkout branch selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a checkout_commit -d 'git checkout commit selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a checkout_file -d 'git checkout-file selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a checkout_tag -d 'git checkout tag selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a cherry_pick -d 'git cherry-picking' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a cherry_pick_from_branch -d 'git cherry-picking with interactive branch selection' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a clean -d 'git clean selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a diff -d 'git diff viewer' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a fixup -d 'git fixup' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a ignore -d 'git ignore generator' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a log -d 'git commit viewer' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a reflog -d 'git reflog viewer' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a rebase -d 'git rebase' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a reset_head -d 'git reset HEAD (unstage) selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a revert_commit -d 'git revert commit selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a stash_show -d 'git stash viewer' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a stash_push -d 'git stash push selector' + +complete -c git-forgit -n '__fish_seen_subcommand_from add' -a "(complete -C 'git add ')" +complete -c git-forgit -n '__fish_seen_subcommand_from branch_delete' -a "(__fish_git_local_branches)" +complete -c git-forgit -n '__fish_seen_subcommand_from checkout_branch' -a "(complete -C 'git switch ')" +complete -c git-forgit -n '__fish_seen_subcommand_from checkout_commit' -a "(__fish_git_commits)" +complete -c git-forgit -n '__fish_seen_subcommand_from checkout_file' -a "(__fish_git_files modified)" +complete -c git-forgit -n '__fish_seen_subcommand_from checkout_tag' -a "(__fish_git_tags)" -d Tag +complete -c git-forgit -n '__fish_seen_subcommand_from cherry_pick' -a "(complete -C 'git cherry-pick ')" +complete -c git-forgit -n '__fish_seen_subcommand_from clean' -a "(__fish_git_files untracked ignored)" +complete -c git-forgit -n '__fish_seen_subcommand_from fixup' -a "(__fish_git_local_branches)" +complete -c git-forgit -n '__fish_seen_subcommand_from log' -a "(complete -C 'git log ')" +complete -c git-forgit -n '__fish_seen_subcommand_from reflog' -a "(complete -C 'git reflog ')" +complete -c git-forgit -n '__fish_seen_subcommand_from rebase' -a "(complete -C 'git rebase ')" +complete -c git-forgit -n '__fish_seen_subcommand_from reset_head' -a "(__fish_git_files all-staged)" +complete -c git-forgit -n '__fish_seen_subcommand_from revert_commit' -a "(__fish_git_commits)" +complete -c git-forgit -n '__fish_seen_subcommand_from stash_show' -a "(__fish_git_complete_stashes)" +complete -c git-forgit -n '__fish_seen_subcommand_from stash_push' -a "(__fish_git_files modified deleted modified-staged-deleted)" diff --git a/.config/fish/conf.d/bin/git-forgit b/.config/fish/conf.d/bin/git-forgit new file mode 100755 index 0000000..7f96f9d --- /dev/null +++ b/.config/fish/conf.d/bin/git-forgit @@ -0,0 +1,1070 @@ +#!/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}" "$@" diff --git a/.config/fish/conf.d/forgit.plugin.fish b/.config/fish/conf.d/forgit.plugin.fish new file mode 100644 index 0000000..18761d0 --- /dev/null +++ b/.config/fish/conf.d/forgit.plugin.fish @@ -0,0 +1,54 @@ +# MIT (c) Chris Apple + +set -l install_dir (dirname (status dirname)) +set -x FORGIT_INSTALL_DIR "$install_dir/conf.d" +set -x FORGIT "$FORGIT_INSTALL_DIR/bin/git-forgit" +if not test -e "$FORGIT" + set -x FORGIT_INSTALL_DIR "$install_dir/vendor_conf.d" + set -x FORGIT "$FORGIT_INSTALL_DIR/bin/git-forgit" +end + +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 not set -x | grep -q "^$var\b" + 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 + +# alias `git-forgit` to the full-path of the command +alias git-forgit "$FORGIT" + +# register abbreviations +if test -z "$FORGIT_NO_ALIASES" + abbr -a -- (string collect $forgit_add; or string collect "ga") git-forgit add + abbr -a -- (string collect $forgit_reset_head; or string collect "grh") git-forgit reset_head + abbr -a -- (string collect $forgit_log; or string collect "glo") git-forgit log + abbr -a -- (string collect $forgit_reflog; or string collect "grl") git-forgit reflog + abbr -a -- (string collect $forgit_diff; or string collect "gd") git-forgit diff + abbr -a -- (string collect $forgit_ignore; or string collect "gi") git-forgit ignore + abbr -a -- (string collect $forgit_checkout_file; or string collect "gcf") git-forgit checkout_file + abbr -a -- (string collect $forgit_checkout_branch; or string collect "gcb") git-forgit checkout_branch + abbr -a -- (string collect $forgit_branch_delete; or string collect "gbd") git-forgit branch_delete + abbr -a -- (string collect $forgit_clean; or string collect "gclean") git-forgit clean + abbr -a -- (string collect $forgit_stash_show; or string collect "gss") git-forgit stash_show + abbr -a -- (string collect $forgit_stash_push; or string collect "gsp") git-forgit stash_push + abbr -a -- (string collect $forgit_cherry_pick; or string collect "gcp") git-forgit cherry_pick_from_branch + abbr -a -- (string collect $forgit_rebase; or string collect "grb") git-forgit rebase + abbr -a -- (string collect $forgit_fixup; or string collect "gfu") git-forgit fixup + abbr -a -- (string collect $forgit_checkout_commit; or string collect "gco") git-forgit checkout_commit + abbr -a -- (string collect $forgit_revert_commit; or string collect "grc") git-forgit revert_commit + abbr -a -- (string collect $forgit_blame; or string collect "gbl") git-forgit blame + abbr -a -- (string collect $forgit_checkout_tag; or string collect "gct") git-forgit checkout_tag +end diff --git a/.config/fish/fish_plugins b/.config/fish/fish_plugins index fb065ce..c9c0d90 100644 --- a/.config/fish/fish_plugins +++ b/.config/fish/fish_plugins @@ -1,3 +1,4 @@ jorgebucaran/fisher jhillyerd/plugin-git jorgebucaran/nvm.fish +wfxr/forgit diff --git a/.config/fish/fish_variables b/.config/fish/fish_variables index f25e0e1..fe88763 100644 --- a/.config/fish/fish_variables +++ b/.config/fish/fish_variables @@ -1,7 +1,7 @@ # This file contains fish universal variable definitions. # VERSION: 3.0 SETUVAR --export ELECTRON_OZONE_PLATFORM_HINT:auto -SETUVAR --export FZF_DEFAULT_OPTS:\x2d\x2dwalker\x2dskip\x3d\x2esteam\x2c\x2evscode\x2c\x2ecargo\x2c\x2enpm\x2c\x2envm\x2cSteam +SETUVAR --export FZF_DEFAULT_OPTS:\x2d\x2dwalker\x2dskip\x3d\x2esteam\x2c\x2evscode\x2c\x2ecargo\x2c\x2enpm\x2c\x2envm\x2cSteam\x2cgo\x2c\x2ecache\x2c\x2epub\x2dcache\x2c\x2erustup SETUVAR --export --path PATH:/usr/local/bin\x1e/usr/local/sbin\x1e/usr/bin\x1e/usr/sbin\x1e/usr/local/go/bin SETUVAR --export RANGER_LOAD_DEFAULT_RC:FALSE SETUVAR Z_DATA_DIR:/home/foton/\x2elocal/share/z @@ -9,8 +9,9 @@ SETUVAR __fish_initialized:3400 SETUVAR _fisher_jhillyerd_2F_plugin_2D_git_files:\x7e/\x2econfig/fish/functions/__git\x2ebranch_has_wip\x2efish\x1e\x7e/\x2econfig/fish/functions/__git\x2ecurrent_branch\x2efish\x1e\x7e/\x2econfig/fish/functions/__git\x2edefault_branch\x2efish\x1e\x7e/\x2econfig/fish/functions/__git\x2edestroy\x2efish\x1e\x7e/\x2econfig/fish/functions/__git\x2einit\x2efish\x1e\x7e/\x2econfig/fish/functions/gbage\x2efish\x1e\x7e/\x2econfig/fish/functions/gbda\x2efish\x1e\x7e/\x2econfig/fish/functions/gdv\x2efish\x1e\x7e/\x2econfig/fish/functions/gignored\x2efish\x1e\x7e/\x2econfig/fish/functions/glp\x2efish\x1e\x7e/\x2econfig/fish/functions/grename\x2efish\x1e\x7e/\x2econfig/fish/functions/grt\x2efish\x1e\x7e/\x2econfig/fish/functions/gtest\x2efish\x1e\x7e/\x2econfig/fish/functions/gtl\x2efish\x1e\x7e/\x2econfig/fish/functions/gunwip\x2efish\x1e\x7e/\x2econfig/fish/functions/gwip\x2efish\x1e\x7e/\x2econfig/fish/conf\x2ed/git\x2efish SETUVAR _fisher_jorgebucaran_2F_fisher_files:\x7e/\x2econfig/fish/functions/fisher\x2efish\x1e\x7e/\x2econfig/fish/completions/fisher\x2efish SETUVAR _fisher_jorgebucaran_2F_nvm_2E_fish_files:\x7e/\x2econfig/fish/functions/_nvm_index_update\x2efish\x1e\x7e/\x2econfig/fish/functions/_nvm_list\x2efish\x1e\x7e/\x2econfig/fish/functions/_nvm_version_activate\x2efish\x1e\x7e/\x2econfig/fish/functions/_nvm_version_deactivate\x2efish\x1e\x7e/\x2econfig/fish/functions/nvm\x2efish\x1e\x7e/\x2econfig/fish/conf\x2ed/nvm\x2efish\x1e\x7e/\x2econfig/fish/completions/nvm\x2efish -SETUVAR _fisher_plugins:jorgebucaran/fisher\x1ejhillyerd/plugin\x2dgit\x1ejorgebucaran/nvm\x2efish +SETUVAR _fisher_plugins:jorgebucaran/fisher\x1ejhillyerd/plugin\x2dgit\x1ejorgebucaran/nvm\x2efish\x1ewfxr/forgit SETUVAR _fisher_upgraded_to_4_4:\x1d +SETUVAR _fisher_wfxr_2F_forgit_files:\x7e/\x2econfig/fish/conf\x2ed/bin\x1e\x7e/\x2econfig/fish/conf\x2ed/forgit\x2eplugin\x2efish\x1e\x7e/\x2econfig/fish/completions/_git\x2dforgit\x1e\x7e/\x2econfig/fish/completions/git\x2dforgit\x2ebash\x1e\x7e/\x2econfig/fish/completions/git\x2dforgit\x2efish SETUVAR fish_color_autosuggestion:4c566a SETUVAR fish_color_cancel:\x2d\x2dreverse SETUVAR fish_color_command:81a1c1 @@ -50,4 +51,4 @@ SETUVAR fish_pager_color_selected_background:\x2d\x2dbackground\x3dbrblack SETUVAR fish_pager_color_selected_completion:\x1d SETUVAR fish_pager_color_selected_description:\x1d SETUVAR fish_pager_color_selected_prefix:\x1d -SETUVAR fish_user_paths:/home/foton/\x2ebin\x1e/opt/Obsidian\x1e/home/foton/go/bin\x1e/home/foton/code/dart\x2dsdk/bin\x1e/home/greg/\x2elocal/bin\x1e/home/greg/\x2ebin\x1e/home/greg/\x2ecargo/bin\x1e/home/greg/go/bin\x1e/usr/local/go/bin +SETUVAR fish_user_paths:/home/foton/\x2escripts\x1e/home/foton/\x2ebin\x1e/opt/Obsidian\x1e/home/foton/go/bin\x1e/home/foton/code/dart\x2dsdk/bin\x1e/home/greg/\x2elocal/bin\x1e/home/greg/\x2ebin\x1e/home/greg/\x2ecargo/bin\x1e/home/greg/go/bin\x1e/usr/local/go/bin diff --git a/.scripts/dnf.sh b/.scripts/dnf.sh new file mode 100755 index 0000000..403a429 --- /dev/null +++ b/.scripts/dnf.sh @@ -0,0 +1,91 @@ + +#!/usr/bin/bash +readonly basename="$(basename "$0")" + +if ! hash fzf &> /dev/null; then + printf 'Error: Missing dep: fzf is required to use %s.\n' "${basename}" >&2 + exit 64 +fi + +#Colors +declare -r esc=$'\033' +declare -r BLUE="${esc}[1m${esc}[34m" +declare -r RED="${esc}[31m" +declare -r GREEN="${esc}[32m" +declare -r YELLOW="${esc}[33m" +declare -r CYAN="${esc}[36m" +# Base commands +readonly QRY="dnf --cacheonly --quiet repoquery " +readonly PRVW="dnf --cacheonly --quiet --color=always info" +readonly QRY_PRFX=' ' +readonly QRY_SFFX=' > ' +# Install mode +readonly INS_QRYS="${QRY} --qf '${CYAN}%{name}'" +readonly INS_PRVW="${PRVW}" +readonly INS_PRMPT="${CYAN}${QRY_PRFX}Install packages${QRY_SFFX}" +# Remove mode +readonly RMV_QRYS="${QRY} --installed --qf '${RED}%{name}'" +readonly RMV_PRVW="${PRVW} --installed" +readonly RMV_PRMPT="${RED}${QRY_PRFX}Remove packages${QRY_SFFX}" +# Remove-userinstalled mode +readonly RUI_QRYS="${QRY} --userinstalled --qf '${YELLOW}%{name}'" +readonly RUI_PRVW="${PRVW} --installed" +readonly RUI_PRMPT="${YELLOW}${QRY_PRFX}Remove User-Installed${QRY_SFFX}" +# Updates mode +readonly UPD_QRY="${QRY} --upgrades --qf '${GREEN}%{name}'" +readonly UPD_QRYS="if [[ $(${UPD_QRY} | wc -c) -ne 0 ]]; then ${UPD_QRY}; else echo ${GREEN}No updates available.; echo Try refreshing metadata cache...; fi" +readonly UPD_PRVW="${PRVW}" +readonly UPD_PRMPT="${GREEN}${QRY_PRFX}Upgrade packages${QRY_SFFX}" + +mapfile -d '' fhelp <<-EOF + + "${basename}" + Interactive package manager for Fedora + + Alt-i Install mode (default) + Alt-r Remove mode + Alt-e Remove User-Installed mode + Alt-u Updates mode + Alt-m Update package metadata cache + + Enter Confirm selection + Tab Mark package () + Shift-Tab Unmark package + Ctrl-a Select all + + ? Help (this page) + ESC Quit +EOF + +declare tmp_file +if tmp_file="$(mktemp --tmpdir "${basename}".XXXXXX)"; then + printf 'in' > "${tmp_file}" && + SHELL='/bin/bash' \ + FZF_DEFAULT_COMMAND="${INS_QRYS}" \ + fzf \ + --ansi \ + --multi \ + --query=$* \ + --header=" ${basename} | Press Alt+? for help or ESC to quit" \ + --header-first \ + --prompt="${INS_PRMPT}" \ + --marker=' ' \ + --preview-window='right,67%,wrap' \ + --preview="${INS_PRVW} {1}" \ + --bind="enter:execute(if grep -q 'in' \"${tmp_file}\"; then sudo dnf install {+}; + elif grep -q 'rm' \"${tmp_file}\"; then sudo dnf remove {+}; \ + elif grep -q 'up' \"${tmp_file}\"; then sudo dnf upgrade {+}; fi; \ + read -s -r -n1 -p $'\n${BLUE}Press any key to continue...' && printf '\n')" \ + --bind="alt-i:unbind(alt-i)+reload(${INS_QRYS})+change-preview(${INS_PRVW} {1})+change-prompt(${INS_PRMPT})+execute-silent(printf 'in' > \"${tmp_file}\")+first+rebind(alt-r,alt-e,alt-u)" \ + --bind="alt-r:unbind(alt-r)+reload(${RMV_QRYS})+change-preview(${RMV_PRVW} {1})+change-prompt(${RMV_PRMPT})+execute-silent(printf 'rm' > \"${tmp_file}\")+first+rebind(alt-i,alt-e,alt-u)" \ + --bind="alt-e:unbind(alt-e)+reload(${RUI_QRYS})+change-preview(${RUI_PRVW} {1})+change-prompt(${RUI_PRMPT})+execute-silent(printf 'rm' > \"${tmp_file}\")+first+rebind(alt-i,alt-r,alt-u)" \ + --bind="alt-u:unbind(alt-u)+reload(${UPD_QRYS})+change-preview(${UPD_PRVW} {1})+change-prompt(${UPD_PRMPT})+execute-silent(printf 'up' > \"${tmp_file}\")+first+rebind(alt-i,alt-r,alt-e)" \ + --bind="alt-m:execute(sudo dnf makecache;read -s -r -n1 -p $'\n${BLUE}Press any key to continue...' && printf '\n')" \ + --bind="alt-?:preview(printf \"${fhelp[0]}\")" \ + --bind="ctrl-a:select-all" + + rm -f "${tmp_file}" &> /dev/null +else + printf 'Error: Failed to create tmp file. $TMPDIR (or /tmp if $TMPDIR is unset) may not be writable.\n' >&2 + exit 65 +fi