[PATCH] commit-reach: parse commits in the given repository
From: Orestis Floros <hidden>
Date: 2026-09-16 13:46:45
Subsystem:
the rest · Maintainer:
Linus Torvalds
`can_all_from_reach()` and `can_all_from_reach_with_flag()` parse the
commits they walk in `the_repository`, even though their caller may be
working in a different repository. `repo_is_descendant_of()` is such a
caller: it is told which repository to work in, but as soon as
generation numbers are enabled it hands the commits over to
`can_all_from_reach()`, which then parses them elsewhere.
This breaks merging a superproject whose submodule pointer advanced on
both sides. merge-ort resolves it by calling `repo_in_merge_bases()` on
the submodule, and with a commit-graph in both the superproject and the
submodule the merge dies:
$ git merge side
fatal: invalid commit position. commit-graph is likely corrupt
`merge_submodule()` looks the submodule commits up in the submodule, so
walking their ancestry pulls in parents whose commit-graph position was
recorded while reading the submodule's commit-graph. The walk then
parses those parents in `the_repository`, where the recorded position
indexes the superproject's commit-graph instead: `fill_commit_graph_info()`
dies when the position is out of bounds, and quietly returns another
commit's date, generation and parents when it is not.
The latter used to be the only symptom. Before bb5da75d61 (commit: use
commit graph in `lookup_commit_reference_gently()`, 2026-02-16) the
initial lookup did not record commit-graph positions, so the walk simply
failed to find the submodule commits in the superproject:
error: Could not read <commit>
Failed to merge submodule sub (commits don't follow merge-base)
Pass the repository into both functions. git-fetch-pack(1) and
git-upload-pack(1) keep passing `the_repository`.
Reported-by: Florian Schmidt <redacted>
Signed-off-by: Orestis Floros <redacted>
---
commit-reach.c | 22 ++++++++++-----------
commit-reach.h | 7 ++++---
fetch-pack.c | 4 ++--
t/helper/test-reach.c | 5 +++--
t/t6437-submodule-merge.sh | 39 ++++++++++++++++++++++++++++++++++++++
upload-pack.c | 6 +++---
6 files changed, 62 insertions(+), 21 deletions(-)
diff --git a/commit-reach.c b/commit-reach.c
index 5df471a313..3d579d8f7f 100644
--- a/commit-reach.c
+++ b/commit-reach.c@@ -572,7 +572,7 @@ int repo_is_descendant_of(struct repository *r, struct commit_list *from_list = NULL; int result; commit_list_insert(commit, &from_list); - result = can_all_from_reach(from_list, with_commit, 0); + result = can_all_from_reach(r, from_list, with_commit, 0); commit_list_free(from_list); return result; } else {
@@ -853,7 +853,8 @@ int commit_contains(struct ref_filter *filter, struct commit *commit, return repo_is_descendant_of(the_repository, commit, list); } -int can_all_from_reach_with_flag(struct object_array *from, +int can_all_from_reach_with_flag(struct repository *r, + struct object_array *from, unsigned int with_flag, unsigned int assign_flag, timestamp_t min_commit_date,
@@ -872,8 +873,7 @@ int can_all_from_reach_with_flag(struct object_array *from, if (!from_one || from_one->flags & assign_flag) continue; - from_one = deref_tag(the_repository, from_one, - "a from object", 0); + from_one = deref_tag(r, from_one, "a from object", 0); if (!from_one || from_one->type != OBJ_COMMIT) { /* * no way to tell if this is reachable by
@@ -886,7 +886,7 @@ int can_all_from_reach_with_flag(struct object_array *from, } list[nr_commits] = (struct commit *)from_one; - if (repo_parse_commit(the_repository, list[nr_commits]) || + if (repo_parse_commit(r, list[nr_commits]) || commit_graph_generation(list[nr_commits]) < min_generation) { result = 0; goto cleanup;
@@ -921,7 +921,7 @@ int can_all_from_reach_with_flag(struct object_array *from, if (!(parent->item->object.flags & assign_flag)) { parent->item->object.flags |= assign_flag; - if (repo_parse_commit(the_repository, parent->item) || + if (repo_parse_commit(r, parent->item) || parent->item->date < min_commit_date || commit_graph_generation(parent->item) < min_generation) continue;
@@ -955,8 +955,8 @@ int can_all_from_reach_with_flag(struct object_array *from, return result; } -int can_all_from_reach(struct commit_list *from, struct commit_list *to, - int cutoff_by_min_date) +int can_all_from_reach(struct repository *r, struct commit_list *from, + struct commit_list *to, int cutoff_by_min_date) { struct object_array from_objs = OBJECT_ARRAY_INIT; struct commit_list *from_iter = from, *to_iter = to;
@@ -967,7 +967,7 @@ int can_all_from_reach(struct commit_list *from, struct commit_list *to, while (from_iter) { add_object_array(&from_iter->item->object, NULL, &from_objs); - if (!repo_parse_commit(the_repository, from_iter->item)) { + if (!repo_parse_commit(r, from_iter->item)) { timestamp_t generation; if (from_iter->item->date < min_commit_date) min_commit_date = from_iter->item->date;
@@ -981,7 +981,7 @@ int can_all_from_reach(struct commit_list *from, struct commit_list *to, } while (to_iter) { - if (!repo_parse_commit(the_repository, to_iter->item)) { + if (!repo_parse_commit(r, to_iter->item)) { timestamp_t generation; if (to_iter->item->date < min_commit_date) min_commit_date = to_iter->item->date;
@@ -996,7 +996,7 @@ int can_all_from_reach(struct commit_list *from, struct commit_list *to, to_iter = to_iter->next; } - result = can_all_from_reach_with_flag(&from_objs, PARENT2, PARENT1, + result = can_all_from_reach_with_flag(r, &from_objs, PARENT2, PARENT1, min_commit_date, min_generation); while (from) {
diff --git a/commit-reach.h b/commit-reach.h
index 3f3a563d8a..5e2b82655b 100644
--- a/commit-reach.h
+++ b/commit-reach.h@@ -88,13 +88,14 @@ int commit_contains(struct ref_filter *filter, struct commit *commit, * commits with date below 'min_commit_date' or generation below * 'min_generation'. */ -int can_all_from_reach_with_flag(struct object_array *from, +int can_all_from_reach_with_flag(struct repository *r, + struct object_array *from, unsigned int with_flag, unsigned int assign_flag, timestamp_t min_commit_date, timestamp_t min_generation); -int can_all_from_reach(struct commit_list *from, struct commit_list *to, - int commit_date_cutoff); +int can_all_from_reach(struct repository *r, struct commit_list *from, + struct commit_list *to, int commit_date_cutoff); /*
diff --git a/fetch-pack.c b/fetch-pack.c
index 120e01f3cf..26dbbdc4f6 100644
--- a/fetch-pack.c
+++ b/fetch-pack.c@@ -2330,8 +2330,8 @@ void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips, die(_("unexpected 'ready' from remote")); else do_check_stateless_delimiter(stateless_rpc, &reader); - if (can_all_from_reach_with_flag(&nt_object_array, COMMON, - REACH_SCRATCH, 0, + if (can_all_from_reach_with_flag(the_repository, &nt_object_array, + COMMON, REACH_SCRATCH, 0, min_generation)) last_iteration = 1; trace2_region_leave_printf("negotiation", "round",
diff --git a/t/helper/test-reach.c b/t/helper/test-reach.c
index 5d86a96c17..8ea77a782c 100644
--- a/t/helper/test-reach.c
+++ b/t/helper/test-reach.c@@ -127,7 +127,7 @@ int cmd__reach(int ac, const char **av) print_sorted_commit_ids(list); commit_list_free(list); } else if (!strcmp(av[1], "can_all_from_reach")) { - printf("%s(X,Y):%d\n", av[1], can_all_from_reach(X, Y, 1)); + printf("%s(X,Y):%d\n", av[1], can_all_from_reach(r, X, Y, 1)); } else if (!strcmp(av[1], "can_all_from_reach_with_flag")) { struct commit_list *iter = Y;
@@ -136,7 +136,8 @@ int cmd__reach(int ac, const char **av) iter = iter->next; } - printf("%s(X,_,_,0,0):%d\n", av[1], can_all_from_reach_with_flag(&X_obj, 2, 4, 0, 0)); + printf("%s(X,_,_,0,0):%d\n", av[1], + can_all_from_reach_with_flag(r, &X_obj, 2, 4, 0, 0)); } else if (!strcmp(av[1], "commit_contains")) { struct ref_filter filter = REF_FILTER_INIT; struct contains_cache cache;
diff --git a/t/t6437-submodule-merge.sh b/t/t6437-submodule-merge.sh
index a564758f52..afb484b963 100755
--- a/t/t6437-submodule-merge.sh
+++ b/t/t6437-submodule-merge.sh@@ -517,4 +517,43 @@ test_expect_success 'merging should fail with no merge base' ' ) ' +test_expect_success 'setup for commit-graphs in superproject and submodule' ' + git init commit-graph && + (cd commit-graph && + git init sub && + for i in 1 2 3 4 5 6 7 8 9 10 + do + test_commit -C sub "sub-$i" || return 1 + done && + + git -C sub checkout --detach sub-1 && + git add sub && + git commit -m base && + + # Write the commit-graph of the superproject while it only + # contains a single commit. Any commit-graph position of the + # submodule is thus out of bounds for the superproject. + git commit-graph write --reachable && + + git checkout -b side && + git -C sub checkout --detach sub-5 && + git add sub && + git commit -m side && + + git checkout main && + git -C sub checkout --detach sub-10 && + git add sub && + git commit -m main && + + git -C sub commit-graph write --reachable) +' + +test_expect_success 'merge does not mix up superproject and submodule commit-graphs' ' + (cd commit-graph && + git merge side && + git rev-parse HEAD:sub >actual && + git -C sub rev-parse sub-10^{commit} >expect && + test_cmp expect actual) +' + test_done
diff --git a/upload-pack.c b/upload-pack.c
index a52856d869..550fea6763 100644
--- a/upload-pack.c
+++ b/upload-pack.c@@ -565,9 +565,9 @@ static int ok_to_give_up(struct upload_pack_data *data) if (!data->have_obj.nr) return 0; - return can_all_from_reach_with_flag(&data->want_obj, THEY_HAVE, - COMMON_KNOWN, data->oldest_have, - min_generation); + return can_all_from_reach_with_flag(the_repository, &data->want_obj, + THEY_HAVE, COMMON_KNOWN, + data->oldest_have, min_generation); } static int get_common_commits(struct upload_pack_data *data,
base-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc -- 2.55.0