Back to Ideas 5 min read

Integrate Against the Trunk, Not Your Copy of It

E
EkoHacks Team
·
Integrate Against the Trunk, Not Your Copy of It

When we untangled the conflict nobody wrote, the fix came down to one instruction: put the branch on top of the current trunk and regenerate. Written out, it read like this.

git fetch origin
git rebase origin/main

A fair question came back. Why origin/main, when there is a perfectly good main sitting right there in the clone? If the trunk has already been pulled in, can the rebase not use the local one?

It can. The answer is yes, and the yes has a condition attached that is worth more than the answer.

Two names, and only one of them is the trunk

main is a branch in your repository. It is a file containing a commit id, and it moves only when you move it, by committing, merging, pulling, or resetting.

origin/main is a remote tracking reference. It is also a file containing a commit id, and it moves only when you run git fetch. What it holds is not what the remote has. It is what the remote had the last time you asked.

So there are three versions of the trunk in play at any moment, and it is easy to believe there is one. There is the trunk on the server, which is the real one, the one everybody else is integrating with and the one the forge uses to decide whether your pull request can merge. There is origin/main in your clone, a snapshot of the server taken at your last fetch. And there is main in your clone, which is a branch you happen to have given the same name as the trunk, and which git treats as no more authoritative than any other branch you have made.

git rebase main and git rebase origin/main are identical commands whenever those two names resolve to the same commit. When they do not, one of them is integration and the other is a rehearsal.

The failure is silent, which is the problem

Suppose your local main is two days old. You rebase onto it. The rebase runs, you resolve the conflict thoughtfully, the tests pass, you push, and the pull request still says conflicting.

Nothing warned you. The rebase did not fail. It could not fail, because you asked it to replay your work on top of a commit and it did precisely that. You integrated with a version of the trunk that exists nowhere but your disk, and every minute you spent resolving that conflict was spent answering a question nobody had asked.

There is a second, worse shape. If your local main carries commits you have not pushed, perhaps something you committed there weeks ago and forgot, rebasing onto it slides those commits underneath your branch. The pull request now contains work unrelated to the change it claims to make, and the reviewer, reading a diff about a quota, finds a stray refactor in it.

The trap that makes it convincing

Here is the part that catches careful people. Run git status on a stale clone and it will tell you, with total confidence:

On branch main
Your branch is up to date with 'origin/main'.

That sentence is true and it does not mean what it appears to mean. Git is comparing your main against your origin/main, both of which are local files. It is telling you that your copy agrees with your copy. It has not spoken to the server, because git status never speaks to the server. Two days of other people's work can sit on the trunk while your clone insists everything is current.

The same applies to git log origin/main, to the ahead and behind counts in your editor's status bar, and to every other reading of the world that has not been preceded by a fetch. Nothing in git reaches across the network unless you ask it to. The remote tracking reference is a cache, and, exactly as we found with the generated document in the previous post, a cache tells you what was true when it was written.

The practice underneath

The old discipline for integrating has two steps and people usually remember only the second. Before you put your work into the repository, you bring the repository's work into yours, and you prove that the combination builds and passes its tests. The first step is not "update your local main". It is "get what everybody else has".

So the shape of an honest integration is:

git fetch origin              # go and actually look
git rebase origin/main        # integrate with what is there
npm test                      # prove the combination works

The fetch is the only line that touches the network, and it is the only line that turns a private opinion about the trunk into a fact about it. If you prefer to work through a local main, fast forward it first and then use it. The name you type afterwards is a matter of taste. Having fetched is not.

Two commands settle the question when you are unsure:

git fetch origin
git rev-parse main origin/main   # same commit id twice? then either name works

Why the difference is the point

It would be easy to file this under command line trivia. It is not, and the reason is that the gap between main and origin/main is a measurement.

If those two references have drifted apart, you are looking at work that other people have finished and you have not yet reckoned with. That is the definition of being unintegrated. The size of the gap is the size of the surprise waiting for you, and the only thing that shrinks it is fetching more often and integrating more often.

Which means the question "should I use main or origin/main?" answers itself under a discipline of continuous integration. A team that brings the trunk into their work every few hours will find the two names pointing at the same commit almost every time they look, and the distinction will feel academic. A team that discovers a real difference between them has not found a naming subtlety. They have found out how far behind they are.

Our own conflict came from a branch that was one merge behind the trunk. One. That is how little divergence it takes, and it is the subject of the next two posts: what a branch costs while you are not looking at it, and why the argument about how to catch up is less interesting than the fact that you had to.


The practice of continuous integration described in this series is set out in the continuous integration chapter of The Art of Agile Development by James Shore and Shane Warden.

E

Written by

EkoHacks Team

More from Ideas

·6 min read

What the Nullable Gave Back

One file, seven behaviours held fixed, the database swapped for a Nullable: about 180 times less time inside the tests, and coverage flat to two decimals.

E
EkoHacks Team
·6 min read

Twenty Six More Tests, Four Fewer Behaviours

Removing the mocks grew the suite from 44 tests to 70 and quietly deleted four behaviours, every one of them a failure path. Test count is not coverage.

E
EkoHacks Team
·6 min read

The Best Coverage Number in the Room

Same commit, same spec, same test count. The mocked suite ran 5.6 times faster, covered 3.5 fewer points of real code, and posted the best branch coverage.

E
EkoHacks Team