Demystifying Git Koans

Posted 1448409600 seconds after the Unix epoch

Steve Losh, whose (lately, rather silent) blog is one of my favourite reads on the Internet), wrote a great piece called Git Koans back in 2013. A lot of my coworkers have recently started using Git, and I often use that to illustrate a lot of what seems frustrating about Git – not just to a newcomer, but also to people who use it regularly. Some of the examples in the post, however, were hard for me to understand initially. So here’s my try at making them more understandable.

Note: Like any proper koan – reading the original in its beauty is necessary before reading through a storied explanation, so go read Steve’s post first if you haven’t already. The headings for each story directly link there as well. If Steve is the Buddhist monk calmly telling a Koan to his students, I am the newcomer coarsely trying to understand the import of what he is saying from the “Ten Things You Should Know about Zen” book.

Silence

A Python programmer handed her ~/.gitconfig to Master Git. Among the many lines were the following:

[alias]
; Explicit is better than implicit.  If we want to merge
; we should do so ourselves.
pull = pull --ff-only

Master Git nodded. “git pull origin master,” said the programmer. Master Git pulled down the latest changes on master and automatically merged them with the programmer’s changes.

“But Master Git, did I not say to only fast-forward in my configuration?!” she cried.

Master Git looked at her, nodded, and said nothing.

“Then why did you not warn me of a problem with my configuration?” she asked.

Master Git replied: “there was no problem.”

…and the last part.

Months later the programmer was reading git --help config for a different reason and found enlightenment.

Let us read git --help config like our programmer, and get to:

alias.*
    Command aliases for the git(1) command wrapper - e.g. after
    defining alias.last = cat-file commit HEAD, the invocation git last
    is equivalent to git cat-file commit HEAD. To avoid confusion and
    troubles with script usage, aliases that hide existing Git commands
    are ignored. Arguments are split by spaces, the usual shell quoting
    and escaping is supported. A quote pair or a backslash can be used
    to quote them.

Aliases that hide existing Git commands are ignored. So the programmer’s alias for pull was completely ignored, in silence. A better behaviour would have been to warn about an ignored alias on invocation, or simply treat this as a case of an invalid config file. Changing the alias to one that doesn’t shadow an existing command would fix it.

[alias]
pff = pull --ff-only

…the downside being that now you have to remember this made-up Git alias.

Thanks to readers Ben Harris and Eugene K for providing the correct explanation for this one.

One Thing Well

A UNIX programmer was working in the cubicle farms. As she saw Master Git traveling down the path, she ran to meet him.

“It is an honor to meet you, Master Git!” she said. “I have been studying the UNIX way of designing programs that each do one thing well. Surely I can learn much from you.”

“Surely,” replied Master Git.

“How should I change to a different branch?” asked the programmer.

“Use git checkout.”

“And how should I create a branch?”

“Use git checkout.”

“And how should I update the contents of a single file in my working directory, without involving branches at all?”

“Use git checkout.”

After this third answer, the programmer was enlightened.

This is actually one of the things I find confusing about git. I don’t quite understand what “update” means in the third case though – running git checkout on a single file will update its contents to be those the latest HEAD.

From what I understand of the Git object model, this is a consequence of a branch simply being a pointer to a specific commit, or state of the tree. So when I say git checkout foo, I am saying something more like “Convert the repository to the state that foo points to”. Which commit is this? Let’s find out.

{~/sandbox/test}$ git add README.txt
{~/sandbox/test}$ git commit -m "Add README"
[master (root-commit) 39d985c] Add README
 1 file changed, 1 insertion(+)
 create mode 100644 README.txt


 {~/sandbox/test} (master)$ git branch test
 {~/sandbox/test} (master)$ git branch
 * master
   test

At this point, if you look at the refs of master, and test, they point to the exact same commit object.

{~/sandbox/test} (master)$ cat .git/refs/heads/master
39d985c2495e9ff2761774bcb2d9a23831a2bcab
{~/sandbox/test} (master)$ cat .git/refs/heads/test
39d985c2495e9ff2761774bcb2d9a23831a2bcab

Let’s add another commit to the master branch.

{~/sandbox/test} (master)$ echo "Not going too well." >> README.txt
{~/sandbox/test} (master)$ cat README.txt
Attempt to explain Git Koans
Not going too well.
{~/sandbox/test} (master)$ git add README.txt
{~/sandbox/test} (master)$ git commit -m "Add new line"
[master 878af2f] Add new line
 1 file changed, 1 insertion(+)

Now, you would expect the ref for master to change, as it does.

