Logo
Logo

Programming by Stealth

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

PBS 104 — Git: Tracking Changes

So far in this mini series we’ve looked at the larger landscape of version control, we’ve explained why we’ve chosen the distributed version control system Git for this series, and we’ve learned how to make our first Git repository and add some files to it.

In this instalment we’ll take things one step further and learn how to manage changes to our files.

Matching Podcast Episode

Listen along to this instalment on episode 658 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, extract the folder pbs104a, change into it in a terminal, and execute the following commands to initialise it as a git repository with all the files added:

cd pbs104a
git init
[ `git symbolic-ref --short HEAD` = 'master' ] && git checkout -b main
git add .
git commit -m "initial version"

The majority of those commands are straight from the previous instalment, but notice that the optional command for re-naming master to main as needed with older versions of git has been pre-fixed with some additional logic. Since this is Programming by Stealth rather than Taming the Terminal I won’t go into detail of exactly how that command works, instead, I suggest you save it for future use, perhaps in your text expansion utility of choice.

Having said that, for anyone who’s curious, the terminal command for conditionally re-naming master to main makes use of the following commands/concepts:

  1. the POSIX test command
  2. the git symbolic-ref command (man git-symbolic-ref)
  3. So-called Lazy Evaluation to only execute the git checkout command when the current branch is master.

Some Housekeeping for Mac Users — Globally Ignore .DS_Store Files

To avoid macOS’s hidden .DS_Store files getting in our way we need to configure git to globally ignore those files. We’ll explain these commands in detail in a future instalment, but for now, we’ll simply make the change without digging into the detail.

Firstly, if you’ve already a Git user and you have a global ignore file set up already, do not follow these instructions! Instead, edit your existing file and make sure it includes the line: **/.DS_Store

You can see if you have an existing global ignore file with the git config command we learned about in the previous instalment:

git config --global core.excludesfile || echo 'NONE'

This will either show the path to your excludes file or NONE.

To create a global excludes file that ignores all .DS_Store files everywhere simply run:

echo '**/.DS_Store' >> ~/.gitignore_global
git config --global core.excludesfile ~/.gitignore_global

You can verify that the change has taken effect by re-running git config --global core.excludesfile || echo 'NONE'.

Staging & Committing A Simple Change

For once, the typo in the first sentence of README.md is not a mistake, I intentionally put it there so we could correct it 🙂.

Open README.md in your favourite text editor and correct ‘example is instalment’ to ‘example in instalment’.

Once the typo is fixed git status will show we have one un-staged change:

bart-imac2018:pbs104a bart% git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")
bart-imac2018:pbs104a bart%

Git is again being helpful, and letting us know that we can add our change to the staging area in the same way entirely new files are staged, with the git add command.

Go ahead and stage the change with:

git add README.md

If we run git status again we can see that our change has been successfully staged and is ready to be committed:

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

	modified:   README.md

bart-imac2018:pbs104a bart%

Like we did last week, we can now commit our staged change, along with a sensible commit message, with a command like:

git commit -m 'Fixed silly typo in README'

Staging & Committing Multiple Changes

Let’s make a more substantial change to our wee project — let’s edit our HTML file to use a local copy of Bootstrap 4.

This involves both adding new files (a contrib folder containing Bootstrap’s CSS and license file), and editing our existing index.html.

To play along at home, copy the contents of the folder pbs104a-v2 into your repository, replacing index.html when asked.

Once you’ve updated the files, you can see all the un-staged changes with git status:

bart-imac2018:pbs104a bart% git status         
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   index.html

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	contrib/

no changes added to commit (use "git add" and/or "git commit -a")
bart-imac2018:pbs104a bart% 

Before we do anything else, let’s stage the new contrib folder:

git add contrib

Now, we know from git status that we have made changes to index.html but how do we see the actual changes we made?

Seeing Changes with git diff

To see the un-staged changes to a file use the command git diff PATH/TO/FILE, for example, to see the changes to index.html use:

git diff index.html

This will show all the changes in a command line pager appropriate for your OS, on Linux/Unix/Mac that will probably be either the less or more terminal commands. Basically, diff will behave like man, allowing you to navigate up and down with the arrow keys, and to exit the pager by pressing q.

If you’d prefer not to use a pager and simply have all the changes printed to the terminal directly, use the --no-pager flag:

git --no-pager diff index.html

Notice that the --no-pager flag applies to the git command as a whole, not to the diff sub-command, so it goes between git and diff. As we’ll see later, this flag can be used with many git subcommands, not just diff.

