forgit.zsh
1 #!/usr/bin/env zsh 2 3 forgit::warn() { printf "%b[Warn]%b %s\n" '\e[0;33m' '\e[0m' "$@" >&2; } 4 forgit::info() { printf "%b[Info]%b %s\n" '\e[0;32m' '\e[0m' "$@" >&2; } 5 forgit::inside_work_tree() { git rev-parse --is-inside-work-tree >/dev/null; } 6 7 # https://github.com/so-fancy/diff-so-fancy 8 hash diff-so-fancy &>/dev/null && forgit_fancy='|diff-so-fancy' 9 # https://github.com/wfxr/emoji-cli 10 hash emojify &>/dev/null && forgit_emojify='|emojify' 11 12 # git commit viewer 13 forgit::log() { 14 forgit::inside_work_tree || return 1 15 local cmd="echo {} |grep -o '[a-f0-9]\{7\}' |head -1 |xargs -I% git show --color=always % $* $forgit_emojify $forgit_fancy" 16 eval "git log --graph --color=always --format='%C(auto)%h%d %s %C(black)%C(bold)%cr' $* $forgit_emojify" | 17 forgit::fzf +s +m --tiebreak=index \ 18 --bind="enter:execute($cmd | $PAGER)" \ 19 --bind="ctrl-y:execute-silent(echo {} |grep -o '[a-f0-9]\{7\}' |${FORGIT_COPY_CMD:-pbcopy})" \ 20 --preview="$cmd" 21 } 22 23 # git diff viewer 24 forgit::diff() { 25 forgit::inside_work_tree || return 1 26 local cmd files 27 cmd="git diff --color=always -- {} $forgit_emojify $forgit_fancy" 28 files="$*" 29 [[ $# -eq 0 ]] && files=$(git rev-parse --show-toplevel) 30 git ls-files --modified "$files"| 31 forgit::fzf +m -0 \ 32 --bind="enter:execute($cmd | $PAGER)" \ 33 --preview="$cmd" 34 } 35 36 # git add selector 37 forgit::add() { 38 forgit::inside_work_tree || return 1 39 local changed unmerged untracked files 40 changed=$(git config --get-color color.status.changed red) 41 unmerged=$(git config --get-color color.status.unmerged red) 42 untracked=$(git config --get-color color.status.untracked red) 43 files=$(git -c color.status=always status --short | 44 grep -F -e "$changed" -e "$unmerged" -e "$untracked"| 45 awk '{printf "[%10s] ", $1; $1=""; print $0}' | 46 forgit::fzf -0 -m --nth 2..,.. \ 47 --preview="git diff --color=always -- {-1} $forgit_emojify $forgit_fancy" | 48 cut -d] -f2 | 49 sed 's/.* -> //') # for rename case 50 [[ -n "$files" ]] && echo "$files" |xargs -I{} git add {} && git status --short && return 51 echo 'Nothing to add.' 52 } 53 54 # git checkout-restore selector 55 forgit::restore() { 56 forgit::inside_work_tree || return 1 57 local cmd files 58 cmd="git diff --color=always -- {} $forgit_emojify $forgit_fancy" 59 files="$(git ls-files --modified "$(git rev-parse --show-toplevel)"| 60 forgit::fzf -m -0 --preview="$cmd")" 61 [[ -n "$files" ]] && echo "$files" |xargs -I{} git checkout {} && git status --short && return 62 echo 'Nothing to restore.' 63 } 64 65 # git clean selector 66 forgit::clean() { 67 forgit::inside_work_tree || return 1 68 local files 69 # Note: Postfix '/' in directory path should be removed. Otherwise the directory itself will not be removed. 70 files=$(git clean -xdfn "$@"| awk '{print $3}'| forgit::fzf -m -0 |sed 's#/$##') 71 [[ -n "$files" ]] && echo "$files" |xargs -I{} git clean -xdf {} 72 echo 'Nothing to clean.' 73 } 74 75 # git ignore generator 76 export FORGIT_GI_REPO_REMOTE=${FORGIT_GI_REPO_REMOTE:-https://github.com/dvcs/gitignore} 77 export FORGIT_GI_REPO_LOCAL=${FORGIT_GI_REPO_LOCAL:-~/.forgit/gi/repos/dvcs/gitignore} 78 export FORGIT_GI_TEMPLATES=${FORGIT_GI_TEMPLATES:-$FORGIT_GI_REPO_LOCAL/templates} 79 80 forgit::ignore() { 81 [ -d "$FORGIT_GI_REPO_LOCAL" ] || forgit::ignore::update 82 local IFS cmd args cat 83 # https://github.com/sharkdp/bat.git 84 hash bat &>/dev/null && cat='bat -l gitignore --color=always' || cat="cat" 85 cmd="$cat $FORGIT_GI_TEMPLATES/{2}{,.gitignore} 2>/dev/null" 86 # shellcheck disable=SC2206,2207 87 IFS=$'\n' args=($@) && [[ $# -eq 0 ]] && args=($(forgit::ignore::list | nl -nrn -w4 -s' ' | 88 forgit::fzf -m --preview="$cmd" --preview-window="right:70%" | awk '{print $2}')) 89 [ ${#args[@]} -eq 0 ] && return 1 90 # shellcheck disable=SC2068 91 if hash bat &>/dev/null; then 92 forgit::ignore::get ${args[@]} | bat -l gitignore 93 else 94 forgit::ignore::get ${args[@]} 95 fi 96 } 97 forgit::ignore::update() { 98 if [[ -d "$FORGIT_GI_REPO_LOCAL" ]]; then 99 forgit::info 'Updating gitignore repo...' 100 (cd "$FORGIT_GI_REPO_LOCAL" && git pull --no-rebase --ff) || return 1 101 else 102 forgit::info 'Initializing gitignore repo...' 103 git clone --depth=1 "$FORGIT_GI_REPO_REMOTE" "$FORGIT_GI_REPO_LOCAL" 104 fi 105 } 106 forgit::ignore::get() { 107 local item filename header 108 for item in "$@"; do 109 if filename=$(find -L "$FORGIT_GI_TEMPLATES" -type f \( -iname "${item}.gitignore" -o -iname "${item}" \) -print -quit); then 110 [[ -z "$filename" ]] && forgit::warn "No gitignore template found for '$item'." && continue 111 header="${filename##*/}" && header="${header%.gitignore}" 112 echo "### $header" && cat "$filename" && echo 113 fi 114 done 115 } 116 forgit::ignore::list() { 117 find "$FORGIT_GI_TEMPLATES" -print |sed -e 's#.gitignore$##' -e 's#.*/##' | sort -fu 118 } 119 120 forgit::fzf() { 121 FZF_DEFAULT_OPTS=" 122 $FZF_DEFAULT_OPTS 123 --ansi 124 --height '80%' 125 --bind='alt-k:preview-up,alt-p:preview-up' 126 --bind='alt-j:preview-down,alt-n:preview-down' 127 --bind='ctrl-r:toggle-all' 128 --bind='ctrl-s:toggle-sort' 129 --bind='?:toggle-preview' 130 --preview-window='right:60%' 131 --bind='alt-w:toggle-preview-wrap' 132 $FORGIT_FZF_DEFAULT_OPTS 133 " fzf "$@" 134 } 135 136 "forgit::$1"