Skip to content

Instantly share code, notes, and snippets.

@junegunn
Last active November 4, 2024 10:33
Show Gist options
  • Save junegunn/f4fca918e937e6bf5bad to your computer and use it in GitHub Desktop.
Save junegunn/f4fca918e937e6bf5bad to your computer and use it in GitHub Desktop.
Browsing git commit history with fzf
# fshow - git commit browser (enter for show, ctrl-d for diff, ` toggles sort)
fshow() {
local out shas sha q k
while out=$(
git log --graph --color=always \
--format="%C(auto)%h%d %s %C(black)%C(bold)%cr" "$@" |
fzf --ansi --multi --no-sort --reverse --query="$q" \
--print-query --expect=ctrl-d --toggle-sort=\`); do
q=$(head -1 <<< "$out")
k=$(head -2 <<< "$out" | tail -1)
shas=$(sed '1,2d;s/^[^a-z0-9]*//;/^$/d' <<< "$out" | awk '{print $1}')
[ -z "$shas" ] && continue
if [ "$k" = ctrl-d ]; then
git diff --color=always $shas | less -R
else
for sha in $shas; do
git show --color=always $sha | less -R
done
fi
done
}
@tobiisdesired
Copy link

Inspired by your git browser I implemented a log browser based on fzf. The connection is done via the line number. Don't know if xargs is really necessary.

logshow() {
    cat -n $1 | fzf --reverse --no-sort --tiebreak=index \
                    --bind "ctrl-m:execute:echo {} | grep -Eo "[0-9]+" | head -n 1 | xargs -Ireplace less -N +replaceg $1"
}

@akatrevorjay
Copy link

I simplified this: https://gist.github.com/akatrevorjay/9fc061e8371529c4007689a696d33c62

All those heredocs made my brain hurt a little. lulz.

@victorbrca
Copy link

Thanks for this... and another modification. This shows preview of the file, while allowing you to still go into it.

Key binds:

  • q = quit
  • j = down
  • k = up
  • alt-k = preview up
  • alt-j = preview down
  • ctrl-f = preview page down
  • ctrl-b = preview page up
git-commit-show () 
{
  git log --graph --color=always --format="%C(auto)%h%d %s %C(black)%C(bold)%cr"  | \
   fzf --ansi --no-sort --reverse --tiebreak=index --preview \
   'f() { set -- $(echo -- "$@" | grep -o "[a-f0-9]\{7\}"); [ $# -eq 0 ] || git show --color=always $1 ; }; f {}' \
   --bind "j:down,k:up,alt-j:preview-down,alt-k:preview-up,ctrl-f:preview-page-down,ctrl-b:preview-page-up,q:abort,ctrl-m:execute:
                (grep -o '[a-f0-9]\{7\}' | head -1 |
                xargs -I % sh -c 'git show --color=always % | less -R') << 'FZF-EOF'
                {}
FZF-EOF" --preview-window=right:60%
}

@avshyz
Copy link

avshyz commented Jun 22, 2019

Cool stuff!
Is it possible to make it work with git log -S (git pickaxe)?

@tamphh
Copy link

tamphh commented Jul 18, 2019

Thanks you guys. Here my modification which support both git log branch & a particular file.
Gist: https://gist.github.com/tamphh/3c9a4aa07ef21232624bacb4b3f3c580
Screen record: https://asciinema.org/a/257939

# git log show with fzf
gli() {

  # param validation
  if [[ ! `git log -n 1 $@ | head -n 1` ]] ;then
    return
  fi

  # filter by file string
  local filter
  # param existed, git log for file if existed
  if [ -n $@ ] && [ -f $@ ]; then
    filter="-- $@"
  fi

  # git command
  local gitlog=(
    git log
    --graph --color=always
    --abbrev=7
    --format='%C(auto)%h %an %C(blue)%s %C(yellow)%cr'
    $@
  )

  # fzf command
  local fzf=(
    fzf
    --ansi --no-sort --reverse --tiebreak=index
    --preview "f() { set -- \$(echo -- \$@ | grep -o '[a-f0-9]\{7\}'); [ \$# -eq 0 ] || git show --color=always \$1 $filter; }; f {}"
    --bind "ctrl-q:abort,ctrl-m:execute:
                (grep -o '[a-f0-9]\{7\}' | head -1 |
                xargs -I % sh -c 'git show --color=always % $filter | less -R') << 'FZF-EOF'
                {}
                FZF-EOF"
   --preview-window=right:60%
  )

  # piping them
  $gitlog | $fzf
}

@maheshsundaram
Copy link

maheshsundaram commented Jul 26, 2019

@tamphh - yours is really neat, but I couldn't get it to work. It would only output git help. I'm not pro enough at bash to see the issue, so I just squashed it together a bit (and added the key bindings from @victorbrca above, and made the fzf window larger) to this:

gli() {
  local filter
  if [ -n $@ ] && [ -f $@ ]; then
    filter="-- $@"
  fi

  git log \
    --graph --color=always --abbrev=7 --format='%C(auto)%h %an %C(blue)%s %C(yellow)%cr' $@ | \
    fzf \
      --ansi --no-sort --reverse --tiebreak=index \
      --preview "f() { set -- \$(echo -- \$@ | grep -o '[a-f0-9]\{7\}'); [ \$# -eq 0 ] || git show --color=always \$1 $filter; }; f {}" \
      --bind "j:down,k:up,alt-j:preview-down,alt-k:preview-up,ctrl-f:preview-page-down,ctrl-b:preview-page-up,q:abort,ctrl-m:execute:
                (grep -o '[a-f0-9]\{7\}' | head -1 |
                xargs -I % sh -c 'git show --color=always % | less -R') << 'FZF-EOF'
                {}
                FZF-EOF" \
      --preview-window=right:60% \
      --height 80%
}

@gabesoft
Copy link

A version for the fish shell

Gist https://gist.github.com/gabesoft/b6e5e959c4cb11ed257d41edb07d47cb

function gbr --description "Git browse commits"
    set -l log_line_to_hash "echo {} | grep -o '[a-f0-9]\{7\}' | head -1"
    set -l view_commit "$log_line_to_hash | xargs -I % sh -c 'git show --color=always % | diff-so-fancy | less -R'"
    set -l copy_commit_hash "$log_line_to_hash | xclip"
    set -l git_checkout "$log_line_to_hash | xargs -I % sh -c 'git checkout %'"
    set -l open_cmd "open"

    if test (uname) = Linux
        set open_cmd "xdg-open"
    end

    set github_open "$log_line_to_hash | xargs -I % sh -c '$open_cmd https://github.\$(git config remote.origin.url | cut -f2 -d. | tr \':\' /)/commit/%'"

    git log --color=always --format='%C(auto)%h%d %s %C(green)%C(bold)%cr% C(blue)%an' | \
        fzf --no-sort --reverse --tiebreak=index --no-multi --ansi \
            --preview="$view_commit" \
            --header="ENTER to view, CTRL-Y to copy hash, CTRL-O to open on GitHub, CTRL-X to checkout, CTRL-C to exit" \
            --bind "enter:execute:$view_commit" \
            --bind "ctrl-y:execute:$copy_commit_hash" \
            --bind "ctrl-x:execute:$git_checkout" \
            --bind "ctrl-o:execute:$github_open"
end

@victorkristof
Copy link

@jasonmendes I also couldn't run the function (showing git help instead).
I solved it by replacing the final "piping" line by "${gitlog[@]}" | "${fzf[@]}".

@rndware
Copy link

rndware commented Jun 27, 2020

Here's my particular version:

  • alt-v = open commit in vim (useful for copying out code you've deleted in a previous commit)
alias glNoGraph='git log --color=always --format="%C(auto)%h%d %s %C(black)%C(bold)%cr% C(auto)%an" "$@"'
_gitLogLineToHash="echo {} | grep -o '[a-f0-9]\{7\}' | head -1"
_viewGitLogLine="$_gitLogLineToHash | xargs -I % sh -c 'git show --color=always % | diff-so-fancy'"
_viewGitLogLineUnfancy="$_gitLogLineToHash | xargs -I % sh -c 'git show %'"

# gls - git commit browser with previews and vim integration 
gls() {
    glNoGraph |
        fzf --no-sort --reverse --tiebreak=index --no-multi \
            --ansi --preview="$_viewGitLogLine" \
                --header "enter to view, alt-y to copy hash, alt-v to open in vim" \
                --bind "enter:execute:$_viewGitLogLine   | less -R" \
                --bind "alt-v:execute:$_viewGitLogLineUnfancy | vim -" \
                --bind "alt-y:execute:$_gitLogLineToHash | xclip"
}

@mawkler
Copy link

mawkler commented Sep 30, 2020

Maybe this is a limitation of fzf, but would it be possible to fuzzy find commit message text that's not necessarily in the header of the commit message, but in the body?

@avshyz
Copy link

avshyz commented Oct 10, 2020

@Melkster - I've managed to accomplish that using sk (skim), which is an alternative to fzf.
I've also used ripgrep to highlight the matches, so you may need to install them both to make the following code work:

ilog() {
  git status >& /dev/null
  if [ $? -eq 128 ]; then return; fi

  sk \
    --ansi \
    -ic"git log --all --oneline --color -G\"{}\" $*" \
    --preview="[[ -n "{1}" ]] && git show --color -G {cq} {1} | rg  --colors=match:fg:blue --colors=line:style:intense --color=always --passthru {cq}" \
    --bind="ctrl-j:preview-down,ctrl-k:preview-up" | awk '{print $1}'
}

It took me way to long to perfect this alias (starting after asking here on this gist, a year or so ago), but it has served me well ever since

@maheshsundaram
Copy link

@avshyz Wow, that's really neat. I like how you can search through the diffs of commits to find relevant code. Thank you for sharing

@junegunn
Copy link
Author

@avshyz fzf provides a way to dynamically or manually reload the list which I believe is more flexible than the way skim approaches the problem.

https://github.com/junegunn/fzf#reloading-the-candidate-list

@avshyz
Copy link

avshyz commented Oct 11, 2020

@maheshsundaram always glad to share :)

@junegunn Ohhhh that's super interesting and useful! I'll definitely find use to that in future scripts! It's just that when I wrote that shell function (a year or two ago) fzf didn't have that option, and I never bothered rewriting it to use fzf instead of skim (though it may be worthwhile to do so)

@Frederick888
Copy link

Frederick888 commented Apr 22, 2021

Many thanks to everyone for sharing. After struggling with gitconfig's escaping for a while, I managed to put @rndware's script into .gitconfig with a few tweaks to use ripgrep, git-delta and clipboard-cli:

[core]
	pager = delta --line-numbers
[alias]
    fzf = !"                                                                                                                                                                              \
        function gfzf() {                                                                                                                                                                 \
            local filter;                                                                                                                                                                 \
            if [ -n $@ ] && [ -e $@ ]; then                                                                                                                                               \
                filter=\"-- $@\";                                                                                                                                                         \
            fi;                                                                                                                                                                           \
            export LESS='-R'                                                                                                                                                              \
            export BAT_PAGER='less -S -R -M -i';                                                                                                                                          \
            git log                                                                                                                                                                       \
                --graph --color=always --abbrev=7                                                                                                                                         \
                --format=format:\"%C(bold blue)%h%C(reset) %C(dim white)%an%C(reset)%C(bold yellow)%d%C(reset) %C(white)%s%C(reset) %C(bold green)(%ar)%C(reset)\" $@ |                   \
                    fzf --ansi --no-sort --layout=reverse --tiebreak=index                                                                                                                \
                        --preview=\"f() { set -- \\$(echo -- \\$@ | rg -o '\\b[a-f0-9]{7,}\\b'); [ \\$# -eq 0 ] || git show --color=always \\$1 $filter | delta --line-numbers; }; f {}\" \
                        --bind=\"ctrl-d:half-page-down,ctrl-u:half-page-up,ctrl-j:preview-down,ctrl-k:preview-up,ctrl-f:preview-page-down,ctrl-b:preview-page-up\"                        \
                        --bind=\"ctrl-m:execute:                                                                                                                                          \
                                (rg -o '\\b[a-f0-9]{7,}\\b' | head -1 |                                                                                                                   \
                                xargs -I % -- git show --color=always %) << 'FZFEOF'\n                                                                                                    \
                                {}                                                                                                                                                        \
                                \nFZFEOF\"                                                                                                                                                \
                        --bind=\"ctrl-y:execute-silent:                                                                                                                                   \
                                (rg -o '\\b[a-f0-9]{7,}\\b' | head -1 | tr -d \\$'\\n' | clipboard) << 'FZFEOF'\n                                                                         \
                                {}                                                                                                                                                        \
                                \nFZFEOF\"                                                                                                                                                \
                        --preview-window=right:60%;                                                                                                                                       \
        };                                                                                                                                                                                \
        gfzf                                                                                                                                                                              \
    "
[interactive]
    diffFilter = delta --color-only --line-numbers

@ultrox
Copy link

ultrox commented Jun 19, 2021

@Frederick888
Copy link

@ultrox Glad to know that you found it useful. I've actually updated the subcommand for a few times since then: https://git.tsundere.moe/Frederick888/frederick-settings/blob/master/.gitconfig

git-fzf.small.mp4

@ultrox
Copy link

ultrox commented Jun 19, 2021

@Frederick888 I'll check it out later. My use-case is actually different , I'm looking to use fzf to find commit to fixup :).

git fixup <command>

I would then go to fzf and look for commit based on the title, enter would return hash.

@benwoodward
Copy link

@Frederick888 thanks for sharing this, so useful—just bumped my git efficiency up a few notches!

@carlfriedrich
Copy link

carlfriedrich commented Feb 23, 2022

Thanks everybody for sharing your versions. All of this was so helpful!

After spending some hours of optimizing, I'd like to throw my version on the table as well. It adds the diff stats in the preview window, using a dimmed presentation:

grafik

When entering a commit, another fzf instance opens up containing a list of files changed in that commit, with the preview window showing the diff for that file:

grafik

Entering a file shows the diff on that file with its complete context.

The git-fuzzy-diff function detects whether diff-so-fancy is installed and, if so, uses it.

Furthermore I tried to split and structure the code a bit in order to not have just one huge intransparent command. I started off from @victorbrca's version and now it looks like this:

GIT_FZF_DEFAULT_OPTS="
	$FZF_DEFAULT_OPTS
	--ansi
	--reverse
	--height=100%
	--bind shift-down:preview-down
	--bind shift-up:preview-up
	--bind pgdn:preview-page-down
	--bind pgup:preview-page-up
	--bind q:abort
	$GIT_FZF_DEFAULT_OPTS
"

git-fuzzy-diff ()
{
	PREVIEW_PAGER="less --tabs=4 -Rc"
	ENTER_PAGER=${PREVIEW_PAGER}
	if [ -x "$(command -v diff-so-fancy)" ]; then
		PREVIEW_PAGER="diff-so-fancy | ${PREVIEW_PAGER}"
		ENTER_PAGER="diff-so-fancy | sed -e '1,4d' | ${ENTER_PAGER}"
	fi

	# Don't just diff the selected file alone, get related files first using
	# '--name-status -R' in order to include moves and renames in the diff.
	# See for reference: https://stackoverflow.com/q/71268388/3018229
	PREVIEW_COMMAND='git diff --color=always '$@' -- \
		$(echo $(git diff --name-status -R '$@' | grep {}) | cut -d" " -f 2-) \
		| '$PREVIEW_PAGER

	# Show additional context compared to preview
	ENTER_COMMAND='git diff --color=always '$@' -U10000 -- \
		$(echo $(git diff --name-status -R '$@' | grep {}) | cut -d" " -f 2-) \
		| '$ENTER_PAGER

	git diff --name-only $@ | \
		fzf ${GIT_FZF_DEFAULT_OPTS} --exit-0 --preview "${PREVIEW_COMMAND}" \
		--preview-window=top:85% --bind "enter:execute:${ENTER_COMMAND}"
}

git-fuzzy-log ()
{
	PREVIEW_COMMAND='f() {
		set -- $(echo -- "$@" | grep -o "[a-f0-9]\{7\}")
		[ $# -eq 0 ] || (
			git show --no-patch --color=always $1
			echo
			git show --stat --format="" --color=always $1 |
			while read line; do
				tput dim
				echo " $line" | sed "s/\x1B\[m/\x1B\[2m/g"
				tput sgr0
			done |
			tac | sed "1 a \ " | tac
		)
	}; f {}'

	ENTER_COMMAND='(grep -o "[a-f0-9]\{7\}" | head -1 |
		xargs -I % bash -ic "git-fuzzy-diff %^1 %") <<- "FZF-EOF"
		{}
		FZF-EOF'

	git log --graph --color=always --format="%C(auto)%h %s%d " | \
		fzf ${GIT_FZF_DEFAULT_OPTS} --no-sort --tiebreak=index \
		--preview "${PREVIEW_COMMAND}" --preview-window=top:15 \
		--bind "enter:execute:${ENTER_COMMAND}"
}

In my gitconfig I have set up aliases for it:

[alias]
	fd = !bash -ic 'git-fuzzy-diff \"$@\"' x
	fl = !bash -ic 'git-fuzzy-log \"$@\"' x

@mawkler
Copy link

mawkler commented Feb 23, 2022

Perhaps someone should turn this into a zsh plugin?

@carlfriedrich
Copy link

@melkster Actually there is a project called forgit, which does something similar based on fzf and is available as a zsh plugin.

@mawkler
Copy link

mawkler commented Mar 3, 2022

@carlfriedrich Interesting, I'll check it out!

@carlfriedrich
Copy link

@melkster FYI I edited my post above with an updated version, adding a git-fuzzy-diff command which shows a diff for each changed file separately.

I think one could plug this into forgit, if it would allow for customizing the preview command, which is not possible at the moment. I created an issue for that. If you are interested in that as well, maybe you could add a comment there to show that I'm not the only person who might want this. :-)

@slerer
Copy link

slerer commented Apr 23, 2023

@Frederick888 I'll check it out later. My use-case is actually different , I'm looking to use fzf to find commit to fixup :).

git fixup <command>

I would then go to fzf and look for commit based on the title, enter would return hash.

How did you hook it up at the end? a fuzzy-find for fixup sounds awesome!

@Antylon
Copy link

Antylon commented Oct 6, 2023

@ultrox @slerer
This article gives you a git alias that is exactly what you are after

@JohanChane
Copy link

Thanks for this... and another modification. This shows preview of the file, while allowing you to still go into it.

Key binds:

  • q = quit
  • j = down
  • k = up
  • alt-k = preview up
  • alt-j = preview down
  • ctrl-f = preview page down
  • ctrl-b = preview page up
git-commit-show () 
{
  git log --graph --color=always --format="%C(auto)%h%d %s %C(black)%C(bold)%cr"  | \
   fzf --ansi --no-sort --reverse --tiebreak=index --preview \
   'f() { set -- $(echo -- "$@" | grep -o "[a-f0-9]\{7\}"); [ $# -eq 0 ] || git show --color=always $1 ; }; f {}' \
   --bind "j:down,k:up,alt-j:preview-down,alt-k:preview-up,ctrl-f:preview-page-down,ctrl-b:preview-page-up,q:abort,ctrl-m:execute:
                (grep -o '[a-f0-9]\{7\}' | head -1 |
                xargs -I % sh -c 'git show --color=always % | less -R') << 'FZF-EOF'
                {}
FZF-EOF" --preview-window=right:60%
}

Add open commit link version:

#!/usr/bin/env python3

import subprocess
import re
from typing import Optional
import argparse
import os

GITLOG_PATH = os.path.abspath(__file__)

USAGE_DOC = """
Usage:
    gitlog [--opencommitlink <shorthash>] [<git-log-options>...]

Examples:
    gitlog --all
    gitlog --since="2 weeks ago" --author="John Doe"
    gitlog --opencommitlink abc1234
"""

def gitlog(*args: str) -> None:
    git_cmd = [
        'git', 'log', '--graph', '--color=always', '--format=%C(auto)%h%d %s %C(bold green)%ar %C(dim white)%an'
    ] + list(args or [])
    
    # Usage:
    # -   ctrl-m: page the commit
    # -   ctrl-l: open the commit link
    # See [ref](https://gist.github.com/junegunn/f4fca918e937e6bf5bad?permalink_comment_id=2731105#gistcomment-2731105)
    fzf_cmd = [
        'fzf', '--ansi', '--no-sort', '--reverse', '--tiebreak=index', '--no-multi',
        '--preview', 'f() { set -- $(echo -- "$@" | grep -o "[a-f0-9]\\{7\\}"); [ $# -eq 0 ] || git show --color=always $1 ; }; f {}',
        '--preview-window=right:60%:hidden',
        '--bind', 'ctrl-j:down,ctrl-k:up',
        '--bind', 'ctrl-f:page-down,ctrl-b:page-up',
        '--bind', 'alt-j:preview-down,alt-k:preview-up',
        '--bind', 'alt-f:preview-page-down,alt-b:preview-page-up',
        '--bind', 'ctrl-/:toggle-preview',
        '--bind', 'ctrl-u:track+clear-query',
        '--bind', 'q:abort',
        '--bind', 'ctrl-m:execute:(echo {} | grep -o "[a-f0-9]\\{7\\}" | head -1 | xargs -I @ sh -c \'git show --color=always @ | command bat --number\')',
        '--bind', 'ctrl-l:execute:(echo {} | grep -o "[a-f0-9]\\{7\\}" | head -1 | xargs -I @ ' + GITLOG_PATH + ' --opencommitlink @)'
    ]

    # Run git log command and pipe to fzf
    process = subprocess.Popen(git_cmd, stdout=subprocess.PIPE)
    subprocess.run(fzf_cmd, stdin=process.stdout)
    process.wait()

def commit_link(shorthash: str) -> Optional[str]:
    # get fullhash
    fullhash = subprocess.run(['git', 'rev-parse', shorthash], stdout=subprocess.PIPE).stdout.decode().strip()
    if not fullhash:
        print(f"Invalid commit hash: {shorthash}")
        return None

    # get the remote URL
    remote_url = subprocess.run(['git', 'config', '--get', 'remote.origin.url'], stdout=subprocess.PIPE).stdout.decode().strip()
    if not remote_url:
        print("Could not determine remote URL")
        return None

    # parse the remote URL
    match = re.search(r'(?:://|@)([^/:]+)[:/](.+)\.git', remote_url)
    if not match:
        print("Could not determine repository or host")
        return None

    host = match.group(1)
    repo = match.group(2)

    return f"https://{host}/{repo}/commit/{fullhash}"

def open_commit_link(shorthash: str) -> None:
    link = commit_link(shorthash)
    if link:
        subprocess.run(['xdg-open', link], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

def main() -> None:
    parser = argparse.ArgumentParser(
        description="A script to display git logs and open commit links.",
        formatter_class=argparse.RawTextHelpFormatter,
        epilog=USAGE_DOC
    )
    parser.add_argument('--opencommitlink', type=str, help="Open the commit link for the given short hash.")
    known_args, other_args = parser.parse_known_args()

    if known_args.opencommitlink:
        open_commit_link(known_args.opencommitlink)
    else:
        gitlog(*other_args)

if __name__ == "__main__":
    main()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment