Skip to content

Instantly share code, notes, and snippets.

@junegunn
Last active July 5, 2024 07:23
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
}
@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