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 swiss-knife of software engineers, is invaluable. Let’s dive in and see how we can use our
git history and easily navigate through it.
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 and the commit hash. 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.
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?
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
git log for this path:
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
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 51cd6bb829c418c5fbf75de1dfbb177233b1b154 Author: David Lee <[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 angry 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 <email@example.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
What are your favourite (or, most effective) ways of navigating through the