{~/sandbox/test} (master)$ cat .git/refs/heads/master
878af2fd711f4c88ef8c9e27aa3bacbdfd835bb7
{~/sandbox/test} (master)$ cat .git/refs/heads/test
39d985c2495e9ff2761774bcb2d9a23831a2bcab

{~/sandbox/test} (master)$ git log
commit 878af2fd711f4c88ef8c9e27aa3bacbdfd835bb7
Author: Mandar Gokhale <mandarg@mandarg.com>
Date:   Wed Nov 25 16:42:46 2015 +0000

    Add new line

commit 39d985c2495e9ff2761774bcb2d9a23831a2bcab
Author: Mandar Gokhale <mandarg@mandarg.com>
Date:   Wed Nov 25 16:41:13 2015 +0000

    Add README

Notice that the ref for branch test still points to the first commit.

Only The Gods

The great historian was trying to unravel the intricacies of an incorrect merge that had happened many months ago. He made a pilgrimage to Master Git to ask for his help.

“Master Git,” said the historian, “what is the nature of history?”

“History is immutable. To rewrite it later is to tamper with the very fabric of existence.”

The historian nodded, then asked: “Is that why rebasing commits that have been pushed is discouraged?”

“Indeed,” said Master Git.

“Splendid!” exclaimed the historian. “I have a historical record of a merge commit with two parents. How can I find out which branch each parent was originally made on?”

“History is ephemeral,” replied Master Git, “the knowledge you seek can be answered only by the gods.”

The historian hung his head as enlightenment crushed down upon him.

This actually ties neatly into the previous point – the SHA1s of the commits themselves are available, and can be traced back, but who they could be the exact same commit on two different branches, and the merge could have come from either of them.

{~/only-the-gods}$ git init
Initialized empty Git repository in /Users/mgokhale/only-the-gods/.git/
{~/only-the-gods}$ echo "This is one line" > README
{~/only-the-gods}$ git add README
{~/only-the-gods}$ git commit -m "Add Readme"
[master (root-commit) 3b5b59c] Add Readme
 1 file changed, 1 insertion(+)
 create mode 100644 README
{~/only-the-gods} (master)$ git checkout -b branch1
Switched to a new branch 'branch1'
{~/only-the-gods} (branch1)$ echo "This is an additional line" >> README
{~/only-the-gods} (branch1)$ git commit -am "Add line to Readme"
[branch1 063b5c8] Add line to Readme
 1 file changed, 1 insertion(+)

{~/only-the-gods} (branch1)$ git show # this shows the last commit
commit 063b5c8d32b6df23faf16a5c2994ce376e63f0ae
Author: Mandar Gokhale <mandarg@mandarg.com>
Date:   Wed Dec 9 03:22:43 2015 +0000

    Add line to Readme

diff --git a/README b/README
index f6f95b8..ec0f995 100644
--- a/README
+++ b/README
@@ -1 +1,2 @@
 This is one line
+This is an additional line

Okay, so far, so good. Let’s create another branch, off of this one.

{~/only-the-gods} (branch1)$ git checkout -b branch2
Switched to a new branch 'branch2'

At this point, what would you expect the latest commit on the branch to be? Yep, the state of branch2 is unchanged from branch1 – so it should be 063b5c8d32b6df23faf16a5c2994ce376e63f0ae. Is it?

{~/only-the-gods} (branch2)$ git show
commit 063b5c8d32b6df23faf16a5c2994ce376e63f0ae
Author: Mandar Gokhale <mandarg@mandarg.com>
Date:   Wed Dec 9 03:22:43 2015 +0000

    Add line to Readme

diff --git a/README b/README
index f6f95b8..ec0f995 100644
--- a/README
+++ b/README
@@ -1 +1,2 @@
 This is one line
+This is an additional line

Yes, it does appear to be the same!

Now, let us merge from branch1 into master.

{~/only-the-gods} (branch2)$ git checkout master
Switched to branch 'master'

{~/only-the-gods} (master)$ git merge --no-ff branch1
Merge made by the 'recursive' strategy.
 README | 1 +
 1 file changed, 1 insertion(+)

Okay, and how about branch2?

{~/only-the-gods} (master)$ git merge --no-ff branch2
Already up-to-date.

So, the real lesson here to me anyway seems to be – branches are ephemeral, commits aren’t (presumably since commits are a more first-class, fundamental object in Git).

Here concludes the first imperfect reading on these koans.


❧ Please send me your suggestions, comments, etc. at comments@mandarg.com