git/list[1] front-page[2] threads[3] people[4] search[5] about
wed 2026-10-07 18:13 UTC

[PATCH v2 3/3] contrib/subtree: reduce recursion during split

From
CSColin Stagner <ask+git@howdoi.land>
Date
Mar 5, 2026, 23:55 UTC
Message-ID
<20260305-cs-subtree-split-recursion-v2-3-7266be870ba9@howdoi.land>
In-Reply-To
<20260305-cs-subtree-split-recursion-v2-0-7266be870ba9@howdoi.land>

On Debian-alikes, POSIX sh has a hardcoded recursion depth of 1000. This limit operates like bash's `$FUNCNEST` [1], but it does not actually respect `$FUNCNEST`. This is non-standard behavior. On other distros, the sh recursion depth is limited only by the available stack size.

With certain history graphs, subtree splits are recursive—with one recursion per commit. Attempting to split complex repos that have thousands of commits, like [2], may fail on these distros.

Reduce the amount of recursion required by eagerly discovering the complete range of commits to process.

The recursion is a side-effect of the rejoin-finder in `find_existing_splits`. Rejoin mode, as in

    git subtree split --rejoin -b hax main ...

improves the speed of later splits by merging the split history back into `main`. This gives the splitting algorithm a stopping point. The rejoin maps one commit on `main` to one split commit on `hax`. If we encounter this commit, we know that it maps to `hax`.

But this is only a single point in the history. Many splits require history from before the rejoin. See patch content for examples.

If pre-rejoin history is required, `check_parents` recursively discovers each individual parent, with one recursion per commit. The recursion deepens the entire tree, even if an older rejoin is available. This quickly overwhelms the Debian sh stack.

Instead of recursively processing each commit, process *all* the commits back to the next obvious starting point: i.e., either the next-oldest --rejoin or the beginning of history. This is where the recursion is likely to stop anyway.

While this still requires recursion, it is *considerably* less recursive.

[1]: https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html#index-FUNCNEST
[2]: https://github.com/christian-heusel/aur.git
Signed-off-by: Colin Stagner <ask+git@howdoi.land>
---
 contrib/subtree/git-subtree.sh | 56 ++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 54 insertions(+), 2 deletions(-)
diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh
index c1756b3e74..c649a9e393 100755
--- a/contrib/subtree/git-subtree.sh
+++ b/contrib/subtree/git-subtree.sh
@@ -315,6 +315,46 @@ cache_miss () {
 }
 
 # Usage: check_parents [REVS...]
+#
+# During a split, check that every commit in REVS has already been
+# processed via `process_split_commit`. If not, deepen the history
+# until it is.
+#
+# Commits authored by `subtree split` have to be created in the
+# same order as every other git commit: ancestor-first, with new
+# commits building on old commits. The traversal order normally
+# ensures this is the case, but it also excludes --rejoins commits
+# by default.
+#
+# The --rejoin tells us, "this mainline commit is equivalent to
+# this split commit." The relationship is only known for that
+# exact commit---and not before or after it. Frequently, commits
+# prior to a rejoin are not needed... but, just as often, they
+# are! Consider this history graph:
+#
+#              --D---
+#             /      \
+#         A--B--C--R--X--Y    main
+#                 /     /
+#          a--b--c     /      split
+#              \      /
+#               --e--/
+#
+# The main branch has commits A, B, and C. main is split into
+# commits a, b, and c. The split history is rejoined at R.
+#
+# There are at least two cases where we might need the A-B-C
+# history that is prior to R:
+#
+# 1. Commit D is based on history prior to R, but
+#    it isn't merged into mainline until after R.
+#
+# 2. Commit e is based on old split history. It is merged
+#    back into mainline with a subtree merge. Again, this
+#    happens after R.
+#
+# check_parents detects these cases and deepens the history
+# to the next available rejoin.
 check_parents () {
 	missed=$(cache_miss "$@") || exit $?
 	local indent=$(($indent + 1))
@@ -322,8 +362,20 @@ check_parents () {
 	do
 		if ! test -r "$cachedir/notree/$miss"
 		then
-			debug "incorrect order: $miss"
-			process_split_commit "$miss" ""
+			debug "found commit excluded by --rejoin: $miss. skipping to the next --rejoin..."
+			unrevs="$(find_existing_splits "$dir" "$miss" "$repository")" || exit 1
+
+			find_commits_to_split "$miss" "$unrevs" |
+			while read -r rev parents
+			do
+				process_split_commit "$rev" "$parents"
+			done
+
+			if ! test -r "$cachedir/$miss" &&
+				! test -r "$cachedir/notree/$miss"
+			then
+				die "failed to deepen history at $miss"
+			fi
 		fi
 	done
 }
-- 
2.43.0
Previous: Colin StagnerNext: Colin Stagner
Message 3 of 22 in “contrib/subtree: reduce function side-effects”
  1. 1/3 contrib/subtree: reduce function side-effectsColin Stagner, Mar 5, 2026
  2. 2/3 contrib/subtree: functionalize split traversalColin Stagner, Mar 5, 2026
  3. 3/3 contrib/subtree: reduce recursion during splitColin Stagner, Mar 5, 2026
  4. 0/3 contrib/subtree: reduce recursion during splitColin Stagner, Mar 5, 2026
  5. Junio C HamanoMar 13, 2026
  6. Junio C HamanoMar 13, 2026
  7. Junio C HamanoApr 15, 2026
  8. Ben KnobleApr 15, 2026
  9. Ian JacksonApr 16, 2026
  10. Junio C HamanoApr 16, 2026
  11. Colin StagnerApr 17, 2026
  12. Ian JacksonApr 19, 2026
  13. Ben KnobleApr 20, 2026
  14. Junio C HamanoApr 20, 2026
  15. Ian JacksonApr 20, 2026
  16. Colin StagnerApr 21, 2026
  17. Johannes SchindelinApr 22, 2026
  18. git-subtree rewriteIan Jackson, Apr 22, 2026
  19. Junio C HamanoJun 1, 2026
  20. Ian JacksonJun 2, 2026
  21. Colin StagnerJun 3, 2026
  22. Ian JacksonJun 3, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.