How to be a more effective git historian with recursive-blame

The ability in Git to search through the commit history locally is one of the major reasons why I never want to work with SVN again. With Git there is no waiting on the server. Git also provides some powerful tools to search that local history, like git-bisect. If you haven’t used bisect, this is super efficient at finding the change that introduced a bug.

Another history tool is git-blame. On its own, its much less efficient for me than bisect, since I mostly have to use it recursively to find the revision that introduced the change I’m looking for. Too often, running ‘blame’ just once doesn’t help. Even worse, the line in question might not even exist in the latest revision anymore, so using blame directly won’t work. When doing a recursive search with blame manually, with Git locally or on GitHub, it can be tricky to track down something with blame since the line in question moved around the file from change to change.

Can we do better? Of course. If one program isn’t efficient enough, write another to compensate. That’s what my friend and colleague Scott González did on his trip to Russia, resulting in recursive-blame. This is a command line tool, written in nodejs, installed via npm (which both run well on Windows, Linux and OSX, a huge plus over other platforms).

Assuming you have node and npm installed, the setup couldn’t be any easier:

npm install -g recursive-blame

Afterwards you can use it likes this:

recursive-blame <pattern> <path>

The pattern is a regular expression, so you may have to escape some characters.

As a simple example, today I was reviewing a pull request against jQuery UI, which removed an unnecessary argument from a method call (PR 1104). I wanted to know why that argument was there in the first place. With recursive-blame, that was easy to figure out, and took only a few seconds. To start, I ran this command:

recursive-blame 'values: function\(' ui/jquery.ui.slider.js

Since parentheses indicate a group in regular expressions, I escape it with a backslash.

The output is this (I only trimmed the commit message to fit here):

Commit: 87ba795467ee447eb2ab7d95ada42de097c7946f
Author: Richard Worth <[email protected]>
Date:   Fri Apr 2 23:16:46 2010 -0400 (3 years, 7 months ago)
Path:   ui/jquery.ui.slider.js
Match:  1 of 2

    slider: jslint cleanup (thanks for the start zhaoz) and style changes to [...]

380)
381)        return this._value();
382)    },
383)
384)   values: function( index, newValue ) {
385)        var vals,
386)            newValues,
387)            i;
388)

Next action [r,n,p,c,d,q,?]?

This points at the last commit that modified this line. Since its just a code style cleanup, I type “r” for “recurse” to continue searching (the other commands are explained by typing “?”; this is the same interface as you get with “git add -p“):

Next action [r,n,p,c,d,q,?]? r

Commit: 2c5d327debfdc2696267f7d4dba5c0a4335bc165
Author: Richard Worth <[email protected]>
Date:   Mon Oct 12 11:23:59 2009 +0000 (4 years ago)
Path:   ui/jquery.ui.slider.js
Match:  1 of 2

    slider: Removed undocumented noPropagation last arg from values method as [...]

446)        return this._value();
447)
448)    },
449)
450)   values: function(index, newValue) {
451)
452)        if (arguments.length > 1) {
453)            this.options.values[index] = this._trimAlignValue(newValue);
454)            this._refreshValue();

Next action [r,n,p,c,d,q,?]?

This is much more interesting: “Removed undocumented noPropagation last arg from values method”. Exactly what I’ve been looking for. Let’s look at the diff for that commit, via “d”:

Next action [r,n,p,c,d,q,?]? d
diff --git a/ui/jquery.ui.slider.js b/ui/jquery.ui.slider.js
index d27d9af..14e92f6 100644
--- a/ui/jquery.ui.slider.js
+++ b/ui/jquery.ui.slider.js
@@ -421,12 +421,12 @@ $.widget("ui.slider", $.extend({}, $.ui.mouse, {

    },

-      values: function(index, newValue, noPropagation) {
+      values: function(index, newValue) {

        if (arguments.length > 1) {
            this.options.values[index] = newValue;
            this._refreshValue();
-           if(!noPropagation) this._change(null, index);
+         this._change(null, index);
        }

        if (arguments.length) {

So this removed the noPropagation argument, along with the if-statement. Afterwards this._change() is always called.

I could dig further to figure out when that flag was introduced, but in this case I’m sure that the flag wasn’t removed by accident, so removing the argument in the call to this.values() must be valid as well. Problem solved!

If you use recursive-blame and run into issues, report them against Scott’s GitHub repo. If you like the tool and want to show some appreciation, donate to Scott on GitTip.

Update 1:

An alternative to recursive-blame might be git-log with the -L option, a rather recent addition that isn’t yet covered widely. The syntax looks like this:

-L <start>,<end>:<file>, -L :<regex>:<file>

The example near the end of the page is this:

git log -L '/int main/',/^}/:main.c

Shows how the function main() in the file main.c evolved over time.

For the example above, the following command seems to be somewhat effective:

git log -L '/values: function/',/\}/:ui/jquery.ui.slider.js

I haven’t yet figured out how to use the <end> argument properly.

It looks like this has some overlap with recursive-blame, but it doesn’t support multiple matches. The rather poor documentation certainly doesn’t help either.

-Jörn