Git to the source

In a legacy software project with a long and storied history, it can be difficult to determine why a given class, method, or line was added in the first place.

The git blame command is a common tool for uncovering the history of a file on a line-by-line basis. However, if a section of code was moved over from a different file, the results from git blame can be less than useful.

Let’s say I’m trying to get to the original commit message relating to code in a method called #fulfill:

$ git blame app/models/subscription_fulfillment.rb
ad36d88b (Ian 2015-09-25  1) class SubscriptionFulfillment
...
ad36d88b (Ian 2015-09-25  6)   def fulfill
ad36d88b (Ian 2015-09-25  7)     ActiveRecord::Base.transaction do
ad36d88b (Ian 2015-09-25  8)       subscription = Subscription.create!(subscription_params)
ad36d88b (Ian 2015-09-25  9)       UserRole.create!(user: current_user, role_type: UserRole::SUBSCRIBER)
ad36d88b (Ian 2015-09-25 10)       BillingSchedule.create!(user: current_user, subscription: subscription)
ad36d88b (Ian 2015-09-25 11)     end
ad36d88b (Ian 2015-09-25 12)   end
...
ad36d88b (Ian 2015-09-25 17) end

Let’s look at the commit provided by the blame command:

$ git show ad36d88b
commit ad36d88b8eabf76e70af1f47fa9f82ac0a2959f8
Author: Ian C. Anderson <iancanderson@thoughtbot.com>
Date:   Fri Sep 25 11:42:22 2015 -0400

    Refactor to SubscriptionFulfillment class

diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb
index 10c7632..12dca61 100644
--- a/app/controllers/subscriptions_controller.rb
+++ b/app/controllers/subscriptions_controller.rb
@@ -1,9 +1,5 @@
 class SubscriptionsController < ActionController::Base
   def create
-    ActiveRecord::Base.transaction do
-      subscription = Subscription.create!(subscription_params)
-      UserRole.create!(user: current_user, role_type: UserRole::SUBSCRIBER)
-      BillingSchedule.create!(user: current_user, subscription: subscription)
-    end
+    SubscriptionFulfillment.new(subscription_params).fulfill
   end
 end
diff --git a/app/models/subscription_fulfillment.rb b/app/models/subscription_fulfillment.rb
new file mode 100644
index 0000000..40c2a43
--- /dev/null
+++ b/app/models/subscription_fulfillment.rb
@@ -0,0 +1,21 @@
+class SubscriptionFulfillment
+  def initialize(subscription_params)
+    @subscription_params = subscription_params
+  end
+
+  def fulfill
+    ActiveRecord::Base.transaction do
+      subscription = Subscription.create!(subscription_params)
+      UserRole.create!(user: current_user, role_type: UserRole::SUBSCRIBER)
+      BillingSchedule.create!(user: current_user, subscription: subscription)
+    end
+  end
+
+  private
+
+  attr_reader :subscription_params
+end

This doesn’t give us a useful commit message about the code’s origin, since this commit simply moved the code out of a controller action into a separate SubscriptionFulfillment class. At this point, we could run git blame on the SubscriptionsController to follow back to the original commit. But in a legacy application, this can lead you on a wild goose chase. Luckily there’s a better way!

$ git blame -C app/models/subscription_fulfillment.rb
ad36d88b app/models/subscription_fulfillment.rb      (Ian 2015-09-25  1) class SubscriptionFulfillment
ad36d88b app/models/subscription_fulfillment.rb      (Ian 2015-09-25  2)   def initialize(subscription_params)
ad36d88b app/models/subscription_fulfillment.rb      (Ian 2015-09-25  3)     @subscription_params = subscription_params
ad36d88b app/models/subscription_fulfillment.rb      (Ian 2015-09-25  4)   end
ad36d88b app/models/subscription_fulfillment.rb      (Ian 2015-09-25  5) 
ad36d88b app/models/subscription_fulfillment.rb      (Ian 2015-09-25  6)   def fulfill
ac97cf4a app/controllers/subscriptions_controller.rb (Kat 2014-05-05  7)     ActiveRecord::Base.transaction do
ac97cf4a app/controllers/subscriptions_controller.rb (Kat 2014-05-05  8)       subscription = Subscription.create!(subscription_params)
ac97cf4a app/controllers/subscriptions_controller.rb (Kat 2014-05-05  9)       UserRole.create!(user: current_user, role_type: UserRole::SUBSCRIBER)
ac97cf4a app/controllers/subscriptions_controller.rb (Kat 2014-05-05 10)       BillingSchedule.create!(user: current_user, subscription: subscription)
ac97cf4a app/controllers/subscriptions_controller.rb (Kat 2014-05-05 11)     end
ac97cf4a app/controllers/subscriptions_controller.rb (Kat 2014-05-05 12)   end
ad36d88b app/models/subscription_fulfillment.rb      (Ian 2015-09-25 13) 
ad36d88b app/models/subscription_fulfillment.rb      (Ian 2015-09-25 14)   private
ad36d88b app/models/subscription_fulfillment.rb      (Ian 2015-09-25 15) 
ad36d88b app/models/subscription_fulfillment.rb      (Ian 2015-09-25 16)   attr_reader :subscription_params
ad36d88b app/models/subscription_fulfillment.rb      (Ian 2015-09-25 17) end

By providing the -C option to git blame, git shows us the original commits that added those lines, even if they were originally added to a different file.

Then we can directly view the original commit to get some more context around the code:

$ git show ac97cf4
commit ac97cf4a8ef12650bbcd80e93f0d45f01675574a
Author: Kat <kat@example.com>
Date:   Mon May 5 11:34:51 2014 -0400

    User subscribes to our service

    When a user subscribes to our super-stealth SaaS app,
    - Create their Subscription record
    - Give the user subscriber rights by assigning them a UserRole
    - Kick off recurring billing by creating a related BillingSchedule

diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb
new file mode 100644
index 0000000..10c7632
--- /dev/null
+++ b/app/controllers/subscriptions_controller.rb
@@ -0,0 +1,9 @@
+class SubscriptionsController < ActionController::Base
+  def create
+    ActiveRecord::Base.transaction do
+      subscription = Subscription.create!(subscription_params)
+      UserRole.create!(user: current_user, role_type: UserRole::SUBSCRIBER)
+      BillingSchedule.create!(user: current_user, subscription: subscription)
+    end
+  end
+end

This option can be particularly useful when investigating a file that has been partially moved from a previous file, e.g. breaking up a Rails routes file, or code refactorings and extractions like the above example.