Florian SchmidtJul 31, 2026, 09:05 UTC on loreHiya,
Hitting an issue during git rebase. I am starting to suspect git is possibly getting confused between main and submodule repos. I noticed this with a self-compiled git 2.54, but I compiled git v2.55 just now, and seem to hit the same issue still. Full bisect at the end of the message.
The first-order issue, trying to rebase a work/foo branch which is tracking origin/master. There's only one commit difference, a commit that changes two submodule pointers:
Show 22 quoted lines
> devvm:~/gitrepo[git:work/foo *%|u+1-116]$ git show --format="" -p
> diff --git a/rpmsrc/kernel b/rpmsrc/kernel
> index d8d411244..17006b605 160000
> --- a/rpmsrc/kernel
> +++ b/rpmsrc/kernel
> @@ -1 +1 @@
> -Subproject commit d8d411244cfb3463ce09b9577970f7955073ed14
> +Subproject commit 17006b605eaed1820ae1c717d4782728b9afc6d3
> diff --git a/rpmsrc/qemu-kvm b/rpmsrc/qemu-kvm
> index e7d525d0f..8bf4019df 160000
> --- a/rpmsrc/qemu-kvm
> +++ b/rpmsrc/qemu-kvm
> @@ -1 +1 @@
> -Subproject commit e7d525d0fdc42fca65c2b70bb5f56adc5aa1d7b1
> +Subproject commit 8bf4019dfcb320108a0f014d92d926320a4c8058
>
> devvm:~/gitrepo[git:work/foo *%|u+1-116]$ git rebase
> error: Could not read 157b7492624beac3ac667dd3a84de9969377866b
> fatal: invalid commit position. commit-graph is likely corrupt
>
> devvm:~/gitrepo[git:work/foo *%|u+1-116]$ git cat-file -t 157b7492624beac3ac667dd3a84de9969377866b
> fatal: git cat-file: could not get object info
git fsck reported no corruption, just a bunch of dangling commits/trees/blobs, no warnings about 157b7492624beac3ac667dd3a84de9969377866b
Now, hint 1: the rebase *does* work when disabling commit-graphs:
Show 8 quoted lines
> devvm:~/gitrepo[git:work/foo *%|u+1-116]$ git -c core.commitGraph=false rebase origin/master
> Rebasing (1/1)
> [... thinking thinking thinking for a long time ...]
> Failed to merge submodule rpmsrc/kernel
> CONFLICT (submodule): Merge conflict in rpmsrc/kernel
> Failed to merge submodule rpmsrc/qemu-kvm
> CONFLICT (submodule): Merge conflict in rpmsrc/qemu-kvm
> [...]
which is fair enough, the submodule pointers moved. And hint 2:
Show 6 quoted lines
> $ git cat-file -t 157b7492624beac3ac667dd3a84de9969377866b
> fatal: git cat-file: could not get object info
> $ git -C rpmsrc/kernel cat-file -t 157b7492624beac3ac667dd3a84de9969377866b
> fatal: git cat-file: could not get object info
> $ git -C rpmsrc/qemu-kvm cat-file -t 157b7492624beac3ac667dd3a84de9969377866b
> commit
So it tries to identify something on the submodule? Maybe it gets confused between submodule and main repo(?)
For good measure, I did a git fsck --full on rpmsrc/qemu-kvm, but again only a few dangling warnings, and no change to the issue. So I went to bisect and noticed the issue appeared somewhere between v2.53 and v2.54. v2.53 also complains, but then continues:
Show 8 quoted lines
> $ ~/upstream/git/git rebase
> error: Could not read 157b7492624beac3ac667dd3a84de9969377866b
> error: Could not read 95d676ad199191d81a000761733ea25c17172d8b
> Failed to merge submodule rpmsrc/kernel (commits don't follow merge-base)
> CONFLICT (submodule): Merge conflict in rpmsrc/kernel
> Failed to merge submodule rpmsrc/qemu-kvm (commits don't follow merge-base)
> CONFLICT (submodule): Merge conflict in rpmsrc/qemu-kvm
> [...]
In any case, to figure out the cause, I bisected it down to commit bb5da75d61 ("commit: use commit graph in `lookup_commit_reference_gently()`"), which makes sense in the context of "it works with commit-graph disabled".
At this point, I wonder what the best next step is. I notice that there are two errors listed even when the "git rebase" works on v2.53.0, which are not there in the commitGraph=false case. I wonder whether that's a sign that something deeper is not as it should be. I'm happy to provide more details as needed, but I didn't want to touch the repo at this point and possibly lose the repro.
Cheers, Florian
[PATCH] commit-reach: parse commits in the given repository
`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 <flosch@nutanix.com>
Signed-off-by: Orestis Floros <orestisflo@gmail.com>
---
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