The format of the output takes some getting used to. New and changed lines are shown pre-fixed with + symbols, and deleted and old versions of changed lines are shows pre-fixed with - symbols. A few lines of un-changed content are shown around the changed lines for context, and there’s some metadata shown too.

Note that the git diff command can be used to compare versions across commits and branches as well. For details see the relevant section of the documentation or man git-diff.

Most Git users don’t look at their changes on the command line, this is where Git GUIs really come into their own. Every Git GUI will allow you to see the changes to your files in a visual way. You may recognise some similarities to this terminal output though.

Let’s go ahead and stage the modified index.html and commit our changes:

git add index.html
git commit -m 'Moved from plain HTML to Bootstrap'

Dividing Changes Into Separate Commits

In an ideal world we would make one logical group of changes, commit it, then move on to something else, but in reality, we don’t work like that. We see an easy fix in a function next to the one we’re working on, so we fix that, or we spot a typo, or we change our mind of the wording of some error message we see in adjacent code. Or, probably the most common of all, we get into a flow and simply forget to commit! The end results is that we often have multiple logical changes in our working copy at the same time, and we really should commit them as multiple separate commits.

Git’s staging area was designed to address this inevitable reality. Conceptually, the approach is very simple, stage one set of changes, commit those, then stage the next, and commit those, and continue until everything is committed. When each logical grouping of changes affects separate files, that’s easy to do, but you can still do it when your changes are intermixed within the same files.

The Simple Case — Changes to Separate Files

To play along, copy both index.html and README.md from the pbs104a-v3 folder in this instalment’s ZIP into your repository (replacing the current versions of the files).

As we can see from git status, there are two changed files:

bart-imac2018:pbs104a bart% git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   README.md
	modified:   index.html

no changes added to commit (use "git add" and/or "git commit -a")
bart-imac2018:pbs104a bart%

These updated files contain two changes, a description of the Hello World meme in index.html, and the inclusion of Bootstrap licensing information in README.md.

Let’s first stage and commit the licensing info:

git add README.md
git commit -m 'Added Bootstrap licensing info to README'

As we can see with git status, we still have one more un-staged and un-committed change:

bart-imac2018:pbs104a bart% git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   index.html

no changes added to commit (use "git add" and/or "git commit -a")
bart-imac2018:pbs104a bart%

Let’s now stage and commit that:

git add index.html
git commit -m 'Added link to Wikipedia article on the Hello World meme'

Separately Staging Changes to a Single File

To play along, copy index.html from the pbs104a-v4 folder in this instalment’s ZIP into your repository (replacing the existing version).

This file contains two sets of edits — firstly the page has been re-worked to move the heading and the lead paragraph into a semantic header tag, and a footer has been added. Secondly, my programming history in different hello worlds has been added into the main tag.

You can explore these changes with git diff index.html or using your favourite Git GUI.

We want to stage these two logical sets of changes as two separate commits. The key to doing this is Git’s ability to treat each edit to a file as a separate change, referred to as a hunk. Git GUIs will let you graphically stage individual hunks, but you can also do it from the terminal by using git add with the --patch (or just -p for short) flag. When you use this flag you’ll be shown each change one-by-one, and you hit y to stage it or n not to. Let’s step through the changes to index.html with:

git add --patch index.html

Git will offer you the changes at different granularities. In this case, the first hunk offered covers all the changes. We need to drill down deeper, so we can do that by splitting the hunk by entering s and hitting return. We will then be offered a hunk for the inclusion of the header tag, so we want to accept that by hitting y and then return. Next we’re offered the closing header tag and the opening main tag, we want that too, so hit y and return again. Next we’re offered the entire programming history as a hunk, we don’t want this so hit n and return. Finally, we’re offered the new footer tag, which we do want, so hit y and return on that.

We have now staged some of our changes to index.html, but not others, so the file is both staged and un-staged, as you can see with git status:

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

	modified:   index.html

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   index.html

bart-imac2018:pbs104a bart%

We can use git diff index.html to see our un-staged changes, but how do we see our staged changes? We can use the --cached flag to do that, i.e. git diff --cached index.html.

Using the --no-pager flag we can output both sets of changes to terminal like so:

bart-imac2018:pbs104a bart% git --no-pager diff index.html
diff --git a/index.html b/index.html
index 4208db7..6cee467 100644
--- a/index.html
+++ b/index.html
@@ -15,6 +15,53 @@
 <main class="container">
 <p><em>Hello World</em> is a long-standing programming tradition, [learn more on Wikipedia](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program).</p>
 
