Learn your tools: Navigating your Git History
Table of Contents
Starting a greenfield application everyday is nearly impossible, especially in
your daily job. In fact, most of us are facing (somewhat) legacy codebases on a
daily basis, and regaining the context of why some feature, or line of code
exists in the codebase is very important. This is where git
, the distributed
version control system, is invaluable. Let’s dive in and see how we can use our
git
history and easily navigate through it.
Git history #
First and foremost, what is git
history? As the name says, it is the commit
history of a git
repo. It contains a bunch of commit messages, with their
authors’ name, the commit hash and the date of the commit. The easiest way to
see the history of a git
repo, is the git log
command.
Sidenote: For the purpose of this post, we will use Ruby on Rails’ repo, the
master
branch. The reason behind this is because Rails has a very good git
history, with nice commit messages, references and explanations behind every
change. Given the size of the codebase, the age and the number of maintainers,
it’s certainly one of the best repositories that I have seen. Of course, I am
not saying there are no other repositories built with good git
practices, but
this is one that has caught my eye.
So back to Rails’ repo. If you run git log
in the Rails’ repo, you will see
something like this:
commit 66ebbc4952f6cfb37d719f63036441ef98149418
Author: Arthur Neves <[email protected]>
Date: Fri Jun 3 17:17:38 2016 -0400
Dont re-define class SQLite3Adapter on test
We were declaring in a few tests, which depending of
the order load will cause an error, as the super class could change.
see https://github.com/rails/rails/commit/ac1c4e141b20c1067af2c2703db6e1b463b985da#commitcomment-17731383
commit 755f6bf3d3d568bc0af2c636be2f6df16c651eb1
Merge: 4e85538 f7b850e
Author: Eileen M. Uchitelle <[email protected]>
Date: Fri Jun 3 10:21:49 2016 -0400
Merge pull request #25263 from abhishekjain16/doc_accessor_thread
[skip ci] Fix grammar
commit f7b850ec9f6036802339e965c8ce74494f731b4a
Author: Abhishek Jain <[email protected]>
Date: Fri Jun 3 16:49:21 2016 +0530
[skip ci] Fix grammar
commit 4e85538dddf47877cacc65cea6c050e349af0405
Merge: 082a515 cf2158c
Author: Vijay Dev <[email protected]>
Date: Fri Jun 3 14:00:47 2016 +0000
Merge branch 'master' of github.com:rails/docrails
Conflicts:
guides/source/action_cable_overview.md
commit 082a5158251c6578714132e5c4f71bd39f462d71
Merge: 4bd11d4 3bd30d9
Author: Yves Senn <[email protected]>
Date: Fri Jun 3 11:30:19 2016 +0200
Merge pull request #25243 from sukesan1984/add_i18n_validation_test
Add i18n_validation_test
commit 4bd11d46de892676830bca51d3040f29200abbfa
Merge: 99d8d45 e98caf8
Author: Arthur Nogueira Neves <[email protected]>
Date: Thu Jun 2 22:55:52 2016 -0400
Merge pull request #25258 from alexcameron89/master
[skip ci] Make header bullets consistent in engines.md
commit e98caf81fef54746126d31076c6d346c48ae8e1b
Author: Alex Kitchens <[email protected]>
Date: Thu Jun 2 21:26:53 2016 -0500
[skip ci] Make header bullets consistent in engines.md
As you can see, the git log
shows the commit hash, the author and his email
and the date of when the commit was created. Of course, git
being super
customisable, it allows you to customise the output format of the git log
command. Let’s say, we want to just see the first line of the commit message,
we could run git log --oneline
, which will produce a more compact log:
66ebbc4 Dont re-define class SQLite3Adapter on test
755f6bf Merge pull request #25263 from abhishekjain16/doc_accessor_thread
f7b850e [skip ci] Fix grammar
4e85538 Merge branch 'master' of github.com:rails/docrails
082a515 Merge pull request #25243 from sukesan1984/add_i18n_validation_test
4bd11d4 Merge pull request #25258 from alexcameron89/master
e98caf8 [skip ci] Make header bullets consistent in engines.md
99d8d45 Merge pull request #25254 from kamipo/fix_debug_helper_test
818397c Merge pull request #25240 from matthewd/reloadable-channels
2c5a8ba Don't blank pad day of the month when formatting dates
14ff8e7 Fix debug helper test
To see all of the git log
options, I recommend checking out manpage of
git log
, available in your terminal via man git-log
or git help log
.
A tip: if git log
is a bit scarse or complicated to use, or maybe you are just
bored, I recommend checking out various git
GUIs and command line tools. In
the past I’ve used GitX which was very good, but since
the command line feels like home to me, after trying
tig I’ve never looked back.
Finding Nemo #
So now, since we know the bare minimum of the git log
command, let’s see how
we can explore the history more effectively in our everyday work.
Let’s say, hypothetically, we are suspecting an unexpected behaviour in the
String#classify
method and we want to find how and where it has been
implemented.
One of the first commands that you can use, to see where the method is defined,
is git grep
. Simply said, this command prints out lines that match a certain
pattern. Now, to find the definition of the method, it’s pretty simple - we can
grep for def classify
and see what we get:
➜ git grep 'def classify'
activesupport/lib/active_support/core_ext/string/inflections.rb: def classify
activesupport/lib/active_support/inflector/methods.rb: def classify(table_name)
tools/profile: def classify
Now, although we can already see where our method is created, we are not sure
on which line it is. If we add the -n
flag to our git grep
command, git
will provide the line numbers of the match:
➜ git grep -n 'def classify'
activesupport/lib/active_support/core_ext/string/inflections.rb:205: def classify
activesupport/lib/active_support/inflector/methods.rb:186: def classify(table_name)
tools/profile:112: def classify
Much better, right? Having the context in mind, we can easily figure out that
the method that we are looking for lives in activesupport/lib/active_support/core_ext/string/inflections.rb
, on line 205. The classify
method, in all of it’s glory looks like
this:
# Creates a class name from a plural table name like Rails does for table names to models.
# Note that this returns a string and not a class. (To convert to an actual class
# follow +classify+ with +constantize+.)
#
# 'ham_and_eggs'.classify # => "HamAndEgg"
# 'posts'.classify # => "Post"
def classify
ActiveSupport::Inflector.classify(self)
end
Although the method we found is the one we usually call on String
s, it invokes
another method on the ActiveSupport::Inflector
, with the same name. Having our
git grep
result available, we can easily navigate there, since we can see the
second line of the result being
activesupport/lib/active_support/inflector/methods.rb
on line 186. The method
that we are are looking for is:
# Creates a class name from a plural table name like Rails does for table
# names to models. Note that this returns a string and not a Class (To
# convert to an actual class follow +classify+ with #constantize).
#
# classify('ham_and_eggs') # => "HamAndEgg"
# classify('posts') # => "Post"
#
# Singular names are not handled correctly:
#
# classify('calculus') # => "Calculus"
def classify(table_name)
# strip out any leading schema name
camelize(singularize(table_name.to_s.sub(/.*\./, ''.freeze)))
end
Boom! Given the size of Rails, finding this should not take us more than 30
seconds with the help of git grep
.
So, what changed last? #
Now, since we have the method available, we need to figure out what were the
changes that this file has gone through. The since we know the correct file name
and line number, we can use git blame
. This command shows what revision and
author last modified each line of a file. Let’s see what were the latest changes
made to this file:
git blame activesupport/lib/active_support/inflector/methods.rb
Whoa! Although we get the last change of every line in the file, we are more
interested in the specific method (lines 176 to 189). Let’s add a flag to
the git blame
command, that will show the blame of just those lines. Also, we
will add the -s
(suppress) option to the command, to skip the author names and
the timestamp of the revision (commit) that changed the line:
git blame -L 176,189 -s activesupport/lib/active_support/inflector/methods.rb
9fe8e19a 176) # Creates a class name from a plural table name like Rails does for table
5ea3f284 177) # names to models. Note that this returns a string and not a Class (To
9fe8e19a 178) # convert to an actual class follow +classify+ with #constantize).
51cd6bb8 179) #
6d077205 180) # classify('ham_and_eggs') # => "HamAndEgg"
9fe8e19a 181) # classify('posts') # => "Post"
51cd6bb8 182) #
51cd6bb8 183) # Singular names are not handled correctly:
5ea3f284 184) #
66d6e7be 185) # classify('calculus') # => "Calculus"
51cd6bb8 186) def classify(table_name)
51cd6bb8 187) # strip out any leading schema name
5bb1d4d2 188) camelize(singularize(table_name.to_s.sub(/.*\./, ''.freeze)))
51cd6bb8 189) end
The output of the git blame
command now shows all of the file lines and their
respective revisions. Now, to see a specific revision, or in other words, what
each of those revisions changed, we can use the git show
command. When
supplied a revision hash (like 66d6e7be
) as an argument, it will show you the
full revision, with the author name, timestamp and the whole revision in it’s
glory. Let’s see what actually changed at the latest revision that changed line
188:
git show 5bb1d4d2
Whoa! Did you test that? If you didn’t, it’s an awesome commit by Schneems that made a very interesting performance optimization by using frozen strings, which makes sense in our current context. But, since we are on this hypothetical debugging session, this doesn’t tell much about our current problem. So, how can we see what changes has our method under investigation gone through?
Searching the logs #
Now, we are back to the git
log. The question is, how can we see all the
revisions that the classify
method went under?
The git log
command is quite powerful, because it has a rich list of options
to apply to it. We can try to see what the git
log has stored for this file,
using the -p
options, which means show me the patch for this entry in the
git
log:
git log -p activesupport/lib/active_support/inflector/methods.rb
This will show us a big list of revisions, for every revision of this file. But, just like before, we are interested in the specific file lines. Let’s modify the command a bit, to show us what we need:
git log -L 176,189:activesupport/lib/active_support/inflector/methods.rb
The git log
command accepts the -L
option, which takes the lines range and
the filename as arguments. The format might be a bit weird for you, but it
translates to:
git log -L <start-line>,<end-line>:<path-to-file>
When we run this command, we can see the list of revisions for these lines, which will lead us to the first revision that created the method:
commit 51xd6bb829c418c5fbf75de1dfbb177233b1b154
Author: Foo Bar <[email protected]>
Date: Tue Jun 7 19:05:09 2011 -0700
Refactor
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -58,0 +135,14 @@
+ # Create a class name from a plural table name like Rails does for table names to models.
+ # Note that this returns a string and not a Class. (To convert to an actual class
+ # follow +classify+ with +constantize+.)
+ #
+ # Examples:
+ # "egg_and_hams".classify # => "EggAndHam"
+ # "posts".classify # => "Post"
+ #
+ # Singular names are not handled correctly:
+ # "business".classify # => "Busines"
+ def classify(table_name)
+ # strip out any leading schema name
+ camelize(singularize(table_name.to_s.sub(/.*\./, '')))
+ end
Now, look at that - it’s a commit from 2011. Practically, git
allows us to
travel back in time. This is a very good example of why a proper commit message
is paramount to regain context, because from the commit message we cannot
really regain context of how this method came to be. But, on the flip side, you
should never ever get frustrated about it, because you are looking at
someone that basically gives away his time and energy for free, doing open
source work.
Coming back from that tangent, we are not sure how the initial implementation
of the classify
method came to be, given that the first commit is just a
refactor. Now, if you are thinking something within the lines of “but maybe,
just maybe, the method was not on the line range 176 to 189, and we should look
more broadly in the file”, you are very correct. The revision that we saw said
“Refactor” in it’s commit message, which means that the method was actually
there, but after that refactor it started to exist on that line range.
So, how can we confirm this? Well, believe it or not, git
comes to the rescue
again. The git log
command accepts the -S
option, which looks for the code
change (additions or deletions) for the specified string as an argument to the
command. This means that, if we call git log -S classify
, we can see all of
the commits that changed a line that contains that string.
If you call this command in the Rails repo, you will first see git
slowing
down a bit. But, when you realise that git
actually parses all of the
revisions in the repo to match the string, it’s actually super fast. Again, the
power of git
at your fingertips. So, to find the first version of the
classify
method, we can run:
git log -S 'def classify'
This will return all of the revisions where this method has been introduced or changed. If you were following along, the last commit in the log that you will see is:
commit db045dbbf60b53dbe013ef25554fd013baf88134
Author: David Heinemeier Hansson <foo@bar.com>
Date: Wed Nov 24 01:04:44 2004 +0000
Initial
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
How cool is that? It’s the initial commit to Rails, made on a svn
repo by
DHH! This means that classify
has been around since the beginning of (Rails)
time. Now, to see the commit with all of it’s changes, we can run:
git show db045dbbf60b53dbe013ef25554fd013baf88134
Great, we got to the bottom of it. Now, by using the output from git log -S 'def classify'
you can track the changes that have happened to this method,
combined with the power of the git log -L
command.
Until next time #
Sure, we didn’t really fix any bugs, because we were trying some git
commands
and following along the evolution of the classify
method. But, nevertheless,
git
is a very powerful tool that we all must learn to use and to embrace. I
hope this article gave you a little bit more knowledge of how useful git
is.
What are your favourite (or, most effective) ways of navigating through the
git
history?