Re: git subtree bugs (mishandled merges, recursion depth)
- From
- Colin Stagner <ask+git@howdoi.land>
- Date
- Apr 17, 2026, 04:14 UTC
- Message-ID
- <d1b51a09-acc8-4354-b5bc-f50a13e7c3fd@howdoi.land>
- In-Reply-To
- <27104.62121.658449.222834@chiark.greenend.org.uk>
On 4/16/26 09:31, Ian Jackson wrote:
Show 13 quoted lines
> Colin Stagner writes ("Re: git subtree bugs (mishandled merges, recursion depth)"):
>> When merges come into play, the task of choosing which history is
>> "important" and which history is "not important" is not always clear-cut.
>
> I have some thoughts about this.
>
> I didn't find a formal description of git-subtree's data model, or how
> git subtree split works, precisely. So I'm going to make some
> suppositions.
>
> I observe that git-subtree split doesn't record any metadata in the
> split versions of the commits (for example, the downstream project
> commitid they were split from).This would be helpful information to have, but git-subtree does not record it in general.
`split --rejoin` mode *does* record the parent→split commitid mapping. But this is only recorded in the input history, and only for a single commit. --rejoin actually isn't as helpful as it may first appear.
Keep in mind that no part of history is fixed. Both the input history and/or the split history might get rebased. If that happens, the original input→split commitid mapping is either unhelpful or misleading.
> Repeated splits ought ideally not to constantly generate additional > material. So the algorithm ought to be deterministic.
The algorithm is deterministic. It does need to reconstruct the complete split history every time, which is inefficient.
The real issue is this line from the git-subtree manual page:
Repeated splits of exactly the same history are guaranteed
to be identical (i.e. to produce the same commit IDs) as
long as the settings passed to split (such as --annotate)
are the same.which means that git-subtree SHOULDN'T change how splits work… but it has anyway. There have been many fixes to subtree-split over the years that have changed its behavior in history-incompatible ways. This guarantee really hasn't held up.
We really need something committed, like a config file, that records:
1. how the user wants splits to happen *now*; and 2. how the old split history was split before
That way, we can introduce new functionality or approaches without introducing breakage.
Show 7 quoted lines
> An easy way to do that is to make splitting a pure > function from downstream commits to subtree commits. > > If one can run git subtree split on every commit in the downstream > that has a git subtree merge as an ancestor, then one might think that > means the split must produce as many commits as there are in the > downtream.
Part of the problem here may be that `subtree split` is not particularly aware of `subtree merge`. All split sees is that the tree has a different prefix. Changing this would introduce history-breakage, and it would be good to make it opt-in.
You can also perform subtree merges without `git-subtree`. Merges done this way don't record any information in the trailers.
Show 8 quoted lines
> But we can map multiple downstream commits to the same subtree > commit. Consider the cases, for some downstream commit D. > > 0. D is a single parent commit that *does* change the subtree. > This becomes a new commit with parent split(D~). > > 1. D is a single parent commit that doesn't change the subtree: > We reuse the parent's split: split(D) = split(D~)
I believe both (0) and (1) are current behavior.
Show 7 quoted lines
> 2. D is a multi-parent commit. Determine \forall{i} split(D^i).
> Discard all split(D^i) which are ancestors of any split(D^j).
> If any remaining split(D^i) is not subtree-treesame D,
> or there is more than one remaining split(D^i),
> construct a new commit with those remaining split(D^i) as parents.
> Otherwise all remaining split(D^i) are the same,
> and they are treesame to D, so discard: split(D) = split(D^i).The merge processing is where a lot of history-breaking changes have occurred. There are probably lots of edge-cases to discover along the way. I recommend drawing lots and lots of pictures.
> 3. D is a subtree merge commit. split(D^1) is explicitly stated > in the git-subtree metadata. Calculate split(D^0) as above. > Then calculate split(D) according to point 2.
In a git-subtree-merge, the merge always has two parents. The split history is always on the second parent. The trailers are mostly useful to tell you what the prefix was… but you should still check the commitid to verify that no rebase has happened.
> Do you think it would be worth me prototyping this?
If it's worth it to you, then it's worth it. If it's good, others will want it too.
I've tried lots of git workflows, including submodules and subtrees. But I've found that a regular, plain `git merge`—with a "merge upwards" workflow—is by far the fastest, easiest, and most flexible. Whenever possible, I arrange projects so they can work this way.
I do use plenty of subtree merges to bring in dependencies. subtree merges work great for this.
At least for now, a `subtree split` does not undo a `subtree merge`. If I'm looking to submit changes I've made on top of a subtree merge, I use format-patch
git format-patch --relative=my/subtree/prefix ...
to make patches which are layout-compatible with the original repo. Then I `git am` the patches in the original repo as a topic branch. This works well for my purposes.
I only use `split` to produce repos with read-only views of subdirectories. This is mostly due to a project design quirk that is beyond my control.
If I fork a project, I'll just fork the entire thing. No need to split it.
This is how I work, but you'll find plenty of other opinions out there.
Best of luck,
Colin