Ask Git to Show a Method

When coding, I often find myself wanting to quickly check the details of a function or method related to what I’m working on. For instance, I might need to understand how the function handles its arguments or recall its side effects.

If you just want to know how to do this, you can jump down and skip my story.

I usually use ctags with Vim for this. A quick ], a brief read, and a <Ctrl-o> and I’m back from whence I came and more informed for my effort. Still, sometimes would be more convenient to just have the code printed right to my terminal from the command line.

If you’re here, you might remember that we previously looked at Git’s ability to walk the history of a specific method using git log -L1. What you might not know is that git log and git show are close siblings—actually defined in the same source file in the Git codebase. I tried pretty hard to find the right incantation to ask git-show to display just a specific function. I often use it to show a file at a given commit with git show some-branch:path/to/file to print a full file at a given commit. I was hopeful that it would have some variant to show what Git calls “hunks”, but despite learning that several Git builtins do indeed have some hidden flags, I just couldn’t make it work.

After that I spent way too long spelunking the Git codebase to try and retrieve the Regex pattern2. git config won’t give it to you unless it’s one you’ve defined in your own ~/.gitconfig. For my purposes that wasn’t useful as I let Git use it’s built-in Ruby patterns. I got as far as figuring out that passing git check-attr diff <filename> will give me the name of the diff Regex that it looks up the actual Regex if I’d defined it in ~/.gitattributes, which was nice to know but not useful.

Pivoting back, I finally thought to ask git log to show me the current state of a method by limiting the result to a single commit and setting the format to empty. This approach nearly worked, but it showed the most recent diff of the function, including the diff header:

$ git log -1 -L ":all:app/models/post.rb" --format=

diff --git a/app/models/post.rb b/app/models/post.rb
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -8,4 +8,7 @@
   def self.all
-    @@all ||= super.each(&:validate!).sort_by(&:date).reverse!
+    @@all ||= super
+      .each(&:validate!)
+      .sort_by(&:date)
+      .reverse!
   end

I could write a grep command to give me what I wanted from this output (just the code, without old lines or + prefixes for changed lines), but something I’d seen while code diving tingled in the back of my mind: the documentation in the Git codebase for the -L flag is actually written as a file that can be included elsewhere. Tracing that inclusion, I found that it’s also in git-blame.txt, which indeed does accept the -L flag with the funcname variant (though not with the filename)!

$ git blame -s -L ":\ball\b" app/models/post.rb
94b0b1db0  8)   def self.all
b1ed34a89  9)     @@all ||= super
b1ed34a89 10)       .each(&:validate!)
b1ed34a89 11)       .sort_by(&:date)
b1ed34a89 12)       .reverse!
94b0b1db0 13)   end
94b0b1db0 14)

That output is much easier to clean up with cut and sed (the extra lines at the beginning of the git log and the end of the git blame output are not accidental in the above snippets, they’re actually printed).

$ git blame -s -L ":\ball\b" "app/models/post.rb" \
    # remove the commit hash and line number
    | cut -d ')' -f2- \
    # remove the inserted leading space, but leave indentation
    | cut -d ' ' -f2- \
    # remove trailing blank lines
    | sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba'
  def self.all
    @@all ||= super
      .each(&:validate!)
      .sort_by(&:date)
      .reverse!
  end

The above won’t work unless you remove those comments.

git blame supports some pretty neat coloring options that, if specified in your gitconfig, will cause cut -d ')' to leave behind some shell escape codes at the beginning of the line. To avoid that, you can pass -c blame.coloring=none3 to git as we see here in this extracted shell script which also adds pagination for long functions but will not page if the output fits into your terminal.

All wrapped up in a script:

#!/bin/sh

# usage: git show-func <name> <file>

git \
  -c blame.coloring=none \
  blame -s --progress \
  -L ":\b$1\b" -- "$2" \
| cut -d')' -f2- \
| sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba' \
| less --quit-if-one-screen --RAW-CONTROL-CHARS --no-init

The downside of using git blame is that if the range of lines in your function has had a lot of history to it, git blame may take a while to generate the prefixes to each line. That’s unfortunate since I immediately throw out all of the commit and authorship information with cut. If the slowdown bothers me enough I may spend the time to filter out the git log cruft instead.

Coming soon: Colorizing that output right to the terminal in your preferred color scheme (as long as your preferred $EDITOR is nvim).

  1. And if you haven’t read “See the History of a Method with git log -L” and enjoy this post, I bet you’ll like that too. 

  2. the xfuncname used by git log to find function definitions 

  3. You can set config values for individual git commands with git -c key=value subcommand --other --flags. It has to come between git and the subcommand like blame in this case.