Tracking method history in git

Have you ever wanted to see the history of a single method in your codebase? Well, with git correctly configured for language-aware diffs, you can:

$ git log -L :some_method:lib/some_class.rb
312732c - Add a friendly method <Simon Coffey>
diff --git a/lib/some_class.rb b/lib/some_class.rb
--- a/lib/some_class.rb
+++ b/lib/some_class.rb
@@ -2,1 +3,5 @@
+  def some_method
+    "Hello Simon"
+  end
+
 end

b8eb1c2 - Make method more inclusive <Simon Coffey>
diff --git a/lib/some_class.rb b/lib/some_class.rb
--- a/lib/some_class.rb
+++ b/lib/some_class.rb
@@ -3,5 +3,5 @@
   def some_method
-    "Hello Simon"
+    "Hello Everybody!"
   end

 end

More generally, the -L option allows you to display the history of ranges within a specified file, specified either using line numbers or a regex:

$ git log -L <start>,<end>:<file>
$ git log -L :<regex>:<file>

The line-number version is self-explanatory, but the regex version is less obvious. What is the regex matching? From the manpage:

If ":<regex>" is given in place of <start> and <end>, it denotes the range from the first funcname line that matches <regex>, up to the next funcname line.

The funcname is where the language-aware diff setting comes in. With the following in our project’s .gitattributes file, git is able to detect Ruby method declarations, which it treats as funcnames:

*.rb diff=ruby

When we use the second form of git -L, the regex we provide is matched only against funcname lines, i.e. the method declarations within the specified file. So for example, the log command above would return the history for only the marked lines:

# lib/some_class.rb
class SomeClass

  def some_method         # funcname match    [start]
    "Hello Everybody!"    #                    ...
  end                     #                    ...
                          #                   [end] 
  def some_other_method   # next funcname
    "Goodbye Everybody!"
  end

end

Because the range is re-evaluated for each commit, the full method will be shown even if its length changes or it moves within the file (which is, after all, pretty likely).

Writing an alias

Now, I don’t know about you, but I can’t remember this syntax for toffee. I’d like to be able to write an alias like so:

$ git lm some_method lib/some_class.rb

Getting positional arguments interpolated into a git alias is a bit fiddly. However, we can bind aliases to arbitrary shell code, supplied as a string. This means we can define a temporary function to capture our input and construct a command, then execute the function. Here’s one possible version (shown on multiple lines for clarity – it needs to be on one line in practice):

# ~/.gitconfig
[alias]
  lm = "!f() {
          local method=$1;
          local file=$2;
          [[ -n \"$method\" && -n \"$file\" ]] || exit 1;
          git log -L :$method:$file;
        }; f"

Caveats

There are some clear limitations to this regex-based approach:

Because git has no real concept of what a Ruby method is (it just knows what the declaration of one looks like), we’re not really tracking the history of a specific method, just some lines that probably contain a method with the same name. For example, we can’t tell the differents between an instance and a class method, so if we defined both SomeClass#some_method and SomeClass.some_method, we’d get the history of whichever came first in the file.

We’re also unable to track a method if it moves to a different file, simply because range logs only let us specify one file to look at.

The method_log gem

Happily, a tool exists (for Ruby, at least) that solves both of these problems: method_log, a ridiculously impressive gem by James Mead that analyses your code at each commit using the parser gem. It can therefore distinguish class methods from instance methods, allowing us to specify exactly what we’re interested in:

$ method_log SomeClass#some_method

This comes at some considerable performance cost, but in my view this is absolutely fine; as you wait for your results, you have ample opportunity to explain to a colleague just how cool history tracking can be with a dash of semantic awareness. Their eyes may glaze over in awe; this is a sign that you are doing fine work. For extra material, I highly recommend reading James’s blog post about the implementation and optimisation of method_log.

comments powered by Disqus