From: Elijah Newren Date: Sat, 20 Sep 2025 19:13:57 GMT Subject: Re: [RFC] git-rebase-clean: mitigating a “semantic conflict cascade” during rebase Message-ID: In-Reply-To: Hi, On Fri, Sep 19, 2025 at 2:17 PM Antonio Mennillo wrote: > > Hi Brian, > > Thank you again for the feedback, it gave me a fundamental piece of > puzzle about Git's design constraints that I hadn't fully considered. > > You're right about tree-sitter being inappropriate for Git core. That > approach is too narrow, covering only specific languages. My v2.0.0 > already prioritizes Git's existing plumbing to track content changes > universally, with tree-sitter as an optional enhancement. This stays > content-agnostic and removes that which I can define as a source of > nondeterminism and heuristic complexity that it's elegant to avoid. > > To summarize the mechanism: > > 1) Squash the feature branch into a single commit: temporarily > compressing the branch makes all internal semantic dependencies > visible at once, avoiding cascade conflicts during step-by-step rebase > with non-atomic commits. > > 2) Resolve conflicts once with full context: the semantic issues are > "suspended" in this unified state where everything is coherent. > > 3) Restore original commits by replaying saved diffs: using a > hash-based approach with git's native diff/apply machinery, the tool > remains file-type agnostic. The restored commits may still break > individually (e.g., won't compile in isolation), but the branch is now > rebased with granular history preserved. I don't understand how this is possible, other than in trivial cases. Lots of questions: How exactly is replaying the saved diffs supposed to work? Replaying those diffs would run into conflicts, otherwise you wouldn't have been trying to search for a modified strategy in the first place. How do you resolve those? Are you assuming all conflicts are orthogonal, i.e. that the conflicts from patches A and B are never on overlapping or even nearby lines to each other? In such a case, there wouldn't be any "cascade of conflicts" to resolve anyway, so the approach would be unnecessary. And if this isn't true, then I don't see how you can resolve all the conflicts at once and magically split them out into separate commits and replay things in a way that doesn't have conflicts -- at least not without further human intervention. And that was focusing on content conflicts. What do you do with non-content conflicts, such as file mode differences, file/directory conflicts, symlink/submodule conflicts, rename conflict, directory rename conflict, etc.? But going back to content conflicts, are you somehow sidestepping the need to assume orthogonality? Perhaps when you attempt to replay and hit a conflict, you just take the end-result for that hunk range for the first patch that touches it, and for any other patch that touches that hunk range you simply discard the changes for those since they've been squashed into the first patch? Or something similar? As another way to look at this problem, what about cases where individual commits being replayed will conflict, but merging the end result has no conflicts? (In other words, only the intermediate states had a conflict of some type -- a simple example of this is someone making a change and then shortly later reverting it.) In such a case, by resolving conflicts only at the end, there was no resolution of the conflicts experienced by those intermediate states. What do you do there? What about cases like https://git-scm.com/docs/gitfaq#Documentation/gitfaq.txt-IfImakeachangeontwobranchesbutrevertitononewhydoesthemergeofthosebranchesincludethechange (both sides of history make a change, and one side also reverts that change) -- by merging the squashed result, you'd end up with the change being applied, but when you try to split it out into separate commits, you have "apply the change" followed by "revert the change" -- how does that result in the change being applied in the end result? Are there cases where your algorithm feels indistinguishable from squashing all the commits, having the user resolve conflicts, and then randomly assigning the hunks to a completely new set of commits (beyond the obvious differences in commit messages and perhaps number of commits)? Every algorithm I can think of for automatically playing existing commits while only resolving conflicts at the end would seem to sometimes suffer from this kind of downside, but it's entirely possible I'm only thinking of a few brute force algorithms and I'm missing something clever that you might be doing. > To address your header file example specifically: when common.h no > longer includes stdlib.h after rebasing onto main, you would indeed > need to add #include during conflict resolution. With my > tool, you'd add this include once while resolving the squashed > commit's conflicts. The fix then persists through all restored commits > - so instead of potentially fixing the same missing header multiple > times across commits during a normal rebase, you handle it once. > You're correct that the tool cannot determine which specific commit > should ideally contain this fix (that would require compilation > testing), but it prevents the cascading resolution burden while > preserving the granular history. I think there may have been a misunderstanding here. The purpose wasn't granular history, it was meaningful logical commits; those tend to come in granular sizes, but granularity was more of a side-effect rather than the purpose. Having a disfigured granular history is quite different than meaningful logical commits. > The key point: this doesn't fix non-atomic commits. The semantic > problems remain if you examine individual commits. But it prevents > these issues from causing redundant conflict resolution during rebase > by compressing the timeline of the branch in a single instant "t" > where the semantic non-atomic misalignment appears simultaneously. > > The tool trades ideal per-commit correctness for efficiency in > real-world scenarios where atomic commits aren't always achievable > (junior developers, emergency fixes, legacy codebases). I'm pretty biased here, but let me take exception with your wording that these are not achievable cases. I'm going to push back on that, rather strongly. In fact, I'd go further and say that legacy codebases are where logical small commits make the *most* sense (sometimes with small new projects the "why" and the "what" you're doing is obvious and so there's not much to write in a commit message, and anyone can follow along, whereas in a legacy codebase you need to carefully document why you are making changes and ensure each change in isolation makes sense so others can figure out why each line of code does what it does). In regards to junio developers, letting junior developers develop bad habits (and even facilitating such) just leads to them later being senior developers with bad habits; I think it'd be better to take the time to coach them on proper practices instead. And in the remaining case, what you describe sounds to me like a rushed emergency fix, and I feel like rushed emergency fixes result in more emergencies later. I understand that many are reluctant to put in the effort to make clear, logical commits, but I feel that each of your cases are examples of unwillingness or lack of training, not an actual inability. As noted above, I'm quite biased here, and would rather encourage people to make clear, logical, easy-to-follow-and-review-years-later steps that are documented as commits. I think tools which encourage or facilitate doing the opposite are things that should be shunned. (I hate squash-merging in GitHub for this reason, and GitHub's lack of focus on treating commit messages as important first-class material for review has long bothered me deeply, as I've commented elsewhere.) Others are free to disagree with me and pursue their own path, but that's my position. > Regarding expectations: documentation will be clear that this only > reduces redundant conflict resolution. It doesn't make commits atomic > or understand semantics. It manages semantic inconsistency by > compression, intended as reduced dimensionality. The defects remain in > history but no longer interfere with rebasing. > > I apologize if the code evolution seems rushed - v2.0.0 was developed > quickly to establish a working implementation of the core concept. For > v3.0.0, I plan to remove tree-sitter entirely to achieve complete > language-agnosticism. I'll also need to properly evaluate edge cases > like submodules and merge commits through integration testing, though > the squash operation should hopefully be quite independent from these > structures. My testing focus will be on ensuring determinism, proper > rollback capabilities, and full idempotency across all supported > scenarios. > > The current implementation is available at > https://github.com/anthem87/clean-rebase/tree/v2.0.0 if you'd like to > explore the approach. > > Thanks again for the patience and the attention. I hope my email doesn't come across as too negative. I'm a big fan of exploring and attempting new things. While I am biased against tools that work against logical & bisectable changes, and I'm worried your tool is going to face an awful lot of problematic cases if you continue with it, hopefully my email provides more context for you and alerts you to some cases you might need to investigate if you want to push the tool further. Even if I am not in the camp that'd be interested in using your tool, I am very curious if you have clever ideas for any of the special cases I listed above and how to handle them; sometimes any such ideas might have ways of being wielded elsewhere.