Part 2 · Lesson 10 of 12 · about 15 minutes

Two diffs, two questions

Two dots ask how two snapshots differ. Three dots ask what a branch changed.

You will name the two commits each diff compares, predict what git diff main..add-loader and git diff main...add-loader will cover before you run them, and check your prediction against Git.

A recipe that never got its reason

Since Lesson 2 this course has told you to review a branch with three dots: git diff main...HEAD. It never said what the third dot is for, or what goes wrong with two. That was fine while you wrote the code yourself. When an agent wrote it, the diff is how you judge the work, so you need to know exactly what the diff is comparing.

Every diff has two ends. The two commands differ only in where they put the left end. To say where, you need one new idea.

The merge base

Walk backwards through the history of main and of your branch. The most recent commit they both contain is their merge base: the point where the branch split off. Git's definition is "the best common ancestor(s) between two commits to use in a three-way merge."1 GitHub's docs describe it as "The most recent common commit of both branches (merge base)."3

One command prints it:

$ git merge-base main add-loader
394e8c73a0127ff8373d51eafb6b2ce45582bf06

The output is "a commit which is reachable from both" names by following parents.1 It is a full 40-character hash, and it is the same whichever name you put first.

merge base main add-loader landed later
The square is the merge base. Three dots cover the two shaded commits on add-loader. Two dots cover those and, in reverse, the dashed commit that landed on main after the split.

Two diffs

Two-dot diffThree-dot diff
Commandgit diff main..add-loadergit diff main...add-loader
QuestionHow do these two snapshots differ right now?What did this branch change since it split?
Left endThe tip of mainThe merge base
Right endThe tip of add-loaderThe tip of add-loader
Same asgit diff main add-loader2git diff $(git merge-base main add-loader) add-loader2
Work that landed on main after the splitShows up, reversedDoesn't show up

The Git reference says the two-dot form "is synonymous to the earlier form (without the ..) for viewing the changes between two arbitrary <commit>." The three-dot form is "to view the changes on the branch containing and up to the second <commit>, starting at a common ancestor of both."2

Why "reversed"?

A diff is a recipe for turning the left end into the right end. Suppose someone adds a line to README.md on main after your branch split. The tip of main has that line. The tip of your branch doesn't, because the branch started before the line existed. To turn the left end into the right end, the recipe has to take the line out. So the two-dot diff prints it with a minus sign.

A red minus line in a two-dot diff doesn't mean the agent deleted anything. It can mean main gained that line after the branch split. If you review an agent's branch with two dots, you will blame it for undoing other people's work.

The three-dot diff avoids this. GitHub's docs put it this way: "Because the three-dot comparison uses the merge base, it focuses on 'what a pull request introduces.'"3 That's why "Pull requests on GitHub show a three-dot diff."3 The Files changed tab you've used since Lesson 4 is this lesson's right-hand column.

When the two are the same

If main hasn't moved since the branch split, the merge base is the tip of main. Both diffs then have the same two ends and print the same text. The distinction only matters once main moves. Lesson 11 is about that moment.

git log uses the dots differently. In git log main..HEAD from Lesson 6, two dots select a range of commits. In git diff the dots pick two endpoints, and the docs warn that there they "do not mean a range."2

Try it: predict, then check

The simulator has two new commands, git merge-base and git diff. It has no files, so its diff prints no diff text. It tells you which commits' changes the diff would cover and marks them on the graph: a square for the merge base, a shaded disc for changes the diff includes, and a dashed ring for changes it includes in reverse.

main has two commits. add-loader split from the second one and has two commits of its own. Before you press Enter on tasks 2, 3, 5 and 6, decide which commits will be marked. Then run the command and see whether you were right.

After task 5 the dashed ring sits on main's new commit: the two-dot diff has spread onto main. After task 6 the marks are back where they were in task 2. The branch didn't change, so the three-dot diff didn't either.

Do it for real: actual diff text

Now the same thing in penguins-lab, with real files. Tick each step as you go.

  1. $ git switch main
    $ git pull
    $ git switch -c add-loader
  2. import pandas as pd
    
    def load_penguins(path: str) -> pd.DataFrame:
        """Read the penguins CSV into a DataFrame."""
        return pd.read_csv(path)
    $ git add load.py
    $ git commit -m "Add load_penguins"
  3. $ git merge-base main add-loader
    394e8c73a0127ff8373d51eafb6b2ce45582bf06
    $ git log --oneline -1 main
    394e8c7 …
    $ git diff --stat main...add-loader
     load.py | 5 +++++
     1 file changed, 5 insertions(+)
    $ git diff --stat main..add-loader
     load.py | 5 +++++
     1 file changed, 5 insertions(+)

    Your hashes will differ from these, but your two hashes will start with the same seven characters.

  4. Run the tests with `python -m pytest`.
    $ git switch main
    $ git switch -c readme-testing
    #   ...edit README.md...
    $ git commit -am "Say how to run the tests"

    Land it with the Lesson 4 loop: git push -u origin readme-testing, gh pr create --fill, gh pr checks --watch, gh pr merge --squash --delete-branch. Then git switch main and git pull.

  5. $ git merge-base main add-loader
    394e8c73a0127ff8373d51eafb6b2ce45582bf06
    $ git log --oneline -1 main
    c9e448a Say how to run the tests (#12)
  6. $ git diff --stat main...add-loader
     load.py | 5 +++++
     1 file changed, 5 insertions(+)
    $ git diff --stat main..add-loader
     README.md | 2 --
     load.py   | 5 +++++
     2 files changed, 5 insertions(+), 2 deletions(-)
  7. $ git diff main..add-loader -- README.md
    --- a/README.md
    +++ b/README.md
    @@ -1,3 +1 @@
     # Palmer Penguins Data Lab
    -
    -Run the tests with `python -m pytest`.

    Nobody deleted that line. add-loader never had it. The two-dot diff shows the README change from main, reversed.

Leave add-loader unmerged. Lesson 11 picks it up from here.

Recall the commands

Type each command from memory.

Print the merge base of main and add-loader.

Show only what add-loader changed since it split from main.

Show how the tip of main and the tip of add-loader differ right now.

Check yourself

Which commit is the merge base of main and add-loader?

The merge base is the best common ancestor: the latest commit that both histories contain. git merge-base main add-loader prints it.

git diff main..add-loader shows a README line being removed. The agent never touched the README. What is the likely cause?

Two dots compare the two tips. A line that landed on main after the split is missing from the branch's snapshot, so it shows as a removal.

When do the two-dot and three-dot diffs print exactly the same text?

If main hasn't moved, the merge base is the tip of main, so both diffs have the same two ends.

Which diff does a pull request's Files changed tab show?

"Pull requests on GitHub show a three-dot diff": the merge base against the latest version of the head branch.

Read "Three-dot and two-dot Git diff comparisons" in GitHub Docs, down to "Merging often" (3 minutes). Then look at the small graph drawings in the Discussion section of git-merge-base.
Ask Claude: "Draw the commit graph of this branch and main as text, mark the merge base, and tell me which commits git diff main..HEAD and git diff main...HEAD each cover." Check its answer with git merge-base.

Sources

  1. Git reference, git-merge-base (description and discussion). Output also verified locally with Git 2.52.
  2. Git reference, git-diff: the two-dot form, the three-dot form, and the note that diff compares endpoints, not ranges.
  3. GitHub Docs, Three-dot and two-dot Git diff comparisons and About three-dot comparison on GitHub.