by Kevin Murphy
A Sweet Surprise
Recently, a project needed to re-order some commits on a git branch. We initially performed this interactive rebase on a separate branch, so we didn't make a mistake on the original branch. After performing that rebase, we then used Github's compare view, expecting to see no files changed.
Instead, what we saw was every change that was introduced in all of the commits that were reordered on the new branch. This very much surprised us. We made no functional changes and were expecting to use this comparison as proof that we didn't accidentally drop a commit or do something terrible on accident.
We then looked to compare the branches locally. When we did that, we saw no diff.
⇒ diff-test|reorder git diff master (END)
Did we find a bug in Github? Did our re-ordering work? At this point, we had no idea.
Read The Recipe
We dug into the documentation for
diff, a tool I use frequently, but don't spend much time considering how it works.
Let's read some of the help on how to use the diff tool:
⇒ git diff --help GIT-DIFF(1) Git Manual GIT-DIFF(1) ... git diff [<options>] <commit> [--] [<path>...] This form is to view the changes you have in your working tree relative to the named <commit>. You can use HEAD to compare it with the latest commit, or a branch name to compare with the tip of a different branch. git diff [<options>] <commit> <commit> [--] [<path>...] This is to view the changes between two arbitrary <commit>. git diff [<options>] <commit>..<commit> [--] [<path>...] This is synonymous to the previous form. If <commit> on one side is omitted, it will have the same effect as using HEAD instead. git diff [<options>] <commit>...<commit> [--] [<path>...] This form is to view the changes on the branch containing and up to the second <commit>, starting at a common ancestor of both <commit>.
Almost exclusively I've used diff in the form of
git diff other-ref. And with that, I would see what, if any, file changes occurred between those two branches or commits. This is the two-dot comparison. However, Github uses the three-dot comparison by default.
In this scenario, the common ancestor is the latest commit on the reordering branch that wasn't reordered. After the rebase, all of those reordered commits have different SHAs, so are seen as different commits. As such, using the three-dot option, git considers all of those SHAs as different or new, so all of them show as changes, even though there are no differences in the file contents themselves.
Let's look at demonstrating this behavior entirely locally.
First, let's create a new repository:
⇒ mkdir diff-test ⇒ cd diff-test diff-test|⇒ git init . Initialized empty Git repository
We'll start by adding a few new files, and making the first commit:
diff-test|master ⇒ echo "Creamy Vanilla Ice Cream done Dippin’ Dots style" > vanilla.txt diff-test|master⚡ ⇒ echo "Creamy Milk Chocolate Ice Cream. Someone pass the spoon" > chocolate.txt diff-test|master⚡ ⇒ git add --all diff-test|master⚡ ⇒ git commit -m "IFO: initial flavor offering"
We will then add two more commits with some additional changes:
diff-test|master ⇒ echo "Dippin' Dots Strawberry Ice Cream is as sweet as a summer's day and so berry, berry good" > strawberry.txt diff-test|master⚡ ⇒ git add --all diff-test|master⚡ ⇒ git commit -m "Introducing strawberry" diff-test|master ⇒ echo "Orange Flavored Ice" > orange.txt diff-test|master⚡ ⇒ git add --all diff-test|master⚡ ⇒ git commit -m "Introducing orange"
Introduce a change
At this point, we realize that it's important to us that orange be introduced in our history before strawberry.
Let's create a new branch to address this, and look at the current order of the commits:
diff-test|master ⇒ git checkout -b reorder Switched to a new branch 'reorder' diff-test|reorder ⇒ git log --oneline e378256 (HEAD -> reorder, master) Introducing orange f455e5b Introducing strawberry e42566b IFO: initial flavor offering
We will then perform an interactive rebase, going back two commits in history. During that rebase, we will switch the order of orange and strawberry, so orange is first, as we need it:
diff-test|reorder ⇒ git rebase -i HEAD~2 pick e378256 Introducing orange pick f455e5b Introducing strawberry
After performing the rebase, here is the order of our commits:
diff-test|reorder ⇒ git log --oneline 5fa36f4 (HEAD -> reorder) Introducing strawberry 7a1d500 Introducing orange e42566b IFO: initial flavor offering
Congratulations! We've done what we set out to do, performing a rebase to reorder our commits. Now let's compare that to our original branch to make sure everything looks to be as we'd expect.
We start by comparing this branch to the original branch:
diff-test|reorder ⇒ git diff master (END)
We see that there's no difference, which we're expecting and hoping for. No files changed, no functionality is different; only the history has changed.
If we diff using the two-dot version, we see the same result:
diff-test|reorder ⇒ git diff master..reorder (END)
That sounds right, because according to the documentation, the two-dot usage is the same as not providing dots:
git diff [<options>] <commit>..<commit> [--] [<path>...] This is synonymous to the previous form. If <commit> on one side is omitted, it will have the same effect as using HEAD instead.
Lastly, let's perform a three-dot diff:
diff-test|reorder ⇒ git diff master...reorder diff --git a/orange.txt b/orange.txt new file mode 100644 index 0000000..09d8732 --- /dev/null +++ b/orange.txt @@ -0,0 +1 @@ +Orange Flavored Ice diff --git a/strawberry.txt b/strawberry.txt new file mode 100644 index 0000000..6ab358d --- /dev/null +++ b/strawberry.txt @@ -0,0 +1 @@ +Dippin' Dots Strawberry Ice Cream is as sweet as a summer's day and so berry, berry good (END)
Here is where we see that it looks like each of those files have been changed as a result of this rebase. However, the files themselves haven't changed. What has is the commits themselves. Their common ancestor is the initial commit of the repository, so everything that happened since then shows as a difference.
The Cherry On Top
Locally, unless you tell git otherwise,
diff will show you the file change differences between your comparisons. Github by default will show you the changes from the common ancestor between what's changed.
Most of the time, this different behavior won't or shouldn't matter in the course of your workflow. However, if you're doing something a bit more adventurous, or perhaps ill-advised, knowing how git will, by default, compare changes locally and how Github, by default, will surface those changes can be paramount.