In Git there are several ways to target specific commits, some easier to comprehend than others. In short, you have two main options: use absolute or relative commit references. In this post I'll illustrate how the two options differ, and highlight why it's important to familiarize yourself with both alternatives.
Absolute vs Relative references
The easiest way to select an arbitrary commit in your history is by using absolute referencing, meaning explicitly selecting the commit using its full SHA hash (or a partial sequence of it). Let's exemplify it with the below history consisting of just two commits on one branch.
Absolute referencing: using explicit SHA
If we for example would like to
show the content of commit C1, explicitly using its SHA, any of the below commands would do the trick.
# Using the full SHA $ git show 14ko32750d81238c424a3889fde067553317e49d --oneline 14ko327 (HEAD -> master) # or, using a paritial SHA (e.g. first 5 characters) $ git show 14ko3 --oneline 14ko327 (HEAD -> master)
That fact is, as you can see in the last alternative above, the SHA identifier can in theory be as short as possible as long as it uniquely identifies a single commit within your history; in general, 4-7 characters are typically enough to uniquely identify a specific commit.
If we instead wanted to show the content of C0, swapping the above statement with its SHA (fg45n), would work equally well.
With explicit SHA referencing any commit in your history can be targeted, even orphaned ones only viewable through the reflog.
Absolute referencing: using implicit symbolic reference
To constantly retrieve and use the SHA for referencing commits can be quite cumbersome in the long run, so in 90% of the time it's enough to use a symbolic reference such as a branch or a tag instead.
If we again consider the simple two-commit history from earlier, we see that we can target C1 using either
HEAD as both of them symbolically references 14ko3 implicitly. With that said, the following lines would produce the same outcome.
# Using HEAD as the symbolic reference $ git show HEAD --oneline 14ko327 (HEAD -> master) # or, using the branch name $ git show master --oneline 14ko327 (HEAD -> master) # or, again using a partial SHA $ git show 14ko3 --oneline 14ko327 (HEAD -> master)
Knowing about how commits can be absolutely referenced, either explicitly or implicitly, is crucial to swiftly navigate your history!
But, even though absolute references allow you to target any commit, it's sometimes not the fastest way to move around. This is particularly true when you just want to select an earlier version from an arbitrary commit – let me introduce relative references!
Relative referencing: using ~ and ^
Let's reconsider the same simple two-commit history from earlier. If again want to
show the content of C0, Git has provided us with some relative selectors that can be used in combination with absolut references.
For example, targeting C0 using relative referencing can be done like this:
# Using HEAD combined with tilde (~) $ git show HEAD~ --oneline fg45n34 # or, HEAD combined with caret (^) $ git show HEAD^ fg45n34
Both commands above would produce the exact same outcome in this given example. But why is that? And how do tilde and caret differ?
Let's first scrutinise what the two selectors actually do, and then consider a slightly more extended commit history!
- Tilde (~):
<n>thgeneration ancestor, following only first* parents
- Caret (^):
<n>thparent of first generation ancestors
* First parent is always the left hand side of a merge, e.g. the commit on the branch that got merged into.
As our two-commit history is linear – C1 only has one parent, and hence caret (^) resolves to the same commit as tilde (~) in this case. With a more extended history, the following illustration showcases how relative selectors can be chained together (in different ways) in order to select any previous ancestor.
Below illustration is heavily inspired by the textual chart originally created by Jon Loeliger, available in the official Git documentation.
It's important to understand that when selectors are chained together, execution order is left to right. That's why for example
HEAD~~^2 targets the same ancestor (H) in this particular case.
Broken down the latter alternative
HEAD~: Find first ancestor of
~: Find first ancestor based on outcome of step 1
^2: Find second parent ancestor based on outcome of step 1 and 2
As shown, the main difference between absolute and relative referencing is that by using absolute any commit in the entire history (orphaned or not) can be targeted, while using relative only ancestors to an absolute starting point can be selected.
Learning to use both types of referencing techniques will for sure speed up your Git game, but if you're just starting out – focus on mastering the absolute alternative first!
A web server analogy
In simple terms, absolute explicit references (SHA) can be viewed as typing the IP address of a specific server into your browser. Similarly, using an absolut implicit reference (branch/tag/HEAD) is like typing the actual web address instead – which is just a symbolic reference. Completing the browser analogy, relative references can be viewed as using the back button in your browser – it requires a starting point and then traverses backwards from there.
😎 Thanks for reading and good luck improving your source code management skills!
If you'd like more pieces like this, make sure to subscribe to the news feed so you don't miss anything!
Any questions or suggestions, try reaching me on Twitter – @Stjaertfena