From bb83b3cba0ec009cf0e8d5dc5abf8cd82bced76e Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Wed, 29 Jan 2025 07:04:07 +0100 Subject: [PATCH] Feature: add forgit show command (#417) Introducing `show` as a new subcommand of `forgit`, an interactive version of `git show`. The configured default alias is `gso`. Alt-T can be used to toggle between showing the diff and the commit message in the fzf preview window. The new command in used instead of `forgit diff` when pressing enter in `forgit log`. This fixes the display of diffs for merge commits. **Note**: we have a requirement for the `fzf` version now. The minimum required `fzf` version is 0.49.0. If the installed version is lower, forgit will exit with an error message. Fixes #416. --- README.md | 13 ++++- bin/git-forgit | 98 ++++++++++++++++++++++++++++++++++++- completions/_git-forgit | 2 + completions/git-forgit.bash | 4 ++ completions/git-forgit.fish | 1 + conf.d/forgit.plugin.fish | 1 + forgit.plugin.zsh | 6 +++ 7 files changed, 123 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ae6d695b..7f4fd2c6 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,13 @@ It's **lightweight** and **easy to use**. # 📥 Installation -*Make sure you have [`fzf`](https://github.com/junegunn/fzf) installed.* +## Requirements + +- [`fzf`](https://github.com/junegunn/fzf) version `0.49.0` or higher + + If your OS package manager bundles an older version of `fzf`, you might install it using [`fzf`'s own install script'](https://github.com/junegunn/fzf?tab=readme-ov-file#using-git). + +## Shell package managers ``` zsh # for zplug @@ -101,6 +107,8 @@ Then add the following to your shell's config file: - **Interactive `git diff` viewer** (`gd`) +- **Interactive `git show` viewer** (`gso`) + - **Interactive `git reset HEAD ` selector** (`grh`) - **Interactive `git checkout ` selector** (`gcf`) @@ -172,6 +180,7 @@ You can change the default aliases by defining these variables below. forgit_log=glo forgit_reflog=grl forgit_diff=gd +forgit_show=gso forgit_add=ga forgit_reset_head=grh forgit_ignore=gi @@ -232,6 +241,7 @@ These are passed to the according `git` calls. | `glo` | `FORGIT_LOG_GIT_OPTS` | | `grl` | `FORGIT_REFLOG_GIT_OPTS` | | `gd` | `FORGIT_DIFF_GIT_OPTS` | +| `gso` | `FORGIT_SHOW_GIT_OPTS` | | `grh` | `FORGIT_RESET_HEAD_GIT_OPTS` | | `gcf` | `FORGIT_CHECKOUT_FILE_GIT_OPTS` | | `gcb` | `FORGIT_CHECKOUT_BRANCH_GIT_OPTS`, `FORGIT_CHECKOUT_BRANCH_BRANCH_GIT_OPTS` | @@ -286,6 +296,7 @@ Customizing fzf options for each command individually is also supported: | `grl` | `FORGIT_REFLOG_FZF_OPTS` | | `gi` | `FORGIT_IGNORE_FZF_OPTS` | | `gd` | `FORGIT_DIFF_FZF_OPTS` | +| `gso` | `FORGIT_SHOW_FZF_OPTS` | | `grh` | `FORGIT_RESET_HEAD_FZF_OPTS` | | `gcf` | `FORGIT_CHECKOUT_FILE_FZF_OPTS` | | `gcb` | `FORGIT_CHECKOUT_BRANCH_FZF_OPTS` | diff --git a/bin/git-forgit b/bin/git-forgit index 07cbfd33..bd1d0dd8 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -12,6 +12,21 @@ # This gives users the choice to set aliases inside of their git config instead # of their shell config if they prefer. +# Check if fzf is installed +installed_fzf_version=$(fzf --version 2>/dev/null | awk '{print $1}') +if [[ -z "$installed_fzf_version" ]]; then + echo "fzf is not installed. Please install fzf first." + exit 1 +fi + +# Check fzf version +required_fzf_version="0.49.0" +higher_fzf_version=$(printf '%s\n' "$required_fzf_version" "$installed_fzf_version" | sort -V | tail -n1) +if [[ "$higher_fzf_version" != "$installed_fzf_version" ]]; then + echo "fzf version $required_fzf_version or higher is required. You have $installed_fzf_version." + exit 1 +fi + # Set shell for fzf preview commands # Disable shellcheck for "which", because it suggests "command -v xxx" instead, # which is not a working replacement. @@ -185,7 +200,7 @@ _forgit_log_enter() { local sha sha=$(echo "$1" | _forgit_extract_sha) shift - echo "$sha" | xargs -I% "${FORGIT}" diff %^! "$@" + echo "$sha" | xargs -I% "${FORGIT}" show % "$@" } # git commit viewer @@ -339,6 +354,83 @@ _forgit_diff() { return $fzf_exit_code } +_forgit_exec_show() { + _forgit_show_git_opts=() + _forgit_parse_array _forgit_show_git_opts "$FORGIT_SHOW_GIT_OPTS" + git show --pretty="" --diff-merges=first-parent --color=always "${_forgit_show_git_opts[@]}" "$@" +} + +_forgit_show_view() { + local input_line=$1 + local diff_context=$2 + local commit=$3 + local repo + repo=$(git rev-parse --show-toplevel) + cd "$repo" || return 1 + echo "$input_line" | _forgit_get_files_from_diff_line | xargs -0 \ + "$FORGIT" exec_show "${commit}^{commit}" -U"$diff_context" -- | _forgit_pager diff +} + +_forgit_show_preview() { + local input_line=$1 + local diff_context=$2 + local commit=$3 + if [[ "$FZF_PREVIEW_LABEL" =~ "Diff" ]]; then + _forgit_show_view "${input_line}" "${diff_context}" "${commit}" + else + git show --quiet --color=always "${FZF_PROMPT%% *}" + fi +} + +_forgit_show_enter() { + file=$1 + commit=$2 + _forgit_show_view "$file" "$_forgit_fullscreen_context" "${commit}" +} + +# git show viewer +_forgit_show() { + _forgit_inside_work_tree || return 1 + local files opts commit escaped_commit + files=() + if [[ $# -ne 0 ]]; then + if git rev-parse "$1" -- &>/dev/null ; then + commit="$1" && files=("${@:2}") + else + commit="HEAD" && files=("$@") + fi + else + commit="HEAD" + fi + # Escape opening brackets to support stashes (see comment in _forgit_diff) + escaped_commit=${commit//\{/\\\\\{} + opts=" + $FORGIT_FZF_DEFAULT_OPTS + +m -0 --bind=\"enter:execute($FORGIT show_enter {} $escaped_commit | $FORGIT pager enter)\" + --preview=\"$FORGIT show_preview {} '$_forgit_preview_context' $escaped_commit\" + --preview-label=\" Diff \" + --bind=\"alt-e:execute($FORGIT edit_diffed_file {})+refresh-preview\" + --bind=\"alt-t:transform:[[ ! \\\"\$FZF_PREVIEW_LABEL\\\" =~ 'Diff' ]] && + echo 'change-preview-label( Diff )+refresh-preview' || + echo 'change-preview-label( Commit Message )+refresh-preview'\" + $FORGIT_DIFF_FZF_OPTS + --prompt=\"${commit} > \" + " + _forgit_show_git_opts=() + _forgit_parse_array _forgit_show_git_opts "$FORGIT_SHOW_GIT_OPTS" + # Add "^{commit}" suffix after the actual commit. This suppresses the tag information in case it is a tag. + # See: https://git-scm.com/docs/git-show#Documentation/git-show.txt-codegitshow-s--formatsv100commitcode + git show --pretty="" --name-status --diff-merges=first-parent "${_forgit_show_git_opts[@]}" "${commit}^{commit}" \ + -- "${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 @@ -1021,6 +1113,7 @@ public_commands=( "rebase" "reset_head" "revert_commit" + "show" "stash_show" "stash_push" ) @@ -1035,10 +1128,13 @@ private_commands=( "cherry_pick_preview" "clean_preview" "diff_enter" + "exec_show" "file_preview" "ignore_preview" "revert_preview" "reset_head_preview" + "show_enter" + "show_preview" "stash_push_preview" "stash_show_preview" "yank_sha" diff --git a/completions/_git-forgit b/completions/_git-forgit index b26e4dbf..00218f2c 100644 --- a/completions/_git-forgit +++ b/completions/_git-forgit @@ -97,6 +97,7 @@ _git-forgit() { reset_head) _git-staged ;; revert_commit) __git_recent_commits ;; stash_show) _git-stash-show ;; + show) _git-show ;; esac } @@ -122,6 +123,7 @@ 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 +compdef _git-show forgit::show # this is the case of calling the command and pressing tab # the very first time of a shell session, we have to manually diff --git a/completions/git-forgit.bash b/completions/git-forgit.bash index 80a443f0..380fc4b0 100755 --- a/completions/git-forgit.bash +++ b/completions/git-forgit.bash @@ -75,6 +75,7 @@ _git_forgit() rebase reset_head revert_commit + show stash_show stash_push " @@ -101,6 +102,7 @@ _git_forgit() rebase) _git_rebase ;; reset_head) _git_reset ;; revert_commit) _git_revert ;; + show) _git_show ;; stash_show) _git_stash_show ;; esac ;; @@ -135,6 +137,7 @@ then __git_complete forgit::rebase _git_rebase __git_complete forgit::reset::head _git_reset __git_complete forgit::revert::commit _git_revert + __git_complete forgit::show _git_show __git_complete forgit::stash::show _git_stash_show # Completion for forgit plugin shell aliases @@ -154,6 +157,7 @@ then __git_complete "${forgit_rebase}" _git_rebase __git_complete "${forgit_reset_head}" _git_reset __git_complete "${forgit_revert_commit}" _git_revert + __git_complete "${forgit_show}" _git_show __git_complete "${forgit_stash_show}" _git_stash_show fi fi diff --git a/completions/git-forgit.fish b/completions/git-forgit.fish index 72c896ba..2b9d5037 100644 --- a/completions/git-forgit.fish +++ b/completions/git-forgit.fish @@ -40,6 +40,7 @@ complete -c git-forgit -n __fish_forgit_needs_subcommand -a reflog -d 'git reflo 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 show -d 'git show viewer' 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' diff --git a/conf.d/forgit.plugin.fish b/conf.d/forgit.plugin.fish index 18761d04..0c25d0a0 100644 --- a/conf.d/forgit.plugin.fish +++ b/conf.d/forgit.plugin.fish @@ -37,6 +37,7 @@ if test -z "$FORGIT_NO_ALIASES" 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_show; or string collect "gso") git-forgit show 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 diff --git a/forgit.plugin.zsh b/forgit.plugin.zsh index b6d4a5ed..ef7aa10e 100755 --- a/forgit.plugin.zsh +++ b/forgit.plugin.zsh @@ -53,6 +53,10 @@ forgit::diff() { "$FORGIT" diff "$@" } +forgit::show() { + "$FORGIT" show "$@" +} + forgit::add() { "$FORGIT" add "$@" } @@ -146,6 +150,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then export forgit_log="${forgit_log:-glo}" export forgit_reflog="${forgit_reflog:-grl}" export forgit_diff="${forgit_diff:-gd}" + export forgit_show="${forgit_show:-gso}" export forgit_ignore="${forgit_ignore:-gi}" export forgit_checkout_file="${forgit_checkout_file:-gcf}" export forgit_checkout_branch="${forgit_checkout_branch:-gcb}" @@ -166,6 +171,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then alias "${forgit_log}"='forgit::log' alias "${forgit_reflog}"='forgit::reflog' alias "${forgit_diff}"='forgit::diff' + alias "${forgit_show}"='forgit::show' alias "${forgit_ignore}"='forgit::ignore' alias "${forgit_checkout_file}"='forgit::checkout::file' alias "${forgit_checkout_branch}"='forgit::checkout::branch'