Git to the source

Ian C. Anderson

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 refactoring and extractions like the above example.

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.