Alex headshot

AlBlue’s Blog

Macs, Modularity and More

Git Tip of the Week: Git Revisions

2011, git, gtotw, tip

This week's Git Tip of the Week is about looking for commits with git revisions syntax. You can subscribe to the feed if you want to receive new instalments automatically.


Git Log

Git log is a versatile tool which can let you introspect the state of your repository. We've been implicitly using it in a number of other examples already; in this post, we're going to look at some of the other options that git log takes, as well as the ways in which we can refer to items in Git's history.

Last week, we looked at Git reflogs, which (together with the previous post on git stash) discussed a notation for looking at commits with the HEAD@{1} (c.f. stash@{1}) syntax. In fact, there's a lot of other mechanisms we can use to refer to items in Git's history, over and above the commit hash or branch name.

A git history is a directed acyclic graph of commits, from the HEAD backwards to one (or more) roots. In most cases, commits have a single parent; but merge commits have two (or more) parents. (Hg, by contrast, can only have one or two parents. Converting from a Git repository to an Hg repository is therefore not totally faithful.)

Since each commit may have more than one parent, the parent operator (^ as a suffix) allows you to disambiguate which parent you are referring to. Given that each merge node is represented as a pair (or more) of commits, the parents are numbered from 1 to n. (The special number 0 is used to refer to itself.)


# Dummy repository
$ git log --oneline
77bc990 Third commit
25d4fc4 Second commit
f0faab6 First commit
$ git log --oneline HEAD^
25d4fc4 Second commit
f0faab6 First commit
$ git log --oneline HEAD^
25d4fc4 Second commit
f0faab6 First commit
$ git log --oneline HEAD^^
f0faab6 First commit
$ git log --oneline HEAD^2
fatal: ambiguous argument 'HEAD^2': unknown revision or path not in the working tree.

Here, HEAD is pointing to 77bc990 Third commit, and so both HEAD^ and HEAD^1 refer to the same item (25d4fc4 Second commit). However, HEAD^^ gives a different answer to HEAD^2; in the former, finding the root of the history and in the latter giving an error.

That's because HEAD^^ means “the (first) parent of the (first) parent of HEAD”, whereas HEAD^2 means “the second parent of HEAD”. Generally, HEAD^n where n >= 2 only makes sense on merge nodes.

However, there is another useful reference; the grandparent selector. Instead of considering a breadth-based lookup, it does a depth-based lookup:


# Dummy repository
$ git log --oneline
77bc990 Third commit
25d4fc4 Second commit
f0faab6 First commit
$ git log --oneline HEAD~
25d4fc4 Second commit
f0faab6 First commit
$ git log --oneline HEAD~~
f0faab6 First commit
$ git log --oneline HEAD~2
f0faab6 First commit

Like the parent operator, the grandparent operator can take a number as well; except instead of referring to the nth parent, HEAD~n refers to the nth grandparent.

Note that the ~ selects up the first parent (much like ^ does); but the two forms can be mixed if needed. In this case, both HEAD^~ and HEAD~^ have the same effect; but you can explicitly select which item you want with a numeric selector; for example, git log HEAD^2~10 gives you the current merge node's second parent's tenth ancestor.

Ranges and sets

As well as single references, it's possible to refer to ranges in Git as well. In fact, these are referred to as sets of commits (since they may not be contiguous on the commit tree). The most common way is to show the commits between one ref and another:


$ git checkout -b other f0faab6
Switched to a new branch 'other'
$ touch file
$ git add file
$ git commit -m "Adding file" file
$ git log --oneline
1762164 Adding file
f0faab6 First commit
$ git log --oneline other..master
77bc990 Third commit
25d4fc4 Second commit
$ git log --oneline master..other
1762164 Adding file

The syntax is between..and, where both references can either be one of the symbolic branch/tag references, or a commit hash etc. What this is saying is “Show me all the commits which are in master but not in other” (and vice versa, for the second one).

However, whilst it looks like a range, really this is just a set selection. The syntax between..and is actually a shorthand for ^between and, which itself is a shorthand for and --not between. In other words, the above code is showing you what is in other but not in master.

What use is the long hand, when the shorthand is much more convenient? Well, the long hand allows you to specify more than two references. For example:


# Show all unmerged changes between features 1,2,3 and master
$ git log ^master feature1 feature2 feature3
# Show changes in hotfix and release branches, but not in master
$ git log ^master hotfix release-1.0 release-1.1

In most cases, the asymmetric diff (using two commits) is probably what you want. It's worth noting, finally, that there is a symmetric diff (using two commits) – instead of two dots, use three:


$ git log master...other
1762164 Adding file
77bc990 Third commit
25d4fc4 Second commit
$ git log other...master
1762164 Adding file
77bc990 Third commit
25d4fc4 Second commit

Note that there's no difference in the order of the commits, since they're ordered by time (or however you want to order them in git log). And, since it's symmetric, the result is the same regardless of the order of operands.

Summary

Git allows you to specify single commits as well as a range of commits using a number of git revision identifiers, and these can be used to find out the state of and differences between trees. In this post, we covered the parent operator (^), the ancestor operator (~), as well as ranges with asymmetric and symmetric differences.


Come back next week for another instalment in the Git Tip of the Week series.