summaryrefslogtreecommitdiff
path: root/www/rebaseharm.md
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /www/rebaseharm.md
downloadrelic-master.tar.gz
initial commitHEADmaster
Diffstat (limited to 'www/rebaseharm.md')
-rw-r--r--www/rebaseharm.md522
1 files changed, 522 insertions, 0 deletions
diff --git a/www/rebaseharm.md b/www/rebaseharm.md
new file mode 100644
index 0000000..9b72d38
--- /dev/null
+++ b/www/rebaseharm.md
@@ -0,0 +1,522 @@
+# Rebase Considered Harmful
+
+Fossil deliberately omits a "rebase" command because the original
+designer of Fossil (and [original author][vhist] of this article) considers rebase to be
+an anti-pattern to be avoided. This article attempts to
+explain that point of view.
+
+[vhist]: /finfo?name=www/rebaseharm.md&ubg
+
+## 1.0 Rebasing is dangerous
+
+Most people, even strident advocates of rebase, agree that rebase can
+cause problems when misused. The Git rebase documentation talks about the
+[golden rule of rebasing][golden]: never rebase on a public
+branch. Horror stories of misused rebase abound, and the rebase
+documentation devotes considerable space toward explaining how to
+recover from rebase errors and/or misuse.
+
+## <a id="cap-loss"></a>2.0 Rebase provides no new capabilities
+
+Sometimes sharp and dangerous tools are justified,
+because they accomplish things that cannot be
+done otherwise, or at least cannot be done easily.
+Rebase does not fall into that category,
+because it provides no new capabilities.
+
+### <a id="orphaning"></a>2.1 A rebase is just a merge with historical references omitted
+
+A rebase is really nothing more than a merge (or a series of merges)
+that deliberately forgets one of the parents of each merge step.
+To help illustrate this fact,
+consider the first rebase example from the
+[Git documentation][gitrebase]. The merge looks like this:
+
+~~~ pikchr toggle center
+scale = 0.8
+circle "C0" fit
+arrow right 50%
+circle same "C1"
+arrow same
+circle same "C2"
+arrow same
+circle same "C3"
+arrow same
+circle same "C5"
+circle same "C4" at 1cm above C3
+arrow from C2 to C4 chop
+arrow from C4 to C5 chop
+~~~
+
+And the rebase looks like this:
+
+~~~ pikchr toggle center
+scale = 0.8
+circle "C0" fit
+arrow right 50%
+circle same "C1"
+arrow same
+circle same "C2"
+arrow same
+circle same "C3"
+arrow same
+circle same "C4'"
+circle same "C4" at 1cm above C3
+arrow from C2 to C4 chop
+~~~
+
+As the [Git documentation][gitrebase] points out, check-ins C4\' and C5
+are identical. The only difference between C4\' and C5 is that C5
+records the fact that C4 is its merge parent but C4\' does not.
+
+Thus, a rebase is just a merge that forgets where it came from.
+
+The Git documentation acknowledges this fact (in so many words) and
+justifies it by saying "rebasing makes for a cleaner history." I read
+that sentence as a tacit admission that the Git history display
+capabilities are weak and need active assistance from the user to
+keep things manageable.
+Surely a better approach is to record
+the complete ancestry of every check-in but then fix the tool to show
+a "clean" history in those instances where a simplified display is
+desirable and edifying, but retain the option to show the real,
+complete, messy history for cases where detail and accuracy are more
+important.
+
+So, another way of thinking about rebase is that it is a kind of
+merge that intentionally forgets some details in order to
+not overwhelm the weak history display mechanisms available in Git.
+Wouldn't it be better, less error-prone, and easier on users
+to enhance the history display mechanisms in Git so that rebasing
+for a clean, linear history became unnecessary?
+
+### <a id="clean-diffs"></a>2.2 Rebase does not actually provide better feature-branch diffs
+
+Another argument, often cited, is that rebasing a feature branch
+allows one to see just the changes in the feature branch without
+the concurrent changes in the main line of development.
+Consider a hypothetical case:
+
+~~~ pikchr toggle center
+scale = 0.8
+circle "C0" fit fill white
+arrow right 50%
+circle same "C1"
+arrow same
+circle same "C2"
+arrow same
+circle same "C4"
+arrow same
+circle same "C6"
+circle same "C3" at last arrow.width + C0.rad*2 heading 30 from C2
+arrow right 50%
+circle same "C5"
+arrow from C2 to C3 chop
+box ht C3.y-C2.y wid C6.e.x-C0.w.x+1.5*C1.rad at C2 behind C0 fill 0xc6e2ff color 0xaac5df
+box ht previous.ht wid previous.wid*0.55 with .se at previous.ne \
+ behind C0 fill 0x9accfc color 0xaac5df
+text "feature" with .s at previous.n
+text "main" with .n at first box.s
+~~~
+
+In the above, a feature branch consisting of check-ins C3 and C5 is
+run concurrently with the main line in check-ins C4 and C6. Advocates
+for rebase say that you should rebase the feature branch to the tip
+of main in order to remove main-line development differences from
+the feature branch's history:
+
+~~~ pikchr toggle center
+# Duplicated below in section 5.0
+scale = 0.8
+circle "C0" fit fill white
+arrow right 50%
+circle same "C1"
+arrow same
+circle same "C2"
+arrow same
+circle same "C4"
+arrow same
+circle same "C6"
+circle same "C3" at last arrow.width + C0.rad*2 heading 30 from C2
+arrow right 50%
+circle same "C5"
+arrow from C2 to C3 chop
+C3P: circle same "C3'" at first arrow.width + C0.rad*2 heading 30 from C6
+arrow right 50% from C3P.e
+C5P: circle same "C5'"
+arrow from C6 to C3P chop
+
+box ht C3.y-C2.y wid C5P.e.x-C0.w.x+1.5*C1.rad with .w at 0.5*(first arrow.wid) west of C0.w \
+ behind C0 fill 0xc6e2ff color 0xaac5df
+box ht previous.ht wid previous.e.x - C2.w.x with .se at previous.ne \
+ behind C0 fill 0x9accfc color 0xaac5df
+~~~
+
+
+You could choose to collapse C3\' and C5\' into a single check-in
+as part of this rebase, but that's a side issue we'll deal with
+[separately](#collapsing).
+
+Because Fossil purposefully lacks rebase, the closest you can get to this same check-in
+history is the following merge:
+
+~~~ pikchr toggle center
+scale = 0.8
+circle "C0" fit fill white
+arrow right 50%
+circle same "C1"
+arrow same
+circle same "C2"
+arrow same
+circle same "C4"
+arrow same
+circle same "C6"
+circle same "C3" at last arrow.width + C0.rad*2 heading 30 from C2
+arrow right 50%
+circle same "C5"
+arrow same
+circle same "C7"
+arrow from C2 to C3 chop
+arrow from C6 to C7 chop
+
+box ht C3.y-C2.y wid C7.e.x-C0.w.x+1.5*C1.rad with .w at 0.5*(first arrow.wid) west of C0.w \
+ behind C0 fill 0xc6e2ff color 0xaac5df
+box ht previous.ht wid previous.e.x - C2.w.x with .se at previous.ne \
+ behind C0 fill 0x9accfc color 0xaac5df
+~~~
+
+
+Check-ins C5\' and C7 check-ins hold identical code. The only
+difference is in their history.
+
+The argument from rebase advocates
+is that with merge it is difficult to see only the changes associated
+with the feature branch without the commingled mainline changes.
+In other words, diff(C2,C7) shows changes from both the feature
+branch and from the mainline, whereas in the rebase case
+diff(C6,C5\') shows only the feature branch changes.
+
+But that argument is comparing apples to oranges, since the two diffs
+do not have the same baseline. The correct way to see only the feature
+branch changes in the merge case is not diff(C2,C7) but rather diff(C6,C7).
+
+<div align=center>
+
+| Rebase | Merge | What You See |
+|---------------|-------------|----------------------------------------|
+| diff(C2,C5\') | diff(C2,C7) | Commingled branch and mainline changes |
+| diff(C6,C5\') | diff(C6,C7) | Branch changes only |
+
+</div>
+
+Remember: C7 and C5\' are bit-for-bit identical, so the output of the
+diff is not determined by whether you select C7 or C5\' as the target
+of the diff, but rather by your choice of the diff source, C2 or C6.
+
+So, to help with the problem of viewing changes associated with a feature
+branch, perhaps what is needed is not rebase but rather better tools to
+help users identify an appropriate baseline for their diffs.
+
+## <a id="siloing"></a>3.0 Rebase encourages siloed development
+
+The [golden rule of rebasing][golden] is that you should never do it
+on public branches, so if you are using rebase as intended, that means
+you are keeping private branches. Or, to put it another way, you are
+doing siloed development. You are not sharing your intermediate work
+with collaborators. This is not good for product quality.
+
+[Nagappan, et. al][nagappan] studied bugs in Windows Vista and found
+that the best predictor of bugs is the distance on the org-chart between
+the stake-holders. The bug rate is inversely related to the
+amount of communication among the engineers.
+Similar findings arise in other disciplines. Keeping
+private branches does not prove that developers are communicating
+insufficiently, but it is a key symptom of that problem.
+
+[Weinberg][weinberg] argues programming should be "egoless." That
+is to say, programmers should avoid linking their code with their sense of
+self, as that makes it more difficult for them to find and respond
+to bugs, and hence makes them less productive. Many developers are
+drawn to private branches out of sense of ego. "I want to get the
+code right before I publish it." I sympathize with this sentiment,
+and am frequently guilty of it myself. It is humbling to display
+your stupid mistake to the whole world on an Internet that
+never forgets. And yet, humble programmers generate better code.
+
+What is the fastest path to solid code? Is it to continue staring at
+your private branch to seek out every last bug, or is it to publish it
+as-is, whereupon the many eyeballs will immediately see that last stupid
+error in the code? Testing and development are often done by separate
+groups within a larger software development organization, because
+developers get too close to their own code to see every problem in it.
+
+Given that, is it better for those many eyeballs to find your problems
+while they're still isolated on a feature branch, or should that vetting
+wait until you finally push a collapsed version of a private working
+branch to the parent repo? Will the many eyeballs even see those errors
+when they’re intermingled with code implementing some compelling new feature?
+
+## <a id="timestamps"></a>4.0 Rebase causes timestamp confusion
+
+Consider the earlier example of rebasing a feature branch:
+
+~~~ pikchr toggle center
+# Copy of second diagram in section 2.2 above
+scale = 0.8
+circle "C0" fit fill white
+arrow right 50%
+circle same "C1"
+arrow same
+circle same "C2"
+arrow same
+circle same "C4"
+arrow same
+circle same "C6"
+circle same "C3" at last arrow.width + C0.rad*2 heading 30 from C2
+arrow right 50%
+circle same "C5"
+arrow from C2 to C3 chop
+C3P: circle same "C3'" at first arrow.width + C0.rad*2 heading 30 from C6
+arrow right 50% from C3P.e
+C5P: circle same "C5'"
+arrow from C6 to C3P chop
+
+box ht C3.y-C2.y wid C5P.e.x-C0.w.x+1.5*C1.rad with .w at 0.5*(first arrow.wid) west of C0.w \
+ behind C0 fill 0xc6e2ff color 0xaac5df
+box ht previous.ht wid previous.e.x - C2.w.x with .se at previous.ne \
+ behind C0 fill 0x9accfc color 0xaac5df
+~~~
+
+What timestamps go on the C3\' and C5\' check-ins? If you choose
+the same timestamps as the original C3 and C5, then you have the
+odd situation C3' is older than its parent C6. We call that a
+"timewarp" in Fossil. Timewarps can also happen due to misconfigured
+system clocks, so they are not unique to rebase, but they are very
+confusing and so best avoided. The other option is to provide new
+unique timestamps for C3' and C5' but then you lose the information
+about when those check-ins were originally created, which can make
+historical analysis of changes more difficult. It might also
+complicate the legal defense of prior art claims.
+
+## <a id="lying"></a>5.0 Rebase misrepresents the project history
+
+By discarding parentage information, rebase attempts to deceive the
+reader about how the code actually came together.
+
+Git’s rebase feature is more than just an
+alternative to merging: it also provides mechanisms for changing the
+project history in order to make editorial changes. Fossil shows that
+you can get similar effects without modifying historical records,
+allowing users to:
+
+ 1. Edit check-in comments to fix typos or enhance clarity
+ 2. Attach supplemental notes to check-ins or whole branches
+ 3. Hide ill-conceived or now-unused branches from routine display
+ 4. Fix faulty check-in date/times resulting from misconfigured
+ system clocks
+ 5. Cross-reference check-ins with each other, or with
+ wiki, tickets, forum posts, and/or embedded documentation
+
+…and so forth.
+
+Fossil allows all of this not by removing or modifying existing
+repository entries, but rather by adding new supplemental records.
+Fossil keeps the original incorrect or unclear inputs and makes them
+readily accessible, preserving the original historical record. Fossil
+doesn’t make the user tell counter-factual “stories,” it only allows the
+user to provide annotations to provide a more readable edited
+presentation for routine display purposes.
+
+Git needs rebase because it lacks these annotation facilities. Rather
+than consider rebase a desirable feature missing in Fossil, ask instead
+why Git lacks support for making editorial changes to check-ins without
+modifying history? Wouldn't it be better to fix the version control
+tool rather than requiring users to fabricate a fictitious project
+history?
+
+## <a id="collapsing"></a>6.0 Collapsing check-ins throws away valuable information
+
+One of the oft-cited advantages of rebasing in Git is that it lets you
+collapse multiple check-ins down to a single check-in to make the
+development history “clean.” The intent is that development appear as
+though every feature were created in a single step: no multi-step
+evolution, no back-tracking, no false starts, no mistakes. This ignores
+actual developer psychology: ideas rarely spring forth from fingers to
+files in faultless finished form. A wish for collapsed, finalized
+check-ins is a wish for a counterfactual situation.
+
+The common counterargument is that collapsed check-ins represent a
+better world, the ideal we're striving for. What that argument overlooks
+is that we must throw away valuable information to get there.
+
+### <a id="empathy"></a>6.1 Individual check-ins support mutual understanding
+
+Ideally, future developers of our software can understand every feature
+in it using only context available in the version of the code they start
+work with. Prior to widespread version control, developers had no choice
+but to work that way. Pre-existing codebases could only be understood
+as-is or not at all. Developers in that world had an incentive to
+develop software that was easy to understand retrospectively, even if
+they were selfish people, because they knew they might end up being
+those future developers!
+
+Yet, sometimes we come upon a piece of code that we simply cannot
+understand. If you have never asked yourself, "What was this code's
+developer thinking?" you haven't been developing software for very long.
+
+When a developer can go back to the individual check-ins leading up to
+the current code, they can work out the answers to such questions using
+only the level of personal brilliance necessary to be a good developer. To
+understand such code using only the finished form, you are asking future
+developers to make intuitive leaps that the original developer was
+unable to make. In other words, you are asking your future maintenance
+developers to be smarter than the original developers! That's a
+beautiful wish, but there's a sharp limit to how far you can carry it.
+Eventually you hit the limits of human brilliance.
+
+When the operation of some bit of code is not obvious, both Fossil and
+Git let you run a [`blame`](/help/blame) on the code file to get
+information about each line of code, and from that which check-in last
+touched a given line of code. If you squash the check-ins on a branch
+down to a single check-in, you throw away the information leading up to
+that finished form. Fossil not only preserves the check-ins surrounding
+the one that included the line of code you're trying to understand, its
+[superior data model][sdm] lets you see the surrounding check-ins in
+both directions; not only what lead up to it, but what came next. Git
+can't do that short of crawling the block-chain backwards from the tip
+of the branch to the check-in you’re looking at, an expensive operation.
+
+We believe it is easier to understand a line of code from the 10-line
+check-in it was a part of — and then to understand the surrounding
+check-ins as necessary — than it is to understand a 500-line check-in
+that collapses a whole branch's worth of changes down to a single
+finished feature.
+
+[sdm]: ./fossil-v-git.wiki#durable
+
+### <a id="bisecting"></a>6.2 Bisecting works better on small check-ins
+
+Git lets a developer write a feature in ten check-ins but collapse it
+down to an eleventh check-in and then deliberately push only that final
+collapsed check-in to the parent repo. Someone else may then do a bisect
+that blames the merged check-in as the source of the problem they’re
+chasing down; they then have to manually work out which of the 10 steps
+the original developer took to create it to find the source of the
+actual problem.
+
+An equivalent push in Fossil will send all 11 check-ins to the parent
+repository so that a later investigator doing the same sort of bisect
+sees the complete check-in history. That bisect will point the
+investigator at the single original check-in that caused the problem.
+
+### <a id="comments"></a>6.3 Multiple check-ins require multiple check-in comments
+
+The more comments you have from a given developer on a given body of
+code, the more concise documentation you have of that developer's
+thought process. To resume the bisecting example, a developer trying to
+work out what the original developer was thinking with a given change
+will have more success given a check-in comment that explains what the
+one check-in out of ten blamed by the "bisect" command was trying to
+accomplish than if they must work that out from the eleventh check-in's
+comment, which only explains the "clean" version of the collapsed
+feature.
+
+### <a id="cherrypicking"></a>6.4 Cherry-picks work better with small check-ins
+
+While working on a new feature in one branch, you may come across a bug
+in the pre-existing code that you need to fix in order for work on that
+feature to proceed. You could choose to switch briefly back to the
+parent branch, develop the fix there, check it in, then merge the parent
+back up to the feature branch in order to continue work, but that's
+distracting. If the fix isn't for a critical bug, fixing it on the
+parent branch can wait, so it's better to maintain your mental working
+state by fixing the problem in place on the feature branch, then check
+the fix in on the feature branch, resume work on the feature, and later
+merge that fix down into the parent branch along with the feature.
+
+But now what happens if another branch *also* needs that fix? Let us say
+our code repository has a branch for the current stable release, a
+development branch for the next major version, and feature branches off
+of the development branch. If we rebase each feature branch down into
+the development branch as a single check-in, pushing only the rebase
+check-in up to the parent repo, only that fix's developer has the
+information locally to perform the cherry-pick of the fix onto the
+stable branch.
+
+Developers working on new features often do not care about old stable
+versions, yet that stable version may have an end user community that
+depends on that version, who either cannot wait for the next stable
+version or who wish to put off upgrading to it for some time. Such users
+want backported bug fixes, yet the developers creating those fixes have
+poor incentives to provide those backports. Thus the existence of
+maintenance and support organizations, who end up doing such work.
+(There is [a famous company][rh] that built a multi-billion dollar
+enterprise on such work.)
+
+This work is far easier when each cherry-pick transfers completely and
+cleanly from one branch to another, and we increase the likelihood of
+achieving that state by working from the smallest check-ins that remain
+complete. If a support organization must manually disentangle a fix from
+a feature check-in, they are likely to introduce new bugs on the stable
+branch. Even if they manage to do their work without error, it takes
+them more time to do the cherry-pick that way.
+
+[rh]: https://en.wikipedia.org/wiki/Red_Hat
+
+### <a id="backouts"></a>6.5 Back-outs also work better with small check-ins
+
+The inverse of the cherry-pick merge is the back-out merge. If you push
+only a collapsed version of a private working branch up to the parent
+repo, those working from that parent repo cannot automatically back out
+any of the individual check-ins that went into that private branch.
+Others must either manually disentangle the problematic part of your
+merge check-in or back out the entire feature.
+
+## <a id="better-plan"></a>7.0 Cherry-pick merges work better than rebase
+
+Perhaps there are some cases where a rebase-like transformation
+is actually helpful, but those cases are rare, and when they do
+come up, running a series of cherry-pick merges achieves the same
+topology with several advantages:
+
+ 1. In Fossil, cherry-pick merges preserve an honest and clear record
+ of history. Fossil remembers where a cherry-pick came from, and
+ it shows this in its timeline, so other developers can understand
+ how a cherry-pick based commit came together.
+
+ Git lacks the ability to remember the source of a cherry-pick as
+ part of the commit. This fact has no direct bearing on this
+ document’s thesis, but we can make a few observations. First, Git
+ forgets history in more cases than in rebasing. Second, if Git
+ remembered the source of cherry-picks in commits, Git users might
+ have a better argument for avoiding rebase, because they’d have an
+ alternative that *didn’t* lose history.
+
+ 2. Fossil’s [test before commit philosophy][tbc] means you can test a
+ cherry-pick before committing it. Because Fossil allows multiple
+ cherry-picks in a single commit and it remembers them all, you can
+ do this for a complicated merge in step-wise fashion.
+
+ Git commits cherry-picks straight to the repository, so that if it
+ results in a bad state, you have to do something drastic like
+ `git reset --hard` to repair the damage.
+
+ 3. Cherry-picks keep both the original and the revised check-ins,
+ so both timestamps are preserved.
+
+[tbc]: ./fossil-v-git.wiki#testing
+
+## <a id="conclusion"></a>8.0 Summary and conclusion
+
+Rebasing is an anti-pattern. It is dishonest. It deliberately
+omits historical information. It causes problems for collaboration.
+And it has no offsetting benefits.
+
+For these reasons, rebase is intentionally and deliberately omitted
+from the design of Fossil.
+
+
+[golden]: https://www.atlassian.com/git/tutorials/merging-vs-rebasing#the-golden-rule-of-rebasing
+[gitrebase]: https://git-scm.com/book/en/v2/Git-Branching-Rebasing
+[nagappan]: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-2008-11.pdf
+[weinberg]: https://books.google.com/books?id=76dIAAAAMAAJ