The answer might be zero dots
You rebase your commits and before you push you want to run a diff to make sure you didn’t mess up the rebase:
git diff origin/myBranch..myBranchBut the diff comes back with a bunch of code you haven’t seen before. A bunch of deleted files, migration you didn’t write, config file somebody else added this morning.
You didn’t mess up rhe rebase, but Git isn’t lying to you either. You just asked it the wrong question.
Every example in this post uses this repo:
* de0e0d6 (HEAD -> myBranch) feat: add the export button* ef9cfee feat: add the export endpoint| * d0fe2fc (main) fix: correct the tax rounding|/ * 2c5c4e0 chore: bump deps* 2c63f3c Initial commitTwo branches that split at chore: bump deps. You wrote the two feat: commits on myBranch. Somebody else landed fix: correct the tax rounding on main after you branched off.
git diff A..B compares the two commits as snapshots. It answers the question:
“If I had the code at A, what would I need to change to end up with exactly the code at B?
git diff main..myBranchdiff --git a/export.ts b/export.tsnew file mode 100644--- /dev/null+++ b/export.ts
diff --git a/tax.ts b/tax.tsdeleted file mode 100644--- a/tax.ts+++ /dev/nullexport.ts gets added, which you expected. And tax.ts gets deleted, which you did not.
But tax.ts shows as deleted because it was created in d0fe2fc, which does not exist in myBranch.
“Delete your teammate’s tax fix” is a completely honest answer to the question you asked — “how do I turn (the code at) main into myBranch”.
Three dots compares against where you branched off
Section titled: Three dots compares against where you branched offI often say that git rebase is a “smart cherry-pick”. Git figures out which commits need rebasing. It does that by calculating a merge base, which is the common ancestor of two branches.
Three dot diff is similar. git diff A...B doesn’t start at A, it starts at the merge base of A and B and diffs from there to B:
git diff $(git merge-base A B) BWhich answers what can be a more useful question: What did I do on this branch?
git diff main...myBranchdiff --git a/export.ts b/export.tsnew file mode 100644--- /dev/null+++ b/export.tstax.ts is gone from the diff, because it was never yours to begin with. The three-dot diff doesn’t care what happened on main after you left.
D---E ← myBranch / A---B---C ← main ↑ ↑ │ └─ git diff main..myBranch compares C → E └───── git diff main...myBranch compares B → ETricksy hobitses: the dots mean something different in git log
Section titled: Tricksy hobitses: the dots mean something different in git logAs we’ve established, in git diff three dots means “start at the merge base.” Confusingly though, in git log, three dots means something completely different.
Three dot log shows the symmetric difference, which is every commit that’s on one branch or the other, but not both.
git log --oneline main..myBranch # the commits I addedde0e0d6 feat: add the export buttonef9cfee feat: add the export endpointgit log --oneline main...myBranch # every commit that isn't sharedd0fe2fc fix: correct the tax roundingde0e0d6 feat: add the export buttonef9cfee feat: add the export endpointWhy would the developers of git have this concept of three dots completely change semantic meaning between two of the most common commands?
Turns out, it was simply a design mistake:
“Please unlearn dot-dot and three-dots when using “git diff”, which is not about ranges but about comparing two endpoints. If we were reinventing Git today from scratch, we would make “git diff A..B” an error. You can consider it a bug that the command accepts a range notation, but this will not change any time soon without a large fight to find and fix uses of the syntax in scripts by longtime Git users have written over the years.
Allowing dot-dot on the command line of “git diff”, instead of diagnosing them as errors and dying, was a stupid mistake we (well, mostly Linus, but I am willing to take the blame too) made due to laziness when we reused the machinery, which we invented to parse the command line of “log” family of commands to specify ranges, to parse the command line of “diff”, which accidentally ended up allowing the syntax for ranges where it shouldn’t be allowed.
And worse yet, since there was only dot-dot and three-dots came much later, “git diff A..B” ended up comparing the endpoints A and B, because there didn’t even A…B notation exist.
This is not limited to you but any user of modern Git is better off to pretend “git diff A..B” does not exist; please unlearn dot-dot and three-dots when using “git diff” and you’d be happier.
-Junio C Hamano, Git maintainer, Dec 2019
But what exactly is Junio suggesting to do instead? Well the two-dot variant git diff A..B is exactly equivalent to git diff A B, so that’s easy.
The three-dot syntax git diff A...B is equivalent to git diff --merge-base A B. That does read a lot nicer, but it’s also more to type.
Have you ever noticed that the url on the “Files changed” tab of a Github PR literally contains the 3-dot syntax?
https://github.com/git/git/compare/master...nextSo if you want to review your PR diff in your terminal, three dots is the command that matches what your reviewers will see:
git diff main...myBranchIn part 2 of the git series I said that the most important step after a rebase is running a diff, and that you want it to come back empty. Which diff is correct there?
Well assuming that you rebased because someone elses changes landed in main, neither one is going to give you an empty diff.
# won't help - shows files everyone else addedgit diff origin/myBranch myBranch
# won't help - shows` additions made by others and yourselfgit diff origin/myBranch...myBranchSo what is a developer to do? You essentially want to ensure that your three-dot diff is exactly the same before and after. So you could do something like this:
git branch myBranch-b4rebase # before you touch anythinggit rebase main
# run both diffs and save the results to tmp filesgit diff main...myBranch-b4rebase > /tmp/before.diffgit diff main...myBranch > /tmp/after.diff
# compare those diff outputs - you want this to be emptydiff /tmp/before.diff /tmp/after.diffBut that’s obviously super annoying. Fortunately Git ships a purpose-built command for this! Allow me to introduce range-diff:
# notice these ranges are identical to what we just usedgit range-diff main...myBranch-b4rebase main...myBranch
# because the base `main` is the same in both ranges,# this abbreviated syntax is equivalent:git range-diff main myBranch-b4rebase myBranchIt diffs the commits rather than the final trees, and prints a = next to every commit that came through untouched:
ef9cfee = 1: b554bfc feat: add the export endpointde0e0d6 = 2: 56c4686 feat: add the export buttonReach for range-diff when you need to know which commit changed between two ranges.
| I want to know… | Command |
|---|---|
| What does my branch change? (my PR diff) | git diff main...myBranch |
| How exactly do these two commits differ? | git diff A B |
| Did my local branch drift from remote? | git diff origin/myBranch myBranch |
| What commits are on my branch? | git log main..myBranch |
| Did my commits change after a rebase? | git range-diff <range1> <range2> |