{"thread":{"id":"66446","subject":"[PATCH 00/13] odb/source-files: move alternates into the backend","startedAt":"2026-10-02T10:08:30Z","lastAt":"2026-10-06T08:42:32Z","messageCount":17,"participants":["Patrick Steinhardt","Karthik Nayak"],"isPatch":true,"patchVersion":1,"patchTotal":13},"messages":[{"id":"553934","messageId":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":null,"subject":"[PATCH 00/13] odb/source-files: move alternates into the backend","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:11Z","receivedAt":"2026-10-02T10:08:30Z","isPatch":true,"body":"Hi,\n\nOriginally, when designing pluggable object databases the goal was that\nthe object database can have multiple sources, and every source attached\nto it could use a different backend. This would have allowed for quite a\nlot of flexibility, as you could trivially mix and match different kinds\nof object storages in whatever way you like.\n\nBut while well-intentioned, this design led to a bunch of conceptual\nproblems:\n\n  - We're now trying to read objects in source order, whereas we\n    previously tried to read objects via packfiles before trying to read\n    them via loose objects. This led to a performance regression when\n    using alternates or when using a quarantine directory.\n\n  - Some data structures are supposed to only ever exist once, like for\n    example bitmaps and commit graphs. At the same time, those data\n    structures also span across the union of all objects, so they may\n    cross sources.\n\n  - It is unclear how we can extend GIT_OBJECT_DIRECTORY or\n    GIT_ALTERNATE_OBJECT_DIRECTORIES to become backend-agnostic in a\n    backwards-compatible way. In general, introducing an object storage\n    extension into the current status quo where alternates may have to\n    be extended to become generic was proving to be painful.\n\n  - Some mechanisms of alternates assume way too much about how exactly\n    their backends work. Alternate refs for example assume that the\n    alternate is backed by a filesystem path, and that this filesystem\n    path may also allow us to read references. This is not a given\n    though, as backends may not even have local data at all.\n\nIn short, there are a bunch of conceptual mismatches when we have\nalternates and pluggable object databases coexist. So while the original\nidea was nice, it does not result in a system that is easy to reason\nabout.\n\nThis patch series corrects course by moving alternates into the \"files\"\nbackend itself so that they become another implementation detail. It's\nunfortunately on the bigger side, and I'm sorry about that, but I\ncouldn't really find a way to split it up further in a sensible way.\n\nNote that the above problems aren't fixed by this series yet, but it is\nthe prerequisite to fix them in subsequent patch series.\n\nThe series is built on top of c46c1e3772 (Start Git 2.98 cycle,\n2026-09-30). Note that there's a couple of small merge conflicts with\n\"seen\". These can be resolved as follows:\n\ndiff --cc builtin/multi-pack-index.c\nindex c48212290c,6b2e58f427..0000000000\n--- a/builtin/multi-pack-index.c\n+++ b/builtin/multi-pack-index.c\n@@@ -225,8 -225,9 +225,9 @@@ static int cmd_multi_pack_index_write(i\n  \n  \t}\n  \n -\tret = write_midx_file(source->packed, opts.preferred_pack,\n +\tret = write_midx_file(packed_source, opts.preferred_pack,\n- \t\t\t      opts.refs_snapshot, opts.flags);\n+ \t\t\t      opts.refs_snapshot, opts.incremental_base,\n+ \t\t\t      opts.flags);\n  \n  \tfree(opts.refs_snapshot);\n  \treturn ret;\ndiff --cc builtin/pack-objects.c\nindex ca3a891dfb,fb603059a9..0000000000\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@@ -4566,10 -4624,10 +4616,10 @@@ static int add_loose_object(const struc\n   * add_object_entry will weed out duplicates, so we just add every\n   * loose object we find.\n   */\n- static void add_unreachable_loose_objects(struct rev_info *revs)\n+ static void add_unreachable_loose_objects(struct stdin_packs_context *ctx)\n  {\n -\tfor_each_loose_file_in_source(the_repository->objects->sources,\n +\tfor_each_loose_file_in_source(the_repository->objects->source,\n- \t\t\t\t      add_loose_object, NULL, NULL, revs);\n+ \t\t\t\t      add_loose_object, NULL, NULL, ctx);\n  }\n  \n  static int has_sha1_pack_kept_or_nonlocal(const struct object_id *oid)\ndiff --cc builtin/repack.c\nindex 5d06872d77,87f03b66d9..0000000000\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@@ -775,7 -809,7 +809,7 @@@ int cmd_repack(int argc\n  \n  \t\tif (git_env_bool(GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL, 0))\n  \t\t\tflags |= MIDX_WRITE_INCREMENTAL;\n- \t\twrite_midx_file(files->dirs->packed, NULL, NULL, flags);\n -\t\twrite_midx_file(files->packed, NULL, NULL, NULL, flags);\n++\t\twrite_midx_file(files->dirs->packed, NULL, NULL, NULL, flags);\n  \t}\n  \n  cleanup:\ndiff --git a/repack-midx.c b/repack-midx.c\nindex d805802f04..7281003473 100644\n--- a/repack-midx.c\n+++ b/repack-midx.c\n@@ -575,7 +575,7 @@ static int midx_compaction_step_include_packs(struct midx_compaction_step *step,\n \n \t\tstrbuf_reset(&path);\n \t\tstrbuf_addf(&path, \"%s/%s\", opts->packdir, item->string);\n-\t\tp = packfile_store_load_pack(files->packed, path.buf, 1);\n+\t\tp = packfile_store_load_pack(files->dirs->packed, path.buf, 1);\n \t\tif (!p || open_pack_index(p)) {\n \t\t\tret = error(_(\"cannot open index for %s\"), path.buf);\n \t\t\tgoto out;\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (13):\n      commit-graph: require resolved packfile paths for `stdin_packs`\n      commit-graph: stop depending on `struct odb_source`\n      odb/source-files: introduce `struct odb_files_dir`\n      odb: refactor `odb_for_each_alternate()` to yield dirs\n      odb: refactor `odb_find_source()` to yield dirs\n      odb/source-files: add the ability to have multiple object dirs\n      tmp-objdir: absorb logic to set and restore primary sources\n      tmp-objdir: manage quarantine as an object directory\n      tmp-objdir: replace primary source at creation time\n      odb/source: make `will_destroy` an implementation detail\n      odb/source-files: extract reading alternates\n      odb/source-files: move alternates into the backend\n      odb/source: drop `read_alternates` callback\n\n builtin/commit-graph.c      |  41 ++--\n builtin/commit.c            |   2 +-\n builtin/count-objects.c     |   6 +-\n builtin/fast-import.c       |  20 +-\n builtin/fetch.c             |   4 +-\n builtin/fsck.c              |   6 +-\n builtin/gc.c                |  15 +-\n builtin/index-pack.c        |   4 +-\n builtin/merge.c             |   2 +-\n builtin/multi-pack-index.c  |  48 ++---\n builtin/pack-objects.c      |  66 +++---\n builtin/prune.c             |   2 +-\n builtin/repack.c            |   4 +-\n builtin/submodule--helper.c |   7 +-\n bundle.c                    |   2 +-\n commit-graph.c              | 150 +++++++-------\n commit-graph.h              |  31 ++-\n diagnose.c                  |   8 +-\n fetch-pack.c                |   2 +-\n http-walker.c               |   4 +-\n http.c                      |  12 +-\n log-tree.c                  |   3 +-\n loose.c                     |  18 +-\n midx.c                      |  43 ++--\n object-file.c               |   8 +-\n odb.c                       | 439 +++++-----------------------------------\n odb.h                       |  62 +-----\n odb/source-files.c          | 482 ++++++++++++++++++++++++++++++++++++--------\n odb/source-files.h          |  65 +++++-\n odb/source-inmemory.c       |   7 -\n odb/source-loose.c          |   9 +-\n odb/source-loose.h          |   3 +\n odb/source-packed.c         |   7 -\n odb/source.c                |   5 +-\n odb/source.h                |  57 +-----\n odb/streaming.c             |   8 +-\n odb/transaction.c           |   2 +-\n pack-bitmap.c               |   8 +-\n packfile.c                  |  28 ++-\n packfile.h                  |  21 +-\n path.c                      |   2 +-\n prune-packed.c              |   2 +-\n repack-geometry.c           |   2 +-\n repack-midx.c               |   6 +-\n repack.c                    |   6 +-\n repository.c                |   4 +-\n setup.c                     |   2 +-\n t/helper/test-read-graph.c  |   5 +-\n t/helper/test-read-midx.c   |   8 +-\n t/t4216-log-bloom.sh        |   4 +-\n tmp-objdir.c                |  67 ++++--\n tmp-objdir.h                |  18 +-\n 52 files changed, 883 insertions(+), 954 deletions(-)\n\n\n---\nbase-commit: 2f92b2890ddaf3d7ea29470c02418271c1a4cd79\nchange-id: 20260924-pks-odb-move-alternates-4a0babe4b0f3\n\n"},{"id":"553935","messageId":"20261002-pks-odb-move-alternates-v1-1-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 01/13] commit-graph: require resolved packfile paths for `stdin_packs`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:12Z","receivedAt":"2026-10-02T10:08:32Z","isPatch":true,"body":"Users can ask git-commit-graph(1) to write a commit graph specifically\nfor a set of packfiles via the \"--stdin-packs\" option. Those users are\nexpected to pass in relative paths, and those eventually get resolved in\n`fill_oids_from_packs()`. This ties the logic in \"commit-graph.c\" to the\nspecific object database source.\n\nRefactor the logic to instead require the caller to pass in resolved\npackfiles to untangle that dependency.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/commit-graph.c | 17 ++++++++++++++---\n commit-graph.c         | 15 +++++----------\n 2 files changed, 19 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/commit-graph.c b/builtin/commit-graph.c\nindex d62005edc0..b5784ad3c7 100644\n--- a/builtin/commit-graph.c\n+++ b/builtin/commit-graph.c\n@@ -8,6 +8,7 @@\n #include \"parse-options.h\"\n #include \"commit-graph.h\"\n #include \"odb.h\"\n+#include \"odb/source.h\"\n #include \"progress.h\"\n #include \"replace-object.h\"\n #include \"strbuf.h\"\n@@ -302,9 +303,19 @@ static int graph_write(int argc, const char **argv, const char *prefix,\n \t}\n \n \tif (opts.stdin_packs) {\n-\t\twhile (strbuf_getline(&buf, stdin) != EOF)\n-\t\t\tstring_list_append_nodup(&pack_indexes,\n-\t\t\t\t\t\t strbuf_detach(&buf, NULL));\n+\t\tstruct strbuf packname = STRBUF_INIT;\n+\t\tsize_t dirlen;\n+\n+\t\tstrbuf_addf(&packname, \"%s/pack/\", source->path);\n+\t\tdirlen = packname.len;\n+\n+\t\twhile (strbuf_getline(&buf, stdin) != EOF) {\n+\t\t\tstrbuf_setlen(&packname, dirlen);\n+\t\t\tstrbuf_addbuf(&packname, &buf);\n+\t\t\tstring_list_append(&pack_indexes, packname.buf);\n+\t\t}\n+\n+\t\tstrbuf_release(&packname);\n \t} else if (opts.stdin_commits) {\n \t\toidset_init(&commits, 0);\n \t\tif (opts.progress)\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 983c11ce85..73814c1622 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -1936,12 +1936,8 @@ static int fill_oids_from_packs(struct write_commit_graph_context *ctx,\n {\n \tuint32_t i;\n \tstruct strbuf progress_title = STRBUF_INIT;\n-\tstruct strbuf packname = STRBUF_INIT;\n-\tint dirlen;\n \tint ret = 0;\n \n-\tstrbuf_addf(&packname, \"%s/pack/\", ctx->odb_source->path);\n-\tdirlen = packname.len;\n \tif (ctx->report_progress) {\n \t\tstrbuf_addf(&progress_title,\n \t\t\t    Q_(\"Finding commits for commit graph in %\"PRIuMAX\" pack\",\n@@ -1954,15 +1950,15 @@ static int fill_oids_from_packs(struct write_commit_graph_context *ctx,\n \t}\n \tfor (i = 0; i < pack_indexes->nr; i++) {\n \t\tstruct packed_git *p;\n-\t\tstrbuf_setlen(&packname, dirlen);\n-\t\tstrbuf_addstr(&packname, pack_indexes->items[i].string);\n-\t\tp = add_packed_git(ctx->r, packname.buf, packname.len, 1);\n+\n+\t\tp = add_packed_git(ctx->r, pack_indexes->items[i].string,\n+\t\t\t\t   strlen(pack_indexes->items[i].string), 1);\n \t\tif (!p) {\n-\t\t\tret = error(_(\"error adding pack %s\"), packname.buf);\n+\t\t\tret = error(_(\"error adding pack %s\"), pack_indexes->items[i].string);\n \t\t\tgoto cleanup;\n \t\t}\n \t\tif (open_pack_index(p)) {\n-\t\t\tret = error(_(\"error opening index for %s\"), packname.buf);\n+\t\t\tret = error(_(\"error opening index for %s\"), pack_indexes->items[i].string);\n \t\t\tclose_pack(p);\n \t\t\tfree(p);\n \t\t\tgoto cleanup;\n@@ -1976,7 +1972,6 @@ static int fill_oids_from_packs(struct write_commit_graph_context *ctx,\n cleanup:\n \tstop_progress(&ctx->progress);\n \tstrbuf_release(&progress_title);\n-\tstrbuf_release(&packname);\n \n \treturn ret;\n }\n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553936","messageId":"20261002-pks-odb-move-alternates-v1-2-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 02/13] commit-graph: stop depending on `struct odb_source`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:13Z","receivedAt":"2026-10-02T10:08:35Z","isPatch":true,"body":"To read or write a commit graph we require access to the repository that\nthe graph should be read from or written for as well as the object\ndirectory to derive its location. Instead of passing in these two bits\nof information explicitly though, we pass in a `struct odb_source`,\nwhich carries with it both of these pieces of information.\n\nBut ultimately, this is somewhat flawed as we now assume that the source\neven has an on-disk representation, and furthermore we assume that the\ncommit graphs would live in \".git/objects/info\". This is true for the\n\"files\" backend, but it's not necessarily true for any other backend\nthat we may eventually want to introduce. So eventually, we'll want to\nevolve the commit-graph subsystem to become agnostic of the backend's\nlayout and let the backend itself decide where to read a commit graph\nfrom or where to write it to.\n\nWe're not there yet to do that switch, but the current design is already\ncausing issues for the intent of this patch series where we want to move\nalternates into the \"files\" backend.\n\nConvert the subsystem to take a repository plus an object directory path\ninstead. This unblocks moving around alternates, and it's also a step\nin the right direction for moving commit graphs into the source in a\nlater series.\n\nNote that we previously compared the `struct odb_source` pointers of two\ncommit graphs to figure out whether they were located in the same object\ndirectory, whereas we now have to compare their paths. Callers may pass\nin those paths in different forms though, for example relative to the\ncurrent working directory or as absolute paths. Convert the object\ndirectory into an absolute path both when loading and when writing\ncommit graphs so that the comparisons become more robust.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/commit-graph.c     |  10 ++--\n builtin/commit.c           |   2 +-\n builtin/fetch.c            |   4 +-\n builtin/gc.c               |   3 +-\n builtin/merge.c            |   2 +-\n commit-graph.c             | 115 +++++++++++++++++++++++++--------------------\n commit-graph.h             |  31 ++++++++----\n t/helper/test-read-graph.c |   3 +-\n t/t4216-log-bloom.sh       |   4 +-\n 9 files changed, 102 insertions(+), 72 deletions(-)\n\ndiff --git a/builtin/commit-graph.c b/builtin/commit-graph.c\nindex b5784ad3c7..a986f08a94 100644\n--- a/builtin/commit-graph.c\n+++ b/builtin/commit-graph.c\n@@ -104,8 +104,8 @@ static int graph_verify(int argc, const char **argv, const char *prefix,\n \t\tflags |= COMMIT_GRAPH_WRITE_PROGRESS;\n \n \tsource = odb_find_source_or_die(the_repository->objects, opts.obj_dir);\n-\tgraph_name = get_commit_graph_filename(source);\n-\tchain_name = get_commit_graph_chain_filename(source);\n+\tgraph_name = get_commit_graph_filename(source->path);\n+\tchain_name = get_commit_graph_chain_filename(source->path);\n \tif (open_commit_graph(graph_name, &fd, &st))\n \t\topened = OPENED_GRAPH;\n \telse if (errno != ENOENT)\n@@ -123,7 +123,7 @@ static int graph_verify(int argc, const char **argv, const char *prefix,\n \tif (opened == OPENED_NONE)\n \t\treturn 0;\n \telse if (opened == OPENED_GRAPH)\n-\t\tgraph = load_commit_graph_one_fd_st(source, fd, &st);\n+\t\tgraph = load_commit_graph_one_fd_st(the_repository, source->path, fd, &st);\n \telse\n \t\tgraph = load_commit_graph_chain_fd_st(the_repository->objects, fd, &st,\n \t\t\t\t\t\t      &incomplete_chain);\n@@ -297,7 +297,7 @@ static int graph_write(int argc, const char **argv, const char *prefix,\n \tsource = odb_find_source_or_die(the_repository->objects, opts.obj_dir);\n \n \tif (opts.reachable) {\n-\t\tif (write_commit_graph_reachable(source, flags, &write_opts))\n+\t\tif (write_commit_graph_reachable(the_repository, source->path, flags, &write_opts))\n \t\t\tresult = 1;\n \t\tgoto cleanup;\n \t}\n@@ -334,7 +334,7 @@ static int graph_write(int argc, const char **argv, const char *prefix,\n \t\tstop_progress(&progress);\n \t}\n \n-\tif (write_commit_graph(source,\n+\tif (write_commit_graph(the_repository, source->path,\n \t\t\t       opts.stdin_packs ? &pack_indexes : NULL,\n \t\t\t       opts.stdin_commits ? &commits : NULL,\n \t\t\t       flags,\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 840b6b4083..0e750f6157 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1959,7 +1959,7 @@ int cmd_commit(int argc,\n \t\t      \"new index file. Check that disk is not full and quota is\\n\"\n \t\t      \"not exceeded, and then \\\"git restore --staged :/\\\" to recover.\"));\n \n-\tgit_test_write_commit_graph_or_die(the_repository->objects->sources);\n+\tgit_test_write_commit_graph_or_die(the_repository);\n \n \trepo_rerere(the_repository, 0);\n \trun_auto_maintenance(the_repository, quiet);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex b2decc6cfd..a4b21d2651 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -15,6 +15,7 @@\n #include \"refspec.h\"\n #include \"object-name.h\"\n #include \"odb.h\"\n+#include \"odb/source.h\"\n #include \"oidset.h\"\n #include \"oid-array.h\"\n #include \"commit.h\"\n@@ -2900,7 +2901,8 @@ int cmd_fetch(int argc,\n \t\t\tcommit_graph_flags |= COMMIT_GRAPH_WRITE_PROGRESS;\n \n \t\ttrace2_region_enter(\"fetch\", \"write-commit-graph\", the_repository);\n-\t\twrite_commit_graph_reachable(the_repository->objects->sources,\n+\t\twrite_commit_graph_reachable(the_repository,\n+\t\t\t\t\t     the_repository->objects->sources->path,\n \t\t\t\t\t     commit_graph_flags,\n \t\t\t\t\t     NULL);\n \t\ttrace2_region_leave(\"fetch\", \"write-commit-graph\", the_repository);\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 57a3520263..7acd4f3215 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -733,7 +733,8 @@ int cmd_gc(int argc,\n \t}\n \n \tif (the_repository->settings.gc_write_commit_graph == 1)\n-\t\twrite_commit_graph_reachable(the_repository->objects->sources,\n+\t\twrite_commit_graph_reachable(the_repository,\n+\t\t\t\t\t     the_repository->objects->sources->path,\n \t\t\t\t\t     !opts.quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,\n \t\t\t\t\t     NULL);\n \ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 5b4eb23a83..d7bf209114 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -1863,7 +1863,7 @@ int cmd_merge(int argc,\n \tif (squash) {\n \t\tfinish(head_commit, remoteheads, NULL, NULL);\n \n-\t\tgit_test_write_commit_graph_or_die(the_repository->objects->sources);\n+\t\tgit_test_write_commit_graph_or_die(the_repository);\n \t} else\n \t\twrite_merge_state(remoteheads);\n \ndiff --git a/commit-graph.c b/commit-graph.c\nindex 73814c1622..80ebc6a542 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -1,6 +1,7 @@\n #define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n+#include \"abspath.h\"\n #include \"config.h\"\n #include \"csum-file.h\"\n #include \"environment.h\"\n@@ -28,7 +29,7 @@\n #include \"tree.h\"\n #include \"chunk-format.h\"\n \n-void git_test_write_commit_graph_or_die(struct odb_source *source)\n+void git_test_write_commit_graph_or_die(struct repository *repo)\n {\n \tint flags = 0;\n \tif (!git_env_bool(GIT_TEST_COMMIT_GRAPH, 0))\n@@ -37,7 +38,7 @@ void git_test_write_commit_graph_or_die(struct odb_source *source)\n \tif (git_env_bool(GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS, 0))\n \t\tflags = COMMIT_GRAPH_WRITE_BLOOM_FILTERS;\n \n-\tif (write_commit_graph_reachable(source, flags, NULL))\n+\tif (write_commit_graph_reachable(repo, repo->objects->sources->path, flags, NULL))\n \t\tdie(\"failed to write commit-graph under GIT_TEST_COMMIT_GRAPH\");\n }\n \n@@ -196,21 +197,21 @@ static int commit_gen_cmp(const void *va, const void *vb)\n \treturn 0;\n }\n \n-char *get_commit_graph_filename(struct odb_source *source)\n+char *get_commit_graph_filename(const char *dir)\n {\n-\treturn xstrfmt(\"%s/info/commit-graph\", source->path);\n+\treturn xstrfmt(\"%s/info/commit-graph\", dir);\n }\n \n-static char *get_split_graph_filename(struct odb_source *source,\n+static char *get_split_graph_filename(const char *dir,\n \t\t\t\t      const char *oid_hex)\n {\n-\treturn xstrfmt(\"%s/info/commit-graphs/graph-%s.graph\", source->path,\n+\treturn xstrfmt(\"%s/info/commit-graphs/graph-%s.graph\", dir,\n \t\t       oid_hex);\n }\n \n-char *get_commit_graph_chain_filename(struct odb_source *source)\n+char *get_commit_graph_chain_filename(const char *dir)\n {\n-\treturn xstrfmt(\"%s/info/commit-graphs/commit-graph-chain\", source->path);\n+\treturn xstrfmt(\"%s/info/commit-graphs/commit-graph-chain\", dir);\n }\n \n static struct commit_graph *alloc_commit_graph(void)\n@@ -253,7 +254,8 @@ int open_commit_graph(const char *graph_file, int *fd, struct stat *st)\n \treturn 1;\n }\n \n-struct commit_graph *load_commit_graph_one_fd_st(struct odb_source *source,\n+struct commit_graph *load_commit_graph_one_fd_st(struct repository *repo,\n+\t\t\t\t\t\t const char *dir,\n \t\t\t\t\t\t int fd, struct stat *st)\n {\n \tvoid *graph_map;\n@@ -262,7 +264,7 @@ struct commit_graph *load_commit_graph_one_fd_st(struct odb_source *source,\n \n \tgraph_size = xsize_t(st->st_size);\n \n-\tif (graph_size < graph_min_size(source->odb->repo->hash_algo)) {\n+\tif (graph_size < graph_min_size(repo->hash_algo)) {\n \t\tclose(fd);\n \t\terror(_(\"commit-graph file is too small\"));\n \t\treturn NULL;\n@@ -270,9 +272,9 @@ struct commit_graph *load_commit_graph_one_fd_st(struct odb_source *source,\n \tgraph_map = xmmap(NULL, graph_size, PROT_READ, MAP_PRIVATE, fd, 0);\n \tclose(fd);\n \n-\tret = parse_commit_graph(source->odb->repo, graph_map, graph_size);\n+\tret = parse_commit_graph(repo, graph_map, graph_size);\n \tif (ret)\n-\t\tret->odb_source = source;\n+\t\tret->dir = absolute_pathdup(dir);\n \telse\n \t\tmunmap(graph_map, graph_size);\n \n@@ -410,6 +412,7 @@ struct commit_graph *parse_commit_graph(struct repository *r,\n \n \tgraph = alloc_commit_graph();\n \n+\tgraph->repo = r;\n \tgraph->hash_algo = r->hash_algo;\n \tgraph->num_chunks = *(unsigned char*)(data + 6);\n \tgraph->data = graph_map;\n@@ -490,7 +493,8 @@ struct commit_graph *parse_commit_graph(struct repository *r,\n \treturn NULL;\n }\n \n-static struct commit_graph *load_commit_graph_one(struct odb_source *source,\n+static struct commit_graph *load_commit_graph_one(struct repository *repo,\n+\t\t\t\t\t\t  const char *dir,\n \t\t\t\t\t\t  const char *graph_file)\n {\n \tstruct stat st;\n@@ -501,17 +505,18 @@ static struct commit_graph *load_commit_graph_one(struct odb_source *source,\n \tif (!open_ok)\n \t\treturn NULL;\n \n-\tg = load_commit_graph_one_fd_st(source, fd, &st);\n+\tg = load_commit_graph_one_fd_st(repo, dir, fd, &st);\n \tif (g)\n-\t\tg->filename = xstrdup(graph_file);\n+\t\tg->filename = absolute_pathdup(graph_file);\n \n \treturn g;\n }\n \n-static struct commit_graph *load_commit_graph_v1(struct odb_source *source)\n+static struct commit_graph *load_commit_graph_v1(struct repository *repo,\n+\t\t\t\t\t\t const char *dir)\n {\n-\tchar *graph_name = get_commit_graph_filename(source);\n-\tstruct commit_graph *g = load_commit_graph_one(source, graph_name);\n+\tchar *graph_name = get_commit_graph_filename(dir);\n+\tstruct commit_graph *g = load_commit_graph_one(repo, dir, graph_name);\n \tfree(graph_name);\n \n \treturn g;\n@@ -666,8 +671,8 @@ struct commit_graph *load_commit_graph_chain_fd_st(struct object_database *odb,\n \n \t\tvalid = 0;\n \t\tfor (source = odb->sources; source; source = source->next) {\n-\t\t\tchar *graph_name = get_split_graph_filename(source, line.buf);\n-\t\t\tstruct commit_graph *g = load_commit_graph_one(source, graph_name);\n+\t\t\tchar *graph_name = get_split_graph_filename(source->path, line.buf);\n+\t\t\tstruct commit_graph *g = load_commit_graph_one(odb->repo, source->path, graph_name);\n \n \t\t\tfree(graph_name);\n \n@@ -700,29 +705,31 @@ struct commit_graph *load_commit_graph_chain_fd_st(struct object_database *odb,\n \treturn graph_chain;\n }\n \n-static struct commit_graph *load_commit_graph_chain(struct odb_source *source)\n+static struct commit_graph *load_commit_graph_chain(struct repository *repo,\n+\t\t\t\t\t\t    const char *dir)\n {\n-\tchar *chain_file = get_commit_graph_chain_filename(source);\n+\tchar *chain_file = get_commit_graph_chain_filename(dir);\n \tstruct stat st;\n \tint fd;\n \tstruct commit_graph *g = NULL;\n \n-\tif (open_commit_graph_chain(chain_file, &fd, &st, source->odb->repo->hash_algo)) {\n+\tif (open_commit_graph_chain(chain_file, &fd, &st, repo->hash_algo)) {\n \t\tint incomplete;\n \t\t/* ownership of fd is taken over by load function */\n-\t\tg = load_commit_graph_chain_fd_st(source->odb, fd, &st, &incomplete);\n+\t\tg = load_commit_graph_chain_fd_st(repo->objects, fd, &st, &incomplete);\n \t}\n \n \tfree(chain_file);\n \treturn g;\n }\n \n-struct commit_graph *read_commit_graph_one(struct odb_source *source)\n+struct commit_graph *read_commit_graph_one(struct repository *repo,\n+\t\t\t\t\t   const char *dir)\n {\n-\tstruct commit_graph *g = load_commit_graph_v1(source);\n+\tstruct commit_graph *g = load_commit_graph_v1(repo, dir);\n \n \tif (!g)\n-\t\tg = load_commit_graph_chain(source);\n+\t\tg = load_commit_graph_chain(repo, dir);\n \n \treturn g;\n }\n@@ -767,7 +774,7 @@ static struct commit_graph *prepare_commit_graph(struct repository *r)\n \t\treturn NULL;\n \n \tfor (source = r->objects->sources; source; source = source->next) {\n-\t\tr->objects->commit_graph = read_commit_graph_one(source);\n+\t\tr->objects->commit_graph = read_commit_graph_one(r, source->path);\n \t\tif (r->objects->commit_graph)\n \t\t\tbreak;\n \t}\n@@ -866,7 +873,7 @@ static struct commit_list **insert_parent_or_die(struct commit_graph *g,\n \t\tdie(\"invalid parent position %\"PRIu32, pos);\n \n \tload_oid_from_graph(g, pos, &oid);\n-\tc = lookup_commit(g->odb_source->odb->repo, &oid);\n+\tc = lookup_commit(g->repo, &oid);\n \tif (!c)\n \t\tdie(_(\"could not find commit %s\"), oid_to_hex(&oid));\n \tcommit_graph_data_at(c)->graph_pos = pos;\n@@ -1103,7 +1110,7 @@ static struct tree *load_tree_for_commit(struct commit_graph *g,\n \t\t\t\tgraph_pos - g->num_commits_in_base);\n \n \toidread(&oid, commit_data, g->hash_algo);\n-\tset_commit_tree(c, lookup_tree(g->odb_source->odb->repo, &oid));\n+\tset_commit_tree(c, lookup_tree(g->repo, &oid));\n \n \treturn c->maybe_tree;\n }\n@@ -1126,7 +1133,7 @@ struct tree *get_commit_tree_in_graph(struct repository *r, const struct commit\n \n struct write_commit_graph_context {\n \tstruct repository *r;\n-\tstruct odb_source *odb_source;\n+\tchar *dir;\n \tchar *graph_name;\n \tstruct oid_array oids;\n \tstruct commit_stack commits;\n@@ -1902,7 +1909,8 @@ static int add_ref_to_set(const struct reference *ref, void *cb_data)\n \treturn 0;\n }\n \n-int write_commit_graph_reachable(struct odb_source *source,\n+int write_commit_graph_reachable(struct repository *repo,\n+\t\t\t\t const char *dir,\n \t\t\t\t enum commit_graph_write_flags flags,\n \t\t\t\t const struct commit_graph_opts *opts)\n {\n@@ -1911,20 +1919,20 @@ int write_commit_graph_reachable(struct odb_source *source,\n \tint result;\n \n \tmemset(&data, 0, sizeof(data));\n-\tdata.repo = source->odb->repo;\n+\tdata.repo = repo;\n \tdata.commits = &commits;\n \n \tif (flags & COMMIT_GRAPH_WRITE_PROGRESS)\n \t\tdata.progress = start_delayed_progress(\n-\t\t\tsource->odb->repo,\n+\t\t\trepo,\n \t\t\t_(\"Collecting referenced commits\"), 0);\n \n-\trefs_for_each_ref(get_main_ref_store(source->odb->repo), add_ref_to_set,\n+\trefs_for_each_ref(get_main_ref_store(repo), add_ref_to_set,\n \t\t\t  &data);\n \n \tstop_progress(&data.progress);\n \n-\tresult = write_commit_graph(source, NULL, &commits,\n+\tresult = write_commit_graph(repo, dir, NULL, &commits,\n \t\t\t\t    flags, opts);\n \n \toidset_clear(&commits);\n@@ -2103,10 +2111,10 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \n \t\tstrbuf_addf(&tmp_file,\n \t\t\t    \"%s/info/commit-graphs/tmp_graph_XXXXXX\",\n-\t\t\t    ctx->odb_source->path);\n+\t\t\t    ctx->dir);\n \t\tctx->graph_name = strbuf_detach(&tmp_file, NULL);\n \t} else {\n-\t\tctx->graph_name = get_commit_graph_filename(ctx->odb_source);\n+\t\tctx->graph_name = get_commit_graph_filename(ctx->dir);\n \t}\n \n \tif (safe_create_leading_directories(ctx->r, ctx->graph_name)) {\n@@ -2116,7 +2124,7 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t}\n \n \tif (ctx->split) {\n-\t\tchar *lock_name = get_commit_graph_chain_filename(ctx->odb_source);\n+\t\tchar *lock_name = get_commit_graph_chain_filename(ctx->dir);\n \n \t\trepo_hold_lock_file_for_update_mode(ctx->r, &lk, lock_name,\n \t\t\t\t\t\t    LOCK_DIE_ON_ERROR, 0444);\n@@ -2205,7 +2213,7 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \n \tif (ctx->split && ctx->base_graph_name && ctx->num_commit_graphs_after > 1) {\n \t\tchar *new_base_hash = xstrdup(oid_to_hex(&ctx->new_base_graph->oid));\n-\t\tchar *new_base_name = get_split_graph_filename(ctx->new_base_graph->odb_source, new_base_hash);\n+\t\tchar *new_base_name = get_split_graph_filename(ctx->new_base_graph->dir, new_base_hash);\n \n \t\tfree(ctx->commit_graph_filenames_after[ctx->num_commit_graphs_after - 2]);\n \t\tfree(ctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 2]);\n@@ -2245,7 +2253,7 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\t\t\t}\n \t\t\t}\n \t\t} else {\n-\t\t\tchar *graph_name = get_commit_graph_filename(ctx->odb_source);\n+\t\t\tchar *graph_name = get_commit_graph_filename(ctx->dir);\n \t\t\tunlink(graph_name);\n \t\t\tfree(graph_name);\n \t\t}\n@@ -2253,7 +2261,7 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\tfree(ctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 1]);\n \t\tctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 1] =\n \t\t\txstrdup(hash_to_hex_algop(file_hash, ctx->r->hash_algo));\n-\t\tfinal_graph_name = get_split_graph_filename(ctx->odb_source,\n+\t\tfinal_graph_name = get_split_graph_filename(ctx->dir,\n \t\t\t\t\tctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 1]);\n \t\tfree(ctx->commit_graph_filenames_after[ctx->num_commit_graphs_after - 1]);\n \t\tctx->commit_graph_filenames_after[ctx->num_commit_graphs_after - 1] = final_graph_name;\n@@ -2305,7 +2313,7 @@ static void split_graph_merge_strategy(struct write_commit_graph_context *ctx,\n \t    flags != COMMIT_GRAPH_SPLIT_REPLACE) {\n \t\twhile (g && (g->num_commits <= st_mult(size_mult, num_commits) ||\n \t\t\t    (max_commits && num_commits > max_commits))) {\n-\t\t\tif (g->odb_source != ctx->odb_source)\n+\t\t\tif (strcmp(g->dir, ctx->dir))\n \t\t\t\tbreak;\n \n \t\t\tif (unsigned_add_overflows(num_commits, g->num_commits))\n@@ -2327,10 +2335,10 @@ static void split_graph_merge_strategy(struct write_commit_graph_context *ctx,\n \t\t    \"should be 1 with --split=replace\");\n \n \tif (ctx->num_commit_graphs_after == 2) {\n-\t\tchar *old_graph_name = get_commit_graph_filename(g->odb_source);\n+\t\tchar *old_graph_name = get_commit_graph_filename(g->dir);\n \n \t\tif (!strcmp(g->filename, old_graph_name) &&\n-\t\t    g->odb_source != ctx->odb_source) {\n+\t\t    strcmp(g->dir, ctx->dir)) {\n \t\t\tctx->num_commit_graphs_after = 1;\n \t\t\tctx->new_base_graph = NULL;\n \t\t}\n@@ -2500,13 +2508,13 @@ static void expire_commit_graphs(struct write_commit_graph_context *ctx)\n \tif (ctx->opts && ctx->opts->expire_time)\n \t\texpire_time = ctx->opts->expire_time;\n \tif (!ctx->split) {\n-\t\tchar *chain_file_name = get_commit_graph_chain_filename(ctx->odb_source);\n+\t\tchar *chain_file_name = get_commit_graph_chain_filename(ctx->dir);\n \t\tunlink(chain_file_name);\n \t\tfree(chain_file_name);\n \t\tctx->num_commit_graphs_after = 0;\n \t}\n \n-\tstrbuf_addstr(&path, ctx->odb_source->path);\n+\tstrbuf_addstr(&path, ctx->dir);\n \tstrbuf_addstr(&path, \"/info/commit-graphs\");\n \tdir = opendir(path.buf);\n \n@@ -2548,16 +2556,15 @@ static void expire_commit_graphs(struct write_commit_graph_context *ctx)\n \tstrbuf_release(&path);\n }\n \n-int write_commit_graph(struct odb_source *source,\n+int write_commit_graph(struct repository *r,\n+\t\t       const char *dir,\n \t\t       const struct string_list *const pack_indexes,\n \t\t       struct oidset *commits,\n \t\t       enum commit_graph_write_flags flags,\n \t\t       const struct commit_graph_opts *opts)\n {\n-\tstruct repository *r = source->odb->repo;\n \tstruct write_commit_graph_context ctx = {\n \t\t.r = r,\n-\t\t.odb_source = source,\n \t\t.append = flags & COMMIT_GRAPH_WRITE_APPEND ? 1 : 0,\n \t\t.report_progress = flags & COMMIT_GRAPH_WRITE_PROGRESS ? 1 : 0,\n \t\t.split = flags & COMMIT_GRAPH_WRITE_SPLIT ? 1 : 0,\n@@ -2588,6 +2595,8 @@ int write_commit_graph(struct odb_source *source,\n \t\treturn 0;\n \t}\n \n+\tctx.dir = absolute_pathdup(dir);\n+\n \tbloom_settings.hash_version = r->settings.commit_graph_changed_paths_version;\n \tbloom_settings.bits_per_entry = git_env_ulong(\"GIT_TEST_BLOOM_SETTINGS_BITS_PER_ENTRY\",\n \t\t\t\t\t\t      bloom_settings.bits_per_entry);\n@@ -2710,6 +2719,7 @@ int write_commit_graph(struct odb_source *source,\n cleanup:\n \tfree(ctx.graph_name);\n \tfree(ctx.base_graph_name);\n+\tfree(ctx.dir);\n \tcommit_stack_clear(&ctx.commits);\n \toid_array_clear(&ctx.oids);\n \tclear_topo_level_slab(&topo_levels);\n@@ -2762,7 +2772,7 @@ static int verify_one_commit_graph(struct commit_graph *g,\n \t\t\t\t   struct progress *progress,\n \t\t\t\t   uint64_t *seen)\n {\n-\tstruct repository *r = g->odb_source->odb->repo;\n+\tstruct repository *r = g->repo;\n \tuint32_t i, cur_fanout_pos = 0;\n \tstruct object_id prev_oid, cur_oid;\n \tstruct commit *seen_gen_zero = NULL;\n@@ -2926,7 +2936,7 @@ int verify_commit_graph(struct commit_graph *g, int flags)\n \t\tif (!(flags & COMMIT_GRAPH_VERIFY_SHALLOW))\n \t\t\ttotal += g->num_commits_in_base;\n \n-\t\tprogress = start_progress(g->odb_source->odb->repo,\n+\t\tprogress = start_progress(g->repo,\n \t\t\t\t\t  _(\"Verifying commits in commit graph\"),\n \t\t\t\t\t  total);\n \t}\n@@ -2949,6 +2959,7 @@ void free_commit_graph(struct commit_graph *g)\n \n \t\tif (g->data)\n \t\t\tmunmap((void *)g->data, g->data_len);\n+\t\tfree(g->dir);\n \t\tfree(g->filename);\n \t\tfree(g->bloom_filter_settings);\n \t\tfree(g);\ndiff --git a/commit-graph.h b/commit-graph.h\nindex 13ca4ff010..bccf9c5c84 100644\n--- a/commit-graph.h\n+++ b/commit-graph.h\n@@ -21,7 +21,7 @@\n  * call this method outside of a builtin, and only if you know what\n  * you are doing!\n  */\n-void git_test_write_commit_graph_or_die(struct odb_source *source);\n+void git_test_write_commit_graph_or_die(struct repository *repo);\n \n struct commit;\n struct bloom_filter_settings;\n@@ -29,8 +29,8 @@ struct repository;\n struct object_database;\n struct string_list;\n \n-char *get_commit_graph_filename(struct odb_source *source);\n-char *get_commit_graph_chain_filename(struct odb_source *source);\n+char *get_commit_graph_filename(const char *dir);\n+char *get_commit_graph_chain_filename(const char *dir);\n int open_commit_graph(const char *graph_file, int *fd, struct stat *st);\n int open_commit_graph_chain(const char *chain_file, int *fd, struct stat *st,\n \t\t\t    const struct git_hash_algo *hash_algo);\n@@ -85,12 +85,13 @@ struct commit_graph {\n \tconst unsigned char *data;\n \tsize_t data_len;\n \n+\tstruct repository *repo;\n \tconst struct git_hash_algo *hash_algo;\n \tunsigned char num_chunks;\n \tuint32_t num_commits;\n \tstruct object_id oid;\n+\tchar *dir;\n \tchar *filename;\n-\tstruct odb_source *odb_source;\n \n \tuint32_t num_commits_in_base;\n \tunsigned int read_generation_data;\n@@ -114,12 +115,20 @@ struct commit_graph {\n \tstruct bloom_filter_settings *bloom_filter_settings;\n };\n \n-struct commit_graph *load_commit_graph_one_fd_st(struct odb_source *source,\n+/*\n+ * Load commit graphs from the given object directory `dir`. The directory may\n+ * be given as a relative path; it is canonicalized internally so that graphs\n+ * loaded from the same directory compare equal regardless of how the caller\n+ * spelled the path.\n+ */\n+struct commit_graph *load_commit_graph_one_fd_st(struct repository *repo,\n+\t\t\t\t\t\t const char *dir,\n \t\t\t\t\t\t int fd, struct stat *st);\n struct commit_graph *load_commit_graph_chain_fd_st(struct object_database *odb,\n \t\t\t\t\t\t   int fd, struct stat *st,\n \t\t\t\t\t\t   int *incomplete_chain);\n-struct commit_graph *read_commit_graph_one(struct odb_source *source);\n+struct commit_graph *read_commit_graph_one(struct repository *repo,\n+\t\t\t\t\t   const char *dir);\n \n struct repo_settings;\n \n@@ -171,11 +180,17 @@ struct commit_graph_opts {\n  * and a negative value on failure. Note that if the repository\n  * is not compatible with the commit-graph feature, then the\n  * methods will return 0 without writing a commit-graph.\n+ *\n+ * The object directory `dir` may be given as a relative path; it is\n+ * canonicalized internally so that it compares equal to the directory of\n+ * graphs that have already been loaded.\n  */\n-int write_commit_graph_reachable(struct odb_source *source,\n+int write_commit_graph_reachable(struct repository *repo,\n+\t\t\t\t const char *dir,\n \t\t\t\t enum commit_graph_write_flags flags,\n \t\t\t\t const struct commit_graph_opts *opts);\n-int write_commit_graph(struct odb_source *source,\n+int write_commit_graph(struct repository *r,\n+\t\t       const char *dir,\n \t\t       const struct string_list *pack_indexes,\n \t\t       struct oidset *commits,\n \t\t       enum commit_graph_write_flags flags,\ndiff --git a/t/helper/test-read-graph.c b/t/helper/test-read-graph.c\nindex 9f07b9c25a..a75c817e47 100644\n--- a/t/helper/test-read-graph.c\n+++ b/t/helper/test-read-graph.c\n@@ -4,6 +4,7 @@\n #include \"commit-graph.h\"\n #include \"repository.h\"\n #include \"odb.h\"\n+#include \"odb/source.h\"\n #include \"bloom.h\"\n #include \"setup.h\"\n \n@@ -81,7 +82,7 @@ int cmd__read_graph(int argc, const char **argv)\n \n \tprepare_repo_settings(the_repository);\n \n-\tgraph = read_commit_graph_one(source);\n+\tgraph = read_commit_graph_one(the_repository, source->path);\n \tif (!graph) {\n \t\tret = 1;\n \t\tgoto done;\ndiff --git a/t/t4216-log-bloom.sh b/t/t4216-log-bloom.sh\nindex ad2686669d..f57a3d6621 100755\n--- a/t/t4216-log-bloom.sh\n+++ b/t/t4216-log-bloom.sh\n@@ -755,7 +755,7 @@ test_expect_success PERL_TEST_HELPERS 'Bloom reader notices too-small data chunk\n test_expect_success PERL_TEST_HELPERS 'Bloom reader notices out-of-bounds filter offsets' '\n \tcheck_corrupt_graph BIDX 12 FFFFFFFF &&\n \t# use grep to avoid depending on exact chunk size\n-\ttest_grep \"warning: ignoring out-of-range offset (4294967295) for changed-path filter at pos 3 of .git/objects/info/commit-graph\" err\n+\ttest_grep \"warning: ignoring out-of-range offset (4294967295) for changed-path filter at pos 3 of $(pwd)/.git/objects/info/commit-graph\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'Bloom reader notices too-small index chunk' '\n@@ -773,7 +773,7 @@ test_expect_success PERL_TEST_HELPERS 'Bloom reader notices out-of-order index o\n \t# actually reading from the bogus offsets anyway.\n \tcorrupt_graph BIDX 4 0000000c00000005 &&\n \techo \"warning: ignoring decreasing changed-path index offsets\" \\\n-\t\t\"(12 > 5) for positions 1 and 2 of .git/objects/info/commit-graph\" >expect.err &&\n+\t\t\"(12 > 5) for positions 1 and 2 of $(pwd)/.git/objects/info/commit-graph\" >expect.err &&\n \tgit -c core.commitGraph=false log -- A/B/file2 >expect.out &&\n \tgit -c core.commitGraph=true log -- A/B/file2 >out 2>err &&\n \ttest_cmp expect.out out &&\n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553937","messageId":"20261002-pks-odb-move-alternates-v1-3-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 03/13] odb/source-files: introduce `struct odb_files_dir`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:14Z","receivedAt":"2026-10-02T10:08:37Z","isPatch":true,"body":"The \"files\" object database source consists of two separate backends for\nstoring loose and packed objects. These are managed as somewhat separate\nentities even though they derive from the same object directory, whether\nit's the primary object directory or that one from an alternate.\n\nIn a subsequent commit we'll move the handling of alternates into the\n\"files\" backend completely so that it becomes another implementation\ndetail thereof. As part of this, we'll want to keep track of both of\nthese sub-sources as a single entity derived from their respective\nobject directory.\n\nPrepare for this change by introducing a new `struct odb_files_dir` that\nencapsulates them. For now, every \"files\" source has exactly one such\ndirectory. In a subsequent commit though, we'll make it a linked list of\ndirectories so that we can manage multiple such directories in a single\n\"files\" source.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fast-import.c      |  6 ++--\n builtin/index-pack.c       |  2 +-\n builtin/multi-pack-index.c | 14 ++++-----\n builtin/pack-objects.c     | 18 +++++------\n builtin/repack.c           |  4 +--\n commit-graph.c             |  2 +-\n http-walker.c              |  2 +-\n http.c                     |  6 ++--\n loose.c                    |  6 ++--\n midx.c                     | 26 ++++++++--------\n odb/source-files.c         | 77 ++++++++++++++++++++++++++++++----------------\n odb/source-files.h         | 27 ++++++++++++++--\n pack-bitmap.c              |  2 +-\n packfile.c                 |  8 ++---\n packfile.h                 |  4 +--\n repack-geometry.c          |  2 +-\n repack-midx.c              |  6 ++--\n repack.c                   |  2 +-\n t/helper/test-read-midx.c  |  2 +-\n 19 files changed, 131 insertions(+), 85 deletions(-)\n\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex fbd919982c..0bf76b028b 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -921,7 +921,7 @@ static void end_packfile(void)\n \t\tidx_name = keep_pack(create_index());\n \n \t\t/* Register the packfile with core git's machinery. */\n-\t\tnew_p = packfile_store_load_pack(files->packed, idx_name, 1);\n+\t\tnew_p = packfile_store_load_pack(files->dirs->packed, idx_name, 1);\n \t\tif (!new_p)\n \t\t\tdie(_(\"core Git rejected index %s\"), idx_name);\n \t\tall_packs[pack_id] = new_p;\n@@ -1005,7 +1005,7 @@ static int store_object(\n \tfor (source = the_repository->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \n-\t\tif (!packfile_list_find_oid(packfile_store_get_packs(files->packed), &oid))\n+\t\tif (!packfile_list_find_oid(packfile_store_get_packs(files->dirs->packed), &oid))\n \t\t\tcontinue;\n \t\te->type = type;\n \t\te->pack_id = MAX_PACK_ID;\n@@ -1215,7 +1215,7 @@ static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark)\n \tfor (source = the_repository->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \n-\t\tif (!packfile_list_find_oid(packfile_store_get_packs(files->packed), &oid))\n+\t\tif (!packfile_list_find_oid(packfile_store_get_packs(files->dirs->packed), &oid))\n \t\t\tcontinue;\n \t\te->type = OBJ_BLOB;\n \t\te->pack_id = MAX_PACK_ID;\ndiff --git a/builtin/index-pack.c b/builtin/index-pack.c\nindex 6b2a87e2d3..70860b8f27 100644\n--- a/builtin/index-pack.c\n+++ b/builtin/index-pack.c\n@@ -1641,7 +1641,7 @@ static void final(const char *final_pack_name, const char *curr_pack_name,\n \tif (do_fsck_object && startup_info->have_repository) {\n \t\tstruct odb_source_files *files =\n \t\t\todb_source_files_downcast(the_repository->objects->sources);\n-\t\tpackfile_store_load_pack(files->packed, final_index_name, 0);\n+\t\tpackfile_store_load_pack(files->dirs->packed, final_index_name, 0);\n \t}\n \n \tif (!from_stdin) {\ndiff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c\nindex 753bd53a70..a170ec80b9 100644\n--- a/builtin/multi-pack-index.c\n+++ b/builtin/multi-pack-index.c\n@@ -213,7 +213,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,\n \n \t\tread_packs_from_stdin(&packs);\n \n-\t\tret = write_midx_file_only(source->packed, &packs,\n+\t\tret = write_midx_file_only(source->dirs->packed, &packs,\n \t\t\t\t\t   opts.preferred_pack,\n \t\t\t\t\t   opts.refs_snapshot,\n \t\t\t\t\t   opts.incremental_base, opts.flags);\n@@ -225,7 +225,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,\n \n \t}\n \n-\tret = write_midx_file(source->packed, opts.preferred_pack,\n+\tret = write_midx_file(source->dirs->packed, opts.preferred_pack,\n \t\t\t      opts.refs_snapshot, opts.flags);\n \n \tfree(opts.refs_snapshot);\n@@ -284,7 +284,7 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,\n \n \tFREE_AND_NULL(options);\n \n-\tm = get_multi_pack_index(source->packed);\n+\tm = get_multi_pack_index(source->dirs->packed);\n \n \tfor (cur = m; cur && !(from_midx && to_midx); cur = cur->base_midx) {\n \t\tconst char *midx_csum = midx_get_checksum_hex(cur);\n@@ -307,7 +307,7 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,\n \t\t\tdie(_(\"MIDX %s must be an ancestor of %s\"), argv[0], argv[1]);\n \t}\n \n-\tret = write_midx_file_compact(source->packed, from_midx, to_midx,\n+\tret = write_midx_file_compact(source->dirs->packed, from_midx, to_midx,\n \t\t\t\t      opts.incremental_base, opts.flags);\n \n \treturn ret;\n@@ -339,7 +339,7 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv,\n \n \tFREE_AND_NULL(options);\n \n-\treturn verify_midx_file(source->packed, opts.flags);\n+\treturn verify_midx_file(source->dirs->packed, opts.flags);\n }\n \n static int cmd_multi_pack_index_expire(int argc, const char **argv,\n@@ -368,7 +368,7 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv,\n \n \tFREE_AND_NULL(options);\n \n-\treturn expire_midx_packs(source->packed, opts.flags);\n+\treturn expire_midx_packs(source->dirs->packed, opts.flags);\n }\n \n static int cmd_multi_pack_index_repack(int argc, const char **argv,\n@@ -400,7 +400,7 @@ static int cmd_multi_pack_index_repack(int argc, const char **argv,\n \n \tFREE_AND_NULL(options);\n \n-\treturn midx_repack(source->packed, (size_t)opts.batch_size, opts.flags);\n+\treturn midx_repack(source->dirs->packed, (size_t)opts.batch_size, opts.flags);\n }\n \n int cmd_multi_pack_index(int argc,\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex af9390a46b..070659b6ed 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -1570,7 +1570,7 @@ static int want_cruft_object_mtime(struct repository *r,\n \n \tfor (source = r->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tstruct packed_git **cache = packfile_store_get_kept_pack_cache(files->packed, flags);\n+\t\tstruct packed_git **cache = packfile_store_get_kept_pack_cache(files->dirs->packed, flags);\n \n \t\tfor (; *cache; cache++) {\n \t\t\tstruct packed_git *p = *cache;\n@@ -1765,7 +1765,7 @@ static int want_object_in_pack_mtime(const struct object_id *oid,\n \t\tstruct odb_source *source = the_repository->objects->sources->next;\n \t\tfor (; source; source = source->next) {\n \t\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\t\tif (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0, NULL))\n+\t\t\tif (!odb_source_read_object_info(&files->dirs->loose->base, oid, NULL, 0, NULL))\n \t\t\t\treturn 0;\n \t\t}\n \t}\n@@ -1787,7 +1787,7 @@ static int want_object_in_pack_mtime(const struct object_id *oid,\n \n \tfor (source = the_repository->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tstruct multi_pack_index *m = get_multi_pack_index(files->packed);\n+\t\tstruct multi_pack_index *m = get_multi_pack_index(files->dirs->packed);\n \t\tstruct pack_entry e;\n \n \t\tif (m && midx_fill_entry(m, oid, &e, NULL) == MIDX_FILL_HIT) {\n@@ -1800,11 +1800,11 @@ static int want_object_in_pack_mtime(const struct object_id *oid,\n \tfor (source = the_repository->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \n-\t\tfor (e = files->packed->packs.head; e; e = e->next) {\n+\t\tfor (e = files->dirs->packed->packs.head; e; e = e->next) {\n \t\t\tstruct packed_git *p = e->pack;\n \t\t\twant = want_object_in_pack_one(p, oid, exclude, found_pack, found_offset, found_mtime);\n \t\t\tif (!exclude && want > 0)\n-\t\t\t\tpackfile_list_prepend(&files->packed->packs, p);\n+\t\t\t\tpackfile_list_prepend(&files->dirs->packed->packs, p);\n \t\t\tif (want != -1)\n \t\t\t\treturn want;\n \t\t}\n@@ -4175,7 +4175,7 @@ static void add_cruft_object_entry(const struct object_id *oid, enum object_type\n \n \t\t\tfor (; !found && source; source = source->next) {\n \t\t\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\t\t\tif (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0, NULL))\n+\t\t\t\tif (!odb_source_read_object_info(&files->dirs->loose->base, oid, NULL, 0, NULL))\n \t\t\t\t\tfound = 1;\n \t\t\t}\n \n@@ -4532,7 +4532,7 @@ static void add_objects_in_unpacked_packs(void)\n \t\tif (!source->local)\n \t\t\tcontinue;\n \n-\t\tif (odb_source_for_each_object(&files->packed->base, &oi,\n+\t\tif (odb_source_for_each_object(&files->dirs->packed->base, &oi,\n \t\t\t\t\t       add_object_in_unpacked_pack, NULL, &opts))\n \t\t\tdie(_(\"cannot open pack index\"));\n \t}\n@@ -4642,7 +4642,7 @@ static int force_object_loose(struct odb_source *source,\n \n \tfor (struct odb_source *s = source->odb->sources; s; s = s->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(s);\n-\t\tif (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0, NULL))\n+\t\tif (!odb_source_read_object_info(&files->dirs->loose->base, oid, NULL, 0, NULL))\n \t\t\treturn 0;\n \t}\n \n@@ -4664,7 +4664,7 @@ static int force_object_loose(struct odb_source *source,\n \t\tcompat_oid_p = &compat_oid;\n \t}\n \n-\tret = odb_source_write_object(&files->loose->base, buf, len, type, oid,\n+\tret = odb_source_write_object(&files->dirs->loose->base, buf, len, type, oid,\n \t\t\t\t      compat_oid_p, mtime, 0);\n \n out:\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex c4360382c1..5d06872d77 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -617,7 +617,7 @@ int cmd_repack(int argc,\n \t\t * midx_has_unknown_packs() will make the decision for\n \t\t * us.\n \t\t */\n-\t\tif (!get_multi_pack_index(files->packed))\n+\t\tif (!get_multi_pack_index(files->dirs->packed))\n \t\t\tmidx_must_contain_cruft = 1;\n \t}\n \n@@ -775,7 +775,7 @@ int cmd_repack(int argc,\n \n \t\tif (git_env_bool(GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL, 0))\n \t\t\tflags |= MIDX_WRITE_INCREMENTAL;\n-\t\twrite_midx_file(files->packed, NULL, NULL, flags);\n+\t\twrite_midx_file(files->dirs->packed, NULL, NULL, flags);\n \t}\n \n cleanup:\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 80ebc6a542..7cc486d140 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -2020,7 +2020,7 @@ static void fill_oids_from_all_packs(struct write_commit_graph_context *ctx)\n \n \tfor (source = ctx->r->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\todb_source_for_each_object(&files->packed->base, &oi, add_packed_commits_oi,\n+\t\todb_source_for_each_object(&files->dirs->packed->base, &oi, add_packed_commits_oi,\n \t\t\t\t\t   ctx, &opts);\n \t}\n \ndiff --git a/http-walker.c b/http-walker.c\nindex abafca84d6..0a6c99f471 100644\n--- a/http-walker.c\n+++ b/http-walker.c\n@@ -542,7 +542,7 @@ static int fetch_object(struct walker *walker, const struct object_id *oid)\n \t} else if (req->rename < 0) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);\n \t\tstruct strbuf buf = STRBUF_INIT;\n-\t\todb_loose_path(files->loose, &buf, &req->oid);\n+\t\todb_loose_path(files->dirs->loose, &buf, &req->oid);\n \t\tret = error(\"unable to write sha1 filename %s\", buf.buf);\n \t\tstrbuf_release(&buf);\n \t}\ndiff --git a/http.c b/http.c\nindex c8fcfd7693..fe6ec88a21 100644\n--- a/http.c\n+++ b/http.c\n@@ -2720,7 +2720,7 @@ void http_install_packfile(struct packed_git *p,\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);\n \tpackfile_list_remove(list_to_remove_from, p);\n-\tpackfile_store_add_pack(files->packed, p);\n+\tpackfile_store_add_pack(files->dirs->packed, p);\n }\n \n struct http_pack_request *new_http_pack_request(\n@@ -2861,7 +2861,7 @@ struct http_object_request *new_http_object_request(const char *base_url,\n \toidcpy(&freq->oid, oid);\n \tfreq->localfile = -1;\n \n-\todb_loose_path(files->loose, &filename, oid);\n+\todb_loose_path(files->dirs->loose, &filename, oid);\n \tstrbuf_addf(&freq->tmpfile, \"%s.temp\", filename.buf);\n \n \tstrbuf_addf(&prevfile, \"%s.prev\", filename.buf);\n@@ -3014,7 +3014,7 @@ int finish_http_object_request(struct http_object_request *freq)\n \t\tunlink_or_warn(freq->tmpfile.buf);\n \t\treturn -1;\n \t}\n-\todb_loose_path(files->loose, &filename, &freq->oid);\n+\todb_loose_path(files->dirs->loose, &filename, &freq->oid);\n \tfreq->rename = finalize_object_file(the_repository, freq->tmpfile.buf, filename.buf);\n \tstrbuf_release(&filename);\n \ndiff --git a/loose.c b/loose.c\nindex c159d29d2d..957bf83e6b 100644\n--- a/loose.c\n+++ b/loose.c\n@@ -117,7 +117,7 @@ int repo_read_loose_object_map(struct repository *repo)\n \n \tfor (source = repo->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tif (loose_object_map_load(files->loose) < 0)\n+\t\tif (loose_object_map_load(files->dirs->loose) < 0)\n \t\t\treturn -1;\n \t}\n \n@@ -127,7 +127,7 @@ int repo_read_loose_object_map(struct repository *repo)\n int repo_write_loose_object_map(struct repository *repo)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(repo->objects->sources);\n-\tkh_oid_map_t *map = files->loose->map->to_compat;\n+\tkh_oid_map_t *map = files->dirs->loose->map->to_compat;\n \tstruct lock_file lock;\n \tint fd;\n \tkhiter_t iter;\n@@ -237,7 +237,7 @@ int repo_loose_object_map_oid(struct repository *repo,\n \n \tfor (source = repo->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tstruct loose_object_map *loose_map = files->loose->map;\n+\t\tstruct loose_object_map *loose_map = files->dirs->loose->map;\n \t\tif (!loose_map)\n \t\t\tcontinue;\n \t\tmap = (to == repo->compat_hash_algo) ?\ndiff --git a/midx.c b/midx.c\nindex 6d1c548e3d..c0f82c4163 100644\n--- a/midx.c\n+++ b/midx.c\n@@ -837,20 +837,20 @@ void clear_midx_file(struct repository *r)\n \n \t\tfor (source = r->objects->sources; source; source = source->next) {\n \t\t\tfiles = odb_source_files_downcast(source);\n-\t\t\tif (files->packed->midx)\n-\t\t\t\tclose_midx(files->packed->midx);\n-\t\t\tfiles->packed->midx = NULL;\n+\t\t\tif (files->dirs->packed->midx)\n+\t\t\t\tclose_midx(files->dirs->packed->midx);\n+\t\t\tfiles->dirs->packed->midx = NULL;\n \t\t}\n \t}\n \n \tfiles = odb_source_files_downcast(r->objects->sources);\n-\tget_midx_filename(files->packed, &midx);\n+\tget_midx_filename(files->dirs->packed, &midx);\n \n \tif (remove_path(midx.buf))\n \t\tdie(_(\"failed to clear multi-pack-index at %s\"), midx.buf);\n \n-\tclear_midx_files_ext(files->packed, MIDX_EXT_BITMAP, NULL);\n-\tclear_midx_files_ext(files->packed, MIDX_EXT_REV, NULL);\n+\tclear_midx_files_ext(files->dirs->packed, MIDX_EXT_BITMAP, NULL);\n+\tclear_midx_files_ext(files->dirs->packed, MIDX_EXT_REV, NULL);\n \n \tstrbuf_release(&midx);\n }\n@@ -864,21 +864,21 @@ void clear_incremental_midx_files(struct repository *r,\n \n \tfor (source = r->objects->sources; source; source = source->next) {\n \t\tfiles = odb_source_files_downcast(source);\n-\t\tif (files->packed->midx)\n-\t\t\tclose_midx(files->packed->midx);\n-\t\tfiles->packed->midx = NULL;\n+\t\tif (files->dirs->packed->midx)\n+\t\t\tclose_midx(files->dirs->packed->midx);\n+\t\tfiles->dirs->packed->midx = NULL;\n \t}\n \n \tfiles = odb_source_files_downcast(r->objects->sources);\n-\tget_midx_chain_filename(files->packed, &chain);\n+\tget_midx_chain_filename(files->dirs->packed, &chain);\n \n \tif (!keep_hashes && remove_path(chain.buf))\n \t\tdie(_(\"failed to clear multi-pack-index chain at %s\"),\n \t\t    chain.buf);\n \n-\tclear_incremental_midx_files_ext(files->packed, MIDX_EXT_BITMAP, keep_hashes);\n-\tclear_incremental_midx_files_ext(files->packed, MIDX_EXT_REV, keep_hashes);\n-\tclear_incremental_midx_files_ext(files->packed, MIDX_EXT_MIDX, keep_hashes);\n+\tclear_incremental_midx_files_ext(files->dirs->packed, MIDX_EXT_BITMAP, keep_hashes);\n+\tclear_incremental_midx_files_ext(files->dirs->packed, MIDX_EXT_REV, keep_hashes);\n+\tclear_incremental_midx_files_ext(files->dirs->packed, MIDX_EXT_MIDX, keep_hashes);\n \n \tstrbuf_release(&chain);\n }\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex f2fc4cd9ab..1f4cedaffa 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -24,6 +24,30 @@\n #include \"tree.h\"\n #include \"write-or-die.h\"\n \n+struct odb_files_dir *odb_files_dir_new(struct object_database *odb,\n+\t\t\t\t\tconst char *path, bool local)\n+{\n+\tstruct odb_files_dir *dir;\n+\n+\tCALLOC_ARRAY(dir, 1);\n+\tdir->abspath = absolute_pathdup(path);\n+\tdir->local = local;\n+\tdir->loose = odb_source_loose_new(odb, path, local);\n+\tdir->packed = odb_source_packed_new(odb, path, local);\n+\n+\treturn dir;\n+}\n+\n+void odb_files_dir_free(struct odb_files_dir *dir)\n+{\n+\tif (!dir)\n+\t\treturn;\n+\todb_source_free(&dir->loose->base);\n+\todb_source_free(&dir->packed->base);\n+\tfree(dir->abspath);\n+\tfree(dir);\n+}\n+\n static void odb_source_files_reparent(const char *old_cwd,\n \t\t\t\t      const char *new_cwd,\n \t\t\t\t      void *cb_data)\n@@ -31,6 +55,7 @@ static void odb_source_files_reparent(const char *old_cwd,\n \tstruct odb_source_files *files = cb_data;\n \tchar *path = reparent_relative_path(old_cwd, new_cwd,\n \t\t\t\t\t    files->base.path);\n+\n \tfree(files->base.path);\n \tfiles->base.path = path;\n }\n@@ -39,8 +64,7 @@ static void odb_source_files_free(struct odb_source *source)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tchdir_notify_unregister(odb_source_files_reparent, files);\n-\todb_source_free(&files->loose->base);\n-\todb_source_free(&files->packed->base);\n+\todb_files_dir_free(files->dirs);\n \todb_source_release(&files->base);\n \tfree(files);\n }\n@@ -48,8 +72,8 @@ static void odb_source_files_free(struct odb_source *source)\n static void odb_source_files_close(struct odb_source *source)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\todb_source_close(&files->loose->base);\n-\todb_source_close(&files->packed->base);\n+\todb_source_close(&files->dirs->loose->base);\n+\todb_source_close(&files->dirs->packed->base);\n }\n \n static int odb_source_files_create_on_disk(struct odb_source *source,\n@@ -144,8 +168,8 @@ static void odb_source_files_prepare(struct odb_source *source,\n \t\t\t\t     enum odb_prepare_flags flags)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\todb_source_prepare(&files->loose->base, flags);\n-\todb_source_prepare(&files->packed->base, flags);\n+\todb_source_prepare(&files->dirs->loose->base, flags);\n+\todb_source_prepare(&files->dirs->packed->base, flags);\n }\n \n static enum odb_read_status odb_source_files_read_object_info(struct odb_source *source,\n@@ -157,12 +181,12 @@ static enum odb_read_status odb_source_files_read_object_info(struct odb_source\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tenum odb_read_status ret_packed, ret_loose;\n \n-\tret_packed = odb_source_read_object_info(&files->packed->base, oid, oi,\n+\tret_packed = odb_source_read_object_info(&files->dirs->packed->base, oid, oi,\n \t\t\t\t\t\t flags, errmsg);\n \tif (!ret_packed)\n \t\treturn 0;\n \n-\tret_loose = odb_source_read_object_info(&files->loose->base, oid, oi, flags,\n+\tret_loose = odb_source_read_object_info(&files->dirs->loose->base, oid, oi, flags,\n \t\t\t\t\t\tret_packed == ODB_READ_NOT_FOUND ? errmsg : NULL);\n \tif (!ret_loose)\n \t\treturn 0;\n@@ -184,8 +208,8 @@ static int odb_source_files_read_object_stream(struct odb_stream **out,\n \t\t\t\t\t       const struct object_id *oid)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tif (!odb_source_read_object_stream(out, &files->packed->base, oid) ||\n-\t    !odb_source_read_object_stream(out, &files->loose->base, oid))\n+\tif (!odb_source_read_object_stream(out, &files->dirs->packed->base, oid) ||\n+\t    !odb_source_read_object_stream(out, &files->dirs->loose->base, oid))\n \t\treturn 0;\n \treturn -1;\n }\n@@ -200,12 +224,12 @@ static int odb_source_files_for_each_object(struct odb_source *source,\n \tint ret;\n \n \tif (!(opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)) {\n-\t\tret = odb_source_for_each_object(&files->loose->base, request, cb, cb_data, opts);\n+\t\tret = odb_source_for_each_object(&files->dirs->loose->base, request, cb, cb_data, opts);\n \t\tif (ret)\n \t\t\treturn ret;\n \t}\n \n-\tret = odb_source_for_each_object(&files->packed->base, request, cb, cb_data, opts);\n+\tret = odb_source_for_each_object(&files->dirs->packed->base, request, cb, cb_data, opts);\n \tif (ret)\n \t\treturn ret;\n \n@@ -220,14 +244,14 @@ static int odb_source_files_count_objects(struct odb_source *source,\n \tunsigned long count;\n \tint ret;\n \n-\tret = odb_source_count_objects(&files->packed->base, flags, &count);\n+\tret = odb_source_count_objects(&files->dirs->packed->base, flags, &count);\n \tif (ret < 0)\n \t\tgoto out;\n \n \tif (!(flags & ODB_COUNT_OBJECTS_APPROXIMATE)) {\n \t\tunsigned long loose_count;\n \n-\t\tret = odb_source_count_objects(&files->loose->base, flags, &loose_count);\n+\t\tret = odb_source_count_objects(&files->dirs->loose->base, flags, &loose_count);\n \t\tif (ret < 0)\n \t\t\tgoto out;\n \n@@ -250,11 +274,11 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source,\n \tunsigned len = min_len;\n \tint ret;\n \n-\tret = odb_source_find_abbrev_len(&files->packed->base, oid, len, &len);\n+\tret = odb_source_find_abbrev_len(&files->dirs->packed->base, oid, len, &len);\n \tif (ret < 0)\n \t\tgoto out;\n \n-\tret = odb_source_find_abbrev_len(&files->loose->base, oid, len, &len);\n+\tret = odb_source_find_abbrev_len(&files->dirs->loose->base, oid, len, &len);\n \tif (ret < 0)\n \t\tgoto out;\n \n@@ -270,8 +294,8 @@ static int odb_source_files_freshen_object(struct odb_source *source,\n \t\t\t\t\t   const time_t *mtime)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tif (odb_source_freshen_object(&files->packed->base, oid, mtime) ||\n-\t    odb_source_freshen_object(&files->loose->base, oid, mtime))\n+\tif (odb_source_freshen_object(&files->dirs->packed->base, oid, mtime) ||\n+\t    odb_source_freshen_object(&files->dirs->loose->base, oid, mtime))\n \t\treturn 1;\n \treturn 0;\n }\n@@ -285,7 +309,7 @@ static int odb_source_files_write_object(struct odb_source *source,\n \t\t\t\t\t enum odb_write_object_flags flags)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\treturn odb_source_write_object(&files->loose->base, buf, len, type,\n+\treturn odb_source_write_object(&files->dirs->loose->base, buf, len, type,\n \t\t\t\t       oid, compat_oid, mtime, flags);\n }\n \n@@ -294,7 +318,7 @@ static int odb_source_files_write_object_stream(struct odb_source *source,\n \t\t\t\t\t\tstruct object_id *oid)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\treturn odb_source_write_object_stream(&files->loose->base, stream, oid);\n+\treturn odb_source_write_object_stream(&files->dirs->loose->base, stream, oid);\n }\n \n static int odb_source_files_begin_transaction(struct odb_source *source,\n@@ -330,7 +354,7 @@ static int too_many_loose_objects(struct odb_source_files *files, int limit)\n \tif (limit <= 0)\n \t\treturn 0;\n \n-\tif (odb_source_count_objects(&files->loose->base, ODB_COUNT_OBJECTS_APPROXIMATE,\n+\tif (odb_source_count_objects(&files->dirs->loose->base, ODB_COUNT_OBJECTS_APPROXIMATE,\n \t\t\t\t     &loose_count) < 0)\n \t\treturn 0;\n \n@@ -349,7 +373,7 @@ static struct packed_git *find_base_packs(struct odb_source_files *files,\n \tstruct packfile_list_entry *e;\n \tstruct packed_git *base = NULL;\n \n-\tfor (e = packfile_store_get_packs(files->packed); e; e = e->next) {\n+\tfor (e = packfile_store_get_packs(files->dirs->packed); e; e = e->next) {\n \t\tif (e->pack->is_cruft)\n \t\t\tcontinue;\n \t\tif (limit) {\n@@ -374,7 +398,7 @@ static int too_many_packs(struct odb_source_files *files, int gc_auto_pack_limit\n \tif (gc_auto_pack_limit <= 0)\n \t\treturn 0;\n \n-\tfor (e = packfile_store_get_packs(files->packed); e; e = e->next) {\n+\tfor (e = packfile_store_get_packs(files->dirs->packed); e; e = e->next) {\n \t\tif (e->pack->pack_keep)\n \t\t\tcontinue;\n \t\t/*\n@@ -937,8 +961,8 @@ static int odb_source_files_fsck(struct odb_source *source,\n \tif (!(opts->flags & ODB_FSCK_FULL) && !source->local)\n \t\treturn 0;\n \n-\tret |= odb_source_fsck(&files->loose->base, opts);\n-\tret |= odb_source_fsck(&files->packed->base, opts);\n+\tret |= odb_source_fsck(&files->dirs->loose->base, opts);\n+\tret |= odb_source_fsck(&files->dirs->packed->base, opts);\n \n \treturn ret;\n }\n@@ -951,8 +975,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb,\n \n \tCALLOC_ARRAY(files, 1);\n \todb_source_init(&files->base, odb, ODB_SOURCE_FILES, path, local);\n-\tfiles->loose = odb_source_loose_new(odb, path, local);\n-\tfiles->packed = odb_source_packed_new(odb, path, local);\n+\tfiles->dirs = odb_files_dir_new(odb, path, local);\n \n \tfiles->base.free = odb_source_files_free;\n \tfiles->base.close = odb_source_files_close;\ndiff --git a/odb/source-files.h b/odb/source-files.h\nindex 9630b5f962..7f465853b1 100644\n--- a/odb/source-files.h\n+++ b/odb/source-files.h\n@@ -6,14 +6,37 @@\n struct odb_source_loose;\n struct odb_source_packed;\n \n+/*\n+ * A single object directory that encapsulates access to both the loose and\n+ * packed backend. This can either be the primary or an alternate object\n+ * directory.\n+ */\n+struct odb_files_dir {\n+\t/* Absolute path to the object directory. */\n+\tchar *abspath;\n+\n+\t/* The two sources derived from this object directory. */\n+\tstruct odb_source_loose *loose;\n+\tstruct odb_source_packed *packed;\n+\n+\t/*\n+\t * Whether this is the local object directory of the owning\n+\t * repository. Directories added via alternates are not local.\n+\t */\n+\tbool local;\n+};\n+\n+struct odb_files_dir *odb_files_dir_new(struct object_database *odb,\n+\t\t\t\t\tconst char *path, bool local);\n+void odb_files_dir_free(struct odb_files_dir *dir);\n+\n /*\n  * The files object database source uses a combination of loose objects and\n  * packfiles. It is the default backend used by Git to store objects.\n  */\n struct odb_source_files {\n \tstruct odb_source base;\n-\tstruct odb_source_loose *loose;\n-\tstruct odb_source_packed *packed;\n+\tstruct odb_files_dir *dirs;\n };\n \n /* Allocate and initialize a new object source. */\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 3de8e9590c..52556b4543 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -720,7 +720,7 @@ static int open_bitmap(struct repository *r,\n \tfor (source = r->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \n-\t\tif (!open_bitmap_for_source(files->packed, bitmap_git))\n+\t\tif (!open_bitmap_for_source(files->dirs->packed, bitmap_git))\n \t\t\tfound = true;\n \n \t\t/*\ndiff --git a/packfile.c b/packfile.c\nindex 4fa5fd67c8..93b69d7f50 100644\n--- a/packfile.c\n+++ b/packfile.c\n@@ -280,7 +280,7 @@ static int unuse_one_window(struct object_database *odb)\n \n \tfor (source = odb->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tfor (e = files->packed->packs.head; e; e = e->next)\n+\t\tfor (e = files->dirs->packed->packs.head; e; e = e->next)\n \t\t\tscan_windows(e->pack, &lru_p, &lru_w, &lru_l);\n \t}\n \n@@ -458,7 +458,7 @@ static int close_one_pack(struct repository *r)\n \n \tfor (source = r->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tfor (e = files->packed->packs.head; e; e = e->next) {\n+\t\tfor (e = files->dirs->packed->packs.head; e; e = e->next) {\n \t\t\tif (e->pack->pack_fd == -1)\n \t\t\t\tcontinue;\n \t\t\tfind_lru_pack(e->pack, &lru_p, &mru_w, &accept_windows_inuse);\n@@ -1925,7 +1925,7 @@ int has_object_pack(struct repository *r, const struct object_id *oid)\n \n \tfor (source = r->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tif (!odb_source_read_object_info(&files->packed->base, oid, NULL, 0, NULL))\n+\t\tif (!odb_source_read_object_info(&files->dirs->packed->base, oid, NULL, 0, NULL))\n \t\t\treturn 1;\n \t}\n \n@@ -1942,7 +1942,7 @@ int has_object_kept_pack(struct repository *r, const struct object_id *oid,\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \t\tstruct packed_git **cache;\n \n-\t\tcache = packfile_store_get_kept_pack_cache(files->packed, flags);\n+\t\tcache = packfile_store_get_kept_pack_cache(files->dirs->packed, flags);\n \n \t\tfor (; *cache; cache++) {\n \t\t\tstruct packed_git *p = *cache;\ndiff --git a/packfile.h b/packfile.h\nindex 6d30d15a00..fd1cf0ad6c 100644\n--- a/packfile.h\n+++ b/packfile.h\n@@ -79,7 +79,7 @@ static inline struct repo_for_each_pack_data repo_for_eack_pack_data_init(struct\n \n \tfor (struct odb_source *source = repo->objects->sources; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tstruct packfile_list_entry *entry = packfile_store_get_packs(files->packed);\n+\t\tstruct packfile_list_entry *entry = packfile_store_get_packs(files->dirs->packed);\n \t\tif (!entry)\n \t\t\tcontinue;\n \t\tdata.source = source;\n@@ -100,7 +100,7 @@ static inline void repo_for_each_pack_data_next(struct repo_for_each_pack_data *\n \n \tfor (source = data->source->next; source; source = source->next) {\n \t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tstruct packfile_list_entry *entry = packfile_store_get_packs(files->packed);\n+\t\tstruct packfile_list_entry *entry = packfile_store_get_packs(files->dirs->packed);\n \t\tif (!entry)\n \t\t\tcontinue;\n \t\tdata->source = source;\ndiff --git a/repack-geometry.c b/repack-geometry.c\nindex 15b3412950..b541c34af5 100644\n--- a/repack-geometry.c\n+++ b/repack-geometry.c\n@@ -33,7 +33,7 @@ void pack_geometry_init(struct pack_geometry *geometry,\n \tstruct packed_git *p;\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct odb_source_files *files = odb_source_files_downcast(existing->source);\n-\tstruct multi_pack_index *m = get_multi_pack_index(files->packed);\n+\tstruct multi_pack_index *m = get_multi_pack_index(files->dirs->packed);\n \n \trepo_for_each_pack(existing->repo, p) {\n \t\tif (geometry->midx_layer_threshold_set && m &&\ndiff --git a/repack-midx.c b/repack-midx.c\nindex 64c7f8d0f4..25140569bd 100644\n--- a/repack-midx.c\n+++ b/repack-midx.c\n@@ -564,7 +564,7 @@ static void repack_make_midx_append_plan(struct repack_write_midx_opts *opts,\n \tsize_t steps_nr = 0, steps_alloc = 0;\n \n \todb_reprepare(opts->existing->repo->objects);\n-\tm = get_multi_pack_index(files->packed);\n+\tm = get_multi_pack_index(files->dirs->packed);\n \n \tif (opts->names->nr) {\n \t\tstruct strbuf buf = STRBUF_INIT;\n@@ -620,7 +620,7 @@ static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts,\n \t\t\t    opts->existing->repo);\n \n \todb_reprepare(opts->existing->repo->objects);\n-\tm = get_multi_pack_index(files->packed);\n+\tm = get_multi_pack_index(files->dirs->packed);\n \n \tfor (i = 0; m && i < m->num_packs + m->num_packs_in_base; i++) {\n \t\tif (prepare_midx_pack(m, i)) {\n@@ -949,7 +949,7 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts)\n \tsize_t i;\n \tint ret = 0;\n \n-\tget_midx_chain_filename(files->packed, &lock_name);\n+\tget_midx_chain_filename(files->dirs->packed, &lock_name);\n \tif (safe_create_leading_directories(opts->existing->repo,\n \t\t\t\t\t    lock_name.buf))\n \t\tdie_errno(_(\"unable to create leading directories of %s\"),\ndiff --git a/repack.c b/repack.c\nindex d2aa58e134..e20431690c 100644\n--- a/repack.c\n+++ b/repack.c\n@@ -60,7 +60,7 @@ void repack_remove_redundant_pack(struct repository *repo, const char *dir_name,\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct odb_source_files *files = odb_source_files_downcast(repo->objects->sources);\n-\tstruct multi_pack_index *m = get_multi_pack_index(files->packed);\n+\tstruct multi_pack_index *m = get_multi_pack_index(files->dirs->packed);\n \tstrbuf_addf(&buf, \"%s.pack\", base_name);\n \tif (m && files->base.local && midx_contains_pack(m, buf.buf)) {\n \t\tclear_midx_file(repo);\ndiff --git a/t/helper/test-read-midx.c b/t/helper/test-read-midx.c\nindex 83b07c6236..3f4bafff61 100644\n--- a/t/helper/test-read-midx.c\n+++ b/t/helper/test-read-midx.c\n@@ -22,7 +22,7 @@ static struct multi_pack_index *setup_midx(const char *object_dir,\n \n \tsource = odb_find_source(the_repository->objects, object_dir);\n \tif (source) {\n-\t\tpacked = odb_source_files_downcast(source)->packed;\n+\t\tpacked = odb_source_files_downcast(source)->dirs->packed;\n \t} else {\n \t\tpacked = odb_source_packed_new(the_repository->objects,\n \t\t\t\t\t       object_dir, false);\n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553938","messageId":"20261002-pks-odb-move-alternates-v1-4-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 04/13] odb: refactor `odb_for_each_alternate()` to yield dirs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:15Z","receivedAt":"2026-10-02T10:08:40Z","isPatch":true,"body":"The function `odb_for_each_alternate()` iterates through all alternates\nof an object database. This is becoming an implementation detail of the\n\"files\" backend though, where each alternate will be represented by one\n`struct odb_files_dir`.\n\nAdapt `odb_for_each_alternate()` to already iterate through these\nstructs instead of iterating through sources.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/count-objects.c     |  4 ++--\n builtin/submodule--helper.c |  7 ++++---\n diagnose.c                  |  8 ++++----\n odb.c                       | 18 +++++++++++-------\n odb.h                       |  3 ++-\n 5 files changed, 23 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/count-objects.c b/builtin/count-objects.c\nindex 18f6e33b6f..f2abfaccec 100644\n--- a/builtin/count-objects.c\n+++ b/builtin/count-objects.c\n@@ -81,10 +81,10 @@ static int count_cruft(const char *basename UNUSED, const char *path,\n \treturn 0;\n }\n \n-static int print_alternate(struct odb_source *alternate, void *data UNUSED)\n+static int print_alternate(struct odb_files_dir *alternate, void *data UNUSED)\n {\n \tprintf(\"alternate: \");\n-\tquote_c_style(alternate->path, NULL, stdout, 0);\n+\tquote_c_style(alternate->abspath, NULL, stdout, 0);\n \tputchar('\\n');\n \treturn 0;\n }\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 40a052d674..64412adf9f 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -30,6 +30,7 @@\n #include \"object-name.h\"\n #include \"odb.h\"\n #include \"odb/source.h\"\n+#include \"odb/source-files.h\"\n #include \"advice.h\"\n #include \"branch.h\"\n #include \"list-objects-filter-options.h\"\n@@ -1770,7 +1771,7 @@ static const char alternate_error_advice[] = N_(\n );\n \n static int add_possible_reference_from_superproject(\n-\t\tstruct odb_source *alt_odb, void *sas_cb)\n+\t\tstruct odb_files_dir *alt_odb, void *sas_cb)\n {\n \tstruct submodule_alternate_setup *sas = sas_cb;\n \tsize_t len;\n@@ -1779,12 +1780,12 @@ static int add_possible_reference_from_superproject(\n \t * If the alternate object store is another repository, try the\n \t * standard layout with .git/(modules/<name>)+/objects\n \t */\n-\tif (strip_suffix(alt_odb->path, \"/objects\", &len)) {\n+\tif (strip_suffix(alt_odb->abspath, \"/objects\", &len)) {\n \t\tstruct repository alternate;\n \t\tchar *sm_alternate;\n \t\tstruct strbuf sb = STRBUF_INIT;\n \t\tstruct strbuf err = STRBUF_INIT;\n-\t\tstrbuf_add(&sb, alt_odb->path, len);\n+\t\tstrbuf_add(&sb, alt_odb->abspath, len);\n \n \t\tif (repo_init(&alternate, sb.buf, NULL) < 0)\n \t\t\tdie(_(\"could not get a repository handle for gitdir '%s'\"),\ndiff --git a/diagnose.c b/diagnose.c\nindex 5092bf80d3..89240e47d6 100644\n--- a/diagnose.c\n+++ b/diagnose.c\n@@ -59,13 +59,13 @@ static void dir_file_stats_objects(const char *full_path,\n \t\t\t    (uintmax_t)st.st_size);\n }\n \n-static int dir_file_stats(struct odb_source *source, void *data)\n+static int dir_file_stats(struct odb_files_dir *source, void *data)\n {\n \tstruct strbuf *buf = data;\n \n-\tstrbuf_addf(buf, \"Contents of %s:\\n\", source->path);\n+\tstrbuf_addf(buf, \"Contents of %s:\\n\", source->abspath);\n \n-\tfor_each_file_in_pack_dir(source->path, dir_file_stats_objects,\n+\tfor_each_file_in_pack_dir(source->abspath, dir_file_stats_objects,\n \t\t\t\t  data);\n \n \treturn 0;\n@@ -228,7 +228,7 @@ int create_diagnostics_archive(struct repository *r,\n \n \tstrbuf_reset(&buf);\n \tstrbuf_addstr(&buf, \"--add-virtual-file=packs-local.txt:\");\n-\tdir_file_stats(r->objects->sources, &buf);\n+\tdir_file_stats(odb_source_files_downcast(r->objects->sources)->dirs, &buf);\n \todb_for_each_alternate(r->objects, dir_file_stats, &buf);\n \tstrvec_push(&archiver_args, buf.buf);\n \ndiff --git a/odb.c b/odb.c\nindex 0200e26f21..9b70859c23 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -14,6 +14,7 @@\n #include \"object-name.h\"\n #include \"odb.h\"\n #include \"odb/source-inmemory.h\"\n+#include \"odb/source-files.h\"\n #include \"path.h\"\n #include \"promisor-remote.h\"\n #include \"quote.h\"\n@@ -435,18 +436,19 @@ static void read_alternate_refs(struct repository *repo,\n }\n \n struct alternate_refs_data {\n+\tstruct repository *repo;\n \todb_for_each_alternate_ref_fn *fn;\n \tvoid *payload;\n };\n \n-static int refs_from_alternate_cb(struct odb_source *alternate,\n+static int refs_from_alternate_cb(struct odb_files_dir *alternate,\n \t\t\t\t  void *payload)\n {\n \tstruct strbuf path = STRBUF_INIT;\n \tsize_t base_len;\n \tstruct alternate_refs_data *cb = payload;\n \n-\tif (!strbuf_realpath(&path, alternate->path, 0))\n+\tif (!strbuf_realpath(&path, alternate->abspath, 0))\n \t\tgoto out;\n \tif (!strbuf_strip_suffix(&path, \"/objects\"))\n \t\tgoto out;\n@@ -458,7 +460,7 @@ static int refs_from_alternate_cb(struct odb_source *alternate,\n \t\tgoto out;\n \tstrbuf_setlen(&path, base_len);\n \n-\tread_alternate_refs(alternate->odb->repo, path.buf, cb->fn, cb->payload);\n+\tread_alternate_refs(cb->repo, path.buf, cb->fn, cb->payload);\n \n out:\n \tstrbuf_release(&path);\n@@ -468,9 +470,11 @@ static int refs_from_alternate_cb(struct odb_source *alternate,\n void odb_for_each_alternate_ref(struct object_database *odb,\n \t\t\t\todb_for_each_alternate_ref_fn cb, void *payload)\n {\n-\tstruct alternate_refs_data data;\n-\tdata.fn = cb;\n-\tdata.payload = payload;\n+\tstruct alternate_refs_data data = {\n+\t\t.fn = cb,\n+\t\t.payload = payload,\n+\t\t.repo = odb->repo,\n+\t};\n \todb_for_each_alternate(odb, refs_from_alternate_cb, &data);\n }\n \n@@ -481,7 +485,7 @@ int odb_for_each_alternate(struct object_database *odb,\n \tint r = 0;\n \n \tfor (alternate = odb->sources->next; alternate; alternate = alternate->next) {\n-\t\tr = cb(alternate, payload);\n+\t\tr = cb(odb_source_files_downcast(alternate)->dirs, payload);\n \t\tif (r)\n \t\t\tbreak;\n \t}\ndiff --git a/odb.h b/odb.h\nindex 797eecbb94..3715351bb3 100644\n--- a/odb.h\n+++ b/odb.h\n@@ -12,6 +12,7 @@\n struct cached_object_entry;\n struct list_objects_filter_options;\n struct odb_source_inmemory;\n+struct odb_files_dir;\n struct packed_git;\n struct repository;\n struct strbuf;\n@@ -257,7 +258,7 @@ void odb_restore_primary_source(struct object_database *odb,\n  * function returns a non-zero value, in which case the value is bubbled up\n  * from the callback.\n  */\n-typedef int odb_for_each_alternate_fn(struct odb_source *, void *);\n+typedef int odb_for_each_alternate_fn(struct odb_files_dir *, void *);\n int odb_for_each_alternate(struct object_database *odb,\n \t\t\t   odb_for_each_alternate_fn cb, void *payload);\n \n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553939","messageId":"20261002-pks-odb-move-alternates-v1-5-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 05/13] odb: refactor `odb_find_source()` to yield dirs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:16Z","receivedAt":"2026-10-02T10:08:42Z","isPatch":true,"body":"Similar as in the preceding commit, `odb_find_source()` can be used to\nyield a single source by its object directory. This function is also\nspecific to the \"files\" backend once alternates are an implementation\ndetail thereof.\n\nRefactor it to be specific to the \"files\" backend and return an `struct\nodb_files_dir` to prepare for moving alternates into the \"files\" source.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/commit-graph.c     | 26 ++++++++++++++++----------\n builtin/multi-pack-index.c | 42 +++++++++++++++++++++---------------------\n odb.c                      | 26 --------------------------\n odb.h                      |  9 ---------\n odb/source-files.c         | 26 ++++++++++++++++++++++++++\n odb/source-files.h         |  6 ++++++\n t/helper/test-read-midx.c  |  8 ++++----\n 7 files changed, 73 insertions(+), 70 deletions(-)\n\ndiff --git a/builtin/commit-graph.c b/builtin/commit-graph.c\nindex a986f08a94..ff244c866f 100644\n--- a/builtin/commit-graph.c\n+++ b/builtin/commit-graph.c\n@@ -9,6 +9,7 @@\n #include \"commit-graph.h\"\n #include \"odb.h\"\n #include \"odb/source.h\"\n+#include \"odb/source-files.h\"\n #include \"progress.h\"\n #include \"replace-object.h\"\n #include \"strbuf.h\"\n@@ -68,7 +69,7 @@ static int graph_verify(int argc, const char **argv, const char *prefix,\n \t\t\tstruct repository *repo UNUSED)\n {\n \tstruct commit_graph *graph = NULL;\n-\tstruct odb_source *source = NULL;\n+\tstruct odb_files_dir *dir = NULL;\n \tchar *graph_name;\n \tchar *chain_name;\n \tenum { OPENED_NONE, OPENED_GRAPH, OPENED_CHAIN } opened = OPENED_NONE;\n@@ -103,9 +104,12 @@ static int graph_verify(int argc, const char **argv, const char *prefix,\n \tif (opts.progress)\n \t\tflags |= COMMIT_GRAPH_WRITE_PROGRESS;\n \n-\tsource = odb_find_source_or_die(the_repository->objects, opts.obj_dir);\n-\tgraph_name = get_commit_graph_filename(source->path);\n-\tchain_name = get_commit_graph_chain_filename(source->path);\n+\tdir = odb_source_files_find_dir(the_repository->objects, opts.obj_dir);\n+\tif (!dir)\n+\t\tdie(_(\"could not find object directory matching %s\"), opts.obj_dir);\n+\n+\tgraph_name = get_commit_graph_filename(dir->abspath);\n+\tchain_name = get_commit_graph_chain_filename(dir->abspath);\n \tif (open_commit_graph(graph_name, &fd, &st))\n \t\topened = OPENED_GRAPH;\n \telse if (errno != ENOENT)\n@@ -123,7 +127,7 @@ static int graph_verify(int argc, const char **argv, const char *prefix,\n \tif (opened == OPENED_NONE)\n \t\treturn 0;\n \telse if (opened == OPENED_GRAPH)\n-\t\tgraph = load_commit_graph_one_fd_st(the_repository, source->path, fd, &st);\n+\t\tgraph = load_commit_graph_one_fd_st(the_repository, dir->abspath, fd, &st);\n \telse\n \t\tgraph = load_commit_graph_chain_fd_st(the_repository->objects, fd, &st,\n \t\t\t\t\t\t      &incomplete_chain);\n@@ -226,7 +230,7 @@ static int graph_write(int argc, const char **argv, const char *prefix,\n \tstruct string_list pack_indexes = STRING_LIST_INIT_DUP;\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct oidset commits = OIDSET_INIT;\n-\tstruct odb_source *source = NULL;\n+\tstruct odb_files_dir *dir = NULL;\n \tint result = 0;\n \tenum commit_graph_write_flags flags = 0;\n \tstruct progress *progress = NULL;\n@@ -294,10 +298,12 @@ static int graph_write(int argc, const char **argv, const char *prefix,\n \t    git_env_bool(GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS, 0))\n \t\tflags |= COMMIT_GRAPH_WRITE_BLOOM_FILTERS;\n \n-\tsource = odb_find_source_or_die(the_repository->objects, opts.obj_dir);\n+\tdir = odb_source_files_find_dir(the_repository->objects, opts.obj_dir);\n+\tif (!dir)\n+\t\tdie(_(\"could not find object directory matching %s\"), opts.obj_dir);\n \n \tif (opts.reachable) {\n-\t\tif (write_commit_graph_reachable(the_repository, source->path, flags, &write_opts))\n+\t\tif (write_commit_graph_reachable(the_repository, dir->abspath, flags, &write_opts))\n \t\t\tresult = 1;\n \t\tgoto cleanup;\n \t}\n@@ -306,7 +312,7 @@ static int graph_write(int argc, const char **argv, const char *prefix,\n \t\tstruct strbuf packname = STRBUF_INIT;\n \t\tsize_t dirlen;\n \n-\t\tstrbuf_addf(&packname, \"%s/pack/\", source->path);\n+\t\tstrbuf_addf(&packname, \"%s/pack/\", dir->abspath);\n \t\tdirlen = packname.len;\n \n \t\twhile (strbuf_getline(&buf, stdin) != EOF) {\n@@ -334,7 +340,7 @@ static int graph_write(int argc, const char **argv, const char *prefix,\n \t\tstop_progress(&progress);\n \t}\n \n-\tif (write_commit_graph(the_repository, source->path,\n+\tif (write_commit_graph(the_repository, dir->abspath,\n \t\t\t       opts.stdin_packs ? &pack_indexes : NULL,\n \t\t\t       opts.stdin_commits ? &commits : NULL,\n \t\t\t       flags,\ndiff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c\nindex a170ec80b9..fc8b494996 100644\n--- a/builtin/multi-pack-index.c\n+++ b/builtin/multi-pack-index.c\n@@ -86,13 +86,13 @@ static int parse_object_dir(const struct option *opt, const char *arg,\n \treturn 0;\n }\n \n-static struct odb_source_files *handle_object_dir_option(struct repository *repo)\n+static struct odb_source_packed *handle_object_dir_option(struct repository *repo)\n {\n-\tstruct odb_source *source = odb_find_source(repo->objects, opts.object_dir);\n-\tif (!source)\n+\tstruct odb_files_dir *dir = odb_source_files_find_dir(repo->objects, opts.object_dir);\n+\tif (!dir)\n \t\tdie(_(\"object directory is not an alternate of the current repository: '%s'\"),\n \t\t    opts.object_dir);\n-\treturn odb_source_files_downcast(source);\n+\treturn dir->packed;\n }\n \n static struct option common_opts[] = {\n@@ -169,7 +169,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,\n \t\t\t     N_(\"refs snapshot for selecting bitmap commits\")),\n \t\tOPT_END(),\n \t};\n-\tstruct odb_source_files *source;\n+\tstruct odb_source_packed *packed_source;\n \tint ret;\n \n \topts.flags |= MIDX_WRITE_BITMAP_HASH_CACHE;\n@@ -204,7 +204,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,\n \t\t\t\t   options);\n \t}\n \n-\tsource = handle_object_dir_option(repo);\n+\tpacked_source = handle_object_dir_option(repo);\n \n \tFREE_AND_NULL(options);\n \n@@ -213,7 +213,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,\n \n \t\tread_packs_from_stdin(&packs);\n \n-\t\tret = write_midx_file_only(source->dirs->packed, &packs,\n+\t\tret = write_midx_file_only(packed_source, &packs,\n \t\t\t\t\t   opts.preferred_pack,\n \t\t\t\t\t   opts.refs_snapshot,\n \t\t\t\t\t   opts.incremental_base, opts.flags);\n@@ -225,7 +225,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,\n \n \t}\n \n-\tret = write_midx_file(source->dirs->packed, opts.preferred_pack,\n+\tret = write_midx_file(packed_source, opts.preferred_pack,\n \t\t\t      opts.refs_snapshot, opts.flags);\n \n \tfree(opts.refs_snapshot);\n@@ -239,7 +239,7 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,\n \tstruct multi_pack_index *m, *cur;\n \tstruct multi_pack_index *from_midx = NULL;\n \tstruct multi_pack_index *to_midx = NULL;\n-\tstruct odb_source_files *source;\n+\tstruct odb_source_packed *packed_source;\n \tint ret;\n \n \tstruct option *options;\n@@ -280,11 +280,11 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,\n \t\t\t\t   options);\n \t}\n \n-\tsource = handle_object_dir_option(the_repository);\n+\tpacked_source = handle_object_dir_option(the_repository);\n \n \tFREE_AND_NULL(options);\n \n-\tm = get_multi_pack_index(source->dirs->packed);\n+\tm = get_multi_pack_index(packed_source);\n \n \tfor (cur = m; cur && !(from_midx && to_midx); cur = cur->base_midx) {\n \t\tconst char *midx_csum = midx_get_checksum_hex(cur);\n@@ -307,7 +307,7 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,\n \t\t\tdie(_(\"MIDX %s must be an ancestor of %s\"), argv[0], argv[1]);\n \t}\n \n-\tret = write_midx_file_compact(source->dirs->packed, from_midx, to_midx,\n+\tret = write_midx_file_compact(packed_source, from_midx, to_midx,\n \t\t\t\t      opts.incremental_base, opts.flags);\n \n \treturn ret;\n@@ -321,7 +321,7 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv,\n \tstatic struct option builtin_multi_pack_index_verify_options[] = {\n \t\tOPT_END(),\n \t};\n-\tstruct odb_source_files *source;\n+\tstruct odb_source_packed *packed_source;\n \n \toptions = add_common_options(builtin_multi_pack_index_verify_options);\n \n@@ -335,11 +335,11 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv,\n \tif (argc)\n \t\tusage_with_options(builtin_multi_pack_index_verify_usage,\n \t\t\t\t   options);\n-\tsource = handle_object_dir_option(the_repository);\n+\tpacked_source = handle_object_dir_option(the_repository);\n \n \tFREE_AND_NULL(options);\n \n-\treturn verify_midx_file(source->dirs->packed, opts.flags);\n+\treturn verify_midx_file(packed_source, opts.flags);\n }\n \n static int cmd_multi_pack_index_expire(int argc, const char **argv,\n@@ -350,7 +350,7 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv,\n \tstatic struct option builtin_multi_pack_index_expire_options[] = {\n \t\tOPT_END(),\n \t};\n-\tstruct odb_source_files *source;\n+\tstruct odb_source_packed *packed_source;\n \n \toptions = add_common_options(builtin_multi_pack_index_expire_options);\n \n@@ -364,11 +364,11 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv,\n \tif (argc)\n \t\tusage_with_options(builtin_multi_pack_index_expire_usage,\n \t\t\t\t   options);\n-\tsource = handle_object_dir_option(the_repository);\n+\tpacked_source = handle_object_dir_option(the_repository);\n \n \tFREE_AND_NULL(options);\n \n-\treturn expire_midx_packs(source->dirs->packed, opts.flags);\n+\treturn expire_midx_packs(packed_source, opts.flags);\n }\n \n static int cmd_multi_pack_index_repack(int argc, const char **argv,\n@@ -381,7 +381,7 @@ static int cmd_multi_pack_index_repack(int argc, const char **argv,\n \t\t  N_(\"during repack, collect pack-files of smaller size into a batch that is larger than this size\")),\n \t\tOPT_END(),\n \t};\n-\tstruct odb_source_files *source;\n+\tstruct odb_source_packed *packed_source;\n \n \toptions = add_common_options(builtin_multi_pack_index_repack_options);\n \n@@ -396,11 +396,11 @@ static int cmd_multi_pack_index_repack(int argc, const char **argv,\n \tif (argc)\n \t\tusage_with_options(builtin_multi_pack_index_repack_usage,\n \t\t\t\t   options);\n-\tsource = handle_object_dir_option(the_repository);\n+\tpacked_source = handle_object_dir_option(the_repository);\n \n \tFREE_AND_NULL(options);\n \n-\treturn midx_repack(source->dirs->packed, (size_t)opts.batch_size, opts.flags);\n+\treturn midx_repack(packed_source, (size_t)opts.batch_size, opts.flags);\n }\n \n int cmd_multi_pack_index(int argc,\ndiff --git a/odb.c b/odb.c\nindex 9b70859c23..1dc8647159 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -348,32 +348,6 @@ char *compute_alternate_path(const char *path, struct strbuf *err)\n \treturn ref_git;\n }\n \n-struct odb_source *odb_find_source(struct object_database *odb, const char *obj_dir)\n-{\n-\tstruct odb_source *source;\n-\tchar *obj_dir_real = real_pathdup(obj_dir, 1);\n-\tstruct strbuf odb_path_real = STRBUF_INIT;\n-\n-\tfor (source = odb->sources; source; source = source->next) {\n-\t\tstrbuf_realpath(&odb_path_real, source->path, 1);\n-\t\tif (!strcmp(obj_dir_real, odb_path_real.buf))\n-\t\t\tbreak;\n-\t}\n-\n-\tfree(obj_dir_real);\n-\tstrbuf_release(&odb_path_real);\n-\n-\treturn source;\n-}\n-\n-struct odb_source *odb_find_source_or_die(struct object_database *odb, const char *obj_dir)\n-{\n-\tstruct odb_source *source = odb_find_source(odb, obj_dir);\n-\tif (!source)\n-\t\tdie(_(\"could not find object directory matching %s\"), obj_dir);\n-\treturn source;\n-}\n-\n static void fill_alternate_refs_command(struct repository *repo,\n \t\t\t\t\tstruct child_process *cmd,\n \t\t\t\t\tconst char *repo_path)\ndiff --git a/odb.h b/odb.h\nindex 3715351bb3..5c86572b5d 100644\n--- a/odb.h\n+++ b/odb.h\n@@ -226,15 +226,6 @@ struct odb_fsck_options {\n  */\n int odb_fsck(struct object_database *odb, struct odb_fsck_options *opts);\n \n-/*\n- * Find source by its object directory path. Returns a `NULL` pointer in case\n- * the source could not be found.\n- */\n-struct odb_source *odb_find_source(struct object_database *odb, const char *obj_dir);\n-\n-/* Same as `odb_find_source()`, but dies in case the source doesn't exist. */\n-struct odb_source *odb_find_source_or_die(struct object_database *odb, const char *obj_dir);\n-\n /*\n  * Replace the current writable object directory with the specified temporary\n  * object directory and return the newly installed primary source. The former\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 1f4cedaffa..e5e43b1543 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -967,6 +967,32 @@ static int odb_source_files_fsck(struct odb_source *source,\n \treturn ret;\n }\n \n+struct odb_files_dir *odb_source_files_find_dir(struct object_database *odb, const char *obj_dir)\n+{\n+\tchar *obj_dir_real = real_pathdup(obj_dir, 1);\n+\tstruct strbuf odb_path_real = STRBUF_INIT;\n+\tstruct odb_files_dir *dir = NULL;\n+\tstruct odb_source *source;\n+\n+\tfor (source = odb->sources; source; source = source->next) {\n+\t\tstruct odb_source_files *files;\n+\n+\t\tif (source->type != ODB_SOURCE_FILES)\n+\t\t\tcontinue;\n+\t\tfiles = odb_source_files_downcast(source);\n+\n+\t\tstrbuf_realpath(&odb_path_real, files->dirs->abspath, 1);\n+\t\tif (!strcmp(obj_dir_real, odb_path_real.buf)) {\n+\t\t\tdir = files->dirs;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tfree(obj_dir_real);\n+\tstrbuf_release(&odb_path_real);\n+\treturn dir;\n+}\n+\n struct odb_source_files *odb_source_files_new(struct object_database *odb,\n \t\t\t\t\t      const char *path,\n \t\t\t\t\t      bool local)\ndiff --git a/odb/source-files.h b/odb/source-files.h\nindex 7f465853b1..77f4d842e0 100644\n--- a/odb/source-files.h\n+++ b/odb/source-files.h\n@@ -72,4 +72,10 @@ static inline struct odb_source_files *odb_source_files_downcast(struct odb_sour\n \treturn container_of(source, struct odb_source_files, base);\n }\n \n+/*\n+ * Find \"files\" directory by its object directory path. Returns a `NULL`\n+ * pointer in case the object directory could not be found.\n+ */\n+struct odb_files_dir *odb_source_files_find_dir(struct object_database *odb, const char *obj_dir);\n+\n #endif\ndiff --git a/t/helper/test-read-midx.c b/t/helper/test-read-midx.c\nindex 3f4bafff61..88a79fdfc8 100644\n--- a/t/helper/test-read-midx.c\n+++ b/t/helper/test-read-midx.c\n@@ -16,13 +16,13 @@ static struct multi_pack_index *setup_midx(const char *object_dir,\n \t\t\t\t\t   struct odb_source_packed **out)\n {\n \tstruct odb_source_packed *packed;\n-\tstruct odb_source *source;\n+\tstruct odb_files_dir *dir;\n \n \tsetup_git_directory(the_repository);\n \n-\tsource = odb_find_source(the_repository->objects, object_dir);\n-\tif (source) {\n-\t\tpacked = odb_source_files_downcast(source)->dirs->packed;\n+\tdir = odb_source_files_find_dir(the_repository->objects, object_dir);\n+\tif (dir) {\n+\t\tpacked = dir->packed;\n \t} else {\n \t\tpacked = odb_source_packed_new(the_repository->objects,\n \t\t\t\t\t       object_dir, false);\n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553940","messageId":"20261002-pks-odb-move-alternates-v1-6-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 06/13] odb/source-files: add the ability to have multiple object dirs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:17Z","receivedAt":"2026-10-02T10:08:44Z","isPatch":true,"body":"In a subsequent commit we'll migrate handling of alternates into the\n\"files\" backend. As part of that, the backend will need to grow the\nability to handle multiple object directories for alternates and for\ntransactions.\n\nIntroduce a `struct odb_files_dir::next` pointer so that we can have\nmultiple directories. Adapt the backend to loop through this list as\nnecessary. In general:\n\n  - For reading paths we will loop through all object directories until\n    we have found the object.\n\n  - For writing paths we will typically write to the first of our object\n    directories.\n\nThis mechanism isn't used yet as we still only ever have a single object\ndirectory at the current point in time. But that will change over\nsubsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb/source-files.c | 147 +++++++++++++++++++++++++++++++++--------------------\n odb/source-files.h |   9 ++++\n 2 files changed, 100 insertions(+), 56 deletions(-)\n\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex e5e43b1543..9389546b3e 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -63,8 +63,15 @@ static void odb_source_files_reparent(const char *old_cwd,\n static void odb_source_files_free(struct odb_source *source)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n+\n \tchdir_notify_unregister(odb_source_files_reparent, files);\n-\todb_files_dir_free(files->dirs);\n+\n+\twhile (files->dirs) {\n+\t\tstruct odb_files_dir *next = files->dirs->next;\n+\t\todb_files_dir_free(files->dirs);\n+\t\tfiles->dirs = next;\n+\t}\n+\n \todb_source_release(&files->base);\n \tfree(files);\n }\n@@ -72,8 +79,11 @@ static void odb_source_files_free(struct odb_source *source)\n static void odb_source_files_close(struct odb_source *source)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\todb_source_close(&files->dirs->loose->base);\n-\todb_source_close(&files->dirs->packed->base);\n+\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\todb_source_close(&dir->loose->base);\n+\t\todb_source_close(&dir->packed->base);\n+\t}\n }\n \n static int odb_source_files_create_on_disk(struct odb_source *source,\n@@ -168,8 +178,11 @@ static void odb_source_files_prepare(struct odb_source *source,\n \t\t\t\t     enum odb_prepare_flags flags)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\todb_source_prepare(&files->dirs->loose->base, flags);\n-\todb_source_prepare(&files->dirs->packed->base, flags);\n+\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\todb_source_prepare(&dir->loose->base, flags);\n+\t\todb_source_prepare(&dir->packed->base, flags);\n+\t}\n }\n \n static enum odb_read_status odb_source_files_read_object_info(struct odb_source *source,\n@@ -179,28 +192,33 @@ static enum odb_read_status odb_source_files_read_object_info(struct odb_source\n \t\t\t\t\t\t\t      struct strbuf *errmsg)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tenum odb_read_status ret_packed, ret_loose;\n-\n-\tret_packed = odb_source_read_object_info(&files->dirs->packed->base, oid, oi,\n-\t\t\t\t\t\t flags, errmsg);\n-\tif (!ret_packed)\n-\t\treturn 0;\n-\n-\tret_loose = odb_source_read_object_info(&files->dirs->loose->base, oid, oi, flags,\n-\t\t\t\t\t\tret_packed == ODB_READ_NOT_FOUND ? errmsg : NULL);\n-\tif (!ret_loose)\n-\t\treturn 0;\n+\tenum odb_read_status status = ODB_READ_NOT_FOUND;\n \n \t/*\n-\t * Reading the packed object may have failed even though the object\n-\t * exists, for example because it is corrupt. Report this failure to\n-\t * the caller in case neither of the sources was able to read the\n-\t * object, and prefer the error of the packed source in case both\n-\t * reads have failed.\n+\t * Reading an object may fail even though the object exists, for\n+\t * example because it is corrupt. Report this failure to the caller in\n+\t * case none of the directories was able to read the object, and\n+\t * prefer the first such error in case multiple reads have failed.\n \t */\n-\tif (ret_packed != ODB_READ_NOT_FOUND)\n-\t\treturn ret_packed;\n-\treturn ret_loose;\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tenum odb_read_status ret;\n+\n+\t\tret = odb_source_read_object_info(&dir->packed->base, oid, oi, flags,\n+\t\t\t\t\t\t  status == ODB_READ_NOT_FOUND ? errmsg : NULL);\n+\t\tif (!ret)\n+\t\t\treturn 0;\n+\t\tif (ret != ODB_READ_NOT_FOUND && status == ODB_READ_NOT_FOUND)\n+\t\t\tstatus = ret;\n+\n+\t\tret = odb_source_read_object_info(&dir->loose->base, oid, oi, flags,\n+\t\t\t\t\t\t  status == ODB_READ_NOT_FOUND ? errmsg : NULL);\n+\t\tif (!ret)\n+\t\t\treturn 0;\n+\t\tif (ret != ODB_READ_NOT_FOUND && status == ODB_READ_NOT_FOUND)\n+\t\t\tstatus = ret;\n+\t}\n+\n+\treturn status;\n }\n \n static int odb_source_files_read_object_stream(struct odb_stream **out,\n@@ -208,9 +226,12 @@ static int odb_source_files_read_object_stream(struct odb_stream **out,\n \t\t\t\t\t       const struct object_id *oid)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tif (!odb_source_read_object_stream(out, &files->dirs->packed->base, oid) ||\n-\t    !odb_source_read_object_stream(out, &files->dirs->loose->base, oid))\n-\t\treturn 0;\n+\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next)\n+\t\tif (!odb_source_read_object_stream(out, &dir->packed->base, oid) ||\n+\t\t    !odb_source_read_object_stream(out, &dir->loose->base, oid))\n+\t\t\treturn 0;\n+\n \treturn -1;\n }\n \n@@ -223,16 +244,21 @@ static int odb_source_files_for_each_object(struct odb_source *source,\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tint ret;\n \n-\tif (!(opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)) {\n-\t\tret = odb_source_for_each_object(&files->dirs->loose->base, request, cb, cb_data, opts);\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tif (opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY && !dir->local)\n+\t\t\tcontinue;\n+\n+\t\tif (!(opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)) {\n+\t\t\tret = odb_source_for_each_object(&dir->loose->base, request, cb, cb_data, opts);\n+\t\t\tif (ret)\n+\t\t\t\treturn ret;\n+\t\t}\n+\n+\t\tret = odb_source_for_each_object(&dir->packed->base, request, cb, cb_data, opts);\n \t\tif (ret)\n \t\t\treturn ret;\n \t}\n \n-\tret = odb_source_for_each_object(&files->dirs->packed->base, request, cb, cb_data, opts);\n-\tif (ret)\n-\t\treturn ret;\n-\n \treturn 0;\n }\n \n@@ -241,21 +267,23 @@ static int odb_source_files_count_objects(struct odb_source *source,\n \t\t\t\t\t  unsigned long *out)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tunsigned long count;\n+\tunsigned long count = 0;\n \tint ret;\n \n-\tret = odb_source_count_objects(&files->dirs->packed->base, flags, &count);\n-\tif (ret < 0)\n-\t\tgoto out;\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tunsigned long dir_count;\n \n-\tif (!(flags & ODB_COUNT_OBJECTS_APPROXIMATE)) {\n-\t\tunsigned long loose_count;\n-\n-\t\tret = odb_source_count_objects(&files->dirs->loose->base, flags, &loose_count);\n+\t\tret = odb_source_count_objects(&dir->packed->base, flags, &dir_count);\n \t\tif (ret < 0)\n \t\t\tgoto out;\n+\t\tcount += dir_count;\n \n-\t\tcount += loose_count;\n+\t\tif (!(flags & ODB_COUNT_OBJECTS_APPROXIMATE)) {\n+\t\t\tret = odb_source_count_objects(&dir->loose->base, flags, &dir_count);\n+\t\t\tif (ret < 0)\n+\t\t\t\tgoto out;\n+\t\t\tcount += dir_count;\n+\t\t}\n \t}\n \n \t*out = count;\n@@ -272,15 +300,17 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source,\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tunsigned len = min_len;\n-\tint ret;\n+\tint ret = 0;\n \n-\tret = odb_source_find_abbrev_len(&files->dirs->packed->base, oid, len, &len);\n-\tif (ret < 0)\n-\t\tgoto out;\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tret = odb_source_find_abbrev_len(&dir->packed->base, oid, len, &len);\n+\t\tif (ret < 0)\n+\t\t\tgoto out;\n \n-\tret = odb_source_find_abbrev_len(&files->dirs->loose->base, oid, len, &len);\n-\tif (ret < 0)\n-\t\tgoto out;\n+\t\tret = odb_source_find_abbrev_len(&dir->loose->base, oid, len, &len);\n+\t\tif (ret < 0)\n+\t\t\tgoto out;\n+\t}\n \n \t*out = len;\n \tret = 0;\n@@ -294,9 +324,12 @@ static int odb_source_files_freshen_object(struct odb_source *source,\n \t\t\t\t\t   const time_t *mtime)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tif (odb_source_freshen_object(&files->dirs->packed->base, oid, mtime) ||\n-\t    odb_source_freshen_object(&files->dirs->loose->base, oid, mtime))\n-\t\treturn 1;\n+\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next)\n+\t\tif (odb_source_freshen_object(&dir->packed->base, oid, mtime) ||\n+\t\t    odb_source_freshen_object(&dir->loose->base, oid, mtime))\n+\t\t\treturn 1;\n+\n \treturn 0;\n }\n \n@@ -958,11 +991,13 @@ static int odb_source_files_fsck(struct odb_source *source,\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tint ret = 0;\n \n-\tif (!(opts->flags & ODB_FSCK_FULL) && !source->local)\n-\t\treturn 0;\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tif (!(opts->flags & ODB_FSCK_FULL) && !dir->local)\n+\t\t\tcontinue;\n \n-\tret |= odb_source_fsck(&files->dirs->loose->base, opts);\n-\tret |= odb_source_fsck(&files->dirs->packed->base, opts);\n+\t\tret |= odb_source_fsck(&dir->loose->base, opts);\n+\t\tret |= odb_source_fsck(&dir->packed->base, opts);\n+\t}\n \n \treturn ret;\n }\ndiff --git a/odb/source-files.h b/odb/source-files.h\nindex 77f4d842e0..36af0c1b8b 100644\n--- a/odb/source-files.h\n+++ b/odb/source-files.h\n@@ -15,6 +15,9 @@ struct odb_files_dir {\n \t/* Absolute path to the object directory. */\n \tchar *abspath;\n \n+\t/* List of alternate object directories. */\n+\tstruct odb_files_dir *next;\n+\n \t/* The two sources derived from this object directory. */\n \tstruct odb_source_loose *loose;\n \tstruct odb_source_packed *packed;\n@@ -36,6 +39,12 @@ void odb_files_dir_free(struct odb_files_dir *dir);\n  */\n struct odb_source_files {\n \tstruct odb_source base;\n+\n+\t/*\n+\t * List of all object directories; the main directory is first (and\n+\t * cannot be NULL after initialization). Subsequent directories are\n+\t * alternates.\n+\t */\n \tstruct odb_files_dir *dirs;\n };\n \n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553941","messageId":"20261002-pks-odb-move-alternates-v1-7-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 07/13] tmp-objdir: absorb logic to set and restore primary sources","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:18Z","receivedAt":"2026-10-02T10:08:47Z","isPatch":true,"body":"The functions `odb_set_temporary_primary_source()` and\n`odb_restore_primary_source()` can be used to adapt the primary object\ndatabase source. Nowadays though we only have a single user of this\nsubsystem left, which is the \"tmp-objdir\" subsystem.\n\nDespite that, this functionality is also becoming less useful overall as\nalternates are becoming an implementation detail of the \"files\" backend.\nAnd with that change, there will only ever be a single source attached\nto the object database anyway.\n\nMove the logic into the \"tmp-objdir\" subsystem accordingly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb.c        | 45 ---------------------------------------------\n odb.h        | 17 -----------------\n tmp-objdir.c | 38 ++++++++++++++++++++++++++++++++++----\n 3 files changed, 34 insertions(+), 66 deletions(-)\n\ndiff --git a/odb.c b/odb.c\nindex 1dc8647159..8b54271c27 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -239,51 +239,6 @@ static struct odb_source *odb_add_alternate_recursively(struct object_database *\n \treturn alternate;\n }\n \n-struct odb_source *odb_set_temporary_primary_source(struct object_database *odb,\n-\t\t\t\t\t\t    const char *dir, int will_destroy,\n-\t\t\t\t\t\t    struct odb_source **prev_source)\n-{\n-\tstruct odb_source *source;\n-\n-\t/*\n-\t * Make a new primary odb and link the old primary ODB in as an\n-\t * alternate\n-\t */\n-\tsource = odb_source_new(odb, dir, false);\n-\n-\t/*\n-\t * Disable ref updates while a temporary odb is active, since\n-\t * the objects in the database may roll back.\n-\t */\n-\todb->repo->disable_ref_updates = true;\n-\tsource->will_destroy = will_destroy;\n-\tsource->next = odb->sources;\n-\todb->sources = source;\n-\n-\tif (prev_source)\n-\t\t*prev_source = source->next;\n-\n-\treturn source;\n-}\n-\n-void odb_restore_primary_source(struct object_database *odb,\n-\t\t\t\tstruct odb_source *restore_source,\n-\t\t\t\tconst char *old_path)\n-{\n-\tstruct odb_source *cur_source = odb->sources;\n-\n-\tif (strcmp(old_path, cur_source->path))\n-\t\tBUG(\"expected %s as primary object store; found %s\",\n-\t\t    old_path, cur_source->path);\n-\n-\tif (cur_source->next != restore_source)\n-\t\tBUG(\"we expect the old primary object store to be the first alternate\");\n-\n-\todb->repo->disable_ref_updates = false;\n-\todb->sources = restore_source;\n-\todb_source_free(cur_source);\n-}\n-\n char *compute_alternate_path(const char *path, struct strbuf *err)\n {\n \tchar *ref_git = NULL;\ndiff --git a/odb.h b/odb.h\nindex 5c86572b5d..4143812f55 100644\n--- a/odb.h\n+++ b/odb.h\n@@ -226,23 +226,6 @@ struct odb_fsck_options {\n  */\n int odb_fsck(struct object_database *odb, struct odb_fsck_options *opts);\n \n-/*\n- * Replace the current writable object directory with the specified temporary\n- * object directory and return the newly installed primary source. The former\n- * primary source is reported via `prev_source` when non-NULL.\n- */\n-struct odb_source *odb_set_temporary_primary_source(struct object_database *odb,\n-\t\t\t\t\t\t    const char *dir, int will_destroy,\n-\t\t\t\t\t\t    struct odb_source **prev_source);\n-\n-/*\n- * Restore the primary source that was previously replaced by\n- * `odb_set_temporary_primary_source()`.\n- */\n-void odb_restore_primary_source(struct object_database *odb,\n-\t\t\t\tstruct odb_source *restore_source,\n-\t\t\t\tconst char *old_path);\n-\n /*\n  * Iterate through all alternates of the database and execute the provided\n  * callback function for each of them. Stop iterating once the callback\ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex deaaf6ba2e..31a7920be7 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -51,6 +51,26 @@ static void tmp_objdir_reparent(const char *old_cwd,\n \tfree(path);\n }\n \n+/*\n+ * Restore the primary source that was previously replaced by\n+ * `tmp_objdir_replace_primary_odb()`.\n+ */\n+static void tmp_objdir_restore_source(struct tmp_objdir *t)\n+{\n+\tstruct odb_source *cur_source = t->repo->objects->sources;\n+\n+\tif (strcmp(t->path.buf, cur_source->path))\n+\t\tBUG(\"expected %s as primary object store; found %s\",\n+\t\t    t->path.buf, cur_source->path);\n+\n+\tif (cur_source->next != t->prev_source)\n+\t\tBUG(\"we expect the old primary object store to be the first alternate\");\n+\n+\tt->repo->disable_ref_updates = false;\n+\tt->repo->objects->sources = t->prev_source;\n+\todb_source_free(cur_source);\n+}\n+\n int tmp_objdir_destroy(struct tmp_objdir *t)\n {\n \tint err;\n@@ -62,7 +82,7 @@ int tmp_objdir_destroy(struct tmp_objdir *t)\n \t\tthe_tmp_objdir = NULL;\n \n \tif (t->prev_source)\n-\t\todb_restore_primary_source(t->repo->objects, t->prev_source, t->path.buf);\n+\t\ttmp_objdir_restore_source(t);\n \n \terr = remove_dir_recursively(&t->path, 0);\n \n@@ -298,7 +318,7 @@ int tmp_objdir_migrate(struct tmp_objdir *t)\n \tif (t->prev_source) {\n \t\tif (t->repo->objects->sources->will_destroy)\n \t\t\tBUG(\"migrating an ODB that was marked for destruction\");\n-\t\todb_restore_primary_source(t->repo->objects, t->prev_source, t->path.buf);\n+\t\ttmp_objdir_restore_source(t);\n \t\tt->prev_source = NULL;\n \t}\n \n@@ -328,6 +348,16 @@ struct odb_source *tmp_objdir_replace_primary_odb(struct tmp_objdir *t,\n \t\tBUG(\"the primary object database is already replaced\");\n \tt->will_destroy = will_destroy;\n \n-\treturn odb_set_temporary_primary_source(t->repo->objects, t->path.buf,\n-\t\t\t\t\t\twill_destroy, &t->prev_source);\n+\t/*\n+\t * Make a new primary source and link the old primary source in as an\n+\t * alternate. Disable ref updates while a temporary source is active,\n+\t * since the objects in the database may roll back.\n+\t */\n+\tt->prev_source = t->repo->objects->sources;\n+\tt->repo->objects->sources = odb_source_new(t->repo->objects, t->path.buf, false);\n+\tt->repo->objects->sources->next = t->prev_source;\n+\tt->repo->objects->sources->will_destroy = will_destroy;\n+\tt->repo->disable_ref_updates = true;\n+\n+\treturn t->repo->objects->sources;\n }\n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553942","messageId":"20261002-pks-odb-move-alternates-v1-8-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 08/13] tmp-objdir: manage quarantine as an object directory","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:19Z","receivedAt":"2026-10-02T10:08:50Z","isPatch":true,"body":"When creating a quarantine directory via the \"tmp-objdir\" subsystem we\ncreate a new \"files\" backend that new objects part of the transaction\ncan be written to. In a future commit though we'll move handling of\nalternates into the \"files\" backend, and as part of that it will no\nlonger be possible for us to have multiple sources attached to a single\nobject database.\n\nIn a preceding commit, we have prepared the \"files\" backend to be able\nto handle multiple object directories. We don't use that mechanism for\nalternates yet, but will start doing so in a subsequent commit. But with\nthat infrastructure ready we can already migrate tmp-objdirs over to use\nthis new mechanism.\n\nAdapt the subsystem so we create a `struct odb_files_dir` instead of a\nnew \"files\" source.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c |  4 ++--\n tmp-objdir.c  | 48 +++++++++++++++++++++++++++++-------------------\n tmp-objdir.h  |  4 ++--\n 3 files changed, 33 insertions(+), 23 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex a4cbf8b081..fe9139c96d 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -488,7 +488,7 @@ struct odb_transaction_files {\n \tenum odb_transaction_flags flags;\n \n \tstruct tmp_objdir *objdir;\n-\tstruct odb_source *quarantine;\n+\tstruct odb_files_dir *quarantine;\n \tstruct transaction_packfile packfile;\n \tconst char *prefix;\n \n@@ -1443,7 +1443,7 @@ static int odb_transaction_files_write_pack(struct odb_transaction *base,\n \t\t\treturn -1;\n \t\t}\n \n-\t\todb_source_prepare(transaction->quarantine,\n+\t\todb_source_prepare(&transaction->quarantine->packed->base,\n \t\t\t\t   ODB_PREPARE_FLUSH_CACHES);\n \t}\n \ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex 31a7920be7..a3903b6e6d 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -12,13 +12,17 @@\n #include \"quote.h\"\n #include \"odb.h\"\n #include \"odb/source.h\"\n+#include \"odb/source-files.h\"\n+#include \"odb/source-loose.h\"\n+#include \"odb/source-packed.h\"\n #include \"repository.h\"\n \n struct tmp_objdir {\n \tstruct repository *repo;\n \tstruct strbuf path;\n \tstruct strvec env;\n-\tstruct odb_source *prev_source;\n+\tstruct odb_files_dir *temp_dir;\n+\tstruct odb_files_dir *orig_dir;\n \tint will_destroy;\n };\n \n@@ -57,18 +61,19 @@ static void tmp_objdir_reparent(const char *old_cwd,\n  */\n static void tmp_objdir_restore_source(struct tmp_objdir *t)\n {\n-\tstruct odb_source *cur_source = t->repo->objects->sources;\n+\tstruct odb_source_files *files = odb_source_files_downcast(t->repo->objects->sources);\n+\tstruct odb_files_dir *cur_dir = files->dirs;\n \n-\tif (strcmp(t->path.buf, cur_source->path))\n+\tif (t->temp_dir != files->dirs)\n \t\tBUG(\"expected %s as primary object store; found %s\",\n-\t\t    t->path.buf, cur_source->path);\n+\t\t    t->temp_dir->abspath, cur_dir->abspath);\n \n-\tif (cur_source->next != t->prev_source)\n+\tif (t->temp_dir->next != t->orig_dir)\n \t\tBUG(\"we expect the old primary object store to be the first alternate\");\n \n \tt->repo->disable_ref_updates = false;\n-\tt->repo->objects->sources = t->prev_source;\n-\todb_source_free(cur_source);\n+\tfiles->dirs = t->orig_dir;\n+\todb_files_dir_free(cur_dir);\n }\n \n int tmp_objdir_destroy(struct tmp_objdir *t)\n@@ -81,7 +86,7 @@ int tmp_objdir_destroy(struct tmp_objdir *t)\n \tif (t == the_tmp_objdir)\n \t\tthe_tmp_objdir = NULL;\n \n-\tif (t->prev_source)\n+\tif (t->orig_dir)\n \t\ttmp_objdir_restore_source(t);\n \n \terr = remove_dir_recursively(&t->path, 0);\n@@ -315,11 +320,11 @@ int tmp_objdir_migrate(struct tmp_objdir *t)\n \tif (!t)\n \t\treturn 0;\n \n-\tif (t->prev_source) {\n-\t\tif (t->repo->objects->sources->will_destroy)\n+\tif (t->orig_dir) {\n+\t\tif (t->will_destroy)\n \t\t\tBUG(\"migrating an ODB that was marked for destruction\");\n \t\ttmp_objdir_restore_source(t);\n-\t\tt->prev_source = NULL;\n+\t\tt->orig_dir = NULL;\n \t}\n \n \tstrbuf_addbuf(&src, &t->path);\n@@ -341,10 +346,12 @@ const char **tmp_objdir_env(const struct tmp_objdir *t)\n \treturn t->env.v;\n }\n \n-struct odb_source *tmp_objdir_replace_primary_odb(struct tmp_objdir *t,\n-\t\t\t\t\t\t  int will_destroy)\n+struct odb_files_dir *tmp_objdir_replace_primary_odb(struct tmp_objdir *t,\n+\t\t\t\t\t\t     int will_destroy)\n {\n-\tif (t->prev_source)\n+\tstruct odb_source_files *files = odb_source_files_downcast(t->repo->objects->sources);\n+\n+\tif (t->temp_dir)\n \t\tBUG(\"the primary object database is already replaced\");\n \tt->will_destroy = will_destroy;\n \n@@ -353,11 +360,14 @@ struct odb_source *tmp_objdir_replace_primary_odb(struct tmp_objdir *t,\n \t * alternate. Disable ref updates while a temporary source is active,\n \t * since the objects in the database may roll back.\n \t */\n-\tt->prev_source = t->repo->objects->sources;\n-\tt->repo->objects->sources = odb_source_new(t->repo->objects, t->path.buf, false);\n-\tt->repo->objects->sources->next = t->prev_source;\n-\tt->repo->objects->sources->will_destroy = will_destroy;\n+\tt->temp_dir = odb_files_dir_new(t->repo->objects, t->path.buf, false);\n+\tt->temp_dir->loose->base.will_destroy = will_destroy;\n+\tt->temp_dir->packed->base.will_destroy = will_destroy;\n+\tt->temp_dir->next = files->dirs;\n+\n+\tt->orig_dir = files->dirs;\n+\tfiles->dirs = t->temp_dir;\n \tt->repo->disable_ref_updates = true;\n \n-\treturn t->repo->objects->sources;\n+\treturn t->temp_dir;\n }\ndiff --git a/tmp-objdir.h b/tmp-objdir.h\nindex 05f0d08d10..5ed0db1ee0 100644\n--- a/tmp-objdir.h\n+++ b/tmp-objdir.h\n@@ -61,7 +61,7 @@ void tmp_objdir_discard_objects(struct tmp_objdir *);\n  * If will_destroy is nonzero, the object directory may not be migrated. Returns\n  * the newly installed primary source.\n  */\n-struct odb_source *tmp_objdir_replace_primary_odb(struct tmp_objdir *,\n-\t\t\t\t\t\t  int will_destroy);\n+struct odb_files_dir *tmp_objdir_replace_primary_odb(struct tmp_objdir *,\n+\t\t\t\t\t\t     int will_destroy);\n \n #endif /* TMP_OBJDIR_H */\n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553943","messageId":"20261002-pks-odb-move-alternates-v1-9-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 09/13] tmp-objdir: replace primary source at creation time","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:20Z","receivedAt":"2026-10-02T10:08:52Z","isPatch":true,"body":"When creating transactions via the \"tmp-objdir\" subsystem callers are\nexpected to first call `tmp_objdir_create()` to create the quarantine\ndirectory and then `tmp_objdir_replace_primary_odb()` to activate that\nquarantine directory as the primary object database source so that all\nnewly written objects are written into it.\n\nThis dance is performed by all users of temporary object directories, so\nthat makes the setup unnecessarily involved. Merge the logic to replace\nthe primary object database source into `tmp_objdir_create()` to\nsimplify the dance.\n\nNote that with the quarantine directory being installed by\n`tmp_objdir_create()`, users of this function no longer get a pointer to\nthe underlying quarantine directory. So instead of repreparing only that\ndirectory, we now reprepare the whole source instead, and as we know\nthat the source has wired up the quarantine directory already, this will\ncause us to reprepare the quarantine directory, too.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n log-tree.c    |  3 +--\n object-file.c |  8 ++------\n tmp-objdir.c  | 47 +++++++++++++++++++----------------------------\n tmp-objdir.h  | 18 +++++-------------\n 4 files changed, 27 insertions(+), 49 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 83a3c4bf9b..361dfadbb3 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -1043,10 +1043,9 @@ static int do_remerge_diff(struct rev_info *opt,\n \t * into the alternative object store list as the primary.\n \t */\n \tif (opt->remerge_diff && !opt->remerge_objdir) {\n-\t\topt->remerge_objdir = tmp_objdir_create(the_repository, \"remerge-diff\");\n+\t\topt->remerge_objdir = tmp_objdir_create(the_repository, \"remerge-diff\", 1);\n \t\tif (!opt->remerge_objdir)\n \t\t\treturn error(_(\"unable to create temporary object directory\"));\n-\t\ttmp_objdir_replace_primary_odb(opt->remerge_objdir, 1);\n \t}\n \n \t/* Setup merge options */\ndiff --git a/object-file.c b/object-file.c\nindex fe9139c96d..02521afcaf 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -488,7 +488,6 @@ struct odb_transaction_files {\n \tenum odb_transaction_flags flags;\n \n \tstruct tmp_objdir *objdir;\n-\tstruct odb_files_dir *quarantine;\n \tstruct transaction_packfile packfile;\n \tconst char *prefix;\n \n@@ -511,12 +510,10 @@ int odb_transaction_files_prepare(struct odb_transaction *base)\n \tif (!transaction || transaction->objdir)\n \t\treturn 0;\n \n-\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, transaction->prefix);\n+\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, transaction->prefix, 0);\n \tif (!transaction->objdir)\n \t\treturn error(_(\"unable to create temporary object directory\"));\n \n-\ttransaction->quarantine = tmp_objdir_replace_primary_odb(transaction->objdir, 0);\n-\n \treturn 0;\n }\n \n@@ -1443,8 +1440,7 @@ static int odb_transaction_files_write_pack(struct odb_transaction *base,\n \t\t\treturn -1;\n \t\t}\n \n-\t\todb_source_prepare(&transaction->quarantine->packed->base,\n-\t\t\t\t   ODB_PREPARE_FLUSH_CACHES);\n+\t\todb_source_prepare(base->source, ODB_PREPARE_FLUSH_CACHES);\n \t}\n \n \treturn 0;\ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex a3903b6e6d..719b55e580 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -57,7 +57,7 @@ static void tmp_objdir_reparent(const char *old_cwd,\n \n /*\n  * Restore the primary source that was previously replaced by\n- * `tmp_objdir_replace_primary_odb()`.\n+ * `tmp_objdir_create()`.\n  */\n static void tmp_objdir_restore_source(struct tmp_objdir *t)\n {\n@@ -157,8 +157,10 @@ static int setup_tmp_objdir(const char *root)\n }\n \n struct tmp_objdir *tmp_objdir_create(struct repository *r,\n-\t\t\t\t     const char *prefix)\n+\t\t\t\t     const char *prefix,\n+\t\t\t\t     int will_destroy)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(r->objects->sources);\n \tstatic int installed_handlers;\n \tstruct tmp_objdir *t;\n \n@@ -167,6 +169,7 @@ struct tmp_objdir *tmp_objdir_create(struct repository *r,\n \n \tt = xcalloc(1, sizeof(*t));\n \tt->repo = r;\n+\tt->will_destroy = will_destroy;\n \tstrbuf_init(&t->path, 0);\n \tstrvec_init(&t->env);\n \n@@ -204,6 +207,20 @@ struct tmp_objdir *tmp_objdir_create(struct repository *r,\n \tenv_replace(&t->env, GIT_QUARANTINE_ENVIRONMENT,\n \t\t    absolute_path(t->path.buf));\n \n+\t/*\n+\t * Make a new primary source and link the old primary source in as an\n+\t * alternate. Disable ref updates while a temporary source is active,\n+\t * since the objects in the database may roll back.\n+\t */\n+\tt->temp_dir = odb_files_dir_new(t->repo->objects, t->path.buf, false);\n+\tt->temp_dir->loose->base.will_destroy = will_destroy;\n+\tt->temp_dir->packed->base.will_destroy = will_destroy;\n+\tt->temp_dir->next = files->dirs;\n+\n+\tt->orig_dir = files->dirs;\n+\tfiles->dirs = t->temp_dir;\n+\tt->repo->disable_ref_updates = true;\n+\n \treturn t;\n }\n \n@@ -345,29 +362,3 @@ const char **tmp_objdir_env(const struct tmp_objdir *t)\n \t\treturn NULL;\n \treturn t->env.v;\n }\n-\n-struct odb_files_dir *tmp_objdir_replace_primary_odb(struct tmp_objdir *t,\n-\t\t\t\t\t\t     int will_destroy)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(t->repo->objects->sources);\n-\n-\tif (t->temp_dir)\n-\t\tBUG(\"the primary object database is already replaced\");\n-\tt->will_destroy = will_destroy;\n-\n-\t/*\n-\t * Make a new primary source and link the old primary source in as an\n-\t * alternate. Disable ref updates while a temporary source is active,\n-\t * since the objects in the database may roll back.\n-\t */\n-\tt->temp_dir = odb_files_dir_new(t->repo->objects, t->path.buf, false);\n-\tt->temp_dir->loose->base.will_destroy = will_destroy;\n-\tt->temp_dir->packed->base.will_destroy = will_destroy;\n-\tt->temp_dir->next = files->dirs;\n-\n-\tt->orig_dir = files->dirs;\n-\tfiles->dirs = t->temp_dir;\n-\tt->repo->disable_ref_updates = true;\n-\n-\treturn t->temp_dir;\n-}\ndiff --git a/tmp-objdir.h b/tmp-objdir.h\nindex 5ed0db1ee0..a301b01d84 100644\n--- a/tmp-objdir.h\n+++ b/tmp-objdir.h\n@@ -11,7 +11,7 @@\n  * Example:\n  *\n  *\tstruct child_process child = CHILD_PROCESS_INIT;\n- *\tstruct tmp_objdir *t = tmp_objdir_create(repo, \"incoming\");\n+ *\tstruct tmp_objdir *t = tmp_objdir_create(repo, \"incoming\", 0);\n  *\tstrvec_push(&child.args, cmd);\n  *\tstrvec_pushv(&child.env, tmp_objdir_env(t));\n  *\tif (!run_command(&child)) && !tmp_objdir_migrate(t))\n@@ -25,10 +25,11 @@ struct repository;\n struct tmp_objdir;\n \n /*\n- * Create a new temporary object directory with the specified prefix;\n- * returns NULL on failure.\n+ * Create a new temporary object directory with the specified prefix and\n+ * install the directory as the primary write target; returns NULL on failure.\n  */\n-struct tmp_objdir *tmp_objdir_create(struct repository *r, const char *prefix);\n+struct tmp_objdir *tmp_objdir_create(struct repository *r, const char *prefix,\n+\t\t\t\t     int will_destroy);\n \n /*\n  * Return a list of environment strings, suitable for use with\n@@ -55,13 +56,4 @@ int tmp_objdir_destroy(struct tmp_objdir *);\n  */\n void tmp_objdir_discard_objects(struct tmp_objdir *);\n \n-/*\n- * Replaces the writable object store in the current process with the temporary\n- * object directory and makes the former main object store an alternate.\n- * If will_destroy is nonzero, the object directory may not be migrated. Returns\n- * the newly installed primary source.\n- */\n-struct odb_files_dir *tmp_objdir_replace_primary_odb(struct tmp_objdir *,\n-\t\t\t\t\t\t     int will_destroy);\n-\n #endif /* TMP_OBJDIR_H */\n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553944","messageId":"20261002-pks-odb-move-alternates-v1-10-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 10/13] odb/source: make `will_destroy` an implementation detail","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:21Z","receivedAt":"2026-10-02T10:08:55Z","isPatch":true,"body":"The `struct odb_source::will_destroy` flag tracks whether a source is\npart of a transaction that we know we'll destruct anyway. If so, the\nbackend can optimize for that particular case, for example by not\nflushing any data to disk.\n\nWhile the intent is sensible, it assumes that transactions are backed by\na separate source that's being linked into the object database. But that\nmay or may not be true, as backends may have significantly better ways\nto achieve the same. So the assumption doesn't make much sense in the\nfirst place, as we're now tracking backend-specific details on the\ngeneric `struct odb_source` level.\n\nMove the field from the generic source into the \"loose\" source, as this\nis the only source that ever makes use of it anyway.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb/source-loose.c | 2 +-\n odb/source-loose.h | 3 +++\n odb/source.h       | 5 -----\n tmp-objdir.c       | 4 +---\n 4 files changed, 5 insertions(+), 9 deletions(-)\n\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 3c9edba46a..b2fbccd3c0 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -599,7 +599,7 @@ static int odb_source_loose_freshen_object(struct odb_source *source,\n static void close_loose_object(struct odb_source_loose *loose,\n \t\t\t       int fd, const char *filename)\n {\n-\tif (loose->base.will_destroy)\n+\tif (loose->will_destroy)\n \t\tgoto out;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\ndiff --git a/odb/source-loose.h b/odb/source-loose.h\nindex 3cf2e1f8f1..7c7e845fe3 100644\n--- a/odb/source-loose.h\n+++ b/odb/source-loose.h\n@@ -28,6 +28,9 @@ struct odb_source_loose {\n \n \t/* Map between object IDs for loose objects. */\n \tstruct loose_object_map *map;\n+\n+\t/* Whether this is a source that will never be committed to disk. */\n+\tint will_destroy;\n };\n \n struct odb_source_loose *odb_source_loose_new(struct object_database *odb,\ndiff --git a/odb/source.h b/odb/source.h\nindex ea00873763..9fd2b2e5b5 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -81,11 +81,6 @@ struct odb_source {\n \t */\n \tbool local;\n \n-\t/*\n-\t * This object store is ephemeral, so there is no need to fsync.\n-\t */\n-\tint will_destroy;\n-\n \t/*\n \t * Path to the source. If this is a relative path, it is relative to\n \t * the current working directory.\ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex 719b55e580..2f2ffbbc7d 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -14,7 +14,6 @@\n #include \"odb/source.h\"\n #include \"odb/source-files.h\"\n #include \"odb/source-loose.h\"\n-#include \"odb/source-packed.h\"\n #include \"repository.h\"\n \n struct tmp_objdir {\n@@ -213,8 +212,7 @@ struct tmp_objdir *tmp_objdir_create(struct repository *r,\n \t * since the objects in the database may roll back.\n \t */\n \tt->temp_dir = odb_files_dir_new(t->repo->objects, t->path.buf, false);\n-\tt->temp_dir->loose->base.will_destroy = will_destroy;\n-\tt->temp_dir->packed->base.will_destroy = will_destroy;\n+\tt->temp_dir->loose->will_destroy = will_destroy;\n \tt->temp_dir->next = files->dirs;\n \n \tt->orig_dir = files->dirs;\n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553945","messageId":"20261002-pks-odb-move-alternates-v1-11-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 11/13] odb/source-files: extract reading alternates","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:22Z","receivedAt":"2026-10-02T10:08:58Z","isPatch":true,"body":"In the next commit we'll add a new callsite that wants to read\nalternates without having a proper object database source for a given\nalternate available to it. Prepare for this by extracting the logic into\na separate function that only requires an object directory path as\ninput.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb/source-files.c | 33 +++++++++++++++++++--------------\n 1 file changed, 19 insertions(+), 14 deletions(-)\n\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 9389546b3e..6aaf625352 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -174,6 +174,24 @@ static int odb_source_files_create_on_disk(struct odb_source *source,\n \treturn ret;\n }\n \n+static int read_alternates(const char *object_dir, struct strvec *out)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tchar *path;\n+\n+\tpath = xstrfmt(\"%s/info/alternates\", object_dir);\n+\tif (strbuf_read_file(&buf, path, 1024) < 0) {\n+\t\twarn_on_fopen_errors(path);\n+\t\tfree(path);\n+\t\treturn 0;\n+\t}\n+\tparse_alternates(buf.buf, '\\n', object_dir, out);\n+\n+\tstrbuf_release(&buf);\n+\tfree(path);\n+\treturn 0;\n+}\n+\n static void odb_source_files_prepare(struct odb_source *source,\n \t\t\t\t     enum odb_prepare_flags flags)\n {\n@@ -364,20 +382,7 @@ static int odb_source_files_begin_transaction(struct odb_source *source,\n static int odb_source_files_read_alternates(struct odb_source *source,\n \t\t\t\t\t    struct strvec *out)\n {\n-\tstruct strbuf buf = STRBUF_INIT;\n-\tchar *path;\n-\n-\tpath = xstrfmt(\"%s/info/alternates\", source->path);\n-\tif (strbuf_read_file(&buf, path, 1024) < 0) {\n-\t\twarn_on_fopen_errors(path);\n-\t\tfree(path);\n-\t\treturn 0;\n-\t}\n-\tparse_alternates(buf.buf, '\\n', source->path, out);\n-\n-\tstrbuf_release(&buf);\n-\tfree(path);\n-\treturn 0;\n+\treturn read_alternates(source->path, out);\n }\n \n static int too_many_loose_objects(struct odb_source_files *files, int limit)\n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553947","messageId":"20261002-pks-odb-move-alternates-v1-12-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 12/13] odb/source-files: move alternates into the backend","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:23Z","receivedAt":"2026-10-02T10:09:02Z","isPatch":true,"body":"Originally, when designing pluggable object databases the goal was that\nthe object database can have multiple sources, and every source attached\nto it could use a different backend. This would have allowed for quite a\nlot of flexibility, as you could trivially mix and match different kinds\nof object storages in whatever way you like.\n\nBut while well-intentioned, this design led to a bunch of conceptual\nproblems:\n\n  - We're now trying to read objects in source order, whereas we\n    previously tried to read objects via packfiles before trying to read\n    them via loose objects. This led to a performance regression when\n    using alternates or when using a quarantine directory.\n\n  - Some data structures are supposed to only ever exist once, like for\n    example bitmaps and commit graphs. At the same time, those data\n    structures also span across the union of all objects, so they may\n    cross sources.\n\n  - It is unclear how we can extend GIT_OBJECT_DIRECTORY or\n    GIT_ALTERNATE_OBJECT_DIRECTORIES to become backend-agnostic in a\n    backwards-compatible way. In general, introducing an object storage\n    extension into the current status quo where alternates may have to\n    be extended to become generic was proving to be painful.\n\n  - Some mechanisms of alternates assume way too much about how exactly\n    their backends work. Alternate refs for example assume that the\n    alternate is backed by a filesystem path, and that this filesystem\n    path may also allow us to read references. This is not a given\n    though, as backends may not even have local data at all.\n\nIn short, there are a bunch of conceptual mismatches when we have\nalternates and pluggable object databases coexist. So while the original\nidea was nice, it does not result in a system that is easy to reason\nabout.\n\nCorrect course by moving alternates into the \"files\" source itself so\nthat it becomes an implementation detail thereof so that we can avoid\nall of these shortcomings. While it's unfortunate that we cannot easily\nmix and match sources now, that ability doesn't go away. It's still very\nmuch feasible to introduce a new backend that allows for exactly that\nuse case, and such a backend may also be a lot more flexible as we can\nnow add new logic to determine which objects should be stored where. So\nthe original motivation for having per-source backends can still be\nrealized with the new architecture.\n\nNote that as part of this move, we also handle the GIT_OBJECT_DIRECTORY\nand GIT_ALTERNATE_OBJECT_DIRECTORIES environment variables in the\n\"files\" backend. This may be surprising at first, but object directories\nare very much a concept of that backend, too. So these variables would\nhave bad interactions with other backends, and they create a bit of a\nmismatch with the eventual object storage extension that we plan to\nintroduce.\n\nNote that this commit is way larger than I'd like it to be. I'm sorry, I\ncouldn't find a way to split it up further. That being said, most of the\nchanges are straight-forward conversions that go from iterating over\nsources to iterating over object directories. The more involved changes\nare moving the infrastructure to track individual sources from \"odb.c\"\nand moving them into \"odb/source-files.c\".\n\nThis also serves to show a bit of a who's-who of commands that don't\nwork properly with pluggable object databases. Almost all of these\ncommands are related to housekeeping though -- some of them will be\nconverted eventually, like for example commit graphs or MIDXs. But many\nof them will stay incompatible going forward as they are simply too\nspecific to the \"files\" backend.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/count-objects.c    |   2 +-\n builtin/fast-import.c      |  18 +--\n builtin/fetch.c            |   2 +-\n builtin/fsck.c             |   6 +-\n builtin/gc.c               |  14 +-\n builtin/index-pack.c       |   2 +-\n builtin/multi-pack-index.c |   6 +-\n builtin/pack-objects.c     |  64 ++++----\n builtin/prune.c            |   2 +-\n bundle.c                   |   2 +-\n commit-graph.c             |  28 ++--\n diagnose.c                 |   2 +-\n fetch-pack.c               |   2 +-\n http-walker.c              |   2 +-\n http.c                     |   6 +-\n loose.c                    |  16 +-\n midx.c                     |  29 ++--\n odb.c                      | 354 +++++----------------------------------------\n odb.h                      |  33 +----\n odb/source-files.c         | 260 ++++++++++++++++++++++++++++++---\n odb/source-files.h         |  23 ++-\n odb/source.c               |   5 +-\n odb/source.h               |  25 +---\n odb/streaming.c            |   8 +-\n odb/transaction.c          |   2 +-\n pack-bitmap.c              |   8 +-\n packfile.c                 |  28 ++--\n packfile.h                 |  21 ++-\n path.c                     |   2 +-\n prune-packed.c             |   2 +-\n repack.c                   |   4 +-\n repository.c               |   4 +-\n setup.c                    |   2 +-\n t/helper/test-read-graph.c |   2 +-\n tmp-objdir.c               |   4 +-\n 35 files changed, 441 insertions(+), 549 deletions(-)\n\ndiff --git a/builtin/count-objects.c b/builtin/count-objects.c\nindex f2abfaccec..3f7b20fde5 100644\n--- a/builtin/count-objects.c\n+++ b/builtin/count-objects.c\n@@ -118,7 +118,7 @@ int cmd_count_objects(int argc,\n \t\treport_linked_checkout_garbage(the_repository);\n \t}\n \n-\tfor_each_loose_file_in_source(the_repository->objects->sources,\n+\tfor_each_loose_file_in_source(the_repository->objects->source,\n \t\t\t\t      count_loose, count_cruft, NULL, NULL);\n \n \tif (verbose) {\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 0bf76b028b..5fc082bcc7 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -895,7 +895,7 @@ static void end_packfile(void)\n \trunning = 1;\n \tclear_delta_base_cache();\n \tif (object_count) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(pack_data->repo->objects->sources);\n+\t\tstruct odb_source_files *files = odb_source_files_downcast(pack_data->repo->objects->source);\n \t\tstruct packed_git *new_p;\n \t\tstruct object_id cur_pack_oid;\n \t\tchar *idx_name;\n@@ -975,7 +975,7 @@ static int store_object(\n \tstruct object_id *oidout,\n \tuintmax_t mark)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->source);\n \tvoid *out, *delta;\n \tstruct object_entry *e;\n \tunsigned char hdr[96];\n@@ -1002,10 +1002,8 @@ static int store_object(\n \t\treturn 1;\n \t}\n \n-\tfor (source = the_repository->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\n-\t\tif (!packfile_list_find_oid(packfile_store_get_packs(files->dirs->packed), &oid))\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tif (!packfile_list_find_oid(packfile_store_get_packs(dir->packed), &oid))\n \t\t\tcontinue;\n \t\te->type = type;\n \t\te->pack_id = MAX_PACK_ID;\n@@ -1125,10 +1123,10 @@ static void truncate_pack(struct hashfile_checkpoint *checkpoint)\n \n static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->source);\n \tsize_t in_sz = 64 * 1024, out_sz = 64 * 1024;\n \tunsigned char *in_buf = xmalloc(in_sz);\n \tunsigned char *out_buf = xmalloc(out_sz);\n-\tstruct odb_source *source;\n \tstruct object_entry *e;\n \tstruct object_id oid;\n \tunsigned long hdrlen;\n@@ -1212,10 +1210,8 @@ static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark)\n \t\tgoto out;\n \t}\n \n-\tfor (source = the_repository->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\n-\t\tif (!packfile_list_find_oid(packfile_store_get_packs(files->dirs->packed), &oid))\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tif (!packfile_list_find_oid(packfile_store_get_packs(dir->packed), &oid))\n \t\t\tcontinue;\n \t\te->type = OBJ_BLOB;\n \t\te->pack_id = MAX_PACK_ID;\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex a4b21d2651..2e9c14d4ed 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -2902,7 +2902,7 @@ int cmd_fetch(int argc,\n \n \t\ttrace2_region_enter(\"fetch\", \"write-commit-graph\", the_repository);\n \t\twrite_commit_graph_reachable(the_repository,\n-\t\t\t\t\t     the_repository->objects->sources->path,\n+\t\t\t\t\t     the_repository->objects->source->path,\n \t\t\t\t\t     commit_graph_flags,\n \t\t\t\t\t     NULL);\n \t\ttrace2_region_leave(\"fetch\", \"write-commit-graph\", the_repository);\ndiff --git a/builtin/fsck.c b/builtin/fsck.c\nindex 9af4cc085b..da90cf84bf 100644\n--- a/builtin/fsck.c\n+++ b/builtin/fsck.c\n@@ -868,7 +868,6 @@ int cmd_fsck(int argc,\n \t\tOPT_BOOL(0, \"references\", &check_references, N_(\"check reference database consistency\")),\n \t\tOPT_END(),\n \t};\n-\tstruct odb_source *source;\n \tstruct snapshot snap = {\n \t\t.nr = 0,\n \t\t.alloc = 0,\n@@ -983,13 +982,14 @@ int cmd_fsck(int argc,\n \tcheck_connectivity(repo);\n \n \tif (repo->settings.core_commit_graph) {\n+\t\tstruct odb_source_files *files = odb_source_files_downcast(repo->objects->source);\n \t\tstruct child_process commit_graph_verify = CHILD_PROCESS_INIT;\n \n-\t\tfor (source = repo->objects->sources; source; source = source->next) {\n+\t\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n \t\t\tchild_process_init(&commit_graph_verify);\n \t\t\tcommit_graph_verify.git_cmd = 1;\n \t\t\tstrvec_pushl(&commit_graph_verify.args, \"commit-graph\",\n-\t\t\t\t     \"verify\", \"--object-dir\", source->path, NULL);\n+\t\t\t\t     \"verify\", \"--object-dir\", dir->abspath, NULL);\n \t\t\tif (show_progress)\n \t\t\t\tstrvec_push(&commit_graph_verify.args, \"--progress\");\n \t\t\telse\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 7acd4f3215..28ada9f566 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -734,7 +734,7 @@ int cmd_gc(int argc,\n \n \tif (the_repository->settings.gc_write_commit_graph == 1)\n \t\twrite_commit_graph_reachable(the_repository,\n-\t\t\t\t\t     the_repository->objects->sources->path,\n+\t\t\t\t\t     the_repository->objects->source->path,\n \t\t\t\t\t     !opts.quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,\n \t\t\t\t\t     NULL);\n \n@@ -998,7 +998,7 @@ static int loose_object_auto_condition(struct gc_config *cfg UNUSED)\n \tif (loose_object_auto_limit < 0)\n \t\treturn 1;\n \n-\treturn for_each_loose_file_in_source(the_repository->objects->sources,\n+\treturn for_each_loose_file_in_source(the_repository->objects->source,\n \t\t\t\t\t     loose_object_count,\n \t\t\t\t\t     NULL, NULL, &count);\n }\n@@ -1033,7 +1033,7 @@ static int pack_loose(struct maintenance_run_opts *opts)\n \t * Do not start pack-objects process\n \t * if there are no loose objects.\n \t */\n-\tif (!for_each_loose_file_in_source(r->objects->sources,\n+\tif (!for_each_loose_file_in_source(r->objects->source,\n \t\t\t\t\t   bail_on_loose,\n \t\t\t\t\t   NULL, NULL, NULL))\n \t\treturn 0;\n@@ -1045,7 +1045,7 @@ static int pack_loose(struct maintenance_run_opts *opts)\n \t\tstrvec_push(&pack_proc.args, \"--quiet\");\n \telse\n \t\tstrvec_push(&pack_proc.args, \"--no-quiet\");\n-\tstrvec_pushf(&pack_proc.args, \"%s/pack/loose\", r->objects->sources->path);\n+\tstrvec_pushf(&pack_proc.args, \"%s/pack/loose\", r->objects->source->path);\n \n \tpack_proc.in = -1;\n \n@@ -1073,7 +1073,7 @@ static int pack_loose(struct maintenance_run_opts *opts)\n \telse if (data.batch_size > 0)\n \t\tdata.batch_size--; /* Decrease for equality on limit. */\n \n-\tfor_each_loose_file_in_source(r->objects->sources,\n+\tfor_each_loose_file_in_source(r->objects->source,\n \t\t\t\t      write_loose_object_to_stdin,\n \t\t\t\t      NULL, NULL, &data);\n \n@@ -1396,7 +1396,7 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \tint result = 0;\n \tstruct lock_file lk;\n \tstruct repository *r = the_repository;\n-\tchar *lock_path = xstrfmt(\"%s/maintenance\", r->objects->sources->path);\n+\tchar *lock_path = xstrfmt(\"%s/maintenance\", r->objects->source->path);\n \tenum auto_gc_hook_result auto_gc_hook_result = AUTO_GC_HOOK_UNDECIDED;\n \n \tif (repo_hold_lock_file_for_update(r, &lk, lock_path, LOCK_NO_DEREF) < 0) {\n@@ -2972,7 +2972,7 @@ static int update_background_schedule(const struct maintenance_start_opts *opts,\n \tunsigned int i;\n \tint result = 0;\n \tstruct lock_file lk;\n-\tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->sources->path);\n+\tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->source->path);\n \n \tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n \t\tif (errno == EEXIST)\ndiff --git a/builtin/index-pack.c b/builtin/index-pack.c\nindex 70860b8f27..0264c18c5f 100644\n--- a/builtin/index-pack.c\n+++ b/builtin/index-pack.c\n@@ -1640,7 +1640,7 @@ static void final(const char *final_pack_name, const char *curr_pack_name,\n \n \tif (do_fsck_object && startup_info->have_repository) {\n \t\tstruct odb_source_files *files =\n-\t\t\todb_source_files_downcast(the_repository->objects->sources);\n+\t\t\todb_source_files_downcast(the_repository->objects->source);\n \t\tpackfile_store_load_pack(files->dirs->packed, final_index_name, 0);\n \t}\n \ndiff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c\nindex fc8b494996..c48212290c 100644\n--- a/builtin/multi-pack-index.c\n+++ b/builtin/multi-pack-index.c\n@@ -80,7 +80,7 @@ static int parse_object_dir(const struct option *opt, const char *arg,\n \tchar **value = opt->value;\n \tfree(*value);\n \tif (unset)\n-\t\t*value = xstrdup(the_repository->objects->sources->path);\n+\t\t*value = xstrdup(the_repository->objects->source->path);\n \telse\n \t\t*value = real_pathdup(arg, 1);\n \treturn 0;\n@@ -426,8 +426,8 @@ int cmd_multi_pack_index(int argc,\n \n \tif (the_repository &&\n \t    the_repository->objects &&\n-\t    the_repository->objects->sources)\n-\t\topts.object_dir = xstrdup(the_repository->objects->sources->path);\n+\t    the_repository->objects->source)\n+\t\topts.object_dir = xstrdup(the_repository->objects->source->path);\n \n \targc = parse_options(argc, argv, prefix, options,\n \t\t\t     builtin_multi_pack_index_usage, 0);\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 070659b6ed..ca3a891dfb 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -1566,11 +1566,10 @@ static int want_cruft_object_mtime(struct repository *r,\n \t\t\t\t   const struct object_id *oid,\n \t\t\t\t   unsigned flags, uint32_t mtime)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files = odb_source_files_downcast(r->objects->source);\n \n-\tfor (source = r->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tstruct packed_git **cache = packfile_store_get_kept_pack_cache(files->dirs->packed, flags);\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tstruct packed_git **cache = packfile_store_get_kept_pack_cache(dir->packed, flags);\n \n \t\tfor (; *cache; cache++) {\n \t\t\tstruct packed_git *p = *cache;\n@@ -1753,21 +1752,19 @@ static int want_object_in_pack_mtime(const struct object_id *oid,\n \t\t\t\t     off_t *found_offset,\n \t\t\t\t     uint32_t found_mtime)\n {\n+\tstruct odb_source_files *files =\n+\t\todb_source_files_downcast(the_repository->objects->source);\n \tint want;\n \tstruct packfile_list_entry *e;\n-\tstruct odb_source *source;\n \n \tif (!exclude && local) {\n \t\t/*\n-\t\t * Note that we start iterating at `sources->next` so that we\n-\t\t * skip the local object source.\n+\t\t * Note that we start iterating at `dirs->next` so that we\n+\t\t * skip the local object directory.\n \t\t */\n-\t\tstruct odb_source *source = the_repository->objects->sources->next;\n-\t\tfor (; source; source = source->next) {\n-\t\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\t\tif (!odb_source_read_object_info(&files->dirs->loose->base, oid, NULL, 0, NULL))\n+\t\tfor (struct odb_files_dir *dir = files->dirs->next; dir; dir = dir->next)\n+\t\t\tif (!odb_source_read_object_info(&dir->loose->base, oid, NULL, 0, NULL))\n \t\t\t\treturn 0;\n-\t\t}\n \t}\n \n \t/*\n@@ -1785,9 +1782,8 @@ static int want_object_in_pack_mtime(const struct object_id *oid,\n \t\t*found_offset = 0;\n \t}\n \n-\tfor (source = the_repository->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tstruct multi_pack_index *m = get_multi_pack_index(files->dirs->packed);\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tstruct multi_pack_index *m = get_multi_pack_index(dir->packed);\n \t\tstruct pack_entry e;\n \n \t\tif (m && midx_fill_entry(m, oid, &e, NULL) == MIDX_FILL_HIT) {\n@@ -1797,14 +1793,12 @@ static int want_object_in_pack_mtime(const struct object_id *oid,\n \t\t}\n \t}\n \n-\tfor (source = the_repository->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\n-\t\tfor (e = files->dirs->packed->packs.head; e; e = e->next) {\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tfor (e = dir->packed->packs.head; e; e = e->next) {\n \t\t\tstruct packed_git *p = e->pack;\n \t\t\twant = want_object_in_pack_one(p, oid, exclude, found_pack, found_offset, found_mtime);\n \t\t\tif (!exclude && want > 0)\n-\t\t\t\tpackfile_list_prepend(&files->dirs->packed->packs, p);\n+\t\t\t\tpackfile_list_prepend(&dir->packed->packs, p);\n \t\t\tif (want != -1)\n \t\t\t\treturn want;\n \t\t}\n@@ -4170,14 +4164,13 @@ static void add_cruft_object_entry(const struct object_id *oid, enum object_type\n \t\tif (!want_object_in_pack_mtime(oid, 0, &pack, &offset, mtime))\n \t\t\treturn;\n \t\tif (!pack && type == OBJ_BLOB) {\n-\t\t\tstruct odb_source *source = the_repository->objects->sources;\n+\t\t\tstruct odb_source_files *files =\n+\t\t\t\todb_source_files_downcast(the_repository->objects->source);\n \t\t\tint found = 0;\n \n-\t\t\tfor (; !found && source; source = source->next) {\n-\t\t\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\t\t\tif (!odb_source_read_object_info(&files->dirs->loose->base, oid, NULL, 0, NULL))\n+\t\t\tfor (struct odb_files_dir *dir = files->dirs; !found && dir; dir = dir->next)\n+\t\t\t\tif (!odb_source_read_object_info(&dir->loose->base, oid, NULL, 0, NULL))\n \t\t\t\t\tfound = 1;\n-\t\t\t}\n \n \t\t\t/*\n \t\t\t * If a traversed tree has a missing blob then we want\n@@ -4512,7 +4505,8 @@ static int add_object_in_unpacked_pack(const struct object_id *oid,\n \n static void add_objects_in_unpacked_packs(void)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files =\n+\t\todb_source_files_downcast(to_pack.repo->objects->source);\n \ttime_t mtime;\n \tstruct odb_for_each_object_options opts = {\n \t\t.flags = ODB_FOR_EACH_OBJECT_PACK_ORDER |\n@@ -4526,13 +4520,11 @@ static void add_objects_in_unpacked_packs(void)\n \t\t.source_infop = &source_info,\n \t};\n \n-\tfor (source = to_pack.repo->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\n-\t\tif (!source->local)\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tif (!dir->local)\n \t\t\tcontinue;\n \n-\t\tif (odb_source_for_each_object(&files->dirs->packed->base, &oi,\n+\t\tif (odb_source_for_each_object(&dir->packed->base, &oi,\n \t\t\t\t\t       add_object_in_unpacked_pack, NULL, &opts))\n \t\t\tdie(_(\"cannot open pack index\"));\n \t}\n@@ -4576,7 +4568,7 @@ static int add_loose_object(const struct object_id *oid, const char *path,\n  */\n static void add_unreachable_loose_objects(struct rev_info *revs)\n {\n-\tfor_each_loose_file_in_source(the_repository->objects->sources,\n+\tfor_each_loose_file_in_source(the_repository->objects->source,\n \t\t\t\t      add_loose_object, NULL, NULL, revs);\n }\n \n@@ -4640,11 +4632,9 @@ static int force_object_loose(struct odb_source *source,\n \tsize_t len;\n \tint ret;\n \n-\tfor (struct odb_source *s = source->odb->sources; s; s = s->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(s);\n-\t\tif (!odb_source_read_object_info(&files->dirs->loose->base, oid, NULL, 0, NULL))\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next)\n+\t\tif (!odb_source_read_object_info(&dir->loose->base, oid, NULL, 0, NULL))\n \t\t\treturn 0;\n-\t}\n \n \toi.typep = &type;\n \toi.sizep = &len;\n@@ -4691,7 +4681,7 @@ static void loosen_unused_packed_objects(void)\n \t\t\tif (!packlist_find(&to_pack, &oid) &&\n \t\t\t    !has_sha1_pack_kept_or_nonlocal(&oid) &&\n \t\t\t    !loosened_object_can_be_discarded(&oid, p->mtime)) {\n-\t\t\t\tif (force_object_loose(the_repository->objects->sources,\n+\t\t\t\tif (force_object_loose(the_repository->objects->source,\n \t\t\t\t\t\t       &oid, &p->mtime))\n \t\t\t\t\tdie(_(\"unable to force loose object\"));\n \t\t\t\tloosened_objects_nr++;\ndiff --git a/builtin/prune.c b/builtin/prune.c\nindex a7e4678d11..97f4c1418b 100644\n--- a/builtin/prune.c\n+++ b/builtin/prune.c\n@@ -198,7 +198,7 @@ int cmd_prune(int argc,\n \t\trevs.exclude_promisor_objects = 1;\n \t}\n \n-\tfor_each_loose_file_in_source(repo->objects->sources,\n+\tfor_each_loose_file_in_source(repo->objects->source,\n \t\t\t\t      prune_object, prune_cruft, prune_subdir, &revs);\n \n \tprune_packed_objects(show_only ? PRUNE_PACKED_DRY_RUN : 0);\ndiff --git a/bundle.c b/bundle.c\nindex f55a521b2a..a81fd29505 100644\n--- a/bundle.c\n+++ b/bundle.c\n@@ -239,7 +239,7 @@ int verify_bundle(struct repository *r,\n \t\t.quiet = 1,\n \t};\n \n-\tif (!r || !r->objects || !r->objects->sources)\n+\tif (!r || !r->objects || !r->objects->source)\n \t\treturn error(_(\"need a repository to verify a bundle\"));\n \n \tfor (i = 0; i < p->nr; i++) {\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 7cc486d140..673d54db29 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -15,6 +15,7 @@\n #include \"hash-lookup.h\"\n #include \"commit-graph.h\"\n #include \"odb.h\"\n+#include \"odb/source-files.h\"\n #include \"oid-array.h\"\n #include \"path.h\"\n #include \"alloc.h\"\n@@ -38,7 +39,7 @@ void git_test_write_commit_graph_or_die(struct repository *repo)\n \tif (git_env_bool(GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS, 0))\n \t\tflags = COMMIT_GRAPH_WRITE_BLOOM_FILTERS;\n \n-\tif (write_commit_graph_reachable(repo, repo->objects->sources->path, flags, NULL))\n+\tif (write_commit_graph_reachable(repo, repo->objects->source->path, flags, NULL))\n \t\tdie(\"failed to write commit-graph under GIT_TEST_COMMIT_GRAPH\");\n }\n \n@@ -657,7 +658,7 @@ struct commit_graph *load_commit_graph_chain_fd_st(struct object_database *odb,\n \tCALLOC_ARRAY(oids, count);\n \n \tfor (i = 0; i < count; i++) {\n-\t\tstruct odb_source *source;\n+\t\tstruct odb_source_files *files;\n \n \t\tif (strbuf_getline_lf(&line, fp) == EOF)\n \t\t\tbreak;\n@@ -670,9 +671,11 @@ struct commit_graph *load_commit_graph_chain_fd_st(struct object_database *odb,\n \t\t}\n \n \t\tvalid = 0;\n-\t\tfor (source = odb->sources; source; source = source->next) {\n-\t\t\tchar *graph_name = get_split_graph_filename(source->path, line.buf);\n-\t\t\tstruct commit_graph *g = load_commit_graph_one(odb->repo, source->path, graph_name);\n+\n+\t\tfiles = odb_source_files_downcast(odb->source);\n+\t\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\t\tchar *graph_name = get_split_graph_filename(dir->abspath, line.buf);\n+\t\t\tstruct commit_graph *g = load_commit_graph_one(odb->repo, dir->abspath, graph_name);\n \n \t\t\tfree(graph_name);\n \n@@ -742,7 +745,7 @@ struct commit_graph *read_commit_graph_one(struct repository *repo,\n  */\n static struct commit_graph *prepare_commit_graph(struct repository *r)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files;\n \n \t/*\n \t * Early return if there is no object database or if the commit graph is\n@@ -773,8 +776,9 @@ static struct commit_graph *prepare_commit_graph(struct repository *r)\n \tif (!commit_graph_compatible(r))\n \t\treturn NULL;\n \n-\tfor (source = r->objects->sources; source; source = source->next) {\n-\t\tr->objects->commit_graph = read_commit_graph_one(r, source->path);\n+\tfiles = odb_source_files_downcast(r->objects->source);\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tr->objects->commit_graph = read_commit_graph_one(r, dir->abspath);\n \t\tif (r->objects->commit_graph)\n \t\t\tbreak;\n \t}\n@@ -2003,7 +2007,7 @@ static int fill_oids_from_commits(struct write_commit_graph_context *ctx,\n \n static void fill_oids_from_all_packs(struct write_commit_graph_context *ctx)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files;\n \tenum object_type type;\n \tstruct odb_for_each_object_options opts = {\n \t\t.flags = ODB_FOR_EACH_OBJECT_PACK_ORDER,\n@@ -2018,9 +2022,9 @@ static void fill_oids_from_all_packs(struct write_commit_graph_context *ctx)\n \t\t\t_(\"Finding commits for commit graph among packed objects\"),\n \t\t\tctx->approx_nr_objects);\n \n-\tfor (source = ctx->r->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\todb_source_for_each_object(&files->dirs->packed->base, &oi, add_packed_commits_oi,\n+\tfiles = odb_source_files_downcast(ctx->r->objects->source);\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\todb_source_for_each_object(&dir->packed->base, &oi, add_packed_commits_oi,\n \t\t\t\t\t   ctx, &opts);\n \t}\n \ndiff --git a/diagnose.c b/diagnose.c\nindex 89240e47d6..2c1bcfe431 100644\n--- a/diagnose.c\n+++ b/diagnose.c\n@@ -228,7 +228,7 @@ int create_diagnostics_archive(struct repository *r,\n \n \tstrbuf_reset(&buf);\n \tstrbuf_addstr(&buf, \"--add-virtual-file=packs-local.txt:\");\n-\tdir_file_stats(odb_source_files_downcast(r->objects->sources)->dirs, &buf);\n+\tdir_file_stats(odb_source_files_downcast(r->objects->source)->dirs, &buf);\n \todb_for_each_alternate(r->objects, dir_file_stats, &buf);\n \tstrvec_push(&archiver_args, buf.buf);\n \ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex aad07b1153..b14e3ba71b 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -1075,7 +1075,7 @@ static int get_pack(struct fetch_pack_args *args,\n \t\tdie(_(\"fetch-pack: unable to fork off %s\"), cmd_name);\n \tif (do_keep && (pack_lockfiles || fsck_objects)) {\n \t\tint is_well_formed;\n-\t\tchar *pack_lockfile = index_pack_lockfile(the_repository->objects->sources,\n+\t\tchar *pack_lockfile = index_pack_lockfile(the_repository->objects->source,\n \t\t\t\t\t\t\t  cmd.out,\n \t\t\t\t\t\t\t  &is_well_formed);\n \ndiff --git a/http-walker.c b/http-walker.c\nindex 0a6c99f471..4f50dd9b8d 100644\n--- a/http-walker.c\n+++ b/http-walker.c\n@@ -540,7 +540,7 @@ static int fetch_object(struct walker *walker, const struct object_id *oid)\n \t} else if (!oideq(&obj_req->oid, &req->real_oid)) {\n \t\tret = error(\"File %s has bad hash\", hex);\n \t} else if (req->rename < 0) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);\n+\t\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->source);\n \t\tstruct strbuf buf = STRBUF_INIT;\n \t\todb_loose_path(files->dirs->loose, &buf, &req->oid);\n \t\tret = error(\"unable to write sha1 filename %s\", buf.buf);\ndiff --git a/http.c b/http.c\nindex fe6ec88a21..230c4de494 100644\n--- a/http.c\n+++ b/http.c\n@@ -2718,7 +2718,7 @@ int finish_http_pack_request(struct http_pack_request *preq)\n void http_install_packfile(struct packed_git *p,\n \t\t\t   struct packfile_list *list_to_remove_from)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);\n+\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->source);\n \tpackfile_list_remove(list_to_remove_from, p);\n \tpackfile_store_add_pack(files->dirs->packed, p);\n }\n@@ -2846,7 +2846,7 @@ static size_t fwrite_sha1_file(char *ptr, size_t eltsize, size_t nmemb,\n struct http_object_request *new_http_object_request(const char *base_url,\n \t\t\t\t\t\t    const struct object_id *oid)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);\n+\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->source);\n \tchar *hex = oid_to_hex(oid);\n \tstruct strbuf filename = STRBUF_INIT;\n \tstruct strbuf prevfile = STRBUF_INIT;\n@@ -2987,7 +2987,7 @@ void process_http_object_request(struct http_object_request *freq)\n \n int finish_http_object_request(struct http_object_request *freq)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);\n+\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->source);\n \tstruct stat st;\n \tstruct strbuf filename = STRBUF_INIT;\n \ndiff --git a/loose.c b/loose.c\nindex 957bf83e6b..d6a2bb5ba3 100644\n--- a/loose.c\n+++ b/loose.c\n@@ -113,11 +113,10 @@ int loose_object_map_load(struct odb_source_loose *loose)\n \n int repo_read_loose_object_map(struct repository *repo)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files = odb_source_files_downcast(repo->objects->source);\n \n-\tfor (source = repo->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tif (loose_object_map_load(files->dirs->loose) < 0)\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tif (loose_object_map_load(dir->loose) < 0)\n \t\t\treturn -1;\n \t}\n \n@@ -126,7 +125,7 @@ int repo_read_loose_object_map(struct repository *repo)\n \n int repo_write_loose_object_map(struct repository *repo)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(repo->objects->sources);\n+\tstruct odb_source_files *files = odb_source_files_downcast(repo->objects->source);\n \tkh_oid_map_t *map = files->dirs->loose->map->to_compat;\n \tstruct lock_file lock;\n \tint fd;\n@@ -231,13 +230,12 @@ int repo_loose_object_map_oid(struct repository *repo,\n \t\t\t      const struct git_hash_algo *to,\n \t\t\t      struct object_id *dest)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files = odb_source_files_downcast(repo->objects->source);\n \tkh_oid_map_t *map;\n \tkhiter_t pos;\n \n-\tfor (source = repo->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tstruct loose_object_map *loose_map = files->dirs->loose->map;\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tstruct loose_object_map *loose_map = dir->loose->map;\n \t\tif (!loose_map)\n \t\t\tcontinue;\n \t\tmap = (to == repo->compat_hash_algo) ?\ndiff --git a/midx.c b/midx.c\nindex c0f82c4163..8638ddf0be 100644\n--- a/midx.c\n+++ b/midx.c\n@@ -829,21 +829,15 @@ void clear_incremental_midx_files_ext(struct odb_source_packed *source, const ch\n \n void clear_midx_file(struct repository *r)\n {\n-\tstruct odb_source_files *files;\n+\tstruct odb_source_files *files = odb_source_files_downcast(r->objects->source);\n \tstruct strbuf midx = STRBUF_INIT;\n \n-\tif (r->objects) {\n-\t\tstruct odb_source *source;\n-\n-\t\tfor (source = r->objects->sources; source; source = source->next) {\n-\t\t\tfiles = odb_source_files_downcast(source);\n-\t\t\tif (files->dirs->packed->midx)\n-\t\t\t\tclose_midx(files->dirs->packed->midx);\n-\t\t\tfiles->dirs->packed->midx = NULL;\n-\t\t}\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tif (dir->packed->midx)\n+\t\t\tclose_midx(dir->packed->midx);\n+\t\tdir->packed->midx = NULL;\n \t}\n \n-\tfiles = odb_source_files_downcast(r->objects->sources);\n \tget_midx_filename(files->dirs->packed, &midx);\n \n \tif (remove_path(midx.buf))\n@@ -858,18 +852,15 @@ void clear_midx_file(struct repository *r)\n void clear_incremental_midx_files(struct repository *r,\n \t\t\t\t  const struct strvec *keep_hashes)\n {\n-\tstruct odb_source_files *files;\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files = odb_source_files_downcast(r->objects->source);\n \tstruct strbuf chain = STRBUF_INIT;\n \n-\tfor (source = r->objects->sources; source; source = source->next) {\n-\t\tfiles = odb_source_files_downcast(source);\n-\t\tif (files->dirs->packed->midx)\n-\t\t\tclose_midx(files->dirs->packed->midx);\n-\t\tfiles->dirs->packed->midx = NULL;\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tif (dir->packed->midx)\n+\t\t\tclose_midx(dir->packed->midx);\n+\t\tdir->packed->midx = NULL;\n \t}\n \n-\tfiles = odb_source_files_downcast(r->objects->sources);\n \tget_midx_chain_filename(files->dirs->packed, &chain);\n \n \tif (!keep_hashes && remove_path(chain.buf))\ndiff --git a/odb.c b/odb.c\nindex 8b54271c27..98c5c862f5 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -13,11 +13,10 @@\n #include \"object-file.h\"\n #include \"object-name.h\"\n #include \"odb.h\"\n-#include \"odb/source-inmemory.h\"\n #include \"odb/source-files.h\"\n+#include \"odb/source-inmemory.h\"\n #include \"path.h\"\n #include \"promisor-remote.h\"\n-#include \"quote.h\"\n #include \"replace-object.h\"\n #include \"run-command.h\"\n #include \"setup.h\"\n@@ -28,48 +27,6 @@\n #include \"trace2.h\"\n #include \"write-or-die.h\"\n \n-/*\n- * NEEDSWORK: we're using \"core.ignoreCase\" to deduplicate alternates that\n- * _may_ be the same. This requires quite a bit of boilerplate for dubious\n- * benefit:\n- *\n- *   - Duplicating alternates should really only lead to regressed performance.\n- *\n- *   - We don't properly resolve symlinks or mointpoints, so we may still end\n- *     up duplicating alternates.\n- *\n- *   - The value may be lying, in which case we might deduplicate alternates\n- *     that are in fact not mapping to the same directory.\n- *\n- * We should investigate whether we can remove this whole mechanism outright.\n- */\n-static int odb_source_paths_cmp(struct object_database *o,\n-\t\t\t\tconst char *a, const char *b)\n-{\n-\tif (o->source_paths_icase < 0) {\n-\t\tint icase = 0;\n-\t\trepo_config_get_bool(o->repo, \"core.ignorecase\", &icase);\n-\t\to->source_paths_icase = icase;\n-\t}\n-\n-\treturn o->source_paths_icase ? strcasecmp(a, b) : strcmp(a, b);\n-}\n-\n-static int odb_source_by_path_cmp(const void *cb_data,\n-\t\t\t\t  const struct hashmap_entry *entry,\n-\t\t\t\t  const struct hashmap_entry *entry_or_key,\n-\t\t\t\t  const void *keydata)\n-{\n-\tstruct object_database *o = (struct object_database *)cb_data;\n-\tconst struct odb_source *source = container_of(entry, const struct odb_source, by_path_entry);\n-\tconst char *path = keydata;\n-\n-\tif (!path)\n-\t\tpath = container_of(entry_or_key, const struct odb_source, by_path_entry)->path;\n-\n-\treturn odb_source_paths_cmp(o, source->path, path);\n-}\n-\n int odb_mkstemp(struct object_database *odb,\n \t\tstruct strbuf *temp_filename, const char *pattern)\n {\n@@ -91,154 +48,6 @@ int odb_mkstemp(struct object_database *odb,\n \treturn xmkstemp_mode(temp_filename->buf, mode);\n }\n \n-/*\n- * Return non-zero iff the path is usable as an alternate object database.\n- */\n-static bool odb_is_source_usable(struct object_database *o, const char *path)\n-{\n-\tstruct strbuf normalized_objdir = STRBUF_INIT;\n-\tstruct hashmap_entry key;\n-\tbool usable = false;\n-\n-\tstrbuf_realpath(&normalized_objdir, o->sources->path, 1);\n-\n-\t/* Detect cases where alternate disappeared */\n-\tif (!is_directory(path)) {\n-\t\terror(_(\"object directory %s does not exist; \"\n-\t\t\t\"check .git/objects/info/alternates\"),\n-\t\t      path);\n-\t\tgoto out;\n-\t}\n-\n-\t/*\n-\t * Prevent the common mistake of listing the same\n-\t * thing twice, or object directory itself.\n-\t */\n-\tif (!hashmap_get_size(&o->source_by_path)) {\n-\t\tassert(!o->sources->next);\n-\t\thashmap_entry_init(&o->sources->by_path_entry,\n-\t\t\t\t   strihash(o->sources->path));\n-\t\thashmap_add(&o->source_by_path, &o->sources->by_path_entry);\n-\t}\n-\n-\tif (!odb_source_paths_cmp(o, path, normalized_objdir.buf))\n-\t\tgoto out;\n-\n-\thashmap_entry_init(&key, strihash(path));\n-\tif (hashmap_get(&o->source_by_path, &key, path))\n-\t\tgoto out;\n-\n-\tusable = true;\n-\n-out:\n-\tstrbuf_release(&normalized_objdir);\n-\treturn usable;\n-}\n-\n-void parse_alternates(const char *string,\n-\t\t      int sep,\n-\t\t      const char *relative_base,\n-\t\t      struct strvec *out)\n-{\n-\tstruct strbuf pathbuf = STRBUF_INIT;\n-\tstruct strbuf buf = STRBUF_INIT;\n-\n-\tif (!string || !*string)\n-\t\treturn;\n-\n-\twhile (*string) {\n-\t\tconst char *end;\n-\n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_reset(&pathbuf);\n-\n-\t\tif (*string == '#') {\n-\t\t\t/* comment; consume up to next separator */\n-\t\t\tend = strchrnul(string, sep);\n-\t\t} else if (*string == '\"' && !unquote_c_style(&buf, string, &end)) {\n-\t\t\t/*\n-\t\t\t * quoted path; unquote_c_style has copied the\n-\t\t\t * data for us and set \"end\". Broken quoting (e.g.,\n-\t\t\t * an entry that doesn't end with a quote) falls\n-\t\t\t * back to the unquoted case below.\n-\t\t\t */\n-\t\t} else {\n-\t\t\t/* normal, unquoted path */\n-\t\t\tend = strchrnul(string, sep);\n-\t\t\tstrbuf_add(&buf, string, end - string);\n-\t\t}\n-\n-\t\tif (*end)\n-\t\t\tend++;\n-\t\tstring = end;\n-\n-\t\tif (!buf.len)\n-\t\t\tcontinue;\n-\n-\t\tif (!is_absolute_path(buf.buf) && relative_base) {\n-\t\t\tstrbuf_realpath(&pathbuf, relative_base, 1);\n-\t\t\tstrbuf_addch(&pathbuf, '/');\n-\t\t}\n-\t\tstrbuf_addbuf(&pathbuf, &buf);\n-\n-\t\tstrbuf_reset(&buf);\n-\t\tif (!strbuf_realpath(&buf, pathbuf.buf, 0)) {\n-\t\t\terror(_(\"unable to normalize alternate object path: %s\"),\n-\t\t\t      pathbuf.buf);\n-\t\t\tcontinue;\n-\t\t}\n-\n-\t\t/*\n-\t\t * The trailing slash after the directory name is given by\n-\t\t * this function at the end. Remove duplicates.\n-\t\t */\n-\t\twhile (buf.len && buf.buf[buf.len - 1] == '/')\n-\t\t\tstrbuf_setlen(&buf, buf.len - 1);\n-\n-\t\tstrvec_push(out, buf.buf);\n-\t}\n-\n-\tstrbuf_release(&pathbuf);\n-\tstrbuf_release(&buf);\n-}\n-\n-static struct odb_source *odb_add_alternate_recursively(struct object_database *odb,\n-\t\t\t\t\t\t\tconst char *source,\n-\t\t\t\t\t\t\tint depth)\n-{\n-\tstruct odb_source *alternate = NULL;\n-\tstruct strvec sources = STRVEC_INIT;\n-\n-\tif (!odb_is_source_usable(odb, source))\n-\t\tgoto error;\n-\n-\talternate = odb_source_new(odb, source, false);\n-\n-\t/* add the alternate entry */\n-\t*odb->sources_tail = alternate;\n-\todb->sources_tail = &(alternate->next);\n-\n-\thashmap_entry_init(&alternate->by_path_entry, strihash(alternate->path));\n-\tif (hashmap_get(&odb->source_by_path, &alternate->by_path_entry,\n-\t\t\talternate->path))\n-\t\tBUG(\"source must not yet exist\");\n-\thashmap_add(&odb->source_by_path, &alternate->by_path_entry);\n-\n-\t/* recursively add alternates */\n-\todb_source_read_alternates(alternate, &sources);\n-\tif (sources.nr && depth + 1 > 5) {\n-\t\terror(_(\"%s: ignoring alternate object stores, nesting too deep\"),\n-\t\t      source);\n-\t} else {\n-\t\tfor (size_t i = 0; i < sources.nr; i++)\n-\t\t\todb_add_alternate_recursively(odb, sources.v[i], depth + 1);\n-\t}\n-\n- error:\n-\tstrvec_clear(&sources);\n-\treturn alternate;\n-}\n-\n char *compute_alternate_path(const char *path, struct strbuf *err)\n {\n \tchar *ref_git = NULL;\n@@ -410,34 +219,22 @@ void odb_for_each_alternate_ref(struct object_database *odb,\n int odb_for_each_alternate(struct object_database *odb,\n \t\t\t odb_for_each_alternate_fn cb, void *payload)\n {\n-\tstruct odb_source *alternate;\n+\tstruct odb_source_files *files = odb_source_files_downcast(odb->source);\n \tint r = 0;\n \n-\tfor (alternate = odb->sources->next; alternate; alternate = alternate->next) {\n-\t\tr = cb(odb_source_files_downcast(alternate)->dirs, payload);\n+\tfor (struct odb_files_dir *dir = files->dirs->next; dir; dir = dir->next) {\n+\t\tr = cb(dir, payload);\n \t\tif (r)\n \t\t\tbreak;\n \t}\n \treturn r;\n }\n \n-static void odb_prepare_alternates(struct object_database *odb,\n-\t\t\t\t   const char *alternate_db)\n-{\n-\tstruct strvec sources = STRVEC_INIT;\n-\n-\tparse_alternates(alternate_db, PATH_SEP, NULL, &sources);\n-\todb_source_read_alternates(odb->sources, &sources);\n-\n-\tfor (size_t i = 0; i < sources.nr; i++)\n-\t\todb_add_alternate_recursively(odb, sources.v[i], 0);\n-\n-\tstrvec_clear(&sources);\n-}\n-\n int odb_has_alternates(struct object_database *odb)\n {\n-\treturn !!odb->sources->next;\n+\tif (odb->source->type != ODB_SOURCE_FILES)\n+\t\treturn 0;\n+\treturn !!odb_source_files_downcast(odb->source)->dirs->next;\n }\n \n int obj_read_use_lock = 0;\n@@ -481,16 +278,12 @@ static enum odb_read_status do_oid_object_info_extended(struct object_database *\n \t\treturn 0;\n \n \twhile (1) {\n-\t\tstruct odb_source *source;\n-\n-\t\tfor (source = odb->sources; source; source = source->next) {\n-\t\t\tret = odb_source_read_object_info(source, real, oi, flags,\n-\t\t\t\t\t\t\t  corrupt_err.len ? NULL : &corrupt_err);\n-\t\t\tif (!ret)\n-\t\t\t\tgoto out;\n-\t\t\tif (ret != ODB_READ_NOT_FOUND)\n-\t\t\t\tcorrupt = true;\n-\t\t}\n+\t\tret = odb_source_read_object_info(odb->source, real, oi, flags,\n+\t\t\t\t\t\t  corrupt_err.len ? NULL : &corrupt_err);\n+\t\tif (!ret)\n+\t\t\tgoto out;\n+\t\tif (ret != ODB_READ_NOT_FOUND)\n+\t\t\tcorrupt = true;\n \n \t\t/*\n \t\t * When the object hasn't been found we try a second read and\n@@ -498,15 +291,13 @@ static enum odb_read_status do_oid_object_info_extended(struct object_database *\n \t\t * caches or reload on-disk state.\n \t\t */\n \t\tif (!(flags & OBJECT_INFO_QUICK)) {\n-\t\t\tfor (source = odb->sources; source; source = source->next) {\n-\t\t\t\tret = odb_source_read_object_info(source, real, oi,\n-\t\t\t\t\t\t\t\t  flags | OBJECT_INFO_SECOND_READ,\n-\t\t\t\t\t\t\t\t  corrupt_err.len ? NULL : &corrupt_err);\n-\t\t\t\tif (!ret)\n-\t\t\t\t\tgoto out;\n-\t\t\t\tif (ret != ODB_READ_NOT_FOUND)\n-\t\t\t\t\tcorrupt = true;\n-\t\t\t}\n+\t\t\tret = odb_source_read_object_info(odb->source, real, oi,\n+\t\t\t\t\t\t\t  flags | OBJECT_INFO_SECOND_READ,\n+\t\t\t\t\t\t\t  corrupt_err.len ? NULL : &corrupt_err);\n+\t\t\tif (!ret)\n+\t\t\t\tgoto out;\n+\t\t\tif (ret != ODB_READ_NOT_FOUND)\n+\t\t\t\tcorrupt = true;\n \t\t}\n \n \t\t/* Check if it is a missing object */\n@@ -748,11 +539,7 @@ int odb_has_object(struct object_database *odb, const struct object_id *oid,\n int odb_freshen_object(struct object_database *odb,\n \t\t       const struct object_id *oid)\n {\n-\tstruct odb_source *source;\n-\tfor (source = odb->sources; source; source = source->next)\n-\t\tif (odb_source_freshen_object(source, oid, NULL))\n-\t\t\treturn 1;\n-\treturn 0;\n+\treturn odb_source_freshen_object(odb->source, oid, NULL);\n }\n \n int odb_for_each_object_ext(struct object_database *odb,\n@@ -761,18 +548,7 @@ int odb_for_each_object_ext(struct object_database *odb,\n \t\t\t    void *cb_data,\n \t\t\t    const struct odb_for_each_object_options *opts)\n {\n-\tint ret;\n-\n-\tfor (struct odb_source *source = odb->sources; source; source = source->next) {\n-\t\tif (opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY && !source->local)\n-\t\t\tcontinue;\n-\n-\t\tret = odb_source_for_each_object(source, request, cb, cb_data, opts);\n-\t\tif (ret)\n-\t\t\treturn ret;\n-\t}\n-\n-\treturn 0;\n+\treturn odb_source_for_each_object(odb->source, request, cb, cb_data, opts);\n }\n \n int odb_for_each_object(struct object_database *odb,\n@@ -791,7 +567,6 @@ int odb_count_objects(struct object_database *odb,\n \t\t      enum odb_count_objects_flags flags,\n \t\t      unsigned long *out)\n {\n-\tstruct odb_source *source;\n \tunsigned long count = 0;\n \tint ret;\n \n@@ -800,15 +575,9 @@ int odb_count_objects(struct object_database *odb,\n \t\treturn 0;\n \t}\n \n-\tfor (source = odb->sources; source; source = source->next) {\n-\t\tunsigned long c;\n-\n-\t\tret = odb_source_count_objects(source, flags, &c);\n-\t\tif (ret < 0)\n-\t\t\tgoto out;\n-\n-\t\tcount += c;\n-\t}\n+\tret = odb_source_count_objects(odb->source, flags, &count);\n+\tif (ret < 0)\n+\t\tgoto out;\n \n \todb->object_count = count;\n \todb->object_count_valid = 1;\n@@ -879,13 +648,7 @@ int odb_find_abbrev_len(struct object_database *odb,\n \t\tgoto out;\n \t}\n \n-\tfor (struct odb_source *source = odb->sources; source; source = source->next) {\n-\t\tret = odb_source_find_abbrev_len(source, oid, len, &len);\n-\t\tif (ret)\n-\t\t\tgoto out;\n-\t}\n-\n-\tret = 0;\n+\tret = odb_source_find_abbrev_len(odb->source, oid, len, &len);\n \t*out = len;\n \n out:\n@@ -941,7 +704,7 @@ int odb_write_object_ext(struct object_database *odb,\n \t\tcompat_oid_p = &compat_oid;\n \t}\n \n-\treturn odb_source_write_object(odb->sources, buf, len, type,\n+\treturn odb_source_write_object(odb->source, buf, len, type,\n \t\t\t\t       oid, compat_oid_p, NULL, flags);\n }\n \n@@ -949,19 +712,19 @@ int odb_write_object_stream(struct object_database *odb,\n \t\t\t    struct odb_stream *stream,\n \t\t\t    struct object_id *oid)\n {\n-\treturn odb_source_write_object_stream(odb->sources, stream, oid);\n+\treturn odb_source_write_object_stream(odb->source, stream, oid);\n }\n \n int odb_optimize(struct object_database *odb,\n \t\t const struct odb_optimize_options *opts)\n {\n-\treturn odb_source_optimize(odb->sources, opts);\n+\treturn odb_source_optimize(odb->source, opts);\n }\n \n bool odb_optimize_required(struct object_database *odb,\n \t\t\t   const struct odb_optimize_options *opts)\n {\n-\treturn odb_source_optimize_required(odb->sources, opts);\n+\treturn odb_source_optimize_required(odb->source, opts);\n }\n \n void odb_generate_pack_options_release(struct odb_generate_pack_options *opts)\n@@ -975,9 +738,9 @@ int odb_generate_pack(struct object_database *odb,\n \t\t      struct odb_pack_generator **out,\n \t\t      const struct odb_generate_pack_options *opts)\n {\n-\tif (!odb->sources->generate_pack)\n+\tif (!odb->source->generate_pack)\n \t\treturn error(_(\"primary object source does not support generating packfiles\"));\n-\treturn odb_source_generate_pack(odb->sources, out, opts);\n+\treturn odb_source_generate_pack(odb->source, out, opts);\n }\n \n int odb_pack_generator_finish(struct odb_pack_generator *generator)\n@@ -988,55 +751,29 @@ int odb_pack_generator_finish(struct odb_pack_generator *generator)\n struct object_database *odb_new(struct repository *repo,\n \t\t\t\tenum odb_new_flags flags)\n {\n-\tchar *primary_source = NULL, *secondary_sources = NULL;\n \tstruct object_database *o;\n \n \tCALLOC_ARRAY(o, 1);\n \to->repo = repo;\n \tpthread_mutex_init(&o->replace_mutex, NULL);\n-\thashmap_init(&o->source_by_path, odb_source_by_path_cmp, o, 0);\n-\to->source_paths_icase = -1;\n-\n-\tif (flags & ODB_NEW_HONOR_ENV) {\n-\t\tprimary_source = xstrdup_or_null(getenv(DB_ENVIRONMENT));\n-\t\tsecondary_sources = xstrdup_or_null(getenv(ALTERNATE_DB_ENVIRONMENT));\n-\t}\n-\tif (!primary_source)\n-\t\tprimary_source = xstrfmt(\"%s/objects\", repo->commondir);\n \n-\to->sources = odb_source_new(o, primary_source, true);\n-\to->sources_tail = &o->sources->next;\n+\to->source = odb_source_new(o, flags);\n \to->inmemory_objects = &odb_source_inmemory_new(o)->base;\n \n-\todb_prepare_alternates(o, secondary_sources);\n-\n-\tfree(secondary_sources);\n-\tfree(primary_source);\n \treturn o;\n }\n \n void odb_close(struct object_database *o)\n {\n-\tstruct odb_source *source;\n-\tfor (source = o->sources; source; source = source->next)\n-\t\todb_source_close(source);\n+\todb_source_close(o->source);\n \tclose_commit_graph(o);\n }\n \n static void odb_free_sources(struct object_database *o)\n {\n-\twhile (o->sources) {\n-\t\tstruct odb_source *next;\n-\n-\t\tnext = o->sources->next;\n-\t\todb_source_free(o->sources);\n-\t\to->sources = next;\n-\t}\n-\n+\todb_source_free(o->source);\n \todb_source_free(o->inmemory_objects);\n \to->inmemory_objects = NULL;\n-\n-\thashmap_clear(&o->source_by_path);\n }\n \n void odb_free(struct object_database *o)\n@@ -1055,24 +792,12 @@ void odb_free(struct object_database *o)\n \n void odb_prepare(struct object_database *o, enum odb_prepare_flags flags)\n {\n-\tstruct odb_source *source;\n-\n \tobj_read_lock();\n \n-\t/*\n-\t * Reprepare alt odbs, in case the alternates file was modified\n-\t * during the course of this process. This only _adds_ odbs to\n-\t * the linked list, so existing odbs will continue to exist for\n-\t * the lifetime of the process. Consequently, we don't have to\n-\t * reprocess GIT_ALTERNATE_OBJECT_DIRECTORIES here.\n-\t */\n-\tif (flags & ODB_PREPARE_FLUSH_CACHES) {\n-\t\todb_prepare_alternates(o, NULL);\n+\tif (flags & ODB_PREPARE_FLUSH_CACHES)\n \t\to->object_count_valid = 0;\n-\t}\n \n-\tfor (source = o->sources; source; source = source->next)\n-\t\todb_source_prepare(source, flags);\n+\todb_source_prepare(o->source, flags);\n \n \tobj_read_unlock();\n }\n@@ -1084,8 +809,5 @@ void odb_reprepare(struct object_database *o)\n \n int odb_fsck(struct object_database *odb, struct odb_fsck_options *options)\n {\n-\tint ret = 0;\n-\tfor (struct odb_source *source = odb->sources; source; source = source->next)\n-\t\tret |= odb_source_fsck(source, options);\n-\treturn ret;\n+\treturn odb_source_fsck(odb->source, options);\n }\ndiff --git a/odb.h b/odb.h\nindex 4143812f55..0ccc47ee73 100644\n--- a/odb.h\n+++ b/odb.h\n@@ -28,13 +28,16 @@ char *compute_alternate_path(const char *path, struct strbuf *err);\n \n /*\n  * The object database encapsulates access to objects in a repository. It\n- * manages one or more sources that store the actual objects which are\n- * configured via alternates.\n+ * manages the object source as well as auxiliary data structures required to\n+ * manage objects.\n  */\n struct object_database {\n \t/* Repository that owns this database. */\n \tstruct repository *repo;\n \n+\t/* The source backing this object database. */\n+\tstruct odb_source *source;\n+\n \t/*\n \t * State of current object database transaction. Only one\n \t * transaction may be pending at a time. Is NULL when no transaction is\n@@ -42,27 +45,6 @@ struct object_database {\n \t */\n \tstruct odb_transaction *transaction;\n \n-\t/*\n-\t * Set of all object directories; the main directory is first (and\n-\t * cannot be NULL after initialization). Subsequent directories are\n-\t * alternates.\n-\t */\n-\tstruct odb_source *sources;\n-\tstruct odb_source **sources_tail;\n-\n-\t/*\n-\t * Map of object database sources, keyed by their respective paths.\n-\t * This map is used to detect the case where the same source is\n-\t * registered multiple times.\n-\t */\n-\tstruct hashmap source_by_path;\n-\n-\t/*\n-\t * Whether source paths shall be compared case-insensitively, as\n-\t * determined by \"core.ignoreCase\".\n-\t */\n-\tint source_paths_icase;\n-\n \t/*\n \t * Objects that should be substituted by other objects\n \t * (see git-replace(1)).\n@@ -822,9 +804,4 @@ int odb_generate_pack(struct object_database *odb,\n  */\n int odb_pack_generator_finish(struct odb_pack_generator *generator);\n \n-void parse_alternates(const char *string,\n-\t\t      int sep,\n-\t\t      const char *relative_base,\n-\t\t      struct strvec *out);\n-\n #endif /* ODB_H */\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 6aaf625352..072f515b36 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -15,6 +15,7 @@\n #include \"packfile.h\"\n #include \"path.h\"\n #include \"promisor-remote.h\"\n+#include \"quote.h\"\n #include \"repack.h\"\n #include \"run-command.h\"\n #include \"strbuf.h\"\n@@ -71,6 +72,7 @@ static void odb_source_files_free(struct odb_source *source)\n \t\todb_files_dir_free(files->dirs);\n \t\tfiles->dirs = next;\n \t}\n+\thashmap_clear(&files->dirs_by_path);\n \n \todb_source_release(&files->base);\n \tfree(files);\n@@ -174,6 +176,160 @@ static int odb_source_files_create_on_disk(struct odb_source *source,\n \treturn ret;\n }\n \n+/*\n+ * NEEDSWORK: we're using \"core.ignoreCase\" to deduplicate alternates that\n+ * _may_ be the same. This requires quite a bit of boilerplate for dubious\n+ * benefit:\n+ *\n+ *   - Duplicating alternates should really only lead to regressed performance.\n+ *\n+ *   - We don't properly resolve symlinks or mointpoints, so we may still end\n+ *     up duplicating alternates.\n+ *\n+ *   - The value may be lying, in which case we might deduplicate alternates\n+ *     that are in fact not mapping to the same directory.\n+ *\n+ * We should investigate whether we can remove this whole mechanism outright.\n+ */\n+static int odb_files_dir_paths_cmp(struct odb_source_files *files,\n+\t\t\t\t   const char *a, const char *b)\n+{\n+\tif (files->dirs_paths_icase < 0) {\n+\t\tint icase = 0;\n+\t\trepo_config_get_bool(files->base.odb->repo, \"core.ignorecase\", &icase);\n+\t\tfiles->dirs_paths_icase = icase;\n+\t}\n+\n+\treturn files->dirs_paths_icase ? strcasecmp(a, b) : strcmp(a, b);\n+}\n+\n+static int odb_files_dir_by_path_cmp(const void *cb_data,\n+\t\t\t\t     const struct hashmap_entry *entry,\n+\t\t\t\t     const struct hashmap_entry *entry_or_key,\n+\t\t\t\t     const void *keydata)\n+{\n+\tstruct odb_source_files *files = (struct odb_source_files *)cb_data;\n+\tconst struct odb_files_dir *dir = container_of(entry, const struct odb_files_dir, by_path_entry);\n+\tconst char *path = keydata;\n+\n+\tif (!path)\n+\t\tpath = container_of(entry_or_key, const struct odb_files_dir, by_path_entry)->abspath;\n+\n+\treturn odb_files_dir_paths_cmp(files, dir->abspath, path);\n+}\n+\n+/*\n+ * Return non-zero iff the path is usable as an alternate object directory.\n+ */\n+static bool odb_files_dir_is_usable(struct odb_source_files *files,\n+\t\t\t\t    const char *path)\n+{\n+\tstruct strbuf normalized_objdir = STRBUF_INIT;\n+\tstruct hashmap_entry key;\n+\tbool usable = false;\n+\n+\tstrbuf_realpath(&normalized_objdir, files->dirs->abspath, 1);\n+\n+\t/* Detect cases where alternate disappeared */\n+\tif (!is_directory(path)) {\n+\t\terror(_(\"object directory %s does not exist; \"\n+\t\t\t\"check .git/objects/info/alternates\"),\n+\t\t      path);\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Prevent the common mistake of listing the same\n+\t * thing twice, or object directory itself.\n+\t */\n+\tif (!hashmap_get_size(&files->dirs_by_path)) {\n+\t\tassert(!files->dirs->next);\n+\t\thashmap_entry_init(&files->dirs->by_path_entry,\n+\t\t\t\t   strihash(files->dirs->abspath));\n+\t\thashmap_add(&files->dirs_by_path, &files->dirs->by_path_entry);\n+\t}\n+\n+\tif (!odb_files_dir_paths_cmp(files, path, normalized_objdir.buf))\n+\t\tgoto out;\n+\n+\thashmap_entry_init(&key, strihash(path));\n+\tif (hashmap_get(&files->dirs_by_path, &key, path))\n+\t\tgoto out;\n+\n+\tusable = true;\n+\n+out:\n+\tstrbuf_release(&normalized_objdir);\n+\treturn usable;\n+}\n+\n+static void parse_alternates(const char *string,\n+\t\t\t     int sep,\n+\t\t\t     const char *relative_base,\n+\t\t\t     struct strvec *out)\n+{\n+\tstruct strbuf pathbuf = STRBUF_INIT;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\n+\tif (!string || !*string)\n+\t\treturn;\n+\n+\twhile (*string) {\n+\t\tconst char *end;\n+\n+\t\tstrbuf_reset(&buf);\n+\t\tstrbuf_reset(&pathbuf);\n+\n+\t\tif (*string == '#') {\n+\t\t\t/* comment; consume up to next separator */\n+\t\t\tend = strchrnul(string, sep);\n+\t\t} else if (*string == '\"' && !unquote_c_style(&buf, string, &end)) {\n+\t\t\t/*\n+\t\t\t * quoted path; unquote_c_style has copied the\n+\t\t\t * data for us and set \"end\". Broken quoting (e.g.,\n+\t\t\t * an entry that doesn't end with a quote) falls\n+\t\t\t * back to the unquoted case below.\n+\t\t\t */\n+\t\t} else {\n+\t\t\t/* normal, unquoted path */\n+\t\t\tend = strchrnul(string, sep);\n+\t\t\tstrbuf_add(&buf, string, end - string);\n+\t\t}\n+\n+\t\tif (*end)\n+\t\t\tend++;\n+\t\tstring = end;\n+\n+\t\tif (!buf.len)\n+\t\t\tcontinue;\n+\n+\t\tif (!is_absolute_path(buf.buf) && relative_base) {\n+\t\t\tstrbuf_realpath(&pathbuf, relative_base, 1);\n+\t\t\tstrbuf_addch(&pathbuf, '/');\n+\t\t}\n+\t\tstrbuf_addbuf(&pathbuf, &buf);\n+\n+\t\tstrbuf_reset(&buf);\n+\t\tif (!strbuf_realpath(&buf, pathbuf.buf, 0)) {\n+\t\t\terror(_(\"unable to normalize alternate object path: %s\"),\n+\t\t\t      pathbuf.buf);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\t/*\n+\t\t * The trailing slash after the directory name is given by\n+\t\t * this function at the end. Remove duplicates.\n+\t\t */\n+\t\twhile (buf.len && buf.buf[buf.len - 1] == '/')\n+\t\t\tstrbuf_setlen(&buf, buf.len - 1);\n+\n+\t\tstrvec_push(out, buf.buf);\n+\t}\n+\n+\tstrbuf_release(&pathbuf);\n+\tstrbuf_release(&buf);\n+}\n+\n static int read_alternates(const char *object_dir, struct strvec *out)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n@@ -192,11 +348,70 @@ static int read_alternates(const char *object_dir, struct strvec *out)\n \treturn 0;\n }\n \n+static void odb_add_alternate_recursively(struct odb_source_files *files,\n+\t\t\t\t\t  const char *path,\n+\t\t\t\t\t  int depth)\n+{\n+\tstruct odb_files_dir *alternate;\n+\tstruct strvec alternates = STRVEC_INIT;\n+\n+\tif (!odb_files_dir_is_usable(files, path))\n+\t\tgoto out;\n+\n+\talternate = odb_files_dir_new(files->base.odb, path, false);\n+\n+\t/* add the alternate entry */\n+\t*files->dirs_tail = alternate;\n+\tfiles->dirs_tail = &(alternate->next);\n+\n+\thashmap_entry_init(&alternate->by_path_entry, strihash(alternate->abspath));\n+\tif (hashmap_get(&files->dirs_by_path, &alternate->by_path_entry,\n+\t\t\talternate->abspath))\n+\t\tBUG(\"object directory must not yet exist\");\n+\thashmap_add(&files->dirs_by_path, &alternate->by_path_entry);\n+\n+\t/* recursively add alternates */\n+\tread_alternates(alternate->abspath, &alternates);\n+\tif (alternates.nr && depth + 1 > 5) {\n+\t\terror(_(\"%s: ignoring alternate object stores, nesting too deep\"),\n+\t\t      path);\n+\t} else {\n+\t\tfor (size_t i = 0; i < alternates.nr; i++)\n+\t\t\todb_add_alternate_recursively(files, alternates.v[i], depth + 1);\n+\t}\n+\n+ out:\n+\tstrvec_clear(&alternates);\n+}\n+\n+static void odb_prepare_alternates(struct odb_source_files *files,\n+\t\t\t\t   const char *alternate_db)\n+{\n+\tstruct strvec alternates = STRVEC_INIT;\n+\n+\tparse_alternates(alternate_db, PATH_SEP, NULL, &alternates);\n+\tread_alternates(files->dirs->abspath, &alternates);\n+\n+\tfor (size_t i = 0; i < alternates.nr; i++)\n+\t\todb_add_alternate_recursively(files, alternates.v[i], 0);\n+\n+\tstrvec_clear(&alternates);\n+}\n+\n static void odb_source_files_prepare(struct odb_source *source,\n \t\t\t\t     enum odb_prepare_flags flags)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \n+\t/*\n+\t * Reprepare alternates, in case the alternates file was modified\n+\t * during the course of this process. This only _adds_ directories to\n+\t * the linked list, so existing directories will continue to exist\n+\t * for the lifetime of the process.\n+\t */\n+\tif (flags & ODB_PREPARE_FLUSH_CACHES)\n+\t\todb_prepare_alternates(files, NULL);\n+\n \tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n \t\todb_source_prepare(&dir->loose->base, flags);\n \t\todb_source_prepare(&dir->packed->base, flags);\n@@ -1009,23 +1224,15 @@ static int odb_source_files_fsck(struct odb_source *source,\n \n struct odb_files_dir *odb_source_files_find_dir(struct object_database *odb, const char *obj_dir)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(odb->source);\n \tchar *obj_dir_real = real_pathdup(obj_dir, 1);\n \tstruct strbuf odb_path_real = STRBUF_INIT;\n-\tstruct odb_files_dir *dir = NULL;\n-\tstruct odb_source *source;\n-\n-\tfor (source = odb->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files;\n-\n-\t\tif (source->type != ODB_SOURCE_FILES)\n-\t\t\tcontinue;\n-\t\tfiles = odb_source_files_downcast(source);\n+\tstruct odb_files_dir *dir;\n \n-\t\tstrbuf_realpath(&odb_path_real, files->dirs->abspath, 1);\n-\t\tif (!strcmp(obj_dir_real, odb_path_real.buf)) {\n-\t\t\tdir = files->dirs;\n+\tfor (dir = files->dirs; dir; dir = dir->next) {\n+\t\tstrbuf_realpath(&odb_path_real, dir->abspath, 1);\n+\t\tif (!strcmp(obj_dir_real, odb_path_real.buf))\n \t\t\tbreak;\n-\t\t}\n \t}\n \n \tfree(obj_dir_real);\n@@ -1034,14 +1241,27 @@ struct odb_files_dir *odb_source_files_find_dir(struct object_database *odb, con\n }\n \n struct odb_source_files *odb_source_files_new(struct object_database *odb,\n-\t\t\t\t\t      const char *path,\n-\t\t\t\t\t      bool local)\n+\t\t\t\t\t      enum odb_new_flags flags)\n {\n \tstruct odb_source_files *files;\n+\tchar *object_dir = NULL;\n+\tchar *alternates = NULL;\n+\n+\tif (flags & ODB_NEW_HONOR_ENV) {\n+\t\tobject_dir = xstrdup_or_null(getenv(DB_ENVIRONMENT));\n+\t\talternates = xstrdup_or_null(getenv(ALTERNATE_DB_ENVIRONMENT));\n+\t}\n+\tif (!object_dir)\n+\t\tobject_dir = xstrfmt(\"%s/objects\", odb->repo->commondir);\n \n \tCALLOC_ARRAY(files, 1);\n-\todb_source_init(&files->base, odb, ODB_SOURCE_FILES, path, local);\n-\tfiles->dirs = odb_files_dir_new(odb, path, local);\n+\todb_source_init(&files->base, odb, ODB_SOURCE_FILES, object_dir, true);\n+\n+\thashmap_init(&files->dirs_by_path, odb_files_dir_by_path_cmp, files, 0);\n+\tfiles->dirs_paths_icase = -1;\n+\n+\tfiles->dirs = odb_files_dir_new(odb, object_dir, true);\n+\tfiles->dirs_tail = &files->dirs->next;\n \n \tfiles->base.free = odb_source_files_free;\n \tfiles->base.close = odb_source_files_close;\n@@ -1067,8 +1287,12 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb,\n \t * is not (yet) possible though because we access and assume relative\n \t * paths in the primary ODB source in some user-facing functionality.\n \t */\n-\tif (!is_absolute_path(path))\n+\tif (!is_absolute_path(object_dir))\n \t\tchdir_notify_register(odb_source_files_reparent, files);\n \n+\todb_prepare_alternates(files, alternates);\n+\n+\tfree(object_dir);\n+\tfree(alternates);\n \treturn files;\n }\ndiff --git a/odb/source-files.h b/odb/source-files.h\nindex 36af0c1b8b..837e890304 100644\n--- a/odb/source-files.h\n+++ b/odb/source-files.h\n@@ -18,6 +18,12 @@ struct odb_files_dir {\n \t/* List of alternate object directories. */\n \tstruct odb_files_dir *next;\n \n+\t/*\n+\t * Entry in the files source's map of directories, keyed by this\n+\t * directory's path.\n+\t */\n+\tstruct hashmap_entry by_path_entry;\n+\n \t/* The two sources derived from this object directory. */\n \tstruct odb_source_loose *loose;\n \tstruct odb_source_packed *packed;\n@@ -46,12 +52,25 @@ struct odb_source_files {\n \t * alternates.\n \t */\n \tstruct odb_files_dir *dirs;\n+\tstruct odb_files_dir **dirs_tail;\n+\n+\t/*\n+\t * Map of object directories, keyed by their respective paths. This\n+\t * map is used to detect the case where the same directory is\n+\t * registered multiple times.\n+\t */\n+\tstruct hashmap dirs_by_path;\n+\n+\t/*\n+\t * Whether directory paths shall be compared case-insensitively, as\n+\t * determined by \"core.ignoreCase\".\n+\t */\n+\tint dirs_paths_icase;\n };\n \n /* Allocate and initialize a new object source. */\n struct odb_source_files *odb_source_files_new(struct object_database *odb,\n-\t\t\t\t\t      const char *path,\n-\t\t\t\t\t      bool local);\n+\t\t\t\t\t      enum odb_new_flags flags);\n \n /*\n  * Optimize the files object database source by repacking loose objects and\ndiff --git a/odb/source.c b/odb/source.c\nindex 30188b806d..b25ef14df8 100644\n--- a/odb/source.c\n+++ b/odb/source.c\n@@ -24,10 +24,9 @@ const char *odb_source_type_to_name(enum odb_source_type type)\n }\n \n struct odb_source *odb_source_new(struct object_database *odb,\n-\t\t\t\t  const char *path,\n-\t\t\t\t  bool local)\n+\t\t\t\t  enum odb_new_flags flags)\n {\n-\treturn &odb_source_files_new(odb, path, local)->base;\n+\treturn &odb_source_files_new(odb, flags)->base;\n }\n \n void odb_source_init(struct odb_source *source,\ndiff --git a/odb/source.h b/odb/source.h\nindex 9fd2b2e5b5..6718aced6a 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -49,24 +49,9 @@ struct odb_create_on_disk_options {\n /*\n  * The source is the part of the object database that stores the actual\n  * objects. It thus encapsulates the logic to read and write the specific\n- * on-disk format. An object database can have multiple sources:\n- *\n- *   - The primary source, which is typically located in \"$GIT_DIR/objects\".\n- *     This is where new objects are usually written to.\n- *\n- *   - Alternate sources, which are configured via \"objects/info/alternates\" or\n- *     via the GIT_ALTERNATE_OBJECT_DIRECTORIES environment variable. These\n- *     alternate sources are only used to read objects.\n+ * on-disk format.\n  */\n struct odb_source {\n-\tstruct odb_source *next;\n-\n-\t/*\n-\t * Entry in the object database's map of sources, keyed by this\n-\t * source's path.\n-\t */\n-\tstruct hashmap_entry by_path_entry;\n-\n \t/* Object database that owns this object source. */\n \tstruct object_database *odb;\n \n@@ -331,13 +316,11 @@ struct odb_source {\n };\n \n /*\n- * Allocate and initialize a new source for the given object database located\n- * at `path`. `local` indicates whether or not the source is the local and thus\n- * primary object source of the object database.\n+ * Allocate and initialize a new source for the given object database. The path\n+ * of the source is derived from repository paths.\n  */\n struct odb_source *odb_source_new(struct object_database *odb,\n-\t\t\t\t  const char *path,\n-\t\t\t\t  bool local);\n+\t\t\t\t  enum odb_new_flags flags);\n \n /*\n  * Initialize the source for the given object database located at `path`.\ndiff --git a/odb/streaming.c b/odb/streaming.c\nindex 8f2143cab5..5ac172beb6 100644\n--- a/odb/streaming.c\n+++ b/odb/streaming.c\n@@ -182,12 +182,8 @@ static int istream_source(struct odb_stream **out,\n \t\t\t  struct object_database *odb,\n \t\t\t  const struct object_id *oid)\n {\n-\tstruct odb_source *source;\n-\n-\tfor (source = odb->sources; source; source = source->next)\n-\t\tif (!odb_source_read_object_stream(out, source, oid))\n-\t\t\treturn 0;\n-\n+\tif (!odb_source_read_object_stream(out, odb->source, oid))\n+\t\treturn 0;\n \treturn open_istream_incore(out, odb, oid);\n }\n \ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex f6f20088ec..69824a3921 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -12,7 +12,7 @@ int odb_transaction_begin(struct object_database *odb,\n \tif (odb->transaction)\n \t\treturn error(_(\"object database transaction already pending\"));\n \n-\tret = odb_source_begin_transaction(odb->sources, out, flags);\n+\tret = odb_source_begin_transaction(odb->source, out, flags);\n \tif (!ret)\n \t\todb->transaction = *out;\n \ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 52556b4543..d3c7997fb3 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -712,15 +712,13 @@ static int open_bitmap_for_source(struct odb_source_packed *source,\n static int open_bitmap(struct repository *r,\n \t\t       struct bitmap_index *bitmap_git)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files = odb_source_files_downcast(r->objects->source);\n \tbool found = false;\n \n \tassert(!bitmap_git->map);\n \n-\tfor (source = r->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\n-\t\tif (!open_bitmap_for_source(files->dirs->packed, bitmap_git))\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tif (!open_bitmap_for_source(dir->packed, bitmap_git))\n \t\t\tfound = true;\n \n \t\t/*\ndiff --git a/packfile.c b/packfile.c\nindex 93b69d7f50..ebe430662e 100644\n--- a/packfile.c\n+++ b/packfile.c\n@@ -273,14 +273,13 @@ static void scan_windows(struct packed_git *p,\n \n static int unuse_one_window(struct object_database *odb)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files = odb_source_files_downcast(odb->source);\n \tstruct packfile_list_entry *e;\n \tstruct packed_git *lru_p = NULL;\n \tstruct pack_window *lru_w = NULL, *lru_l = NULL;\n \n-\tfor (source = odb->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tfor (e = files->dirs->packed->packs.head; e; e = e->next)\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tfor (e = dir->packed->packs.head; e; e = e->next)\n \t\t\tscan_windows(e->pack, &lru_p, &lru_w, &lru_l);\n \t}\n \n@@ -450,15 +449,14 @@ static void find_lru_pack(struct packed_git *p, struct packed_git **lru_p, struc\n \n static int close_one_pack(struct repository *r)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files = odb_source_files_downcast(r->objects->source);\n \tstruct packfile_list_entry *e;\n \tstruct packed_git *lru_p = NULL;\n \tstruct pack_window *mru_w = NULL;\n \tint accept_windows_inuse = 1;\n \n-\tfor (source = r->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tfor (e = files->dirs->packed->packs.head; e; e = e->next) {\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tfor (e = dir->packed->packs.head; e; e = e->next) {\n \t\t\tif (e->pack->pack_fd == -1)\n \t\t\t\tcontinue;\n \t\t\tfind_lru_pack(e->pack, &lru_p, &mru_w, &accept_windows_inuse);\n@@ -1921,11 +1919,10 @@ struct packed_git **packfile_store_get_kept_pack_cache(struct odb_source_packed\n \n int has_object_pack(struct repository *r, const struct object_id *oid)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files = odb_source_files_downcast(r->objects->source);\n \n-\tfor (source = r->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tif (!odb_source_read_object_info(&files->dirs->packed->base, oid, NULL, 0, NULL))\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tif (!odb_source_read_object_info(&dir->packed->base, oid, NULL, 0, NULL))\n \t\t\treturn 1;\n \t}\n \n@@ -1935,14 +1932,13 @@ int has_object_pack(struct repository *r, const struct object_id *oid)\n int has_object_kept_pack(struct repository *r, const struct object_id *oid,\n \t\t\t unsigned flags)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files = odb_source_files_downcast(r->objects->source);\n \tstruct pack_entry e;\n \n-\tfor (source = r->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n \t\tstruct packed_git **cache;\n \n-\t\tcache = packfile_store_get_kept_pack_cache(files->dirs->packed, flags);\n+\t\tcache = packfile_store_get_kept_pack_cache(dir->packed, flags);\n \n \t\tfor (; *cache; cache++) {\n \t\t\tstruct packed_git *p = *cache;\ndiff --git a/packfile.h b/packfile.h\nindex fd1cf0ad6c..ed3378c457 100644\n--- a/packfile.h\n+++ b/packfile.h\n@@ -69,20 +69,20 @@ void packfile_store_add_pack(struct odb_source_packed *store,\n struct packfile_list_entry *packfile_store_get_packs(struct odb_source_packed *store);\n \n struct repo_for_each_pack_data {\n-\tstruct odb_source *source;\n+\tstruct odb_files_dir *dir;\n \tstruct packfile_list_entry *entry;\n };\n \n static inline struct repo_for_each_pack_data repo_for_eack_pack_data_init(struct repository *repo)\n {\n \tstruct repo_for_each_pack_data data = { 0 };\n+\tstruct odb_source_files *files = odb_source_files_downcast(repo->objects->source);\n \n-\tfor (struct odb_source *source = repo->objects->sources; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tstruct packfile_list_entry *entry = packfile_store_get_packs(files->dirs->packed);\n+\tfor (struct odb_files_dir *dir = files->dirs; dir; dir = dir->next) {\n+\t\tstruct packfile_list_entry *entry = packfile_store_get_packs(dir->packed);\n \t\tif (!entry)\n \t\t\tcontinue;\n-\t\tdata.source = source;\n+\t\tdata.dir = dir;\n \t\tdata.entry = entry;\n \t\tbreak;\n \t}\n@@ -92,23 +92,22 @@ static inline struct repo_for_each_pack_data repo_for_eack_pack_data_init(struct\n \n static inline void repo_for_each_pack_data_next(struct repo_for_each_pack_data *data)\n {\n-\tstruct odb_source *source;\n+\tstruct odb_files_dir *dir;\n \n \tdata->entry = data->entry->next;\n \tif (data->entry)\n \t\treturn;\n \n-\tfor (source = data->source->next; source; source = source->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\t\tstruct packfile_list_entry *entry = packfile_store_get_packs(files->dirs->packed);\n+\tfor (dir = data->dir->next; dir; dir = dir->next) {\n+\t\tstruct packfile_list_entry *entry = packfile_store_get_packs(dir->packed);\n \t\tif (!entry)\n \t\t\tcontinue;\n-\t\tdata->source = source;\n+\t\tdata->dir = dir;\n \t\tdata->entry = entry;\n \t\treturn;\n \t}\n \n-\tdata->source = NULL;\n+\tdata->dir = NULL;\n \tdata->entry = NULL;\n }\n \ndiff --git a/path.c b/path.c\nindex c3a709a928..4608a56927 100644\n--- a/path.c\n+++ b/path.c\n@@ -396,7 +396,7 @@ static void adjust_git_path(struct repository *repo,\n \t\tstrbuf_splice(buf, 0, buf->len,\n \t\t\t      repo->index_file, strlen(repo->index_file));\n \telse if (dir_prefix(base, \"objects\"))\n-\t\treplace_dir(buf, git_dir_len + 7, repo->objects->sources->path);\n+\t\treplace_dir(buf, git_dir_len + 7, repo->objects->source->path);\n \telse if (repo_settings_get_hooks_path(repo) && dir_prefix(base, \"hooks\"))\n \t\treplace_dir(buf, git_dir_len + 5, repo_settings_get_hooks_path(repo));\n \telse if (repo->different_commondir)\ndiff --git a/prune-packed.c b/prune-packed.c\nindex d49dc11957..b6749b8d13 100644\n--- a/prune-packed.c\n+++ b/prune-packed.c\n@@ -40,7 +40,7 @@ void prune_packed_objects(int opts)\n \t\tprogress = start_delayed_progress(the_repository,\n \t\t\t\t\t\t  _(\"Removing duplicate objects\"), 256);\n \n-\tfor_each_loose_file_in_source(the_repository->objects->sources,\n+\tfor_each_loose_file_in_source(the_repository->objects->source,\n \t\t\t\t      prune_object, NULL, prune_subdir, &opts);\n \n \t/* Ensure we show 100% before finishing progress */\ndiff --git a/repack.c b/repack.c\nindex e20431690c..53c34197f3 100644\n--- a/repack.c\n+++ b/repack.c\n@@ -59,7 +59,7 @@ void repack_remove_redundant_pack(struct repository *repo, const char *dir_name,\n \t\t\t\t  bool wrote_incremental_midx)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n-\tstruct odb_source_files *files = odb_source_files_downcast(repo->objects->sources);\n+\tstruct odb_source_files *files = odb_source_files_downcast(repo->objects->source);\n \tstruct multi_pack_index *m = get_multi_pack_index(files->dirs->packed);\n \tstrbuf_addf(&buf, \"%s.pack\", base_name);\n \tif (m && files->base.local && midx_contains_pack(m, buf.buf)) {\n@@ -158,7 +158,7 @@ void existing_packs_collect(struct existing_packs *existing,\n \t\t\tstring_list_append(&existing->non_kept_packs, buf.buf);\n \t}\n \n-\texisting->source = existing->repo->objects->sources;\n+\texisting->source = existing->repo->objects->source;\n \n \tstring_list_sort(&existing->kept_packs);\n \tstring_list_sort(&existing->non_kept_packs);\ndiff --git a/repository.c b/repository.c\nindex b857e1c580..fa4214ff41 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -126,9 +126,9 @@ const char *repo_get_common_dir(struct repository *repo)\n \n const char *repo_get_object_directory(struct repository *repo)\n {\n-\tif (!repo->objects->sources)\n+\tif (!repo->objects->source)\n \t\tBUG(\"repository hasn't been set up\");\n-\treturn repo->objects->sources->path;\n+\treturn repo->objects->source->path;\n }\n \n const char *repo_get_index_file(struct repository *repo)\ndiff --git a/setup.c b/setup.c\nindex 29474fc292..bc7d451917 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2669,7 +2669,7 @@ void create_object_database(struct repository *repo,\n \n \trepo->objects = odb_new(repo, ODB_NEW_HONOR_ENV);\n \n-\tif (odb_source_create_on_disk(repo->objects->sources, &opts) < 0)\n+\tif (odb_source_create_on_disk(repo->objects->source, &opts) < 0)\n \t\tdie(_(\"failed creating object database\"));\n }\n \ndiff --git a/t/helper/test-read-graph.c b/t/helper/test-read-graph.c\nindex a75c817e47..62e2384052 100644\n--- a/t/helper/test-read-graph.c\n+++ b/t/helper/test-read-graph.c\n@@ -78,7 +78,7 @@ int cmd__read_graph(int argc, const char **argv)\n \tint ret = 0;\n \n \tsetup_git_directory(the_repository);\n-\tsource = the_repository->objects->sources;\n+\tsource = the_repository->objects->source;\n \n \tprepare_repo_settings(the_repository);\n \ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex 2f2ffbbc7d..3debb60270 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -60,7 +60,7 @@ static void tmp_objdir_reparent(const char *old_cwd,\n  */\n static void tmp_objdir_restore_source(struct tmp_objdir *t)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(t->repo->objects->sources);\n+\tstruct odb_source_files *files = odb_source_files_downcast(t->repo->objects->source);\n \tstruct odb_files_dir *cur_dir = files->dirs;\n \n \tif (t->temp_dir != files->dirs)\n@@ -159,7 +159,7 @@ struct tmp_objdir *tmp_objdir_create(struct repository *r,\n \t\t\t\t     const char *prefix,\n \t\t\t\t     int will_destroy)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(r->objects->sources);\n+\tstruct odb_source_files *files = odb_source_files_downcast(r->objects->source);\n \tstatic int installed_handlers;\n \tstruct tmp_objdir *t;\n \n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"553946","messageId":"20261002-pks-odb-move-alternates-v1-13-8a63507b88c4@pks.im","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-0-8a63507b88c4@pks.im","subject":"[PATCH 13/13] odb/source: drop `read_alternates` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-02T10:08:24Z","receivedAt":"2026-10-02T10:09:04Z","isPatch":true,"body":"Now that alternates are an implementation detail of the \"files\" backend\nthere is no reason anymore to expose them anywhere else. The callback to\nread alternates is thus no longer required. Remove it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb/source-files.c    |  7 -------\n odb/source-inmemory.c |  7 -------\n odb/source-loose.c    |  7 -------\n odb/source-packed.c   |  7 -------\n odb/source.h          | 27 ---------------------------\n 5 files changed, 55 deletions(-)\n\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 072f515b36..455f409db0 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -594,12 +594,6 @@ static int odb_source_files_begin_transaction(struct odb_source *source,\n \treturn odb_transaction_files_begin(source, out, flags);\n }\n \n-static int odb_source_files_read_alternates(struct odb_source *source,\n-\t\t\t\t\t    struct strvec *out)\n-{\n-\treturn read_alternates(source->path, out);\n-}\n-\n static int too_many_loose_objects(struct odb_source_files *files, int limit)\n {\n \tunsigned long loose_count;\n@@ -1277,7 +1271,6 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb,\n \tfiles->base.write_object = odb_source_files_write_object;\n \tfiles->base.write_object_stream = odb_source_files_write_object_stream;\n \tfiles->base.begin_transaction = odb_source_files_begin_transaction;\n-\tfiles->base.read_alternates = odb_source_files_read_alternates;\n \tfiles->base.optimize = odb_source_files_optimize;\n \tfiles->base.optimize_required = odb_source_files_optimize_required;\n \tfiles->base.generate_pack = odb_source_files_generate_pack;\ndiff --git a/odb/source-inmemory.c b/odb/source-inmemory.c\nindex f67c74a724..868e723c81 100644\n--- a/odb/source-inmemory.c\n+++ b/odb/source-inmemory.c\n@@ -322,12 +322,6 @@ static int odb_source_inmemory_begin_transaction(struct odb_source *source UNUSE\n \treturn error(\"in-memory source does not support transactions\");\n }\n \n-static int odb_source_inmemory_read_alternates(struct odb_source *source UNUSED,\n-\t\t\t\t\t       struct strvec *out UNUSED)\n-{\n-\treturn 0;\n-}\n-\n static void odb_source_inmemory_close(struct odb_source *source UNUSED)\n {\n }\n@@ -390,7 +384,6 @@ struct odb_source_inmemory *odb_source_inmemory_new(struct object_database *odb)\n \tsource->base.write_object_stream = odb_source_inmemory_write_object_stream;\n \tsource->base.freshen_object = odb_source_inmemory_freshen_object;\n \tsource->base.begin_transaction = odb_source_inmemory_begin_transaction;\n-\tsource->base.read_alternates = odb_source_inmemory_read_alternates;\n \n \treturn source;\n }\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex b2fbccd3c0..0664634938 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -985,12 +985,6 @@ static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED,\n \treturn error(\"loose source does not support transactions\");\n }\n \n-static int odb_source_loose_read_alternates(struct odb_source *source UNUSED,\n-\t\t\t\t\t    struct strvec *out UNUSED)\n-{\n-\treturn 0;\n-}\n-\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -1145,7 +1139,6 @@ struct odb_source_loose *odb_source_loose_new(struct object_database *odb,\n \tloose->base.write_object = odb_source_loose_write_object;\n \tloose->base.write_object_stream = odb_source_loose_write_object_stream;\n \tloose->base.begin_transaction = odb_source_loose_begin_transaction;\n-\tloose->base.read_alternates = odb_source_loose_read_alternates;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(odb_source_loose_reparent, loose);\ndiff --git a/odb/source-packed.c b/odb/source-packed.c\nindex 831501f397..2226fbd313 100644\n--- a/odb/source-packed.c\n+++ b/odb/source-packed.c\n@@ -687,12 +687,6 @@ static int odb_source_packed_begin_transaction(struct odb_source *source UNUSED,\n \treturn error(\"packed backend cannot begin transactions\");\n }\n \n-static int odb_source_packed_read_alternates(struct odb_source *source UNUSED,\n-\t\t\t\t\t     struct strvec *out UNUSED)\n-{\n-\treturn 0;\n-}\n-\n void (*report_garbage)(unsigned seen_bits, const char *path);\n \n static void report_helper(const struct string_list *list,\n@@ -1019,7 +1013,6 @@ struct odb_source_packed *odb_source_packed_new(struct object_database *odb,\n \tpacked->base.write_object = odb_source_packed_write_object;\n \tpacked->base.write_object_stream = odb_source_packed_write_object_stream;\n \tpacked->base.begin_transaction = odb_source_packed_begin_transaction;\n-\tpacked->base.read_alternates = odb_source_packed_read_alternates;\n \n \tif (!is_absolute_path(path))\n \t\tchdir_notify_register(odb_source_packed_reparent, packed);\ndiff --git a/odb/source.h b/odb/source.h\nindex 6718aced6a..1867347dfc 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -258,19 +258,6 @@ struct odb_source {\n \t\t\t\t struct odb_transaction **out,\n \t\t\t\t enum odb_transaction_flags flags);\n \n-\t/*\n-\t * This callback is expected to read the list of alternate object\n-\t * database sources connected to it and write them into the `strvec`.\n-\t *\n-\t * The result is expected to be paths to the alternates. All paths must\n-\t * be resolved to absolute paths.\n-\t *\n-\t * The callback is expected to return 0 on success, a negative error\n-\t * code otherwise.\n-\t */\n-\tint (*read_alternates)(struct odb_source *source,\n-\t\t\t       struct strvec *out);\n-\n \t/*\n \t * This callback is expected to optimize the object database source.\n \t * Returns 0 on success, a negative error code otherwise.\n@@ -507,20 +494,6 @@ static inline int odb_source_write_object_stream(struct odb_source *source,\n \treturn source->write_object_stream(source, stream, oid);\n }\n \n-/*\n- * Read the list of alternative object database sources from the given backend\n- * and populate the `strvec` with them. The listing is not recursive -- that\n- * is, if any of the yielded alternate sources has alternates itself, those\n- * will not be yielded as part of this function call.\n- *\n- * Return 0 on success, a negative error code otherwise.\n- */\n-static inline int odb_source_read_alternates(struct odb_source *source,\n-\t\t\t\t\t     struct strvec *out)\n-{\n-\treturn source->read_alternates(source, out);\n-}\n-\n /*\n  * Create a new transaction that can be used to write objects into a temporary\n  * staging area. The objects will only be persisted when the transaction is\n\n-- \n2.56.0.379.gc618271300.dirty\n\n"},{"id":"554215","messageId":"CAOLa=ZS_S3bYXEufor7AgXpX76NwtV4NJdGRO57-bpSkYZrzvw@mail.gmail.com","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-1-8a63507b88c4@pks.im","subject":"Re: [PATCH 01/13] commit-graph: require resolved packfile paths for `stdin_packs`","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-10-05T19:27:27Z","receivedAt":"2026-10-05T19:27:27Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Users can ask git-commit-graph(1) to write a commit graph specifically\n> for a set of packfiles via the \"--stdin-packs\" option. Those users are\n> expected to pass in relative paths, and those eventually get resolved in\n> `fill_oids_from_packs()`. This ties the logic in \"commit-graph.c\" to the\n> specific object database source.\n>\n> Refactor the logic to instead require the caller to pass in resolved\n> packfiles to untangle that dependency.\n>\n\nNit: the changes look good, what I'm missing is 'why' are we doing this\nchange.\n\n[snip]\n"},{"id":"554216","messageId":"CAOLa=ZSNHWFw5Vj_5qg16ipp1QA0pDcV8h=hOA=ma4hy6F_LcQ@mail.gmail.com","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-2-8a63507b88c4@pks.im","subject":"Re: [PATCH 02/13] commit-graph: stop depending on `struct odb_source`","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-10-05T19:43:44Z","receivedAt":"2026-10-05T19:43:44Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n[snip]\n\n> @@ -28,7 +29,7 @@\n>  #include \"tree.h\"\n>  #include \"chunk-format.h\"\n>\n> -void git_test_write_commit_graph_or_die(struct odb_source *source)\n> +void git_test_write_commit_graph_or_die(struct repository *repo)\n>  {\n>  \tint flags = 0;\n>  \tif (!git_env_bool(GIT_TEST_COMMIT_GRAPH, 0))\n> @@ -37,7 +38,7 @@ void git_test_write_commit_graph_or_die(struct odb_source *source)\n>  \tif (git_env_bool(GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS, 0))\n>  \t\tflags = COMMIT_GRAPH_WRITE_BLOOM_FILTERS;\n>\n> -\tif (write_commit_graph_reachable(source, flags, NULL))\n> +\tif (write_commit_graph_reachable(repo, repo->objects->sources->path, flags, NULL))\n>  \t\tdie(\"failed to write commit-graph under GIT_TEST_COMMIT_GRAPH\");\n>  }\n>\n\nShouldn't the caller of `git_test_write_commit_graph_or_die()` send in\n(repo, path) and we forward that path, instead of using the path from\n`repo->objects->sources->path`?\n"},{"id":"554248","messageId":"CAOLa=ZT-=VQNXBs=i8ZkB1yZoJV_i_n90S+Ygdgt5=tmquVOOA@mail.gmail.com","threadId":"66446","inReplyTo":"20261002-pks-odb-move-alternates-v1-4-8a63507b88c4@pks.im","subject":"Re: [PATCH 04/13] odb: refactor `odb_for_each_alternate()` to yield dirs","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-10-06T08:42:32Z","receivedAt":"2026-10-06T08:42:32Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n[snip]\n\n> @@ -468,9 +470,11 @@ static int refs_from_alternate_cb(struct odb_source *alternate,\n>  void odb_for_each_alternate_ref(struct object_database *odb,\n>  \t\t\t\todb_for_each_alternate_ref_fn cb, void *payload)\n>  {\n> -\tstruct alternate_refs_data data;\n> -\tdata.fn = cb;\n> -\tdata.payload = payload;\n> +\tstruct alternate_refs_data data = {\n> +\t\t.fn = cb,\n> +\t\t.payload = payload,\n> +\t\t.repo = odb->repo,\n> +\t};\n>  \todb_for_each_alternate(odb, refs_from_alternate_cb, &data);\n>  }\n>\n> @@ -481,7 +485,7 @@ int odb_for_each_alternate(struct object_database *odb,\n>  \tint r = 0;\n>\n>  \tfor (alternate = odb->sources->next; alternate; alternate = alternate->next) {\n> -\t\tr = cb(alternate, payload);\n> +\t\tr = cb(odb_source_files_downcast(alternate)->dirs, payload);\n>  \t\tif (r)\n>  \t\t\tbreak;\n>  \t}\n>\n\nOkay, so here we call the callbacks with the `dirs` now and this\ncorresponds with the changes in the rest of the patch. Makes sense.\n\n[snip]\n"}]}