Logo
Logo

Programming by Stealth

A blog and podcast series by Bart Busschots & Allison Sheridan.

PBS 106 — Git: Time Travel

In the previous instalment we learned to look back in time with git diff, to export the contents of our repository from some point in the past with git archive, and finally, to travel to the past by using git checkout to load the contents of an arbitrary commit into our working directory.

We did our time traveling very cautiously — making sure to have a clean working copy before departing, and being careful not to change anything while in the past. Why? Because Git can’t create new commits while our currently checked out commit is not at the head (end) of a branch. We learned that Git refers to this state somewhat melodramatically as a detached head state.

The focus of this instalment will be on reaching back into the past to retrieve files and/or changes, and to do so while keeping our head firmly attached 🙂.

Matching Podcast Episode

Listen along to this instalment on episode 661 of the Chit Chat Across the Pond Podcast.

You can also Download the MP3

Instalment Resources

Playing Along

If you’d like to play along with the examples, you’ll need to download this instalment’s ZIP file and unzip it. Open a terminal and change into the folder you extracted the ZIP into. You’ll find a file in there named pbs106a.bundle, this is a bundled version of the repository we created in the previous instalment, with a few additional commits added so we have something to go back in time to fetch.

Like we did in the previous instalment, we need to make a new repository and import all the commits from the bundle. We’ll name our new repository pbs106a. To create this new repo we’ll take the following steps:

  1. create a folder named pbs106a
  2. change into that folder
  3. initialise it as a Git repo
  4. if needed, change the default branch from master to main
  5. import all the commits from the bundle into our new repository

The commands to do all this are

mkdir pbs106a
cd pbs106a
git init
[ `git symbolic-ref --short HEAD` = 'master' ] && git checkout -b main
git pull ../pbs106a.bundle

Git Hashes Revisited — It’s OK to be Brief!

In the podcast episode accompanying the previous instalment, myself and Allison talked about the existence of ‘shorter hashes’ for Git commits, but we didn’t go into detail on them for a very simple reason — I hadn’t gotten around to figuring out exactly what the situation was with those shorter hashes.

Thanks to a flood of wonderful listener feedback I was saved the hassle of figuring it out for myself 🙂.

The shorter hashes are simply the first few characters of the longer hashes! Git is perfectly happy to use truncated hashes as long as the following two things are true:

  1. The truncated hash is at least 4 characters long.
  2. The truncated hash is unambiguous, that is to say, there is only one commit in the repo whose full hash starts with the truncated hash.

From now on we will be using truncated hashes in our examples, allowing us to keep our commands from scrolling quite as far off the edge of the page 😉.

More Compact Git Logs

On a related note — when looking for a specific commit, the rather verbose default output from the git log command requires a lot of scrolling. We can greatly shorten the output with the --oneline flag. This will show each commit on a single line as just a truncated hash followed by the first line of the commit message. You don’t see date or author details, but assuming you use good commit messages, this should be all the information you need.

Let’s use this flag to see what changes I made to the example repository between instalments:

bart-imac2018:pbs106a bart% git log --oneline
cef53ce (HEAD -> main) updated instalment references to 106
9e0df06 Removed the Easter Eggs
43bca0f Added Easter Egg into HTML file
c50e09c added a little Easter Egg
27d4f35 Updated PBS instalment number to 105 in all files
ff15d67 Fixed Markdown link in HTML file
ff8bc62 Added my history of programming in Hello World programs
2e3a0ce Correct semantic markup by moving title and lead into header and adding a footer
80025b1 Added link to Wikipedia article on the Hello World meme
a6d4bfd Added Bootstrap licensing info to README
7cf6b0b Moved from plain HTML to Bootstrap
c88546e Fixed silly typo in README
d58f072 initial version
bart-imac2018:pbs106a bart%

We can shorten the output even further by using the -n flag to limit the output to just the n most recent commits:

bart-imac2018:pbs106a bart% git log --oneline -n 5
cef53ce (HEAD -> main) updated instalment references to 106
9e0df06 Removed the Easter Eggs
43bca0f Added Easter Egg into HTML file
c50e09c added a little Easter Egg
27d4f35 Updated PBS instalment number to 105 in all files
bart-imac2018:pbs106a bart%

