Git Diff Triple Dot And Double Dot Git

Dec 11th, 2020 - written by Kimserey with .

When using a branching mechanism like GitFlow or Mainline, we usually create pull request from short lived branches to long lived branches (e.g. develop, master). The pull request diff page gives a view of the changes that were brought by the branch. By default all repositories managers like GitHub, GitLab or Bitbucket uses “triple dot” diff to show diff. In this post we’ll look at the difference between “triple dot” and “double dot” diff.

Double-Dot Diff

In order to check the diff between two branches in the follownig scenario:

1
2
3
      C (3fd5643, my-branch)
     /
B---D (5b07cc7, master)

with git we can use

1
git diff master my-branch

which is the same as

1
git diff master..my-branch

This will give us the difference between master and my-branch, which is the difference between D and C.

It will show:

  1. anything present in D but not present in C
  2. anything present in C but not present in D

In this particular example, because my-branch is branched from master, by definition there is no extra commit on master that C does not contain hence a simple git diff master is guaranteed to show only the changes brought by my-branch.

In other words, for branches, diff double dot gives us the difference between the tip of two branches.

Triple-Dot Diff

We saw how double dot allows us to see the diff between two latest of two branches.

Now with our previous example, let’s say someone merge a change:

1
2
3
      C (3fd5643, my-branch)
     /
B---D---E (5b07cc7, master)

If we make a pull request, we only want to show our changes based of the merge-base (also known as most common ancestor). We don’t want E to show up in our pull request diff - because first it wasn’t brought by us, and second it’s already on master, there is no value in adding it for review “again”.

In order to do this, repository managers like GitHub or GitLab use the “triple dot”:

1
git diff master...my-branch

The triple dot compares my-branch with the most common ancestor between master and my-branch. This allows to only show changes brought up by my-branch. To find the base, we can use merge-base:

1
git merge-base master my-branch

This would give us the most recent common ancestor between master and my-branch which we could then use with a double dot diff to find just the changes added by my-branch:

1
git diff `git merge-base master my-branch` my-branch

This essentially results in the same as a triple dot diff.

Squash and Diff

A common scenario where pull-requests diffs can be confusing is when we branch from master, then branch out of the first branch and employ a squash merge strategy to merge pull-requestes. This scenario can happen when we want to break a pull-requests in smaller bits to gradually get them reviewed and don’t want to keep intermediate commits:

1
2
3
4
5
    D (feature-2 / open PR against master #2)
   /
  C (feature-1 / open PR against master #1)
 /
B (master)

In this scenario, we branch out of master B where we add a commit C in feature-1 and submit a pull-request #1 to merge back to master. At the same time, we branch out from feature-1 at C and create a commit D on feature-2 where we open a second pull-request against master#2.

At this point, PR #1 contains the changes from C and PR #2 contains the changes from C and D in their diffs.

Now if PR #1 is merged,

1
2
git checkout master
git merge --squash feature-1

we’ll get the following graph:

1
2
3
4
5
    D (feature-2: open PR against master #2)
   /
  C (feature-1)
 /
B---E (master: squashed commit of feature-1)

master now has E which contains the merge of C, the result of the squash merge.

But if we check the pull-request #2 of feature-2, because it uses triple-dot diff, the pull-request will still show the diff of C plus D even though C has been merged to master (via E). We can see the same diff if we do:

1
git diff master...feature-2

This is because even thought C is merged, the merge-base hasn’t changed due to the squash behaviour. To fix this we can either merge master into feature-2:

1
2
git checkout feature-2
git merge master

which will bring the graph to:

1
2
3
4
5
    D---F feature-2 (Merge branch 'master' into feature-2) 
   /   /
  C   / (featuer-1)
 /   /
B---E (master: squashed commit of feature-1)

Hence moving the common ancestor to E and therefore the difference would only be what is in F versus E which would be only D, as the changes in D.

Or the other way would be to rebase feature-2 onto master:

1
git rebase --onto master feature-1 feature-2

which will result in the following graph:

1
2
3
4
5
  C (featuer-1)
 /
B---E (master: squashed commit of feature-1)
     \
       D' (feature-2: rebased from feature-1 onto master)

And that concludes today’s post, I hope you learnt the difference between triple dot and double dot diff in git and how repository managers display the diffs in pull-request!

External Sources

Designed, built and maintained by Kimserey Lam.