{"thread":{"id":"66379","subject":"[BUG] submodule merge tries to read B's commit from A","startedAt":"2026-09-23T20:20:37Z","lastAt":"2026-10-02T08:10:50Z","messageCount":4,"participants":["Guillaume CHAUVEL","Philippe Blain","Patrick Steinhardt"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"553108","messageId":"CAP4DsUexEmm1qo6jH+Qzy+n3dQs_OCJ8yg=ReF+aVrcTrC7NeQ@mail.gmail.com","threadId":"66379","inReplyTo":null,"subject":"[BUG] submodule merge tries to read B's commit from A","fromName":"Guillaume CHAUVEL","fromEmail":"guillaume.chauvel@gmail.com","sentAt":"2026-09-23T20:20:20Z","receivedAt":"2026-09-23T20:20:37Z","isPatch":false,"body":"I ran into two problems while merging a superproject with submodules.\n\nOne problem, involving the repository used for commit-graph lookups, was\nreported in this thread:\nhttps://lore.kernel.org/git/d3241733-d015-4646-88e0-06e56a04e77b@nutanix.com/T/#m174067937aaf76e9fa844386961b3e9e66c1e4d9\n\nThe other problem is that during a merge, Git sometimes tries to read\nfrom submodule A a commit that exists only in submodule B. I reproduced\nthis with Git v2.56.0-rc2, built from source in an Ubuntu 26.04\ncontainer and an Alpine container. The reproducer below triggered the\nissue in all 50 Ubuntu runs and in 43 out of 50 Alpine runs.\n\nThe merge should report a submodule conflict, not look for B's commit\nin A or report A as corrupt. The script checks the OID's presence in\nboth submodules and prints the \"BUG\" line when it finds this case.\n\n---------\n#!/usr/bin/env bash\n\nset -euo pipefail\n\nunset $(git rev-parse --local-env-vars)\nexport LC_ALL=C\nexport GIT_CONFIG_NOSYSTEM=1\nexport GIT_CONFIG_GLOBAL=/dev/null\nexport GIT_DEFAULT_HASH=sha1\nexport GIT_TEMPLATE_DIR=\nexport GIT_AUTHOR_NAME=Reproducer\nexport GIT_AUTHOR_EMAIL=reproducer@example.invalid\nexport GIT_COMMITTER_NAME=\"$GIT_AUTHOR_NAME\"\nexport GIT_COMMITTER_EMAIL=\"$GIT_AUTHOR_EMAIL\"\nexport GIT_AUTHOR_DATE='2000-01-01T00:00:00 +0000'\nexport GIT_COMMITTER_DATE='2000-01-01T00:00:00 +0000'\n\ntmpdir=$(mktemp -d)\n\nfor name in A B; do\nmkdir \"$tmpdir/source-$name\"\ncd \"$tmpdir/source-$name\"\ngit init -q -b main\nprintf '%s base\\n' \"$name\" >file\ngit add file\ngit commit -qm \"$name base\"\ngit switch -qc branch-a\ngit commit --allow-empty -qm \"$name branch-a\"\ngit switch -qc branch-b main\ngit commit --allow-empty -qm \"$name branch-b\"\ngit switch -q main\ndone\n\nmkdir \"$tmpdir/super\"\ncd \"$tmpdir/super\"\ngit init -q -b base\ngit config --local protocol.file.allow always\nfor name in A B; do\n# reproduces the bug\ngit -c protocol.file.allow=always submodule add -q\n\"file://$tmpdir/source-$name\" \"$name\"\n\n# does not reproduce the bug\n# git  -c protocol.file.allow=always submodule add -q\n\"$tmpdir/source-$name\" \"$name\"\ndone\ngit add .\ngit commit -qm base\n\ngit switch -qc branch-a\nfor name in A B; do\n(cd \"$name\" && git switch -q -c branch-a --track origin/branch-a)\ndone\ngit add A B\ngit commit -qm branch-a\n\ngit switch -qc branch-b base\nfor name in A B; do\n(cd \"$name\" && git switch -q -c branch-b --track origin/branch-b)\ndone\ngit add A B\ngit commit -qm branch-b\n\ncd \"$tmpdir\"\ngit -c protocol.file.allow=always clone -q --no-local\n\"file://$tmpdir/super\" clone\ncd clone\ngit -c protocol.file.allow=always submodule update --init -q\ngit switch -q -c branch-a --track origin/branch-a\nif merge_output=$(git merge branch-b 2>&1); then\nmerge_status=0\nelse\nmerge_status=$?\nfi\nprintf 'git merge exit status: %s\\n%s\\n' \"$merge_status\" \"$merge_output\"\n\nif [[ $merge_output =~ Could\\ not\\ read\\ ([0-9a-f]{40}|[0-9a-f]{64}) ]]; then\nforeign_oid=${BASH_REMATCH[1]}\nif ! (cd A && git cat-file -e \"$foreign_oid\" 2>/dev/null) &&\n(cd B && git cat-file -e \"$foreign_oid\" 2>/dev/null); then\nprintf 'BUG: OID %s belongs to B instead of A\\n' \"$foreign_oid\"\nfi\nfi\n---------\n\nOne run produced:\n\ngit merge exit status: 2\nerror: Could not read 7d549ba7e9152029e66ddca8dd23ee7da32b036f\nerror: could not parse commit 7d549ba7e9152029e66ddca8dd23ee7da32b036f\nerror: failed to merge submodule A (repository corrupt)\nMerge with strategy ort failed.\nBUG: OID 7d549ba7e9152029e66ddca8dd23ee7da32b036f belongs to B instead of A\n\nAn AI analysis identified a likely cause: a delta-base cache entry may\nremain after its pack is closed. If a pack from another submodule reuses\nthe same packed_git address and base offset, Git may return stale cached\ndata.\n"},{"id":"553739","messageId":"764b8c2e-cf09-4531-94f2-268f97a889d7@gmail.com","threadId":"66379","inReplyTo":"CAP4DsUexEmm1qo6jH+Qzy+n3dQs_OCJ8yg=ReF+aVrcTrC7NeQ@mail.gmail.com","subject":"Re: [BUG] submodule merge tries to read B's commit from A","fromName":"Philippe Blain","fromEmail":"levraiphilippeblain@gmail.com","sentAt":"2026-09-30T18:31:44Z","receivedAt":"2026-09-30T18:31:48Z","isPatch":false,"body":"Hi Guillaume,\n\nLe 2026-09-23 à 16 h 20, Guillaume CHAUVEL a écrit :\n> I ran into two problems while merging a superproject with submodules.\n> \n> One problem, involving the repository used for commit-graph lookups, was\n> reported in this thread:\n> https://lore.kernel.org/git/d3241733-d015-4646-88e0-06e56a04e77b@nutanix.com/T/#m174067937aaf76e9fa844386961b3e9e66c1e4d9\n\nFYI, the above bug was fixed in 700f7b74de (commit-reach: parse commits in \nthe given repository, 2026-09-16), which is currently in 'next' but not yet\nin master.\n\n> The other problem is that during a merge, Git sometimes tries to read\n> from submodule A a commit that exists only in submodule B. I reproduced\n> this with Git v2.56.0-rc2, built from source in an Ubuntu 26.04\n> container and an Alpine container. The reproducer below triggered the\n> issue in all 50 Ubuntu runs and in 43 out of 50 Alpine runs.\n> \n> The merge should report a submodule conflict, not look for B's commit\n> in A or report A as corrupt. The script checks the OID's presence in\n> both submodules and prints the \"BUG\" line when it finds this case.\n\nThanks for the reproducer, I confirm I see the same behaviour with v2.56.0-rc2, \non RHEL 9. With v2.48.1, the merge results in a conflict, instead of aborting, \nalthough I get a spurious \"hash mismatch\" message, and the reason for the \nconflict (\"commits not present\") is wrong:\n\ngit version 2.48.1\ngit merge exit status: 1\nerror: hash mismatch 2ca9f0f330e976b992fc18633d1d267b8aad596e\nFailed to merge submodule A (commits not present)\nCONFLICT (submodule): Merge conflict in A\nFailed to merge submodule B\nCONFLICT (submodule): Merge conflict in B\nAutomatic merge failed; fix conflicts and then commit the result.\n\nWith 2.33.0, which I chose randomly, we get the correct behaviour:\n\ngit version 2.33.0\ngit merge exit status: 1\nFailed to merge submodule A\nCONFLICT (submodule): Merge conflict in A\nFailed to merge submodule B\nCONFLICT (submodule): Merge conflict in B\nAutomatic merge failed; fix conflicts and then commit the result.\n\nI turned your reproducer into a bisection script (~/bisect-merge.sh) \nby tweaking the final 'if':\n\n```\nif [[ $merge_output =~ Could\\ not\\ read\\ ([0-9a-f]{40}|[0-9a-f]{64}) ]]; then\n    foreign_oid=${BASH_REMATCH[1]}\n    if ! (cd A && git cat-file -e \"$foreign_oid\" 2>/dev/null) &&\n         (cd B && git cat-file -e \"$foreign_oid\" 2>/dev/null); then\n        printf 'BUG: OID %s belongs to B instead of A\\n' \"$foreign_oid\"\n        exit 1\n    fi\nelif [[ $merge_output =~ hash\\ mismatch ]];then\n        [ ${1:-\"\"} = MISMATCH ] && exit 1 || exit 0\nelse\n    exit 0\nfi\n```\n\nand invoking it in my ~/bisect-git.sh script:\n\n```\n#!/bin/bash\n\nmake clean > /dev/null\n# build but keep the output on one line\nif\tmake -j |& { while read line; do  printf \"\\033[K%s\\r\" \"${line}\" ; done; \n                     printf \"\\033[KFinished building $(cat GIT-VERSION-FILE)\\n\" ; }\nthen\n\t# run project specific test and report its status\n\texport PATH=\"$PWD/bin-wrappers/:$PATH\"\n\t~/bisect-merge.sh \"$@\"\n\tstatus=$?\nelse\n\t# tell the caller this is untestable\n\tstatus=125\nfi\n\n# return control\necho\nexit $status\n```\n\nBisecting the merge failure with:\n\n\tgit bisect start v2.56.0-rc2 v2.48.1 && git bisect run ~/bisect-git.sh\n\nfinds bb5da75d61 (commit: use commit graph in lookup_commit_reference_gently(), \n2026-02-16), i.e. v2.54.0-rc0~136^2, which is the same commit from which the \ncommit-graph bug mentioned above originates. I CC'ed Patrick, its author.\n\nBisecting the \"hash mismatch\" behaviour with:\n\n\tgit bisect start v2.48.1 v2.33.0 && git bisect run ~/bisect-git.sh MISMATCH\n\nfinds 6f1e9394e2 (object: fix leaking packfiles when closing object store, 2024-08-08),\ni.e. v2.47.0-rc0~123^2, which is also authored by Patrick.\n\nI did not yet dig further, but I have a few additional observations:\n\n- in contrast to the commit-graph bug, disabling the use of commit-graphs via\n  'git config --global core.commitGraph false' early in the script, by moving the 'tmpdir'\n  definition to the top and setting GIT_CONFIG_GLOBAL=$tmpdir/.gitconfig, does not change\n  the behaviour, neither in the \"repository corrupt\" case, nor in the \"hash mismatch\" case.\n- On Ubuntu 22.02 under WSL, the reproducer does not trigger the bug on v2.56.0-rc2 (on a dozen runs),\n  but it does trigger it on v2.55.0. Funnily on that system with v2.56.0-rc2 I get the correct behaviour !\n  (no \"hash mismatch\" either).\n- On a Ubuntu 22.04 Docker container, I get the same behaviour as on RHEL 9.\n\n> An AI analysis identified a likely cause: a delta-base cache entry may\n> remain after its pack is closed. If a pack from another submodule reuses\n> the same packed_git address and base offset, Git may return stale cached\n> data.\n> \n"},{"id":"553837","messageId":"ar5ppRMQ8NkGnbGp@pks.im","threadId":"66379","inReplyTo":"764b8c2e-cf09-4531-94f2-268f97a889d7@gmail.com","subject":"Re: [BUG] submodule merge tries to read B's commit from A","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-01T14:09:41Z","receivedAt":"2026-10-01T14:09:46Z","isPatch":false,"body":"On Wed, Sep 30, 2026 at 02:31:44PM -0400, Philippe Blain wrote:\n> I did not yet dig further, but I have a few additional observations:\n> \n> - in contrast to the commit-graph bug, disabling the use of commit-graphs via\n>   'git config --global core.commitGraph false' early in the script, by moving the 'tmpdir'\n>   definition to the top and setting GIT_CONFIG_GLOBAL=$tmpdir/.gitconfig, does not change\n>   the behaviour, neither in the \"repository corrupt\" case, nor in the \"hash mismatch\" case.\n> - On Ubuntu 22.02 under WSL, the reproducer does not trigger the bug on v2.56.0-rc2 (on a dozen runs),\n>   but it does trigger it on v2.55.0. Funnily on that system with v2.56.0-rc2 I get the correct behaviour !\n>   (no \"hash mismatch\" either).\n> - On a Ubuntu 22.04 Docker container, I get the same behaviour as on RHEL 9.\n> \n> > An AI analysis identified a likely cause: a delta-base cache entry may\n> > remain after its pack is closed. If a pack from another submodule reuses\n> > the same packed_git address and base offset, Git may return stale cached\n> > data.\n\nYup, that seems to be the issue indeed. We should really be clearing\npackfiles out of the delta base cache when closing packfiles, but we\ndon't right now. I'll investigate tomorrow.\n\nThanks!\n\nPatrick\n"},{"id":"553913","messageId":"ar9nBA2e_sEiFZ4k@pks.im","threadId":"66379","inReplyTo":"ar5ppRMQ8NkGnbGp@pks.im","subject":"Re: [BUG] submodule merge tries to read B's commit from A","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T08:10:44Z","receivedAt":"2026-10-02T08:10:50Z","isPatch":false,"body":"On Thu, Oct 01, 2026 at 04:09:41PM +0200, Patrick Steinhardt wrote:\n> On Wed, Sep 30, 2026 at 02:31:44PM -0400, Philippe Blain wrote:\n> > I did not yet dig further, but I have a few additional observations:\n> > \n> > - in contrast to the commit-graph bug, disabling the use of commit-graphs via\n> >   'git config --global core.commitGraph false' early in the script, by moving the 'tmpdir'\n> >   definition to the top and setting GIT_CONFIG_GLOBAL=$tmpdir/.gitconfig, does not change\n> >   the behaviour, neither in the \"repository corrupt\" case, nor in the \"hash mismatch\" case.\n> > - On Ubuntu 22.02 under WSL, the reproducer does not trigger the bug on v2.56.0-rc2 (on a dozen runs),\n> >   but it does trigger it on v2.55.0. Funnily on that system with v2.56.0-rc2 I get the correct behaviour !\n> >   (no \"hash mismatch\" either).\n> > - On a Ubuntu 22.04 Docker container, I get the same behaviour as on RHEL 9.\n> > \n> > > An AI analysis identified a likely cause: a delta-base cache entry may\n> > > remain after its pack is closed. If a pack from another submodule reuses\n> > > the same packed_git address and base offset, Git may return stale cached\n> > > data.\n> \n> Yup, that seems to be the issue indeed. We should really be clearing\n> packfiles out of the delta base cache when closing packfiles, but we\n> don't right now. I'll investigate tomorrow.\n\nI've sent [1] now to fix this issue. Thanks!\n\nPatrick\n\n[1]: <20261002-pks-packfile-stale-delta-base-cache-v1-0-7592a3e31ae0@pks.im>\n"}]}