Note that this compact view does show that commit cef53ce is the currently checked out commit (HEAD), and that it is at the head of the branch main.

Viewing the Details of a Commit with git show

As we’ve seen, four commits have been added to the repo between the end of the previous instalment and the start of this one. How can we easily see the changes?

We could use git checkout to go to each commit, then use our browser and Markdown viewer of choice to look at the files, but that doesn’t seem practical.

We could also use the git diff command to see the changes between each new commit and the one before, e.g.

bart-imac2018:pbs106a bart% git diff 27d4f35 c50e09c
diff --git a/EasterEgg.png b/EasterEgg.png
new file mode 100644
index 0000000..490693b
Binary files /dev/null and b/EasterEgg.png differ
bart-imac2018:pbs106a bart%

Believe it or not, that output is telling us a binary (non-text) file named EasterEgg.png was added to the repository.

Notice that to get the changes in the right order, we had to put the newer of the two commits first.

When all we really want is to see the changes a single commit made, having to specify two hashes seems like needless additional work, and it is! The simplest way to see what changes a commit introduced is to ask Git to simply show us the commit’s details with git show. As an example, let’s look at the first new commit:

bart-imac2018:pbs106a bart% git show c50e09c
commit c50e09c5ec79beb9f1839cbe006f8d0f502c1705
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Nov 14 14:28:13 2020 +0000

    added a little Easter Egg

diff --git a/EasterEgg.png b/EasterEgg.png
new file mode 100644
index 0000000..490693b
Binary files /dev/null and b/EasterEgg.png differ
bart-imac2018:pbs106a bart%

As you can see, by default git show displays the commit’s metadata including the author, the time, and the commit message, as well as the list of changes the commit made to the branch. Again, we can see that the only change was the addition of the binary file EasterEgg.png.

We can similarly examine at the next commit:

bart-imac2018:pbs106a bart% git --no-pager show 43bca0f
commit 43bca0f7cc7befa88172f217f4d74b3ec58dffc6
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Nov 14 14:39:16 2020 +0000

    Added Easter Egg into HTML file

