Back to Ideas 6 min read

Rebase or Merge Is the Wrong Argument

E
EkoHacks Team
·
Rebase or Merge Is the Wrong Argument

Ask a room of engineers whether to rebase a branch or merge the trunk into it and you will get a debate with the emotional register of a religious schism. Ask the same room how old their branches are and the room goes quiet.

The two questions are related, and only one of them is worth the argument.

They do the same arithmetic

Both commands perform a three way merge. Git finds the most recent commit the two lines of work have in common, the merge base, and then compares each side against it. A line changed on one side and untouched on the other is taken. A line changed identically on both is taken once. A line changed differently on both is a conflict, and git stops and asks.

That procedure is the same whether you rebase or merge. Neither command is cleverer than the other about text. Neither will spare you a conflict the other would have raised, given the same base and the same tips. Anyone who tells you that rebasing "avoids conflicts" has been getting away with short branches.

What the two commands differ on is the shape of the result, and that turns out to be three separate trade offs wearing one name.

What actually differs

Where the work ends up. A rebase takes your commits, sets them aside, moves your branch to the trunk's tip, and replays them one at a time. The commits that come out are not the commits that went in. They have the same changes and different identities. A merge leaves your commits exactly where they were and adds one new commit whose job is to record that two lines of history came together.

When you resolve. Because a rebase replays commit by commit, a conflict is raised once for every one of your commits that touches the contested file. Because a merge reconciles two tips, it is raised once, full stop.

Our own quota branch had three commits: a failing test, the implementation that made it pass, and a regeneration of the behaviour document. Only the third touched the document that conflicted, so the rebase asked once and it happened to cost nothing extra. Had we been less lucky, and the file been touched in all three, we would have resolved the same conflict three times, watching our own edits arrive back in front of us like a bad dream. Git has a facility for exactly this, git rerere, which records how you resolved a conflict and replays your resolution when it recognises the same one again. That it exists at all is a confession about how often people hit this.

What you can do afterwards. New commit identities mean the branch cannot be pushed normally, only force pushed, because the remote branch and your local one no longer share a tip. That is fine on a branch nobody else has. It is destructive on a branch somebody else has pulled, because their next pull will produce a snarl of duplicated commits.

That gives us the one rule in this whole area that admits no exceptions and needs no judgement.

Never rewrite history that other people have. Everything else is preference. This is not.

What history is for

Underneath the tooling sits a question people rarely ask out loud: what do you want your history to be able to answer later?

A rebased history is a story about what the code became. It is linear. Every commit sits on top of a trunk that once existed, git bisect walks it without ambiguity, and reading it feels like reading a narrative someone edited for clarity, because it is.

A merged history is a record of what actually happened. It preserves the fact that this work was developed alongside that work, and that on Thursday they were reconciled. It is truer and it is harder to read, and on a busy repository the graph turns into cable spaghetti.

Neither is dishonest. They answer different questions. Choose by which question your team asks more often, and notice that the answer usually correlates with how many people touch the repository at once.

The option that quietly destroys evidence

There is a third button on the forge, and it deserves naming because it is the default in many teams and it is not neutral.

A squash merge takes every commit on your branch, flattens them into one, and puts that on the trunk. The branch's internal history is gone.

For a branch of six commits named "wip", "fix", "fix again", that is a mercy. But we work in a way where the history of a branch carries information we deliberately put there. A fix arrives as a pair of commits: first the failing test, committed red, then the code that turns it green. That pairing is the evidence that the test was written before the code and that it genuinely failed without it, which is the only thing separating test driven development from writing tests afterwards and hoping.

Squash that branch and the pair becomes one commit containing a test and its implementation, indistinguishable from the work of someone who wrote the code first and reverse engineered a test to cover it. The claim survives. The proof does not.

So the squash question is not about tidiness either. It is: does the sequence of your commits mean something? If it does, do not flatten it. If it does not, ask why you are producing commits that mean nothing.

Why the argument is loud

Here is the thing worth taking away. Every cost above scales with divergence.

The number of times a rebase asks about the same conflict scales with how many of your commits touched the file. Whether a force push endangers anyone scales with how long the branch has been public. Whether the merge graph becomes unreadable scales with how many branches were open at once. Whether the conflict is hard scales with how far the trunk moved while you were away.

Shrink the divergence and every one of these shrinks with it. On a branch that is four hours old and three commits long, rebase and merge produce the same practical outcome in the same amount of time, and the choice really is a matter of taste. On a branch that is three weeks old, the choice is a decision about how much pain to take and in what shape, and the team argues about it fiercely because it hurts, and because arguing about the tool is more comfortable than admitting that the branch should never have been allowed to get that old.

The practice that dissolves the argument is the one this series is about: bring the trunk into your work every few hours, so that there is never much to reconcile, and so the reconciliation is boring however you spell it.

Our recommendation for the Dojo, where a slice of work lives for a day at most and belongs to one person, is to rebase onto origin/main and force push the branch, keeping the trunk linear and the red and green pair intact. That recommendation is worth exactly as much as the premise underneath it. On the day a branch of ours lives for three weeks, the right answer will change, and the right response will not be to change the answer.


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