+<h2>Bart's Programming History in <em>Hello Worlds</em></h2>
+
+<pre class="border rounded p-3 bg-success text-light">
+// 1997 - Java
+public class HelloWorld{
+  public static void main(String[] args){
+    System.out.println('Hello World!');
+  }
+}
+
+// 1998 — JavaScript
+document.write("Hello World!&lt;br&gt;");
+
+// 1998 — C++
+#include <iostream>
+using namespace std;
+int main() {
+  cout << "Hello World!\n";
+  return 0;
+}
+
+-- 1998 SQL
+SELECT "Hello World\n";
+
+# 1999 Maple
+"Hello World!";
+
+# 1999 - PHP
+&lt;?php echo 'Hello World!' ?&gt;
+
+# 2000 - Perl
+print "Hello, World!\n";
+
+; 2000 - LISP
+(format t "Hello, World!~%"))
+
+/* 2001 - C */
+#include <stdio.h>  
+int main(void){
+  printf("Hello World!\n);
+  return 0;
+}
+
+// 2015 - NodeJS
+console.info('Hello World!');
+</pre>
+
 </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:pbs104a bart% git --no-pager diff --cached index.html
diff --git a/index.html b/index.html
index 00ef106..4208db7 100644
--- a/index.html
+++ b/index.html
@@ -8,12 +8,14 @@
   <link rel="stylesheet" href="contrib/Bootstrap4.5/bootstrap.min.css">
 </head>
 <body>
-<main class="container">
+<header class="container">
 <h1 class="display-1">Hello World!!!</h1>
 <p class="lead">Welcome to some dummy content for instalment 104 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, [learn more on Wikipedia](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program).</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>
 </html>
\ No newline at end of file
bart-imac2018:pbs104a bart%

Let’s now go ahead and commit the layout changes:

git commit -m 'Correct semantic markup by moving title and lead into header and adding a footer'

Running git status again you can see we’re now down to just un-staged changes in one file, and no staged changes:

bart-imac2018:pbs104a bart% git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   index.html

no changes added to commit (use "git add" and/or "git commit -a")
bart-imac2018:pbs104a bart% 

We can now stage and commit our last change (my history of programming) as normal:

git add index.html
git commit -m 'Added my history of programming in Hello World programs'

Viewing Our Commits

We’ll dive into this in more detail in the next instalment, but to quickly see all our commits we can use the git log command. If we give it no arguments it will show all our commits in reverse order in a pager. Like with git diff we can force direct output to the terminal with the --no-pager flag:

bart-imac2018:pbs104a bart% git --no-pager log
commit cb1e12277518e9e714bd72c2fc95e44e3108b26f (HEAD -> main)
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Oct 24 16:10:32 2020 +0100

    Added my history of programming in Hello World programs

commit 51d923966741bf2a3bf56577a12d0523cea4e4f9
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Oct 24 16:07:36 2020 +0100

    Correct semantic markup by moving title and lead into header and adding a footer

commit 91f1f73ea2adb93d6d5a913939a9fc4ba3db8d27
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Oct 24 15:14:10 2020 +0100

    Added link to Wikipedia article on the Hello World meme

commit c4af22abd56ea75da4c80eb3fc50612e5e5e5fff
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Oct 24 15:12:36 2020 +0100

    Added Bootstrap licensing info to README

commit 90d890f7d364665f3c662eea440db550c296b721
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Oct 24 15:07:32 2020 +0100

    Moved from plain HTML to Bootstrap

commit daeab432cfe23297e2c76637c9fc9c4d34ac220a
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Oct 24 11:40:12 2020 +0100

    Fixed silly typo in README

commit efd2b072af4340c7d3e86de6b7cb1eada5ca306e
Author: Bart Busschots <opensource@bartificer.net>
Date:   Sat Oct 24 11:30:40 2020 +0100

    initial version
bart-imac2018:pbs104a bart%

If you open your repository in your favourite Git client you’ll be able to see our list of commits graphically.

GitKraken Screen Shot

SourceTree Screen Shot

Final Thoughts

Our Git journey is progressing nicely. We can now create repositories, add files to them, and track our changes to those files.

In the next instalment we’ll start by learning how to explore our repository’s history, and how to time-travel by checking out older commits.

Join the Community

Find us in the PBS channel on the Podfeet Slack.

Podfeet Slack