diff --git a/index.html b/index.html
index 9bf599c..1ffff9f 100644
--- a/index.html
+++ b/index.html
@@ -62,6 +62,12 @@ int main(void){
 console.info('Hello World!');
 </pre>
 
+<h2>Bart's Favourite Time Travel Movie</h2>
+
+<p>Perhaps not the most original choice, but <a href="https://en.wikipedia.org/wiki/Back_to_the_Future" target="_blank" rel="noopener">Back to the Future</a> has a special place in Bart's Heart.</p>
+
+<p class="text-center"><img src="EasterEgg.png" class="img-fluid" title="The original Back to the Future movie poster" alt="The original Back to the Future movie poster"></p>
+
 </main>
 <footer class="container small text-muted">&copy; Bart Busschots 2020 — Released under <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="noopener">Creative Commons Attribution-NonCommercial-ShareAlike</a>.</footer>
 </body>
bart-imac2018:pbs106a bart%

This shows that the only introduced by this commit was the addition of a sub-heading and two paragraphs to index.html.

Similarly, we can see that the third commit deleted the PNG image and removed the inserted sub-heading and paragraph from the HTML file:

bart-imac2018:pbs106a bart% git --no-pager show 9e0df06
commit 9e0df06f0e72062fbc9cddcc90257f9acba8ddbd
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Nov 14 14:41:17 2020 +0000

    Removed the Easter Eggs

diff --git a/EasterEgg.png b/EasterEgg.png
deleted file mode 100644
index 490693b..0000000
Binary files a/EasterEgg.png and /dev/null differ
diff --git a/index.html b/index.html
index 1ffff9f..9bf599c 100644
--- a/index.html
+++ b/index.html
@@ -62,12 +62,6 @@ int main(void){
 console.info('Hello World!');
 </pre>
 
-<h2>Bart's Favourite Time Travel Movie</h2>
-
-<p>Perhaps not the most original choice, but <a href="https://en.wikipedia.org/wiki/Back_to_the_Future" target="_blank" rel="noopener">Back to the Future</a> has a special place in Bart's Heart.</p>
-
-<p class="text-center"><img src="EasterEgg.png" class="img-fluid" title="The original Back to the Future movie poster" alt="The original Back to the Future movie poster"></p>
-
 </main>
 <footer class="container small text-muted">&copy; Bart Busschots 2020 — Released under <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="noopener">Creative Commons Attribution-NonCommercial-ShareAlike</a>.</footer>
 </body>
bart-imac2018:pbs106a bart% 

Finally, we can see that the final commit simply updated the references to PBS 105 to 106 in both the Markdown and HTML files. This is a good opportunity to point out that git show defaults to showing the details of the currently checked out commit, which happens to be the one we are interested in:

bart-imac2018:pbs106a bart% git --no-pager show
commit cef53ce7d6f9e8f43449b1e01a7ad7d905966c13 (HEAD -> main)
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Nov 14 14:42:24 2020 +0000

    updated instalment references to 106

diff --git a/README.md b/README.md
index de6776c..5274ab0 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Hello World Mark II
 
-This is a dummy project for use as an example in instalment 105 of the [Programming by Stealth](https://pbs.bartificer.net/) blog/podcast series.
+This is a dummy project for use as an example in instalment 106 of the [Programming by Stealth](https://pbs.bartificer.net/) blog/podcast series.
 
 This project contains a single HTML 5 web pages that says hello to the world.
 
diff --git a/index.html b/index.html
index 9bf599c..54f2edb 100644
--- a/index.html
+++ b/index.html
@@ -10,7 +10,7 @@
 <body>
 <header class="container">
 <h1 class="display-1">Hello World!!!</h1>
-<p class="lead">Welcome to some dummy content for instalment 105 of the <a href="https://pbs.bart.ficer.net/" target="_blank" rel="noopener">Programming by Stealth</a> blog/podcast series.</p>
+<p class="lead">Welcome to some dummy content for instalment 106 of the <a href="https://pbs.bart.ficer.net/" target="_blank" rel="noopener">Programming by Stealth</a> blog/podcast series.</p>
 </header>
 <main class="container">
 <p><em>Hello World</em> is a long-standing programming tradition, <a href="https://en.wikipedia.org/wiki/%22Hello,_World!%22_program" target="_blank" rel="noopener">learn more on Wikipedia</a>.</p>
bart-imac2018:pbs106a bart% 

Before we Begin — Let’s View the Changes In the Flesh

We’ve now seen exactly what changes were made with each commit, but before we start bringing changes from the past back into the present, let’s start by looking at the current state of the HTML page in a browser.

Notice that the file looks just like we left it, except, it now says it’s associated with instalment 106 rather than 105.

Next, let’s go back and look at the easter egg that was added and then removed by checking out the commit just before the Easter Egg was deleted and opening the index file in your browser of choice again:

git checkout 43bca0f

As you can see, the Easter Egg I added is the original movie poster for the first Back to the Future movie, and some accompanying text.

OK, let’s return to the present:

git checkout main

Retrieving a File from the Past with git checkout

We’ve just used git checkout to bring an entire commit into the working copy, and then, to bring the entire commit at the head of a branch into the working copy. In effect we’ve used git checkout to go to a point in the past, and then, to go back to the present (head of the main branch).

This is not the only thing git checkout can do, it can also reach back in time and bring previous versions of individual files into the present for us. To enable this fetch mode simply specify one or more file paths as the final arguments to git checkout.

Because file names can be the same as flags to git, it’s important to tell git to stop looking for any more flags after the hash. This can be done with the special -- flag.

This means that the general form of the command is:

git checkout HASH -- FILE_PATH(S)

Where HASH is the hash for a commit, and FILE_PATH(S) is one or more file paths within the repo.

So, to retrieve the PNG file from commit c50e09c into the working copy we run:

git checkout c50e09c -- EasterEgg.png

We can use git status to see what has happened:

bart-imac2018:pbs106a bart% git status
On branch main
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   EasterEgg.png

bart-imac2018:pbs106a bart% 

So, we have not moved to the specified commit (we are still On branch main), and the file we fetched has been retrieved and staged.

Retrieving a Change from the Past with git cherry-pick

As we’ve seen, we can use git checkout to fetch an entire file from the past, but what if we want to apply the changes made in a past commit to the current version of our file?

In the past the HTML file had the Easter Egg, but it still referenced instalment 105, now, it references instalment 106, but is missing the Easter Egg. If we checked out index.html from the past we would get both the change we want (the Easter Egg), but we would lose a later change we also want (the instalment bump). Can we eat our proverbial cake and still have it? Yes, of course we can 🙂.

We can use the git cherry-pick command to reapply the changes from a previous commit to the currently checked out branch. This command will reapply and commit the changes, so you can only use it when the following two things are true:

  1. You are currently on the head of a branch (you don’t have a detached head).
  2. Your working copy is clean (you have no un-staged or un-committed changes).

At the moment we have staged the PNG file, but not committed it, so we don’t have a clean working copy. Let’s remedy that:

git commit -m "retrieved the Easter Egg PNG from the past"

To reapply all the changes from a given commit we simply pass that commit as the only argument to git cherry-pick. This will reapply all the changes and commit them with the identical message to the original commit. We can specify an entirely new commit message with the --edit flag, or, we can automatically insert a note specifying that the commit is a cherry-pick and where it was picked from with the -x flag.

Let’s add the Easter Egg back into the HTML file without losing our changes to the instalment number, and let’s automatically append the fact that we cherry picked to the commit message:

git cherry-pick -x 43bca0f

We can use git log to see that a new commit has been added to the main branch:

bart-imac2018:pbs106a bart% git log --oneline -n 5    
cf47673 (HEAD -> main) Added Easter Egg into HTML file
46e114a retrieved the Easter Egg PNG from the past
cef53ce updated instalment references to 106
9e0df06 Removed the Easter Eggs
43bca0f Added Easter Egg into HTML file
bart-imac2018:pbs106a bart%

We can also use git show to see the full details of our newly added commit:

bart-imac2018:pbs106a bart% git --no-pager show
commit cf476734625e6296b3dd25dcded2dcd23080b156 (HEAD -> main)
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Nov 14 14:39:16 2020 +0000

    Added Easter Egg into HTML file
    
    (cherry picked from commit 43bca0f7cc7befa88172f217f4d74b3ec58dffc6)

diff --git a/index.html b/index.html
index 54f2edb..69a731b 100644
--- a/index.html
+++ b/index.html
@@ -62,6 +62,12 @@ int main(void){
 console.info('Hello World!');
 </pre>
 
+<h2>Bart's Favourite Time Travel Movie</h2>
+
+<p>Perhaps not the most original choice, but <a href="https://en.wikipedia.org/wiki/Back_to_the_Future" target="_blank" rel="noopener">Back to the Future</a> has a special place in Bart's Heart.</p>
+
+<p class="text-center"><img src="EasterEgg.png" class="img-fluid" title="The original Back to the Future movie poster" alt="The original Back to the Future movie poster"></p>
+
 </main>
 <footer class="container small text-muted">&copy; Bart Busschots 2020 — Released under <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="noopener">Creative Commons Attribution-NonCommercial-ShareAlike</a>.</footer>
 </body>
bart-imac2018:pbs106a bart%

Notice the note added to the end of the message stating that the commit was cherry picked, and where it was cherry picked from.

Final Thoughts

So far in our Git journey we’ve learned how to track our changes as we go by creating commits, we’ve learned how to compare those commits to each other with git diff, to see a list of commits with git log, and to see the details of our commits with git show. We’ve also learned how to travel back in time with git checkout, and how to export snapshots of commits with git archive. Finally, we’ve learned how to reach back in time and retrieve entire files with git checkout --, and to retrieve and reapply changes with git cherry-pick.

Up to this point our repository has consisted of a simple linear timeline of commits. This is the simplest possible repository, and is analogous to a tree with a trunk but no branches. A tree with no branches is not a realistic tree, and similarly, a repository with just a single main branch is not a very realistic repository. In the real world you’ll almost certainly need, or at the very least want, to create multiple branches in your repository. You can think of these as alternative time-lines.

Join the Community

Find us in the PBS channel on the Podfeet Slack.

Podfeet Slack