Browsing git commits with fzf
2015. 03. 25.

The latest version of fzf can now process ANSI color codes and show colored text in the finder. With this new --ansi option, we can build a decent git commit browser as shown below.

screencast

The code is given as follows:

# fshow - git commit browser
fshow() {
  local out sha q
  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); do
    q=$(head -1 <<< "$out")
    while read sha; do
      git show --color=always $sha | less -R
    done < <(sed '1d;s/^[^a-z0-9]*//;/^$/d' <<< "$out" | awk '{print $1}')
  done
}

And here's the description of the options used:

So that's it. You can upgrade fzf as usual or just download the new binary from here. (Note that Homebrew formula for 0.9.5 is not ready yet.) Please try it and report any issues you run into. Thanks!

Update: To "show" or to "diff"

fzf 0.9.6 was just released with --expect option. We can use it to implement basic key bindings inside fzf. I'll demonstrate how we can update the original fshow function to show the diff between the commits when CTRL-D is pressed on fzf. So here's the updated version:

# fshow - git commit browser (enter for show, ctrl-d for diff)
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); 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
}

What's new is the use of --expect=ctrl-d option. It makes fzf "expect" CTRL-D, and you can press the key instead of the default enter key to select the item(s) and complete fzf. When the option is applied, fzf will print the name of the key pressed, and we can use the information ("$k" = 'ctrl-d') to decide what to do next with the rest of the output, in this case, to git show, or to git diff.

One thing I noticed after publishing this post is that git show command takes an arbitrary number of commit hashes. So the inner loop is not necessary, but I didn't remove it as I prefer to see git show output for each commit instead of one merged output.

Another update: Enabling sort

One thing leads to another. After playing with fshow for a while, I realized that --no-sort makes it really hard to quickly get to the right commit when the repository has long commit history. Extended-search mode (--extended) I mentioned above does help a little, but it's just not good enough in many cases. Leveraging the code written for --expect option, I added --toggle-sort option to fzf 0.9.7. The option takes the name of a key, so you can bind any key of your liking for toggling sort.

So here's the "final" version where I use backtick for the purpose. Notice that the backtick character had to be escaped with a backslash.

# 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
}

Yet another update: Using --execute option

A limitation of using an external while loop is that the new fzf process does not retain the position of the cursor. fzf 0.10.0 introduced --execute option which allows you to start an external program without leaving fzf, which is useful for previewing things before making the final selection.

The following version uses the new option and never leaves the initial fzf process, which addresses the limitation described above and greatly simplifies the script.

# fshow - git commit browser
fshow() {
  git log --graph --color=always \
      --format="%C(auto)%h%d %s %C(black)%C(bold)%cr" "$@" |
  fzf --ansi --no-sort --reverse --tiebreak=index --bind=ctrl-s:toggle-sort \
      --bind "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"
}

I posted the snippet to the Gist so you can see the changes between the versions here.

» capture | close