{"thread":{"id":"65585","subject":"git clone with --dissociate sometimes fails to check out target commit","startedAt":"2026-05-04T08:20:40Z","lastAt":"2026-05-04T11:36:51Z","messageCount":4,"participants":["Rasmus Villemoes","Jeff King"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"542645","messageId":"87h5onsi0f.fsf@prevas.dk","threadId":"65585","inReplyTo":null,"subject":"git clone with --dissociate sometimes fails to check out target commit","fromName":"Rasmus Villemoes","fromEmail":"ravi@prevas.dk","sentAt":"2026-05-04T08:20:32Z","receivedAt":"2026-05-04T08:20:40Z","isPatch":false,"body":"Hi\n\nWe have now seen this error a couple of times in our CI, and this time I\nmanaged to grab a snapshot of the local mirror for which it fails. The\nfailing command is\n\n  git clone --verbose --depth=20 --branch=whinlatter --reference-if-able=/yocto/meta-mirrors/core --dissociate https://git.openembedded.org/openembedded-core core\n  Cloning into 'core'...\n  POST git-upload-pack (388 bytes)\n  POST git-upload-pack (986 bytes)\n  POST git-upload-pack (gzip 1836 to 958 bytes)\n  fatal: unable to parse commit 8751ec83421192fc0f8495fb95798f9eb7be77a0\n  warning: Clone succeeded, but checkout failed.\n  You can inspect what was checked out with 'git status'\n  and retry with 'git restore --source=HEAD :/'\n\nI wrapped up that local copy /yocto/meta-mirrors/core in a tarball, but\nit's ~200M, and I don't know another way of reproducing. I also don't\nhave a better way of sharing such a file than [1], apologies.\n\nUsing that repository as both the remote url to clone and the local\nreference, I can consistently reproduce the problem. That is:\n\n  cd /tmp\n  # fetch that core.tar.gz\n  mkdir upstream-core local-core\n  tar -xf core.tar.gz -C upstream-core/\n  tar -xf core.tar.gz -C local-core/\n  git clone --verbose --branch=whinlatter --reference-if-able=/tmp/local-core --dissociate --depth=20 file:///tmp/upstream-core core\n\nfails in the same way, with both git 2.47.3 (Debian trixie) and 2.53.0\n(Arch). Removing --depth=20 doesn't change anything, neither does\nremoving --branch=whinlatter (except of course for the commit it tries\nto check out). But dropping --dissociate, the clone works as expected.\n\nIt doesn't happen very often, the last time was around January 30, where\nit was for another repository\n(https://github.com/openembedded/meta-openembedded.git), but exactly the\nsame symptoms, so about 100 nightly pipelines ago.\n\nAre we using --dissociate wrongly, or are we perhaps not maintaining\nthose local mirror repos properly? They are essentially just created\nwith 'git clone --mirror', with 'git remote update' run periodically.\n\nNaively, I'd expect the effects of --dissociate to only happen after\neverything else the clone command does has been done, but it seems that\nthe ties to the reference repo are cut too soon.\n\nRasmus\n\n[1] https://prevasonline-my.sharepoint.com/:u:/g/personal/rasmus_villemoes_prevas_dk/IQCRaxpwj5NfQYZNQJWc9PJTAY0C33XvXn8CnqPEdPAbpDA?e=zQAfg7\n"},{"id":"542653","messageId":"20260504095442.GA603346@coredump.intra.peff.net","threadId":"65585","inReplyTo":"20260504095110.GA599780@coredump.intra.peff.net","subject":"Re: git clone with --dissociate sometimes fails to check out target commit","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-05-04T09:54:42Z","receivedAt":"2026-05-04T09:54:44Z","isPatch":false,"body":"On Mon, May 04, 2026 at 05:51:10AM -0400, Jeff King wrote:\n\n> No, you're using it correctly. The dissociate step should copy all of\n> the shared objects into the new repo, so it shouldn't matter whether we\n> do it before or after checkout. The objects are there either way.\n> \n> But there's an interesting bug here with commit graphs. What happens is\n> this:\n\nOh, and ironically dissociating later _would_ fix this bug, like so:\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex fba3c9c508..7b7c83c717 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -1616,11 +1616,6 @@ int cmd_clone(int argc,\n \ttransport_unlock_pack(transport, 0);\n \ttransport_disconnect(transport);\n \n-\tif (option_dissociate) {\n-\t\todb_close(the_repository->objects);\n-\t\tdissociate_from_references();\n-\t}\n-\n \tif (option_sparse_checkout && git_sparse_checkout_init(dir))\n \t\treturn 1;\n \n@@ -1630,6 +1625,11 @@ int cmd_clone(int argc,\n \t\t       filter_submodules,\n \t\t       ref_storage_format);\n \n+\tif (option_dissociate) {\n+\t\todb_close(the_repository->objects);\n+\t\tdissociate_from_references();\n+\t}\n+\n \tlist_objects_filter_release(&filter_options);\n \n \tstring_list_clear(&option_not, 0);\n\n\nBut only because we are working around it: if we dissociate at the very\nend, then there is no in-process code that will look at the objects\nafter that odb_close() call, and thus the bug cannot be triggered. It\nwould still potentially be lurking for other odb_close() callers,\nthough.\n\n-Peff\n"},{"id":"542654","messageId":"20260504095110.GA599780@coredump.intra.peff.net","threadId":"65585","inReplyTo":"87h5onsi0f.fsf@prevas.dk","subject":"Re: git clone with --dissociate sometimes fails to check out target commit","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-05-04T09:51:10Z","receivedAt":"2026-05-04T09:57:53Z","isPatch":false,"body":"On Mon, May 04, 2026 at 10:20:32AM +0200, Rasmus Villemoes wrote:\n\n> Are we using --dissociate wrongly, or are we perhaps not maintaining\n> those local mirror repos properly? They are essentially just created\n> with 'git clone --mirror', with 'git remote update' run periodically.\n> \n> Naively, I'd expect the effects of --dissociate to only happen after\n> everything else the clone command does has been done, but it seems that\n> the ties to the reference repo are cut too soon.\n\nNo, you're using it correctly. The dissociate step should copy all of\nthe shared objects into the new repo, so it shouldn't matter whether we\ndo it before or after checkout. The objects are there either way.\n\nBut there's an interesting bug here with commit graphs. What happens is\nthis:\n\n  1. During the initial part of the clone, we may load a commit object\n     using the commit-graph file from the --reference repo. The commit\n     struct is left with a blank \"maybe_tree\" field, because we know we\n     can load it from the commit graph later (and don't want to spend\n     the effort to make a \"struct tree\" unless somebody asks for it).\n\n  2. During the dissociate step, we call odb_close(), since we're\n     throwing away the link to the reference repo, and we don't want to\n     use our in-process structs that point to it. That step also throws\n     away our open reference to the commit-graph file, and the\n     in-process slab that holds the graph positions we've loaded.\n\n  3. The checkout process needs the tree, so it calls\n     repo_get_commit_tree(). That sees that maybe_commit is NULL, so we\n     check whether it might be loaded from the graph file. But when we\n     ask about the graph position, we don't have one! It was in the slab\n     we threw away. So we return NULL, and the caller thinks the commit\n     is corrupt.\n\nThis is a bug that we theorized existed in a thread a while ago:\n\n  https://lore.kernel.org/git/20240110113914.GE16674@coredump.intra.peff.net/\n\nbut we didn't have a way to trigger it. Now we do. Hooray, I guess? ;)\n\nThe fallback load suggested in that message fixes it (modulo the fact\nthat it forgot to return commit->maybe_tree at the end of the function).\nBelow is a slightly safer version of the same concept that likewise\nfixes the problem.\n\nIt's kind of ugly, but I think may be the least-bad solution. See that\nearlier thread for more discussion of alternatives.\n\nIn the meantime, doing your dissociate clone with:\n\n  git -c core.commitGraph=false clone ...\n\nshould work around the problem.\n\n---\ndiff --git a/commit.c b/commit.c\nindex 80d8d07875..50d736b339 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -434,16 +434,46 @@ static inline void set_commit_tree(struct commit *c, struct tree *t)\n \tc->maybe_tree = t;\n }\n \n+static void load_tree_from_commit_contents(struct repository *r, struct commit *commit)\n+{\n+\tenum object_type type;\n+\tunsigned long size;\n+\tchar *buf;\n+\tconst char *p;\n+\tstruct object_id tree_oid;\n+\n+\tbuf = odb_read_object(r->objects, &commit->object.oid, &type, &size);\n+\tif (!buf)\n+\t\treturn;\n+\n+\tif (type == OBJ_COMMIT &&\n+\t    skip_prefix(buf, \"tree \", &p) &&\n+\t    !parse_oid_hex(p, &tree_oid, &p) &&\n+\t    *p == '\\n')\n+\t\tcommit->maybe_tree = lookup_tree(r, &tree_oid);\n+\n+\tfree(buf);\n+}\n+\n struct tree *repo_get_commit_tree(struct repository *r,\n-\t\t\t\t  const struct commit *commit)\n+\t\t\t\t  struct commit *commit)\n {\n \tif (commit->maybe_tree || !commit->object.parsed)\n \t\treturn commit->maybe_tree;\n \n \tif (commit_graph_position(commit) != COMMIT_NOT_FROM_GRAPH)\n \t\treturn get_commit_tree_in_graph(r, commit);\n \n-\treturn NULL;\n+\t/*\n+\t * This is either a corrupt commit, or one which we partially loaded\n+\t * from a graph file but then subsequently threw away the graph data.\n+\t *\n+\t * Optimistically assume it's the latter and try to reload from\n+\t * scratch. This gives a performance penalty if it really is a corrupt\n+\t * commit, but presumably that happens rarely.\n+\t */\n+\tload_tree_from_commit_contents(r, commit);\n+\treturn commit->maybe_tree;\n }\n \n struct object_id *get_commit_tree_oid(const struct commit *commit)\ndiff --git a/commit.h b/commit.h\nindex 58150045af..5eb1264077 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -163,7 +163,7 @@ void repo_unuse_commit_buffer(struct repository *r,\n  */\n void free_commit_buffer(struct parsed_object_pool *pool, struct commit *);\n \n-struct tree *repo_get_commit_tree(struct repository *, const struct commit *);\n+struct tree *repo_get_commit_tree(struct repository *, struct commit *);\n struct object_id *get_commit_tree_oid(const struct commit *);\n \n /*\n"},{"id":"542659","messageId":"874ikns8xd.fsf@prevas.dk","threadId":"65585","inReplyTo":"20260504095110.GA599780@coredump.intra.peff.net","subject":"Re: git clone with --dissociate sometimes fails to check out target commit","fromName":"Rasmus Villemoes","fromEmail":"ravi@prevas.dk","sentAt":"2026-05-04T11:36:46Z","receivedAt":"2026-05-04T11:36:51Z","isPatch":false,"body":"On Mon, May 04 2026, Jeff King <peff@peff.net> wrote:\n\n> On Mon, May 04, 2026 at 10:20:32AM +0200, Rasmus Villemoes wrote:\n>\n>> Are we using --dissociate wrongly, or are we perhaps not maintaining\n>> those local mirror repos properly? They are essentially just created\n>> with 'git clone --mirror', with 'git remote update' run periodically.\n>> \n>> Naively, I'd expect the effects of --dissociate to only happen after\n>> everything else the clone command does has been done, but it seems that\n>> the ties to the reference repo are cut too soon.\n>\n> No, you're using it correctly. The dissociate step should copy all of\n> the shared objects into the new repo, so it shouldn't matter whether we\n> do it before or after checkout. The objects are there either way.\n>\n[snip]\n>\n> It's kind of ugly, but I think may be the least-bad solution. See that\n> earlier thread for more discussion of alternatives.\n>\n> In the meantime, doing your dissociate clone with:\n>\n>   git -c core.commitGraph=false clone ...\n>\n> should work around the problem.\n\nThanks for the extremely fast reply, analysis, patch and workaround!\n\nI can confirm that the commit graph disabling workaround works on both\nthe Debian and Arch machines.\n\nI can also confirm that the patch applied on top of v2.54.0 works,\nalthough the build does throw this warning:\n\ncommit.c: In function ‘get_commit_tree_oid’:                                                                                                                                                                          \ncommit.c:481:66: warning: passing argument 2 of ‘repo_get_commit_tree’ discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]                                                                   \n  481 |         struct tree *tree = repo_get_commit_tree(the_repository, commit);                                                                                                                                     \n      |                                                                  ^~~~~~                                                                                                                                       \ncommit.c:459:50: note: expected ‘struct commit *’ but argument is of type ‘const struct commit *’                                                                                                                     \n  459 |                                   struct commit *commit)                                                                                                                                                      \n      |                                   ~~~~~~~~~~~~~~~^~~~~~                                                          \n\nThanks again,\nRasmus\n"}]}