{"thread":{"id":"65667","subject":"[PATCH 00/18] odb: make loose object source a proper `struct odb_source`","startedAt":"2026-05-21T08:22:32Z","lastAt":"2026-06-03T20:04:30Z","messageCount":46,"participants":["Patrick Steinhardt","Junio C Hamano","Karthik Nayak"],"isPatch":true,"patchVersion":1,"patchTotal":18},"messages":[{"id":"543787","messageId":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","threadId":"65667","inReplyTo":null,"subject":"[PATCH 00/18] odb: make loose object source a proper `struct odb_source`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:20Z","receivedAt":"2026-05-21T08:22:32Z","isPatch":true,"body":"Hi,\n\nthis patch series converts the loose object source into a proper `struct\nodb_source` so that it can be used via our generic interfaces.\n\nThe patch series is relatively straight-forward, as the source basically\nalready exists as such and the interfaces already match. So for most of\nthe part we are just moving around some code and converting functions\nthat were previously called directly into callbacks.\n\nI guess the only part that needs some attention is that there is some\nconfusion at first with the `struct odb_source_loose::source` parent\npointer that initially points at the owning `struct odb_source_files`.\nThis relationship doesn't make much sense, as a loose source can totally\nexist standalone without the files source.\n\nWe're thus getting rid of this relationship in this series, too. I found\nit quite hard to reason about which pointer one is holding at any point\nin time though, doubly so because the parent pointer was named \"source\",\nwhich is rather generic. The second commit thus renames the pointer to\n`files` and converts it into `struct odb_source_files` to make the\ntransition cleaner, but the whole pointer will be dropped at the end of\nthis series.\n\nThe series is built on top of aec3f58750 (Sync with 'maint', 2026-05-21)\nwith ps/odb-in-memory at d2902a4549 (t/unit-tests: add tests for the\nin-memory object source, 2026-04-10) merged into it.\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (18):\n      odb/source-loose: move loose source into \"odb/\" subsystem\n      odb/source-loose: store pointer to \"files\" instead of generic source\n      odb/source-loose: start converting to a proper `struct odb_source`\n      odb/source-loose: wire up `reprepare()` callback\n      odb/source-loose: wire up `close()` callback\n      odb/source-loose: wire up `read_object_info()` callback\n      odb/source-loose: wire up `read_object_stream()` callback\n      odb/source-loose: wire up `for_each_object()` callback\n      odb/source-loose: wire up `find_abbrev_len()` callback\n      odb/source-loose: wire up `count_objects()` callback\n      odb/source-loose: drop `odb_source_loose_has_object()`\n      odb/source-loose: wire up `freshen_object()` callback\n      loose: refactor object map to operate on `struct odb_source_loose`\n      odb/source-loose: wire up `write_object()` callback\n      object-file: refactor writing objects to use loose source\n      odb/source-loose: wire up `write_object_stream()` callback\n      odb/source-loose: stub out remaining callbacks\n      odb/source-loose: drop pointer to the \"files\" source\n\n Makefile               |   1 +\n builtin/cat-file.c     |   5 +-\n builtin/gc.c           |   6 +-\n builtin/pack-objects.c |  12 +-\n http-walker.c          |   3 +-\n http.c                 |   6 +-\n loose.c                |  45 ++-\n loose.h                |   4 +-\n meson.build            |   1 +\n object-file.c          | 796 ++++---------------------------------------------\n object-file.h          | 149 ++++-----\n odb/source-files.c     |  28 +-\n odb/source-loose.c     | 736 +++++++++++++++++++++++++++++++++++++++++++++\n odb/source-loose.h     |  48 +++\n odb/source.h           |   3 +\n 15 files changed, 973 insertions(+), 870 deletions(-)\n\n\n---\nbase-commit: 072edab49f312c80561b2899f03f361f74fc38e4\nchange-id: 20260413-b4-pks-odb-source-loose-4900c8ca91db\n\n"},{"id":"543788","messageId":"20260521-b4-pks-odb-source-loose-v1-1-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 01/18] odb/source-loose: move loose source into \"odb/\" subsystem","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:21Z","receivedAt":"2026-05-21T08:22:34Z","isPatch":true,"body":"In subsequent patches we'll be turning `struct odb_source_loose` into a\nproper `struct odb_source`. As a first step towards this goal, move its\nstruct out of \"object-file.c\" and into \"odb/source-loose.c\".\n\nThis detaches the implementation of the loose object source from the\ngeneric object file code, following the same convention already used by\nthe \"files\" and \"in-memory\" sources.\n\nNo functional changes are intended.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile           |  1 +\n meson.build        |  1 +\n object-file.c      |  8 --------\n object-file.h      | 21 +--------------------\n odb/source-loose.c | 10 ++++++++++\n odb/source-loose.h | 34 ++++++++++++++++++++++++++++++++++\n 6 files changed, 47 insertions(+), 28 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex a43b8ee067..01356235c3 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1217,6 +1217,7 @@ LIB_OBJS += odb.o\n LIB_OBJS += odb/source.o\n LIB_OBJS += odb/source-files.o\n LIB_OBJS += odb/source-inmemory.o\n+LIB_OBJS += odb/source-loose.o\n LIB_OBJS += odb/streaming.o\n LIB_OBJS += odb/transaction.o\n LIB_OBJS += oid-array.o\ndiff --git a/meson.build b/meson.build\nindex 664d831329..c85e598835 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -405,6 +405,7 @@ libgit_sources = [\n   'odb/source.c',\n   'odb/source-files.c',\n   'odb/source-inmemory.c',\n+  'odb/source-loose.c',\n   'odb/streaming.c',\n   'odb/transaction.c',\n   'oid-array.c',\ndiff --git a/object-file.c b/object-file.c\nindex 90f995d000..641bd9c079 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -2205,14 +2205,6 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n \treturn &transaction->base;\n }\n \n-struct odb_source_loose *odb_source_loose_new(struct odb_source *source)\n-{\n-\tstruct odb_source_loose *loose;\n-\tCALLOC_ARRAY(loose, 1);\n-\tloose->source = source;\n-\treturn loose;\n-}\n-\n void odb_source_loose_free(struct odb_source_loose *loose)\n {\n \tif (!loose)\ndiff --git a/object-file.h b/object-file.h\nindex 5241b8dd5c..1d8312cf7f 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -4,6 +4,7 @@\n #include \"git-zlib.h\"\n #include \"object.h\"\n #include \"odb.h\"\n+#include \"odb/source-loose.h\"\n \n struct index_state;\n \n@@ -20,26 +21,6 @@ struct object_info;\n struct odb_read_stream;\n struct odb_source;\n \n-struct odb_source_loose {\n-\tstruct odb_source *source;\n-\n-\t/*\n-\t * Used to store the results of readdir(3) calls when we are OK\n-\t * sacrificing accuracy due to races for speed. That includes\n-\t * object existence with OBJECT_INFO_QUICK, as well as\n-\t * our search for unique abbreviated hashes. Don't use it for tasks\n-\t * requiring greater accuracy!\n-\t *\n-\t * Be sure to call odb_load_loose_cache() before using.\n-\t */\n-\tuint32_t subdir_seen[8]; /* 256 bits */\n-\tstruct oidtree *cache;\n-\n-\t/* Map between object IDs for loose objects. */\n-\tstruct loose_object_map *map;\n-};\n-\n-struct odb_source_loose *odb_source_loose_new(struct odb_source *source);\n void odb_source_loose_free(struct odb_source_loose *loose);\n \n /* Reprepare the loose source by emptying the loose object cache. */\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nnew file mode 100644\nindex 0000000000..b944d21813\n--- /dev/null\n+++ b/odb/source-loose.c\n@@ -0,0 +1,10 @@\n+#include \"git-compat-util.h\"\n+#include \"odb/source-loose.h\"\n+\n+struct odb_source_loose *odb_source_loose_new(struct odb_source *source)\n+{\n+\tstruct odb_source_loose *loose;\n+\tCALLOC_ARRAY(loose, 1);\n+\tloose->source = source;\n+\treturn loose;\n+}\ndiff --git a/odb/source-loose.h b/odb/source-loose.h\nnew file mode 100644\nindex 0000000000..8b4bac77ea\n--- /dev/null\n+++ b/odb/source-loose.h\n@@ -0,0 +1,34 @@\n+#ifndef ODB_SOURCE_LOOSE_H\n+#define ODB_SOURCE_LOOSE_H\n+\n+#include \"odb/source.h\"\n+\n+struct object_database;\n+struct oidtree;\n+\n+/*\n+ * An object database source that stores its objects in loose format, one\n+ * file per object. This source is part of the files source.\n+ */\n+struct odb_source_loose {\n+\tstruct odb_source *source;\n+\n+\t/*\n+\t * Used to store the results of readdir(3) calls when we are OK\n+\t * sacrificing accuracy due to races for speed. That includes\n+\t * object existence with OBJECT_INFO_QUICK, as well as\n+\t * our search for unique abbreviated hashes. Don't use it for tasks\n+\t * requiring greater accuracy!\n+\t *\n+\t * Be sure to call odb_load_loose_cache() before using.\n+\t */\n+\tuint32_t subdir_seen[8]; /* 256 bits */\n+\tstruct oidtree *cache;\n+\n+\t/* Map between object IDs for loose objects. */\n+\tstruct loose_object_map *map;\n+};\n+\n+struct odb_source_loose *odb_source_loose_new(struct odb_source *source);\n+\n+#endif\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543789","messageId":"20260521-b4-pks-odb-source-loose-v1-2-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 02/18] odb/source-loose: store pointer to \"files\" instead of generic source","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:22Z","receivedAt":"2026-05-21T08:22:37Z","isPatch":true,"body":"The `struct odb_source_loose` holds a pointer to its owning parent\nsource. The way that Git is currently structured, this parent is always\nthe \"files\" source. In subsequent commits we're going to detangle that\nso that the \"loose\" source doesn't have any owning parent source at all\nso that it can be used as a completely standalone source.\n\nDetangling this mess is somewhat intricate though, and is made even more\nintricate because it's not always clear which kind of source one is\nholding at a specific point in time -- either the parent \"files\" source,\nor the child \"loose\" source.\n\nMake this relationship more explicit by storing a pointer to the \"files\"\nsource instead of storing a pointer to a generic `struct odb_source`.\nThis will help make subsequent steps a bit clearer.\n\nNote that this is a temporary step, only. At the end of this series\nwe will have dropped the parent pointer completely.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 4 ++--\n odb/source-files.c | 2 +-\n odb/source-loose.c | 4 ++--\n odb/source-loose.h | 5 +++--\n 4 files changed, 8 insertions(+), 7 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 641bd9c079..7a1908bfc0 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -178,7 +178,7 @@ static int open_loose_object(struct odb_source_loose *loose,\n \tstatic struct strbuf buf = STRBUF_INIT;\n \tint fd;\n \n-\t*path = odb_loose_path(loose->source, &buf, oid);\n+\t*path = odb_loose_path(&loose->files->base, &buf, oid);\n \tfd = git_open(*path);\n \tif (fd >= 0)\n \t\treturn fd;\n@@ -189,7 +189,7 @@ static int open_loose_object(struct odb_source_loose *loose,\n static int quick_has_loose(struct odb_source_loose *loose,\n \t\t\t   const struct object_id *oid)\n {\n-\treturn !!oidtree_contains(odb_source_loose_cache(loose->source, oid), oid);\n+\treturn !!oidtree_contains(odb_source_loose_cache(&loose->files->base, oid), oid);\n }\n \n /*\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex b5abd20e97..185cc6903e 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -264,7 +264,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(&files->base);\n+\tfiles->loose = odb_source_loose_new(files);\n \tfiles->packed = packfile_store_new(&files->base);\n \n \tfiles->base.free = odb_source_files_free;\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex b944d21813..c9e7414814 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -1,10 +1,10 @@\n #include \"git-compat-util.h\"\n #include \"odb/source-loose.h\"\n \n-struct odb_source_loose *odb_source_loose_new(struct odb_source *source)\n+struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n {\n \tstruct odb_source_loose *loose;\n \tCALLOC_ARRAY(loose, 1);\n-\tloose->source = source;\n+\tloose->files = files;\n \treturn loose;\n }\ndiff --git a/odb/source-loose.h b/odb/source-loose.h\nindex 8b4bac77ea..bf61e767c8 100644\n--- a/odb/source-loose.h\n+++ b/odb/source-loose.h\n@@ -3,6 +3,7 @@\n \n #include \"odb/source.h\"\n \n+struct odb_source_files;\n struct object_database;\n struct oidtree;\n \n@@ -11,7 +12,7 @@ struct oidtree;\n  * file per object. This source is part of the files source.\n  */\n struct odb_source_loose {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files;\n \n \t/*\n \t * Used to store the results of readdir(3) calls when we are OK\n@@ -29,6 +30,6 @@ struct odb_source_loose {\n \tstruct loose_object_map *map;\n };\n \n-struct odb_source_loose *odb_source_loose_new(struct odb_source *source);\n+struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files);\n \n #endif\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543790","messageId":"20260521-b4-pks-odb-source-loose-v1-3-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 03/18] odb/source-loose: start converting to a proper `struct odb_source`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:23Z","receivedAt":"2026-05-21T08:22:39Z","isPatch":true,"body":"Start converting `struct odb_source_loose` into a proper pluggable\n`struct odb_source` by embedding the base struct and assigning it the\nnew `ODB_SOURCE_LOOSE` type. Furthermore, wire up lifecycle management\nof this source by implementing the `free` callback and taking ownership\nof the chdir notifications.\n\nNote that the loose source is not yet functional as a standalone `struct\nodb_source`, as it's missing all of the callback implementations. These\nwill be wired up in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 17 -----------------\n object-file.h      |  2 --\n odb/source-files.c |  2 +-\n odb/source-loose.c | 45 +++++++++++++++++++++++++++++++++++++++++++++\n odb/source-loose.h | 14 ++++++++++++++\n odb/source.h       |  3 +++\n 6 files changed, 63 insertions(+), 20 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 7a1908bfc0..977d959d33 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -2041,14 +2041,6 @@ static struct oidtree *odb_source_loose_cache(struct odb_source *source,\n \treturn files->loose->cache;\n }\n \n-static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n-{\n-\toidtree_clear(loose->cache);\n-\tFREE_AND_NULL(loose->cache);\n-\tmemset(&loose->subdir_seen, 0,\n-\t       sizeof(loose->subdir_seen));\n-}\n-\n void odb_source_loose_reprepare(struct odb_source *source)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n@@ -2205,15 +2197,6 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n \treturn &transaction->base;\n }\n \n-void odb_source_loose_free(struct odb_source_loose *loose)\n-{\n-\tif (!loose)\n-\t\treturn;\n-\todb_source_loose_clear_cache(loose);\n-\tloose_object_map_clear(&loose->map);\n-\tfree(loose);\n-}\n-\n struct odb_loose_read_stream {\n \tstruct odb_read_stream base;\n \tgit_zstream z;\ndiff --git a/object-file.h b/object-file.h\nindex 1d8312cf7f..02c9680980 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -21,8 +21,6 @@ struct object_info;\n struct odb_read_stream;\n struct odb_source;\n \n-void odb_source_loose_free(struct odb_source_loose *loose);\n-\n /* Reprepare the loose source by emptying the loose object cache. */\n void odb_source_loose_reprepare(struct odb_source *source);\n \ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 185cc6903e..ccc637311b 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -27,7 +27,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(NULL, odb_source_files_reparent, files);\n-\todb_source_loose_free(files->loose);\n+\todb_source_free(&files->loose->base);\n \tpackfile_store_free(files->packed);\n \todb_source_release(&files->base);\n \tfree(files);\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex c9e7414814..92e18f5adb 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -1,10 +1,55 @@\n #include \"git-compat-util.h\"\n+#include \"abspath.h\"\n+#include \"chdir-notify.h\"\n+#include \"loose.h\"\n+#include \"odb.h\"\n+#include \"odb/source-files.h\"\n #include \"odb/source-loose.h\"\n+#include \"oidtree.h\"\n+\n+void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n+{\n+\toidtree_clear(loose->cache);\n+\tFREE_AND_NULL(loose->cache);\n+\tmemset(&loose->subdir_seen, 0,\n+\t       sizeof(loose->subdir_seen));\n+}\n+\n+static void odb_source_loose_reparent(const char *name UNUSED,\n+\t\t\t\t      const char *old_cwd,\n+\t\t\t\t      const char *new_cwd,\n+\t\t\t\t      void *cb_data)\n+{\n+\tstruct odb_source_loose *loose = cb_data;\n+\tchar *path = reparent_relative_path(old_cwd, new_cwd,\n+\t\t\t\t\t    loose->base.path);\n+\tfree(loose->base.path);\n+\tloose->base.path = path;\n+}\n+\n+static void odb_source_loose_free(struct odb_source *source)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\todb_source_loose_clear_cache(loose);\n+\tloose_object_map_clear(&loose->map);\n+\tchdir_notify_unregister(NULL, odb_source_loose_reparent, loose);\n+\todb_source_release(&loose->base);\n+\tfree(loose);\n+}\n \n struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n {\n \tstruct odb_source_loose *loose;\n+\n \tCALLOC_ARRAY(loose, 1);\n+\todb_source_init(&loose->base, files->base.odb, ODB_SOURCE_LOOSE,\n+\t\t\tfiles->base.path, files->base.local);\n \tloose->files = files;\n+\n+\tloose->base.free = odb_source_loose_free;\n+\n+\tif (!is_absolute_path(loose->base.path))\n+\t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n+\n \treturn loose;\n }\ndiff --git a/odb/source-loose.h b/odb/source-loose.h\nindex bf61e767c8..441da9e418 100644\n--- a/odb/source-loose.h\n+++ b/odb/source-loose.h\n@@ -12,6 +12,7 @@ struct oidtree;\n  * file per object. This source is part of the files source.\n  */\n struct odb_source_loose {\n+\tstruct odb_source base;\n \tstruct odb_source_files *files;\n \n \t/*\n@@ -32,4 +33,17 @@ struct odb_source_loose {\n \n struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files);\n \n+/*\n+ * Cast the given object database source to the loose backend. This will cause\n+ * a BUG in case the source uses doesn't use this backend.\n+ */\n+static inline struct odb_source_loose *odb_source_loose_downcast(struct odb_source *source)\n+{\n+\tif (source->type != ODB_SOURCE_LOOSE)\n+\t\tBUG(\"trying to downcast source of type '%d' to loose\", source->type);\n+\treturn container_of(source, struct odb_source_loose, base);\n+}\n+\n+void odb_source_loose_clear_cache(struct odb_source_loose *loose);\n+\n #endif\ndiff --git a/odb/source.h b/odb/source.h\nindex 0a440884e4..8bcb67787e 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -14,6 +14,9 @@ enum odb_source_type {\n \t/* The \"files\" backend that uses loose objects and packfiles. */\n \tODB_SOURCE_FILES,\n \n+\t/* The \"loose\" backend that uses loose objects, only. */\n+\tODB_SOURCE_LOOSE,\n+\n \t/* The \"in-memory\" backend that stores objects in memory. */\n \tODB_SOURCE_INMEMORY,\n };\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543791","messageId":"20260521-b4-pks-odb-source-loose-v1-4-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 04/18] odb/source-loose: wire up `reprepare()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:24Z","receivedAt":"2026-05-21T08:22:42Z","isPatch":true,"body":"Move `odb_source_loose_reprepare()` from \"object-file.c\" into\n\"odb/source-loose.c\" and wire it up as the `reprepare()` callback of the\nloose source.\n\nWhile at it, make `odb_source_loose_clear_cache()` static, as it is no\nlonger needed outside of its file.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 6 ------\n object-file.h      | 3 ---\n odb/source-files.c | 2 +-\n odb/source-loose.c | 9 ++++++++-\n odb/source-loose.h | 2 --\n 5 files changed, 9 insertions(+), 13 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 977d959d33..0f4f1e7bdc 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -2041,12 +2041,6 @@ static struct oidtree *odb_source_loose_cache(struct odb_source *source,\n \treturn files->loose->cache;\n }\n \n-void odb_source_loose_reprepare(struct odb_source *source)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\todb_source_loose_clear_cache(files->loose);\n-}\n-\n static int check_stream_oid(git_zstream *stream,\n \t\t\t    const char *hdr,\n \t\t\t    unsigned long size,\ndiff --git a/object-file.h b/object-file.h\nindex 02c9680980..420a0fff2e 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -21,9 +21,6 @@ struct object_info;\n struct odb_read_stream;\n struct odb_source;\n \n-/* Reprepare the loose source by emptying the loose object cache. */\n-void odb_source_loose_reprepare(struct odb_source *source);\n-\n int odb_source_loose_read_object_info(struct odb_source *source,\n \t\t\t\t      const struct object_id *oid,\n \t\t\t\t      struct object_info *oi,\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex ccc637311b..10832e81e4 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -42,7 +42,7 @@ static void odb_source_files_close(struct odb_source *source)\n static void odb_source_files_reprepare(struct odb_source *source)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\todb_source_loose_reprepare(&files->base);\n+\todb_source_reprepare(&files->loose->base);\n \tpackfile_store_reprepare(files->packed);\n }\n \ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 92e18f5adb..e0fe0d513d 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -7,7 +7,7 @@\n #include \"odb/source-loose.h\"\n #include \"oidtree.h\"\n \n-void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n+static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n \tFREE_AND_NULL(loose->cache);\n@@ -15,6 +15,12 @@ void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n \t       sizeof(loose->subdir_seen));\n }\n \n+static void odb_source_loose_reprepare(struct odb_source *source)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\todb_source_loose_clear_cache(loose);\n+}\n+\n static void odb_source_loose_reparent(const char *name UNUSED,\n \t\t\t\t      const char *old_cwd,\n \t\t\t\t      const char *new_cwd,\n@@ -47,6 +53,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->files = files;\n \n \tloose->base.free = odb_source_loose_free;\n+\tloose->base.reprepare = odb_source_loose_reprepare;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\ndiff --git a/odb/source-loose.h b/odb/source-loose.h\nindex 441da9e418..825e703072 100644\n--- a/odb/source-loose.h\n+++ b/odb/source-loose.h\n@@ -44,6 +44,4 @@ static inline struct odb_source_loose *odb_source_loose_downcast(struct odb_sour\n \treturn container_of(source, struct odb_source_loose, base);\n }\n \n-void odb_source_loose_clear_cache(struct odb_source_loose *loose);\n-\n #endif\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543792","messageId":"20260521-b4-pks-odb-source-loose-v1-5-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 05/18] odb/source-loose: wire up `close()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:25Z","receivedAt":"2026-05-21T08:22:44Z","isPatch":true,"body":"Wire up a new `close()` callback for the loose source and call it from\nthe \"files\" source via the generic `odb_source_close()` interface. The\ncallback itself is a no-op as the loose source has no resources that\nneed to be released on close.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb/source-files.c | 1 +\n odb/source-loose.c | 6 ++++++\n 2 files changed, 7 insertions(+)\n\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 10832e81e4..59e3a70d80 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -36,6 +36,7 @@ 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 \tpackfile_store_close(files->packed);\n }\n \ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex e0fe0d513d..65c1076659 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -21,6 +21,11 @@ static void odb_source_loose_reprepare(struct odb_source *source)\n \todb_source_loose_clear_cache(loose);\n }\n \n+static void odb_source_loose_close(struct odb_source *source UNUSED)\n+{\n+\t/* Nothing to do. */\n+}\n+\n static void odb_source_loose_reparent(const char *name UNUSED,\n \t\t\t\t      const char *old_cwd,\n \t\t\t\t      const char *new_cwd,\n@@ -53,6 +58,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->files = files;\n \n \tloose->base.free = odb_source_loose_free;\n+\tloose->base.close = odb_source_loose_close;\n \tloose->base.reprepare = odb_source_loose_reprepare;\n \n \tif (!is_absolute_path(loose->base.path))\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543793","messageId":"20260521-b4-pks-odb-source-loose-v1-6-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 06/18] odb/source-loose: wire up `read_object_info()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:26Z","receivedAt":"2026-05-21T08:22:47Z","isPatch":true,"body":"Move `odb_source_loose_read_object_info()` from \"object-file.c\" into\n\"odb/source-loose.c\" and wire it up as the `read_object_info()` callback\nof the loose source. Callers that previously invoked it directly now go\nthrough the generic `odb_source_read_object_info()` interface instead.\n\nThe function `read_object_info_from_path()` cannot be moved along with\nit because it is still called by `for_each_object_wrapper_cb()`. It is\ntherefore kept in place, but adjusted to take a loose source to clarify\nthat it's always operating on this structure.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 46 +++++++++++++---------------------------------\n object-file.h      | 11 ++++++-----\n odb/source-files.c |  2 +-\n odb/source-loose.c | 24 ++++++++++++++++++++++++\n 4 files changed, 44 insertions(+), 39 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 0f4f1e7bdc..fa174512a4 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -396,13 +396,12 @@ static int parse_loose_header(const char *hdr, struct object_info *oi)\n \treturn 0;\n }\n \n-static int read_object_info_from_path(struct odb_source *source,\n-\t\t\t\t      const char *path,\n-\t\t\t\t      const struct object_id *oid,\n-\t\t\t\t      struct object_info *oi,\n-\t\t\t\t      enum object_info_flags flags)\n+int read_object_info_from_path(struct odb_source_loose *loose,\n+\t\t\t       const char *path,\n+\t\t\t       const struct object_id *oid,\n+\t\t\t       struct object_info *oi,\n+\t\t\t       enum object_info_flags flags)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tint ret;\n \tint fd;\n \tunsigned long mapsize;\n@@ -425,7 +424,7 @@ static int read_object_info_from_path(struct odb_source *source,\n \t\tstruct stat st;\n \n \t\tif ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) {\n-\t\t\tret = quick_has_loose(files->loose, oid) ? 0 : -1;\n+\t\t\tret = quick_has_loose(loose, oid) ? 0 : -1;\n \t\t\tgoto out;\n \t\t}\n \n@@ -532,7 +531,7 @@ static int read_object_info_from_path(struct odb_source *source,\n \t\tif (oi->typep == &type_scratch)\n \t\t\toi->typep = NULL;\n \t\tif (oi->delta_base_oid)\n-\t\t\toidclr(oi->delta_base_oid, source->odb->repo->hash_algo);\n+\t\t\toidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo);\n \t\tif (!ret)\n \t\t\toi->whence = OI_LOOSE;\n \t}\n@@ -540,26 +539,6 @@ static int read_object_info_from_path(struct odb_source *source,\n \treturn ret;\n }\n \n-int odb_source_loose_read_object_info(struct odb_source *source,\n-\t\t\t\t      const struct object_id *oid,\n-\t\t\t\t      struct object_info *oi,\n-\t\t\t\t      enum object_info_flags flags)\n-{\n-\tstatic struct strbuf buf = STRBUF_INIT;\n-\n-\t/*\n-\t * The second read shouldn't cause new loose objects to show up, unless\n-\t * there was a race condition with a secondary process. We don't care\n-\t * about this case though, so we simply skip reading loose objects a\n-\t * second time.\n-\t */\n-\tif (flags & OBJECT_INFO_SECOND_READ)\n-\t\treturn -1;\n-\n-\todb_loose_path(source, &buf, oid);\n-\treturn read_object_info_from_path(source, buf.buf, oid, oi, flags);\n-}\n-\n static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c,\n \t\t\t     const void *buf, unsigned long len,\n \t\t\t     struct object_id *oid,\n@@ -1833,7 +1812,7 @@ int for_each_loose_file_in_source(struct odb_source *source,\n }\n \n struct for_each_object_wrapper_data {\n-\tstruct odb_source *source;\n+\tstruct odb_source_loose *loose;\n \tconst struct object_info *request;\n \todb_for_each_object_cb cb;\n \tvoid *cb_data;\n@@ -1848,7 +1827,7 @@ static int for_each_object_wrapper_cb(const struct object_id *oid,\n \tif (data->request) {\n \t\tstruct object_info oi = *data->request;\n \n-\t\tif (read_object_info_from_path(data->source, path, oid, &oi, 0) < 0)\n+\t\tif (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0)\n \t\t\treturn -1;\n \n \t\treturn data->cb(oid, &oi, data->cb_data);\n@@ -1865,8 +1844,8 @@ static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid,\n \tif (data->request) {\n \t\tstruct object_info oi = *data->request;\n \n-\t\tif (odb_source_loose_read_object_info(data->source,\n-\t\t\t\t\t\t      oid, &oi, 0) < 0)\n+\t\tif (odb_source_read_object_info(&data->loose->base,\n+\t\t\t\t\t\toid, &oi, 0) < 0)\n \t\t\treturn -1;\n \n \t\treturn data->cb(oid, &oi, data->cb_data);\n@@ -1881,8 +1860,9 @@ int odb_source_loose_for_each_object(struct odb_source *source,\n \t\t\t\t     void *cb_data,\n \t\t\t\t     const struct odb_for_each_object_options *opts)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tstruct for_each_object_wrapper_data data = {\n-\t\t.source = source,\n+\t\t.loose = files->loose,\n \t\t.request = request,\n \t\t.cb = cb,\n \t\t.cb_data = cb_data,\ndiff --git a/object-file.h b/object-file.h\nindex 420a0fff2e..8ac2832dac 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -21,11 +21,6 @@ struct object_info;\n struct odb_read_stream;\n struct odb_source;\n \n-int odb_source_loose_read_object_info(struct odb_source *source,\n-\t\t\t\t      const struct object_id *oid,\n-\t\t\t\t      struct object_info *oi,\n-\t\t\t\t      enum object_info_flags flags);\n-\n int odb_source_loose_read_object_stream(struct odb_read_stream **out,\n \t\t\t\t\tstruct odb_source *source,\n \t\t\t\t\tconst struct object_id *oid);\n@@ -198,6 +193,12 @@ int read_loose_object(struct repository *repo,\n \t\t      void **contents,\n \t\t      struct object_info *oi);\n \n+int read_object_info_from_path(struct odb_source_loose *loose,\n+\t\t\t       const char *path,\n+\t\t\t       const struct object_id *oid,\n+\t\t\t       struct object_info *oi,\n+\t\t\t       enum object_info_flags flags);\n+\n struct odb_transaction;\n \n /*\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 59e3a70d80..8d6924755f 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -55,7 +55,7 @@ static int odb_source_files_read_object_info(struct odb_source *source,\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \n \tif (!packfile_store_read_object_info(files->packed, oid, oi, flags) ||\n-\t    !odb_source_loose_read_object_info(source, oid, oi, flags))\n+\t    !odb_source_read_object_info(&files->loose->base, oid, oi, flags))\n \t\treturn 0;\n \n \treturn -1;\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 65c1076659..50f387ecf3 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -2,10 +2,33 @@\n #include \"abspath.h\"\n #include \"chdir-notify.h\"\n #include \"loose.h\"\n+#include \"object-file.h\"\n #include \"odb.h\"\n #include \"odb/source-files.h\"\n #include \"odb/source-loose.h\"\n #include \"oidtree.h\"\n+#include \"strbuf.h\"\n+\n+static int odb_source_loose_read_object_info(struct odb_source *source,\n+\t\t\t\t\t     const struct object_id *oid,\n+\t\t\t\t\t     struct object_info *oi,\n+\t\t\t\t\t     enum object_info_flags flags)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tstatic struct strbuf buf = STRBUF_INIT;\n+\n+\t/*\n+\t * The second read shouldn't cause new loose objects to show up, unless\n+\t * there was a race condition with a secondary process. We don't care\n+\t * about this case though, so we simply skip reading loose objects a\n+\t * second time.\n+\t */\n+\tif (flags & OBJECT_INFO_SECOND_READ)\n+\t\treturn -1;\n+\n+\todb_loose_path(source, &buf, oid);\n+\treturn read_object_info_from_path(loose, buf.buf, oid, oi, flags);\n+}\n \n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n@@ -60,6 +83,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.free = odb_source_loose_free;\n \tloose->base.close = odb_source_loose_close;\n \tloose->base.reprepare = odb_source_loose_reprepare;\n+\tloose->base.read_object_info = odb_source_loose_read_object_info;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543794","messageId":"20260521-b4-pks-odb-source-loose-v1-7-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 07/18] odb/source-loose: wire up `read_object_stream()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:27Z","receivedAt":"2026-05-21T08:22:50Z","isPatch":true,"body":"Move `odb_source_loose_read_object_stream()` and its associated helpers\nfrom \"object-file.c\" into \"odb/source-loose.c\" and wire it up as the\n`read_object_stream()` callback of the loose source.\n\nAs part of the move we are also forced to expose a couple of functions\nfrom \"object-file.h\" that parse object headers in a somewhat-generic\nway, as those functions are now used by both subsystems.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 200 ++---------------------------------------------------\n object-file.h      |  31 +++++++--\n odb/source-files.c |   2 +-\n odb/source-loose.c | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 222 insertions(+), 200 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex fa174512a4..adfb672493 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -164,28 +164,6 @@ int stream_object_signature(struct repository *r,\n \treturn !oideq(oid, &real_oid) ? -1 : 0;\n }\n \n-/*\n- * Find \"oid\" as a loose object in given source, open the object and return its\n- * file descriptor. Returns the file descriptor on success, negative on failure.\n- *\n- * The \"path\" out-parameter will give the path of the object we found (if any).\n- * Note that it may point to static storage and is only valid until another\n- * call to stat_loose_object().\n- */\n-static int open_loose_object(struct odb_source_loose *loose,\n-\t\t\t     const struct object_id *oid, const char **path)\n-{\n-\tstatic struct strbuf buf = STRBUF_INIT;\n-\tint fd;\n-\n-\t*path = odb_loose_path(&loose->files->base, &buf, oid);\n-\tfd = git_open(*path);\n-\tif (fd >= 0)\n-\t\treturn fd;\n-\n-\treturn -1;\n-}\n-\n static int quick_has_loose(struct odb_source_loose *loose,\n \t\t\t   const struct object_id *oid)\n {\n@@ -215,42 +193,11 @@ static void *map_fd(int fd, const char *path, unsigned long *size)\n \treturn map;\n }\n \n-static void *odb_source_loose_map_object(struct odb_source *source,\n-\t\t\t\t\t const struct object_id *oid,\n-\t\t\t\t\t unsigned long *size)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tconst char *p;\n-\tint fd = open_loose_object(files->loose, oid, &p);\n-\n-\tif (fd < 0)\n-\t\treturn NULL;\n-\treturn map_fd(fd, p, size);\n-}\n-\n-enum unpack_loose_header_result {\n-\tULHR_OK,\n-\tULHR_BAD,\n-\tULHR_TOO_LONG,\n-};\n-\n-/**\n- * unpack_loose_header() initializes the data stream needed to unpack\n- * a loose object header.\n- *\n- * Returns:\n- *\n- * - ULHR_OK on success\n- * - ULHR_BAD on error\n- * - ULHR_TOO_LONG if the header was too long\n- *\n- * It will only parse up to MAX_HEADER_LEN bytes.\n- */\n-static enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n-\t\t\t\t\t\t\t   unsigned char *map,\n-\t\t\t\t\t\t\t   unsigned long mapsize,\n-\t\t\t\t\t\t\t   void *buffer,\n-\t\t\t\t\t\t\t   unsigned long bufsiz)\n+enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n+\t\t\t\t\t\t    unsigned char *map,\n+\t\t\t\t\t\t    unsigned long mapsize,\n+\t\t\t\t\t\t    void *buffer,\n+\t\t\t\t\t\t    unsigned long bufsiz)\n {\n \tint status;\n \n@@ -340,7 +287,7 @@ static void *unpack_loose_rest(git_zstream *stream,\n  * too permissive for what we want to check. So do an anal\n  * object header parse by hand.\n  */\n-static int parse_loose_header(const char *hdr, struct object_info *oi)\n+int parse_loose_header(const char *hdr, struct object_info *oi)\n {\n \tconst char *type_buf = hdr;\n \tsize_t size;\n@@ -2170,138 +2117,3 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n \n \treturn &transaction->base;\n }\n-\n-struct odb_loose_read_stream {\n-\tstruct odb_read_stream base;\n-\tgit_zstream z;\n-\tenum {\n-\t\tODB_LOOSE_READ_STREAM_INUSE,\n-\t\tODB_LOOSE_READ_STREAM_DONE,\n-\t\tODB_LOOSE_READ_STREAM_ERROR,\n-\t} z_state;\n-\tvoid *mapped;\n-\tunsigned long mapsize;\n-\tchar hdr[32];\n-\tint hdr_avail;\n-\tint hdr_used;\n-};\n-\n-static ssize_t read_istream_loose(struct odb_read_stream *_st, char *buf, size_t sz)\n-{\n-\tstruct odb_loose_read_stream *st =\n-\t\tcontainer_of(_st, struct odb_loose_read_stream, base);\n-\tsize_t total_read = 0;\n-\n-\tswitch (st->z_state) {\n-\tcase ODB_LOOSE_READ_STREAM_DONE:\n-\t\treturn 0;\n-\tcase ODB_LOOSE_READ_STREAM_ERROR:\n-\t\treturn -1;\n-\tdefault:\n-\t\tbreak;\n-\t}\n-\n-\tif (st->hdr_used < st->hdr_avail) {\n-\t\tsize_t to_copy = st->hdr_avail - st->hdr_used;\n-\t\tif (sz < to_copy)\n-\t\t\tto_copy = sz;\n-\t\tmemcpy(buf, st->hdr + st->hdr_used, to_copy);\n-\t\tst->hdr_used += to_copy;\n-\t\ttotal_read += to_copy;\n-\t}\n-\n-\twhile (total_read < sz) {\n-\t\tint status;\n-\n-\t\tst->z.next_out = (unsigned char *)buf + total_read;\n-\t\tst->z.avail_out = sz - total_read;\n-\t\tstatus = git_inflate(&st->z, Z_FINISH);\n-\n-\t\ttotal_read = st->z.next_out - (unsigned char *)buf;\n-\n-\t\tif (status == Z_STREAM_END) {\n-\t\t\tgit_inflate_end(&st->z);\n-\t\t\tst->z_state = ODB_LOOSE_READ_STREAM_DONE;\n-\t\t\tbreak;\n-\t\t}\n-\t\tif (status != Z_OK && (status != Z_BUF_ERROR || total_read < sz)) {\n-\t\t\tgit_inflate_end(&st->z);\n-\t\t\tst->z_state = ODB_LOOSE_READ_STREAM_ERROR;\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-\treturn total_read;\n-}\n-\n-static int close_istream_loose(struct odb_read_stream *_st)\n-{\n-\tstruct odb_loose_read_stream *st =\n-\t\tcontainer_of(_st, struct odb_loose_read_stream, base);\n-\n-\tif (st->z_state == ODB_LOOSE_READ_STREAM_INUSE)\n-\t\tgit_inflate_end(&st->z);\n-\tmunmap(st->mapped, st->mapsize);\n-\treturn 0;\n-}\n-\n-int odb_source_loose_read_object_stream(struct odb_read_stream **out,\n-\t\t\t\t\tstruct odb_source *source,\n-\t\t\t\t\tconst struct object_id *oid)\n-{\n-\tstruct object_info oi = OBJECT_INFO_INIT;\n-\tstruct odb_loose_read_stream *st;\n-\tunsigned long mapsize;\n-\tunsigned long size_ul;\n-\tvoid *mapped;\n-\n-\tmapped = odb_source_loose_map_object(source, oid, &mapsize);\n-\tif (!mapped)\n-\t\treturn -1;\n-\n-\t/*\n-\t * Note: we must allocate this structure early even though we may still\n-\t * fail. This is because we need to initialize the zlib stream, and it\n-\t * is not possible to copy the stream around after the fact because it\n-\t * has self-referencing pointers.\n-\t */\n-\tCALLOC_ARRAY(st, 1);\n-\n-\tswitch (unpack_loose_header(&st->z, mapped, mapsize, st->hdr,\n-\t\t\t\t    sizeof(st->hdr))) {\n-\tcase ULHR_OK:\n-\t\tbreak;\n-\tcase ULHR_BAD:\n-\tcase ULHR_TOO_LONG:\n-\t\tgoto error;\n-\t}\n-\n-\t/*\n-\t * object_info.sizep is unsigned long* (32-bit on Windows), but\n-\t * st->base.size is size_t (64-bit). Use temporary variable.\n-\t * Note: loose objects >4GB would still truncate here, but such\n-\t * large loose objects are uncommon (they'd normally be packed).\n-\t */\n-\toi.sizep = &size_ul;\n-\toi.typep = &st->base.type;\n-\n-\tif (parse_loose_header(st->hdr, &oi) < 0 || st->base.type < 0)\n-\t\tgoto error;\n-\tst->base.size = size_ul;\n-\n-\tst->mapped = mapped;\n-\tst->mapsize = mapsize;\n-\tst->hdr_used = strlen(st->hdr) + 1;\n-\tst->hdr_avail = st->z.total_out;\n-\tst->z_state = ODB_LOOSE_READ_STREAM_INUSE;\n-\tst->base.close = close_istream_loose;\n-\tst->base.read = read_istream_loose;\n-\n-\t*out = &st->base;\n-\n-\treturn 0;\n-error:\n-\tgit_inflate_end(&st->z);\n-\tmunmap(mapped, mapsize);\n-\tfree(st);\n-\treturn -1;\n-}\ndiff --git a/object-file.h b/object-file.h\nindex 8ac2832dac..d93b7ffad7 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -18,13 +18,8 @@ int index_fd(struct index_state *istate, struct object_id *oid, int fd, struct s\n int index_path(struct index_state *istate, struct object_id *oid, const char *path, struct stat *st, unsigned flags);\n \n struct object_info;\n-struct odb_read_stream;\n struct odb_source;\n \n-int odb_source_loose_read_object_stream(struct odb_read_stream **out,\n-\t\t\t\t\tstruct odb_source *source,\n-\t\t\t\t\tconst struct object_id *oid);\n-\n /*\n  * Return true iff an object database source has a loose object\n  * with the specified name.  This function does not respect replace\n@@ -199,6 +194,32 @@ int read_object_info_from_path(struct odb_source_loose *loose,\n \t\t\t       struct object_info *oi,\n \t\t\t       enum object_info_flags flags);\n \n+enum unpack_loose_header_result {\n+\tULHR_OK,\n+\tULHR_BAD,\n+\tULHR_TOO_LONG,\n+};\n+\n+/**\n+ * unpack_loose_header() initializes the data stream needed to unpack\n+ * a loose object header.\n+ *\n+ * Returns:\n+ *\n+ * - ULHR_OK on success\n+ * - ULHR_BAD on error\n+ * - ULHR_TOO_LONG if the header was too long\n+ *\n+ * It will only parse up to MAX_HEADER_LEN bytes.\n+ */\n+enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n+\t\t\t\t\t\t    unsigned char *map,\n+\t\t\t\t\t\t    unsigned long mapsize,\n+\t\t\t\t\t\t    void *buffer,\n+\t\t\t\t\t\t    unsigned long bufsiz);\n+\n+int parse_loose_header(const char *hdr, struct object_info *oi);\n+\n struct odb_transaction;\n \n /*\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 8d6924755f..90806ddf86 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -67,7 +67,7 @@ static int odb_source_files_read_object_stream(struct odb_read_stream **out,\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tif (!packfile_store_read_object_stream(out, files->packed, oid) ||\n-\t    !odb_source_loose_read_object_stream(out, source, oid))\n+\t    !odb_source_read_object_stream(out, &files->loose->base, oid))\n \t\treturn 0;\n \treturn -1;\n }\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 50f387ecf3..4b82c6f316 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -1,11 +1,13 @@\n #include \"git-compat-util.h\"\n #include \"abspath.h\"\n #include \"chdir-notify.h\"\n+#include \"gettext.h\"\n #include \"loose.h\"\n #include \"object-file.h\"\n #include \"odb.h\"\n #include \"odb/source-files.h\"\n #include \"odb/source-loose.h\"\n+#include \"odb/streaming.h\"\n #include \"oidtree.h\"\n #include \"strbuf.h\"\n \n@@ -30,6 +32,192 @@ static int odb_source_loose_read_object_info(struct odb_source *source,\n \treturn read_object_info_from_path(loose, buf.buf, oid, oi, flags);\n }\n \n+/*\n+ * Find \"oid\" as a loose object in given source, open the object and return its\n+ * file descriptor. Returns the file descriptor on success, negative on failure.\n+ *\n+ * The \"path\" out-parameter will give the path of the object we found (if any).\n+ * Note that it may point to static storage and is only valid until another\n+ * call to open_loose_object().\n+ */\n+static int open_loose_object(struct odb_source_loose *loose,\n+\t\t\t     const struct object_id *oid, const char **path)\n+{\n+\tstatic struct strbuf buf = STRBUF_INIT;\n+\tint fd;\n+\n+\t*path = odb_loose_path(&loose->base, &buf, oid);\n+\tfd = git_open(*path);\n+\tif (fd >= 0)\n+\t\treturn fd;\n+\n+\treturn -1;\n+}\n+\n+static void *odb_source_loose_map_object(struct odb_source_loose *loose,\n+\t\t\t\t\t const struct object_id *oid,\n+\t\t\t\t\t unsigned long *size)\n+{\n+\tconst char *p;\n+\tint fd = open_loose_object(loose, oid, &p);\n+\tvoid *map = NULL;\n+\tstruct stat st;\n+\n+\tif (fd < 0)\n+\t\treturn NULL;\n+\n+\tif (!fstat(fd, &st)) {\n+\t\t*size = xsize_t(st.st_size);\n+\t\tif (!*size) {\n+\t\t\t/* mmap() is forbidden on empty files */\n+\t\t\terror(_(\"object file %s is empty\"), p);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tmap = xmmap(NULL, *size, PROT_READ, MAP_PRIVATE, fd, 0);\n+\t}\n+\n+out:\n+\tclose(fd);\n+\treturn map;\n+}\n+\n+struct odb_loose_read_stream {\n+\tstruct odb_read_stream base;\n+\tgit_zstream z;\n+\tenum {\n+\t\tODB_LOOSE_READ_STREAM_INUSE,\n+\t\tODB_LOOSE_READ_STREAM_DONE,\n+\t\tODB_LOOSE_READ_STREAM_ERROR,\n+\t} z_state;\n+\tvoid *mapped;\n+\tunsigned long mapsize;\n+\tchar hdr[32];\n+\tint hdr_avail;\n+\tint hdr_used;\n+};\n+\n+static ssize_t read_istream_loose(struct odb_read_stream *_st, char *buf, size_t sz)\n+{\n+\tstruct odb_loose_read_stream *st =\n+\t\tcontainer_of(_st, struct odb_loose_read_stream, base);\n+\tsize_t total_read = 0;\n+\n+\tswitch (st->z_state) {\n+\tcase ODB_LOOSE_READ_STREAM_DONE:\n+\t\treturn 0;\n+\tcase ODB_LOOSE_READ_STREAM_ERROR:\n+\t\treturn -1;\n+\tdefault:\n+\t\tbreak;\n+\t}\n+\n+\tif (st->hdr_used < st->hdr_avail) {\n+\t\tsize_t to_copy = st->hdr_avail - st->hdr_used;\n+\t\tif (sz < to_copy)\n+\t\t\tto_copy = sz;\n+\t\tmemcpy(buf, st->hdr + st->hdr_used, to_copy);\n+\t\tst->hdr_used += to_copy;\n+\t\ttotal_read += to_copy;\n+\t}\n+\n+\twhile (total_read < sz) {\n+\t\tint status;\n+\n+\t\tst->z.next_out = (unsigned char *)buf + total_read;\n+\t\tst->z.avail_out = sz - total_read;\n+\t\tstatus = git_inflate(&st->z, Z_FINISH);\n+\n+\t\ttotal_read = st->z.next_out - (unsigned char *)buf;\n+\n+\t\tif (status == Z_STREAM_END) {\n+\t\t\tgit_inflate_end(&st->z);\n+\t\t\tst->z_state = ODB_LOOSE_READ_STREAM_DONE;\n+\t\t\tbreak;\n+\t\t}\n+\t\tif (status != Z_OK && (status != Z_BUF_ERROR || total_read < sz)) {\n+\t\t\tgit_inflate_end(&st->z);\n+\t\t\tst->z_state = ODB_LOOSE_READ_STREAM_ERROR;\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\treturn total_read;\n+}\n+\n+static int close_istream_loose(struct odb_read_stream *_st)\n+{\n+\tstruct odb_loose_read_stream *st =\n+\t\tcontainer_of(_st, struct odb_loose_read_stream, base);\n+\n+\tif (st->z_state == ODB_LOOSE_READ_STREAM_INUSE)\n+\t\tgit_inflate_end(&st->z);\n+\tmunmap(st->mapped, st->mapsize);\n+\treturn 0;\n+}\n+\n+static int odb_source_loose_read_object_stream(struct odb_read_stream **out,\n+\t\t\t\t\t       struct odb_source *source,\n+\t\t\t\t\t       const struct object_id *oid)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tstruct object_info oi = OBJECT_INFO_INIT;\n+\tstruct odb_loose_read_stream *st;\n+\tunsigned long mapsize;\n+\tunsigned long size_ul;\n+\tvoid *mapped;\n+\n+\tmapped = odb_source_loose_map_object(loose, oid, &mapsize);\n+\tif (!mapped)\n+\t\treturn -1;\n+\n+\t/*\n+\t * Note: we must allocate this structure early even though we may still\n+\t * fail. This is because we need to initialize the zlib stream, and it\n+\t * is not possible to copy the stream around after the fact because it\n+\t * has self-referencing pointers.\n+\t */\n+\tCALLOC_ARRAY(st, 1);\n+\n+\tswitch (unpack_loose_header(&st->z, mapped, mapsize, st->hdr,\n+\t\t\t\t    sizeof(st->hdr))) {\n+\tcase ULHR_OK:\n+\t\tbreak;\n+\tcase ULHR_BAD:\n+\tcase ULHR_TOO_LONG:\n+\t\tgoto error;\n+\t}\n+\n+\t/*\n+\t * object_info.sizep is unsigned long* (32-bit on Windows), but\n+\t * st->base.size is size_t (64-bit). Use temporary variable.\n+\t * Note: loose objects >4GB would still truncate here, but such\n+\t * large loose objects are uncommon (they'd normally be packed).\n+\t */\n+\toi.sizep = &size_ul;\n+\toi.typep = &st->base.type;\n+\n+\tif (parse_loose_header(st->hdr, &oi) < 0 || st->base.type < 0)\n+\t\tgoto error;\n+\tst->base.size = size_ul;\n+\n+\tst->mapped = mapped;\n+\tst->mapsize = mapsize;\n+\tst->hdr_used = strlen(st->hdr) + 1;\n+\tst->hdr_avail = st->z.total_out;\n+\tst->z_state = ODB_LOOSE_READ_STREAM_INUSE;\n+\tst->base.close = close_istream_loose;\n+\tst->base.read = read_istream_loose;\n+\n+\t*out = &st->base;\n+\n+\treturn 0;\n+error:\n+\tgit_inflate_end(&st->z);\n+\tmunmap(mapped, mapsize);\n+\tfree(st);\n+\treturn -1;\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -84,6 +272,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.close = odb_source_loose_close;\n \tloose->base.reprepare = odb_source_loose_reprepare;\n \tloose->base.read_object_info = odb_source_loose_read_object_info;\n+\tloose->base.read_object_stream = odb_source_loose_read_object_stream;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543795","messageId":"20260521-b4-pks-odb-source-loose-v1-8-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 08/18] odb/source-loose: wire up `for_each_object()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:28Z","receivedAt":"2026-05-21T08:22:52Z","isPatch":true,"body":"Move `odb_source_loose_for_each_object()` and its associated helpers\nfrom \"object-file.c\" into \"odb/source-loose.c\" and wire it up as the\n`for_each_object()` callback of the loose source.\n\nAgain, as in the preceding commit, we are forced to expose a couple of\nfunctions from \"object-file.c\" that are now used by both subsystems.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/cat-file.c |   5 +-\n object-file.c      | 299 +++--------------------------------------------------\n object-file.h      |  32 +++---\n odb/source-files.c |   2 +-\n odb/source-loose.c | 264 ++++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 297 insertions(+), 305 deletions(-)\n\ndiff --git a/builtin/cat-file.c b/builtin/cat-file.c\nindex d9fbad5358..2958fc5357 100644\n--- a/builtin/cat-file.c\n+++ b/builtin/cat-file.c\n@@ -862,8 +862,9 @@ static void batch_each_object(struct batch_options *opt,\n \t */\n \todb_prepare_alternates(the_repository->objects);\n \tfor (source = the_repository->objects->sources; source; source = source->next) {\n-\t\tint ret = odb_source_loose_for_each_object(source, NULL, batch_one_object_oi,\n-\t\t\t\t\t\t\t   &payload, &opts);\n+\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n+\t\tint ret = odb_source_for_each_object(&files->loose->base, NULL, batch_one_object_oi,\n+\t\t\t\t\t\t     &payload, &opts);\n \t\tif (ret)\n \t\t\tbreak;\n \t}\ndiff --git a/object-file.c b/object-file.c\nindex adfb672493..157ecad3ea 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -22,7 +22,6 @@\n #include \"odb.h\"\n #include \"odb/streaming.h\"\n #include \"odb/transaction.h\"\n-#include \"oidtree.h\"\n #include \"pack.h\"\n #include \"packfile.h\"\n #include \"path.h\"\n@@ -31,12 +30,6 @@\n #include \"tempfile.h\"\n #include \"tmp-objdir.h\"\n \n-/* The maximum size for an object header. */\n-#define MAX_HEADER_LEN 32\n-\n-static struct oidtree *odb_source_loose_cache(struct odb_source *source,\n-\t\t\t\t\t      const struct object_id *oid);\n-\n static int get_conv_flags(unsigned flags)\n {\n \tif (flags & INDEX_RENORMALIZE)\n@@ -164,12 +157,6 @@ int stream_object_signature(struct repository *r,\n \treturn !oideq(oid, &real_oid) ? -1 : 0;\n }\n \n-static int quick_has_loose(struct odb_source_loose *loose,\n-\t\t\t   const struct object_id *oid)\n-{\n-\treturn !!oidtree_contains(odb_source_loose_cache(&loose->files->base, oid), oid);\n-}\n-\n /*\n  * Map and close the given loose object fd. The path argument is used for\n  * error reporting.\n@@ -227,9 +214,9 @@ enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n \treturn ULHR_TOO_LONG;\n }\n \n-static void *unpack_loose_rest(git_zstream *stream,\n-\t\t\t       void *buffer, unsigned long size,\n-\t\t\t       const struct object_id *oid)\n+void *unpack_loose_rest(git_zstream *stream,\n+\t\t\tvoid *buffer, unsigned long size,\n+\t\t\tconst struct object_id *oid)\n {\n \tsize_t bytes = strlen(buffer) + 1, n;\n \tunsigned char *buf = xmallocz(size);\n@@ -343,149 +330,6 @@ int parse_loose_header(const char *hdr, struct object_info *oi)\n \treturn 0;\n }\n \n-int read_object_info_from_path(struct odb_source_loose *loose,\n-\t\t\t       const char *path,\n-\t\t\t       const struct object_id *oid,\n-\t\t\t       struct object_info *oi,\n-\t\t\t       enum object_info_flags flags)\n-{\n-\tint ret;\n-\tint fd;\n-\tunsigned long mapsize;\n-\tvoid *map = NULL;\n-\tgit_zstream stream, *stream_to_end = NULL;\n-\tchar hdr[MAX_HEADER_LEN];\n-\tunsigned long size_scratch;\n-\tenum object_type type_scratch;\n-\tstruct stat st;\n-\n-\t/*\n-\t * If we don't care about type or size, then we don't\n-\t * need to look inside the object at all. Note that we\n-\t * do not optimize out the stat call, even if the\n-\t * caller doesn't care about the disk-size, since our\n-\t * return value implicitly indicates whether the\n-\t * object even exists.\n-\t */\n-\tif (!oi || (!oi->typep && !oi->sizep && !oi->contentp)) {\n-\t\tstruct stat st;\n-\n-\t\tif ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) {\n-\t\t\tret = quick_has_loose(loose, oid) ? 0 : -1;\n-\t\t\tgoto out;\n-\t\t}\n-\n-\t\tif (lstat(path, &st) < 0) {\n-\t\t\tret = -1;\n-\t\t\tgoto out;\n-\t\t}\n-\n-\t\tif (oi) {\n-\t\t\tif (oi->disk_sizep)\n-\t\t\t\t*oi->disk_sizep = st.st_size;\n-\t\t\tif (oi->mtimep)\n-\t\t\t\t*oi->mtimep = st.st_mtime;\n-\t\t}\n-\n-\t\tret = 0;\n-\t\tgoto out;\n-\t}\n-\n-\tfd = git_open(path);\n-\tif (fd < 0) {\n-\t\tif (errno != ENOENT)\n-\t\t\terror_errno(_(\"unable to open loose object %s\"), oid_to_hex(oid));\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\tif (fstat(fd, &st)) {\n-\t\tclose(fd);\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\tmapsize = xsize_t(st.st_size);\n-\tif (!mapsize) {\n-\t\tclose(fd);\n-\t\tret = error(_(\"object file %s is empty\"), path);\n-\t\tgoto out;\n-\t}\n-\n-\tmap = xmmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, fd, 0);\n-\tclose(fd);\n-\tif (!map) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\tif (oi->disk_sizep)\n-\t\t*oi->disk_sizep = mapsize;\n-\tif (oi->mtimep)\n-\t\t*oi->mtimep = st.st_mtime;\n-\n-\tstream_to_end = &stream;\n-\n-\tswitch (unpack_loose_header(&stream, map, mapsize, hdr, sizeof(hdr))) {\n-\tcase ULHR_OK:\n-\t\tif (!oi->sizep)\n-\t\t\toi->sizep = &size_scratch;\n-\t\tif (!oi->typep)\n-\t\t\toi->typep = &type_scratch;\n-\n-\t\tif (parse_loose_header(hdr, oi) < 0) {\n-\t\t\tret = error(_(\"unable to parse %s header\"), oid_to_hex(oid));\n-\t\t\tgoto corrupt;\n-\t\t}\n-\n-\t\tif (*oi->typep < 0)\n-\t\t\tdie(_(\"invalid object type\"));\n-\n-\t\tif (oi->contentp) {\n-\t\t\t*oi->contentp = unpack_loose_rest(&stream, hdr, *oi->sizep, oid);\n-\t\t\tif (!*oi->contentp) {\n-\t\t\t\tret = -1;\n-\t\t\t\tgoto corrupt;\n-\t\t\t}\n-\t\t}\n-\n-\t\tbreak;\n-\tcase ULHR_BAD:\n-\t\tret = error(_(\"unable to unpack %s header\"),\n-\t\t\t    oid_to_hex(oid));\n-\t\tgoto corrupt;\n-\tcase ULHR_TOO_LONG:\n-\t\tret = error(_(\"header for %s too long, exceeds %d bytes\"),\n-\t\t\t    oid_to_hex(oid), MAX_HEADER_LEN);\n-\t\tgoto corrupt;\n-\t}\n-\n-\tret = 0;\n-\n-corrupt:\n-\tif (ret && (flags & OBJECT_INFO_DIE_IF_CORRUPT))\n-\t\tdie(_(\"loose object %s (stored in %s) is corrupt\"),\n-\t\t    oid_to_hex(oid), path);\n-\n-out:\n-\tif (stream_to_end)\n-\t\tgit_inflate_end(stream_to_end);\n-\tif (map)\n-\t\tmunmap(map, mapsize);\n-\tif (oi) {\n-\t\tif (oi->sizep == &size_scratch)\n-\t\t\toi->sizep = NULL;\n-\t\tif (oi->typep == &type_scratch)\n-\t\t\toi->typep = NULL;\n-\t\tif (oi->delta_base_oid)\n-\t\t\toidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo);\n-\t\tif (!ret)\n-\t\t\toi->whence = OI_LOOSE;\n-\t}\n-\n-\treturn ret;\n-}\n-\n static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c,\n \t\t\t     const void *buf, unsigned long len,\n \t\t\t     struct object_id *oid,\n@@ -1667,13 +1511,13 @@ int read_pack_header(int fd, struct pack_header *header)\n \treturn 0;\n }\n \n-static int for_each_file_in_obj_subdir(unsigned int subdir_nr,\n-\t\t\t\t       struct strbuf *path,\n-\t\t\t\t       const struct git_hash_algo *algop,\n-\t\t\t\t       each_loose_object_fn obj_cb,\n-\t\t\t\t       each_loose_cruft_fn cruft_cb,\n-\t\t\t\t       each_loose_subdir_fn subdir_cb,\n-\t\t\t\t       void *data)\n+int for_each_file_in_obj_subdir(unsigned int subdir_nr,\n+\t\t\t\tstruct strbuf *path,\n+\t\t\t\tconst struct git_hash_algo *algop,\n+\t\t\t\teach_loose_object_fn obj_cb,\n+\t\t\t\teach_loose_cruft_fn cruft_cb,\n+\t\t\t\teach_loose_subdir_fn subdir_cb,\n+\t\t\t\tvoid *data)\n {\n \tsize_t origlen, baselen;\n \tDIR *dir;\n@@ -1758,78 +1602,6 @@ int for_each_loose_file_in_source(struct odb_source *source,\n \treturn r;\n }\n \n-struct for_each_object_wrapper_data {\n-\tstruct odb_source_loose *loose;\n-\tconst struct object_info *request;\n-\todb_for_each_object_cb cb;\n-\tvoid *cb_data;\n-};\n-\n-static int for_each_object_wrapper_cb(const struct object_id *oid,\n-\t\t\t\t      const char *path,\n-\t\t\t\t      void *cb_data)\n-{\n-\tstruct for_each_object_wrapper_data *data = cb_data;\n-\n-\tif (data->request) {\n-\t\tstruct object_info oi = *data->request;\n-\n-\t\tif (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0)\n-\t\t\treturn -1;\n-\n-\t\treturn data->cb(oid, &oi, data->cb_data);\n-\t} else {\n-\t\treturn data->cb(oid, NULL, data->cb_data);\n-\t}\n-}\n-\n-static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid,\n-\t\t\t\t\t       void *node_data UNUSED,\n-\t\t\t\t\t       void *cb_data)\n-{\n-\tstruct for_each_object_wrapper_data *data = cb_data;\n-\tif (data->request) {\n-\t\tstruct object_info oi = *data->request;\n-\n-\t\tif (odb_source_read_object_info(&data->loose->base,\n-\t\t\t\t\t\toid, &oi, 0) < 0)\n-\t\t\treturn -1;\n-\n-\t\treturn data->cb(oid, &oi, data->cb_data);\n-\t} else {\n-\t\treturn data->cb(oid, NULL, data->cb_data);\n-\t}\n-}\n-\n-int odb_source_loose_for_each_object(struct odb_source *source,\n-\t\t\t\t     const struct object_info *request,\n-\t\t\t\t     odb_for_each_object_cb cb,\n-\t\t\t\t     void *cb_data,\n-\t\t\t\t     const struct odb_for_each_object_options *opts)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tstruct for_each_object_wrapper_data data = {\n-\t\t.loose = files->loose,\n-\t\t.request = request,\n-\t\t.cb = cb,\n-\t\t.cb_data = cb_data,\n-\t};\n-\n-\t/* There are no loose promisor objects, so we can return immediately. */\n-\tif ((opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY))\n-\t\treturn 0;\n-\tif ((opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !source->local)\n-\t\treturn 0;\n-\n-\tif (opts->prefix)\n-\t\treturn oidtree_each(odb_source_loose_cache(source, opts->prefix),\n-\t\t\t\t    opts->prefix, opts->prefix_hex_len,\n-\t\t\t\t    for_each_prefixed_object_wrapper_cb, &data);\n-\n-\treturn for_each_loose_file_in_source(source, for_each_object_wrapper_cb,\n-\t\t\t\t\t     NULL, NULL, &data);\n-}\n-\n static int count_loose_object(const struct object_id *oid UNUSED,\n \t\t\t      struct object_info *oi UNUSED,\n \t\t\t      void *payload)\n@@ -1843,6 +1615,7 @@ int odb_source_loose_count_objects(struct odb_source *source,\n \t\t\t\t   enum odb_count_objects_flags flags,\n \t\t\t\t   unsigned long *out)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tconst unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2;\n \tchar *path = NULL;\n \tDIR *dir = NULL;\n@@ -1878,8 +1651,8 @@ int odb_source_loose_count_objects(struct odb_source *source,\n \t} else {\n \t\tstruct odb_for_each_object_options opts = { 0 };\n \t\t*out = 0;\n-\t\tret = odb_source_loose_for_each_object(source, NULL, count_loose_object,\n-\t\t\t\t\t\t       out, &opts);\n+\t\tret = odb_source_for_each_object(&files->loose->base, NULL, count_loose_object,\n+\t\t\t\t\t\t out, &opts);\n \t}\n \n out:\n@@ -1910,6 +1683,7 @@ int odb_source_loose_find_abbrev_len(struct odb_source *source,\n \t\t\t\t     unsigned min_len,\n \t\t\t\t     unsigned *out)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tstruct odb_for_each_object_options opts = {\n \t\t.prefix = oid,\n \t\t.prefix_hex_len = min_len,\n@@ -1920,54 +1694,13 @@ int odb_source_loose_find_abbrev_len(struct odb_source *source,\n \t};\n \tint ret;\n \n-\tret = odb_source_loose_for_each_object(source, NULL, find_abbrev_len_cb,\n-\t\t\t\t\t       &data, &opts);\n+\tret = odb_source_for_each_object(&files->loose->base, NULL, find_abbrev_len_cb,\n+\t\t\t\t\t &data, &opts);\n \t*out = data.len;\n \n \treturn ret;\n }\n \n-static int append_loose_object(const struct object_id *oid,\n-\t\t\t       const char *path UNUSED,\n-\t\t\t       void *data)\n-{\n-\toidtree_insert(data, oid, NULL);\n-\treturn 0;\n-}\n-\n-static struct oidtree *odb_source_loose_cache(struct odb_source *source,\n-\t\t\t\t\t      const struct object_id *oid)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tint subdir_nr = oid->hash[0];\n-\tstruct strbuf buf = STRBUF_INIT;\n-\tsize_t word_bits = bitsizeof(files->loose->subdir_seen[0]);\n-\tsize_t word_index = subdir_nr / word_bits;\n-\tsize_t mask = (size_t)1u << (subdir_nr % word_bits);\n-\tuint32_t *bitmap;\n-\n-\tif (subdir_nr < 0 ||\n-\t    (size_t) subdir_nr >= bitsizeof(files->loose->subdir_seen))\n-\t\tBUG(\"subdir_nr out of range\");\n-\n-\tbitmap = &files->loose->subdir_seen[word_index];\n-\tif (*bitmap & mask)\n-\t\treturn files->loose->cache;\n-\tif (!files->loose->cache) {\n-\t\tALLOC_ARRAY(files->loose->cache, 1);\n-\t\toidtree_init(files->loose->cache);\n-\t}\n-\tstrbuf_addstr(&buf, source->path);\n-\tfor_each_file_in_obj_subdir(subdir_nr, &buf,\n-\t\t\t\t    source->odb->repo->hash_algo,\n-\t\t\t\t    append_loose_object,\n-\t\t\t\t    NULL, NULL,\n-\t\t\t\t    files->loose->cache);\n-\t*bitmap |= mask;\n-\tstrbuf_release(&buf);\n-\treturn files->loose->cache;\n-}\n-\n static int check_stream_oid(git_zstream *stream,\n \t\t\t    const char *hdr,\n \t\t\t    unsigned long size,\ndiff --git a/object-file.h b/object-file.h\nindex d93b7ffad7..9ee5649220 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -6,6 +6,9 @@\n #include \"odb.h\"\n #include \"odb/source-loose.h\"\n \n+/* The maximum size for an object header. */\n+#define MAX_HEADER_LEN 32\n+\n struct index_state;\n \n enum {\n@@ -85,19 +88,13 @@ int for_each_loose_file_in_source(struct odb_source *source,\n \t\t\t\t  each_loose_cruft_fn cruft_cb,\n \t\t\t\t  each_loose_subdir_fn subdir_cb,\n \t\t\t\t  void *data);\n-\n-/*\n- * Iterate through all loose objects in the given object database source and\n- * invoke the callback function for each of them. If an object info request is\n- * given, then the object info will be read for every individual object and\n- * passed to the callback as if `odb_source_loose_read_object_info()` was\n- * called for the object.\n- */\n-int odb_source_loose_for_each_object(struct odb_source *source,\n-\t\t\t\t     const struct object_info *request,\n-\t\t\t\t     odb_for_each_object_cb cb,\n-\t\t\t\t     void *cb_data,\n-\t\t\t\t     const struct odb_for_each_object_options *opts);\n+int for_each_file_in_obj_subdir(unsigned int subdir_nr,\n+\t\t\t\tstruct strbuf *path,\n+\t\t\t\tconst struct git_hash_algo *algop,\n+\t\t\t\teach_loose_object_fn obj_cb,\n+\t\t\t\teach_loose_cruft_fn cruft_cb,\n+\t\t\t\teach_loose_subdir_fn subdir_cb,\n+\t\t\t\tvoid *data);\n \n /*\n  * Count the number of loose objects in this source.\n@@ -188,12 +185,6 @@ int read_loose_object(struct repository *repo,\n \t\t      void **contents,\n \t\t      struct object_info *oi);\n \n-int read_object_info_from_path(struct odb_source_loose *loose,\n-\t\t\t       const char *path,\n-\t\t\t       const struct object_id *oid,\n-\t\t\t       struct object_info *oi,\n-\t\t\t       enum object_info_flags flags);\n-\n enum unpack_loose_header_result {\n \tULHR_OK,\n \tULHR_BAD,\n@@ -217,6 +208,9 @@ enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n \t\t\t\t\t\t    unsigned long mapsize,\n \t\t\t\t\t\t    void *buffer,\n \t\t\t\t\t\t    unsigned long bufsiz);\n+void *unpack_loose_rest(git_zstream *stream,\n+\t\t\tvoid *buffer, unsigned long size,\n+\t\t\tconst struct object_id *oid);\n \n int parse_loose_header(const char *hdr, struct object_info *oi);\n \ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 90806ddf86..676a641739 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -82,7 +82,7 @@ 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_loose_for_each_object(source, request, cb, cb_data, opts);\n+\t\tret = odb_source_for_each_object(&files->loose->base, request, cb, cb_data, opts);\n \t\tif (ret)\n \t\t\treturn ret;\n \t}\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 4b82c6f316..4e8b923498 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -2,6 +2,7 @@\n #include \"abspath.h\"\n #include \"chdir-notify.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n #include \"loose.h\"\n #include \"object-file.h\"\n #include \"odb.h\"\n@@ -9,8 +10,198 @@\n #include \"odb/source-loose.h\"\n #include \"odb/streaming.h\"\n #include \"oidtree.h\"\n+#include \"repository.h\"\n #include \"strbuf.h\"\n \n+static int append_loose_object(const struct object_id *oid,\n+\t\t\t       const char *path UNUSED,\n+\t\t\t       void *data)\n+{\n+\toidtree_insert(data, oid, NULL);\n+\treturn 0;\n+}\n+\n+static struct oidtree *odb_source_loose_cache(struct odb_source_loose *loose,\n+\t\t\t\t\t      const struct object_id *oid)\n+{\n+\tint subdir_nr = oid->hash[0];\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tsize_t word_bits = bitsizeof(loose->subdir_seen[0]);\n+\tsize_t word_index = subdir_nr / word_bits;\n+\tsize_t mask = (size_t)1u << (subdir_nr % word_bits);\n+\tuint32_t *bitmap;\n+\n+\tif (subdir_nr < 0 ||\n+\t    (size_t) subdir_nr >= bitsizeof(loose->subdir_seen))\n+\t\tBUG(\"subdir_nr out of range\");\n+\n+\tbitmap = &loose->subdir_seen[word_index];\n+\tif (*bitmap & mask)\n+\t\treturn loose->cache;\n+\tif (!loose->cache) {\n+\t\tALLOC_ARRAY(loose->cache, 1);\n+\t\toidtree_init(loose->cache);\n+\t}\n+\tstrbuf_addstr(&buf, loose->base.path);\n+\tfor_each_file_in_obj_subdir(subdir_nr, &buf,\n+\t\t\t\t    loose->base.odb->repo->hash_algo,\n+\t\t\t\t    append_loose_object,\n+\t\t\t\t    NULL, NULL,\n+\t\t\t\t    loose->cache);\n+\t*bitmap |= mask;\n+\tstrbuf_release(&buf);\n+\treturn loose->cache;\n+}\n+\n+static int quick_has_loose(struct odb_source_loose *loose,\n+\t\t\t   const struct object_id *oid)\n+{\n+\treturn !!oidtree_contains(odb_source_loose_cache(loose, oid), oid);\n+}\n+\n+static int read_object_info_from_path(struct odb_source_loose *loose,\n+\t\t\t\t      const char *path,\n+\t\t\t\t      const struct object_id *oid,\n+\t\t\t\t      struct object_info *oi,\n+\t\t\t\t      enum object_info_flags flags)\n+{\n+\tint ret;\n+\tint fd;\n+\tunsigned long mapsize;\n+\tvoid *map = NULL;\n+\tgit_zstream stream, *stream_to_end = NULL;\n+\tchar hdr[MAX_HEADER_LEN];\n+\tunsigned long size_scratch;\n+\tenum object_type type_scratch;\n+\tstruct stat st;\n+\n+\t/*\n+\t * If we don't care about type or size, then we don't\n+\t * need to look inside the object at all. Note that we\n+\t * do not optimize out the stat call, even if the\n+\t * caller doesn't care about the disk-size, since our\n+\t * return value implicitly indicates whether the\n+\t * object even exists.\n+\t */\n+\tif (!oi || (!oi->typep && !oi->sizep && !oi->contentp)) {\n+\t\tstruct stat st;\n+\n+\t\tif ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) {\n+\t\t\tret = quick_has_loose(loose, oid) ? 0 : -1;\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (lstat(path, &st) < 0) {\n+\t\t\tret = -1;\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (oi) {\n+\t\t\tif (oi->disk_sizep)\n+\t\t\t\t*oi->disk_sizep = st.st_size;\n+\t\t\tif (oi->mtimep)\n+\t\t\t\t*oi->mtimep = st.st_mtime;\n+\t\t}\n+\n+\t\tret = 0;\n+\t\tgoto out;\n+\t}\n+\n+\tfd = git_open(path);\n+\tif (fd < 0) {\n+\t\tif (errno != ENOENT)\n+\t\t\terror_errno(_(\"unable to open loose object %s\"), oid_to_hex(oid));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tif (fstat(fd, &st)) {\n+\t\tclose(fd);\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tmapsize = xsize_t(st.st_size);\n+\tif (!mapsize) {\n+\t\tclose(fd);\n+\t\tret = error(_(\"object file %s is empty\"), path);\n+\t\tgoto out;\n+\t}\n+\n+\tmap = xmmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, fd, 0);\n+\tclose(fd);\n+\tif (!map) {\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tif (oi->disk_sizep)\n+\t\t*oi->disk_sizep = mapsize;\n+\tif (oi->mtimep)\n+\t\t*oi->mtimep = st.st_mtime;\n+\n+\tstream_to_end = &stream;\n+\n+\tswitch (unpack_loose_header(&stream, map, mapsize, hdr, sizeof(hdr))) {\n+\tcase ULHR_OK:\n+\t\tif (!oi->sizep)\n+\t\t\toi->sizep = &size_scratch;\n+\t\tif (!oi->typep)\n+\t\t\toi->typep = &type_scratch;\n+\n+\t\tif (parse_loose_header(hdr, oi) < 0) {\n+\t\t\tret = error(_(\"unable to parse %s header\"), oid_to_hex(oid));\n+\t\t\tgoto corrupt;\n+\t\t}\n+\n+\t\tif (*oi->typep < 0)\n+\t\t\tdie(_(\"invalid object type\"));\n+\n+\t\tif (oi->contentp) {\n+\t\t\t*oi->contentp = unpack_loose_rest(&stream, hdr, *oi->sizep, oid);\n+\t\t\tif (!*oi->contentp) {\n+\t\t\t\tret = -1;\n+\t\t\t\tgoto corrupt;\n+\t\t\t}\n+\t\t}\n+\n+\t\tbreak;\n+\tcase ULHR_BAD:\n+\t\tret = error(_(\"unable to unpack %s header\"),\n+\t\t\t    oid_to_hex(oid));\n+\t\tgoto corrupt;\n+\tcase ULHR_TOO_LONG:\n+\t\tret = error(_(\"header for %s too long, exceeds %d bytes\"),\n+\t\t\t    oid_to_hex(oid), MAX_HEADER_LEN);\n+\t\tgoto corrupt;\n+\t}\n+\n+\tret = 0;\n+\n+corrupt:\n+\tif (ret && (flags & OBJECT_INFO_DIE_IF_CORRUPT))\n+\t\tdie(_(\"loose object %s (stored in %s) is corrupt\"),\n+\t\t    oid_to_hex(oid), path);\n+\n+out:\n+\tif (stream_to_end)\n+\t\tgit_inflate_end(stream_to_end);\n+\tif (map)\n+\t\tmunmap(map, mapsize);\n+\tif (oi) {\n+\t\tif (oi->sizep == &size_scratch)\n+\t\t\toi->sizep = NULL;\n+\t\tif (oi->typep == &type_scratch)\n+\t\t\toi->typep = NULL;\n+\t\tif (oi->delta_base_oid)\n+\t\t\toidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo);\n+\t\tif (!ret)\n+\t\t\toi->whence = OI_LOOSE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n static int odb_source_loose_read_object_info(struct odb_source *source,\n \t\t\t\t\t     const struct object_id *oid,\n \t\t\t\t\t     struct object_info *oi,\n@@ -218,6 +409,78 @@ static int odb_source_loose_read_object_stream(struct odb_read_stream **out,\n \treturn -1;\n }\n \n+struct for_each_object_wrapper_data {\n+\tstruct odb_source_loose *loose;\n+\tconst struct object_info *request;\n+\todb_for_each_object_cb cb;\n+\tvoid *cb_data;\n+};\n+\n+static int for_each_object_wrapper_cb(const struct object_id *oid,\n+\t\t\t\t      const char *path,\n+\t\t\t\t      void *cb_data)\n+{\n+\tstruct for_each_object_wrapper_data *data = cb_data;\n+\n+\tif (data->request) {\n+\t\tstruct object_info oi = *data->request;\n+\n+\t\tif (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0)\n+\t\t\treturn -1;\n+\n+\t\treturn data->cb(oid, &oi, data->cb_data);\n+\t} else {\n+\t\treturn data->cb(oid, NULL, data->cb_data);\n+\t}\n+}\n+\n+static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid,\n+\t\t\t\t\t       void *node_data UNUSED,\n+\t\t\t\t\t       void *cb_data)\n+{\n+\tstruct for_each_object_wrapper_data *data = cb_data;\n+\tif (data->request) {\n+\t\tstruct object_info oi = *data->request;\n+\n+\t\tif (odb_source_read_object_info(&data->loose->base,\n+\t\t\t\t\t\toid, &oi, 0) < 0)\n+\t\t\treturn -1;\n+\n+\t\treturn data->cb(oid, &oi, data->cb_data);\n+\t} else {\n+\t\treturn data->cb(oid, NULL, data->cb_data);\n+\t}\n+}\n+\n+static int odb_source_loose_for_each_object(struct odb_source *source,\n+\t\t\t\t\t    const struct object_info *request,\n+\t\t\t\t\t    odb_for_each_object_cb cb,\n+\t\t\t\t\t    void *cb_data,\n+\t\t\t\t\t    const struct odb_for_each_object_options *opts)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tstruct for_each_object_wrapper_data data = {\n+\t\t.loose = loose,\n+\t\t.request = request,\n+\t\t.cb = cb,\n+\t\t.cb_data = cb_data,\n+\t};\n+\n+\t/* There are no loose promisor objects, so we can return immediately. */\n+\tif ((opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY))\n+\t\treturn 0;\n+\tif ((opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !source->local)\n+\t\treturn 0;\n+\n+\tif (opts->prefix)\n+\t\treturn oidtree_each(odb_source_loose_cache(loose, opts->prefix),\n+\t\t\t\t    opts->prefix, opts->prefix_hex_len,\n+\t\t\t\t    for_each_prefixed_object_wrapper_cb, &data);\n+\n+\treturn for_each_loose_file_in_source(source, for_each_object_wrapper_cb,\n+\t\t\t\t\t     NULL, NULL, &data);\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -273,6 +536,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.reprepare = odb_source_loose_reprepare;\n \tloose->base.read_object_info = odb_source_loose_read_object_info;\n \tloose->base.read_object_stream = odb_source_loose_read_object_stream;\n+\tloose->base.for_each_object = odb_source_loose_for_each_object;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543796","messageId":"20260521-b4-pks-odb-source-loose-v1-9-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 09/18] odb/source-loose: wire up `find_abbrev_len()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:29Z","receivedAt":"2026-05-21T08:22:54Z","isPatch":true,"body":"Move `odb_source_loose_find_abbrev_len()` and its associated helpers\nfrom \"object-file.c\" into \"odb/source-loose.c\" and wire it up as the\n`find_abbrev_len` callback of the loose source.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 39 ---------------------------------------\n object-file.h      | 12 ------------\n odb/source-files.c |  2 +-\n odb/source-loose.c | 40 ++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 41 insertions(+), 52 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 157ecad3ea..11957aa44f 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1662,45 +1662,6 @@ int odb_source_loose_count_objects(struct odb_source *source,\n \treturn ret;\n }\n \n-struct find_abbrev_len_data {\n-\tconst struct object_id *oid;\n-\tunsigned len;\n-};\n-\n-static int find_abbrev_len_cb(const struct object_id *oid,\n-\t\t\t      struct object_info *oi UNUSED,\n-\t\t\t      void *cb_data)\n-{\n-\tstruct find_abbrev_len_data *data = cb_data;\n-\tunsigned len = oid_common_prefix_hexlen(oid, data->oid);\n-\tif (len != hash_algos[oid->algo].hexsz && len >= data->len)\n-\t\tdata->len = len + 1;\n-\treturn 0;\n-}\n-\n-int odb_source_loose_find_abbrev_len(struct odb_source *source,\n-\t\t\t\t     const struct object_id *oid,\n-\t\t\t\t     unsigned min_len,\n-\t\t\t\t     unsigned *out)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tstruct odb_for_each_object_options opts = {\n-\t\t.prefix = oid,\n-\t\t.prefix_hex_len = min_len,\n-\t};\n-\tstruct find_abbrev_len_data data = {\n-\t\t.oid = oid,\n-\t\t.len = min_len,\n-\t};\n-\tint ret;\n-\n-\tret = odb_source_for_each_object(&files->loose->base, NULL, find_abbrev_len_cb,\n-\t\t\t\t\t &data, &opts);\n-\t*out = data.len;\n-\n-\treturn ret;\n-}\n-\n static int check_stream_oid(git_zstream *stream,\n \t\t\t    const char *hdr,\n \t\t\t    unsigned long size,\ndiff --git a/object-file.h b/object-file.h\nindex 9ee5649220..96760db0e1 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -110,18 +110,6 @@ int odb_source_loose_count_objects(struct odb_source *source,\n \t\t\t\t   enum odb_count_objects_flags flags,\n \t\t\t\t   unsigned long *out);\n \n-/*\n- * Find the shortest unique prefix for the given object ID, where `min_len` is\n- * the minimum length that the prefix should have.\n- *\n- * Returns 0 on success, in which case the computed length will be written to\n- * `out`. Otherwise, a negative error code is returned.\n- */\n-int odb_source_loose_find_abbrev_len(struct odb_source *source,\n-\t\t\t\t     const struct object_id *oid,\n-\t\t\t\t     unsigned min_len,\n-\t\t\t\t     unsigned *out);\n-\n /**\n  * format_object_header() is a thin wrapper around s xsnprintf() that\n  * writes the initial \"<type> <obj-len>\" part of the loose object\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 676a641739..4a54b10e4a 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -136,7 +136,7 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source,\n \tif (ret < 0)\n \t\tgoto out;\n \n-\tret = odb_source_loose_find_abbrev_len(source, oid, len, &len);\n+\tret = odb_source_find_abbrev_len(&files->loose->base, oid, len, &len);\n \tif (ret < 0)\n \t\tgoto out;\n \ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 4e8b923498..4b8d10bc87 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -481,6 +481,45 @@ static int odb_source_loose_for_each_object(struct odb_source *source,\n \t\t\t\t\t     NULL, NULL, &data);\n }\n \n+struct find_abbrev_len_data {\n+\tconst struct object_id *oid;\n+\tunsigned len;\n+};\n+\n+static int find_abbrev_len_cb(const struct object_id *oid,\n+\t\t\t      struct object_info *oi UNUSED,\n+\t\t\t      void *cb_data)\n+{\n+\tstruct find_abbrev_len_data *data = cb_data;\n+\tunsigned len = oid_common_prefix_hexlen(oid, data->oid);\n+\tif (len != hash_algos[oid->algo].hexsz && len >= data->len)\n+\t\tdata->len = len + 1;\n+\treturn 0;\n+}\n+\n+static int odb_source_loose_find_abbrev_len(struct odb_source *source,\n+\t\t\t\t\t    const struct object_id *oid,\n+\t\t\t\t\t    unsigned min_len,\n+\t\t\t\t\t    unsigned *out)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tstruct odb_for_each_object_options opts = {\n+\t\t.prefix = oid,\n+\t\t.prefix_hex_len = min_len,\n+\t};\n+\tstruct find_abbrev_len_data data = {\n+\t\t.oid = oid,\n+\t\t.len = min_len,\n+\t};\n+\tint ret;\n+\n+\tret = odb_source_for_each_object(&loose->base, NULL, find_abbrev_len_cb,\n+\t\t\t\t\t &data, &opts);\n+\t*out = data.len;\n+\n+\treturn ret;\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -537,6 +576,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.read_object_info = odb_source_loose_read_object_info;\n \tloose->base.read_object_stream = odb_source_loose_read_object_stream;\n \tloose->base.for_each_object = odb_source_loose_for_each_object;\n+\tloose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543797","messageId":"20260521-b4-pks-odb-source-loose-v1-10-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 10/18] odb/source-loose: wire up `count_objects()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:30Z","receivedAt":"2026-05-21T08:22:57Z","isPatch":true,"body":"Move `odb_source_loose_count_objects()` and its associated helpers from\n\"object-file.c\" into \"odb/source-loose.c\" and wire it up as the\n`count_objects()` callback of the loose source.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c       |  6 +++---\n object-file.c      | 60 -----------------------------------------------------\n object-file.h      | 14 -------------\n odb/source-files.c |  2 +-\n odb/source-loose.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 65 insertions(+), 78 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 84a66d3240..c26c93ee0f 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -466,6 +466,7 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED)\n \n static int too_many_loose_objects(int limit)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);\n \t/*\n \t * This is weird, but stems from legacy behaviour: the GC auto\n \t * threshold was always essentially interpreted as if it was rounded up\n@@ -474,9 +475,8 @@ static int too_many_loose_objects(int limit)\n \tint auto_threshold = DIV_ROUND_UP(limit, 256) * 256;\n \tunsigned long loose_count;\n \n-\tif (odb_source_loose_count_objects(the_repository->objects->sources,\n-\t\t\t\t\t   ODB_COUNT_OBJECTS_APPROXIMATE,\n-\t\t\t\t\t   &loose_count) < 0)\n+\tif (odb_source_count_objects(&files->loose->base, ODB_COUNT_OBJECTS_APPROXIMATE,\n+\t\t\t\t     &loose_count) < 0)\n \t\treturn 0;\n \n \treturn loose_count > auto_threshold;\ndiff --git a/object-file.c b/object-file.c\nindex 11957aa44f..9b2044de37 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1602,66 +1602,6 @@ int for_each_loose_file_in_source(struct odb_source *source,\n \treturn r;\n }\n \n-static int count_loose_object(const struct object_id *oid UNUSED,\n-\t\t\t      struct object_info *oi UNUSED,\n-\t\t\t      void *payload)\n-{\n-\tunsigned long *count = payload;\n-\t(*count)++;\n-\treturn 0;\n-}\n-\n-int odb_source_loose_count_objects(struct odb_source *source,\n-\t\t\t\t   enum odb_count_objects_flags flags,\n-\t\t\t\t   unsigned long *out)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tconst unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2;\n-\tchar *path = NULL;\n-\tDIR *dir = NULL;\n-\tint ret;\n-\n-\tif (flags & ODB_COUNT_OBJECTS_APPROXIMATE) {\n-\t\tunsigned long count = 0;\n-\t\tstruct dirent *ent;\n-\n-\t\tpath = xstrfmt(\"%s/17\", source->path);\n-\n-\t\tdir = opendir(path);\n-\t\tif (!dir) {\n-\t\t\tif (errno == ENOENT) {\n-\t\t\t\t*out = 0;\n-\t\t\t\tret = 0;\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\n-\t\t\tret = error_errno(\"cannot open object shard '%s'\", path);\n-\t\t\tgoto out;\n-\t\t}\n-\n-\t\twhile ((ent = readdir(dir)) != NULL) {\n-\t\t\tif (strspn(ent->d_name, \"0123456789abcdef\") != hexsz ||\n-\t\t\t    ent->d_name[hexsz] != '\\0')\n-\t\t\t\tcontinue;\n-\t\t\tcount++;\n-\t\t}\n-\n-\t\t*out = count * 256;\n-\t\tret = 0;\n-\t} else {\n-\t\tstruct odb_for_each_object_options opts = { 0 };\n-\t\t*out = 0;\n-\t\tret = odb_source_for_each_object(&files->loose->base, NULL, count_loose_object,\n-\t\t\t\t\t\t out, &opts);\n-\t}\n-\n-out:\n-\tif (dir)\n-\t\tclosedir(dir);\n-\tfree(path);\n-\treturn ret;\n-}\n-\n static int check_stream_oid(git_zstream *stream,\n \t\t\t    const char *hdr,\n \t\t\t    unsigned long size,\ndiff --git a/object-file.h b/object-file.h\nindex 96760db0e1..bc72d89f54 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -96,20 +96,6 @@ int for_each_file_in_obj_subdir(unsigned int subdir_nr,\n \t\t\t\teach_loose_subdir_fn subdir_cb,\n \t\t\t\tvoid *data);\n \n-/*\n- * Count the number of loose objects in this source.\n- *\n- * The object count is approximated by opening a single sharding directory for\n- * loose objects and scanning its contents. The result is then extrapolated by\n- * 256. This should generally work as a reasonable estimate given that the\n- * object hash is supposed to be indistinguishable from random.\n- *\n- * Returns 0 on success, a negative error code otherwise.\n- */\n-int odb_source_loose_count_objects(struct odb_source *source,\n-\t\t\t\t   enum odb_count_objects_flags flags,\n-\t\t\t\t   unsigned long *out);\n-\n /**\n  * format_object_header() is a thin wrapper around s xsnprintf() that\n  * writes the initial \"<type> <obj-len>\" part of the loose object\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 4a54b10e4a..d5454e170d 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -109,7 +109,7 @@ static int odb_source_files_count_objects(struct odb_source *source,\n \tif (!(flags & ODB_COUNT_OBJECTS_APPROXIMATE)) {\n \t\tunsigned long loose_count;\n \n-\t\tret = odb_source_loose_count_objects(source, flags, &loose_count);\n+\t\tret = odb_source_count_objects(&files->loose->base, flags, &loose_count);\n \t\tif (ret < 0)\n \t\t\tgoto out;\n \ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 4b8d10bc87..27be066327 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -520,6 +520,66 @@ static int odb_source_loose_find_abbrev_len(struct odb_source *source,\n \treturn ret;\n }\n \n+static int count_loose_object(const struct object_id *oid UNUSED,\n+\t\t\t      struct object_info *oi UNUSED,\n+\t\t\t      void *payload)\n+{\n+\tunsigned long *count = payload;\n+\t(*count)++;\n+\treturn 0;\n+}\n+\n+static int odb_source_loose_count_objects(struct odb_source *source,\n+\t\t\t\t\t  enum odb_count_objects_flags flags,\n+\t\t\t\t\t  unsigned long *out)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tconst unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2;\n+\tchar *path = NULL;\n+\tDIR *dir = NULL;\n+\tint ret;\n+\n+\tif (flags & ODB_COUNT_OBJECTS_APPROXIMATE) {\n+\t\tunsigned long count = 0;\n+\t\tstruct dirent *ent;\n+\n+\t\tpath = xstrfmt(\"%s/17\", source->path);\n+\n+\t\tdir = opendir(path);\n+\t\tif (!dir) {\n+\t\t\tif (errno == ENOENT) {\n+\t\t\t\t*out = 0;\n+\t\t\t\tret = 0;\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\n+\t\t\tret = error_errno(\"cannot open object shard '%s'\", path);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\twhile ((ent = readdir(dir)) != NULL) {\n+\t\t\tif (strspn(ent->d_name, \"0123456789abcdef\") != hexsz ||\n+\t\t\t    ent->d_name[hexsz] != '\\0')\n+\t\t\t\tcontinue;\n+\t\t\tcount++;\n+\t\t}\n+\n+\t\t*out = count * 256;\n+\t\tret = 0;\n+\t} else {\n+\t\tstruct odb_for_each_object_options opts = { 0 };\n+\t\t*out = 0;\n+\t\tret = odb_source_for_each_object(&loose->base, NULL, count_loose_object,\n+\t\t\t\t\t\t out, &opts);\n+\t}\n+\n+out:\n+\tif (dir)\n+\t\tclosedir(dir);\n+\tfree(path);\n+\treturn ret;\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -577,6 +637,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.read_object_stream = odb_source_loose_read_object_stream;\n \tloose->base.for_each_object = odb_source_loose_for_each_object;\n \tloose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;\n+\tloose->base.count_objects = odb_source_loose_count_objects;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543798","messageId":"20260521-b4-pks-odb-source-loose-v1-11-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 11/18] odb/source-loose: drop `odb_source_loose_has_object()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:31Z","receivedAt":"2026-05-21T08:22:59Z","isPatch":true,"body":"The function `odb_source_loose_has_object()` checks whether a specific\nobject exists as a loose object on disk by using lstat(3p). This\ninterface is somewhat redundant, as we typically check for object\nexistence in a generic way via `odb_source_read_object_info()`.\n\nIn fact, these two calls are redundant in case the latter is called in a\nspecific way: when called without an object info request and without the\n`OBJECT_INFO_QUICK` flag, then we will end up doing the same call to\nlstat(3p) in `read_object_info_from_path()`.\n\nDrop the function and adapt callers to instead use the generic\ninterface so that its calling conventions align with that of other\nsources.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/pack-objects.c | 12 ++++++++----\n object-file.c          | 12 ++++--------\n object-file.h          |  8 --------\n 3 files changed, 12 insertions(+), 20 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 480cc0bd8c..a6be3d659f 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -1750,9 +1750,11 @@ static int want_object_in_pack_mtime(const struct object_id *oid,\n \t\t * skip the local object source.\n \t\t */\n \t\tstruct odb_source *source = the_repository->objects->sources->next;\n-\t\tfor (; source; source = source->next)\n-\t\t\tif (odb_source_loose_has_object(source, oid))\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))\n \t\t\t\treturn 0;\n+\t\t}\n \t}\n \n \t/*\n@@ -4135,9 +4137,11 @@ static void add_cruft_object_entry(const struct object_id *oid, enum object_type\n \t\t\tstruct odb_source *source = the_repository->objects->sources;\n \t\t\tint found = 0;\n \n-\t\t\tfor (; !found && source; source = source->next)\n-\t\t\t\tif (odb_source_loose_has_object(source, oid))\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))\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\ndiff --git a/object-file.c b/object-file.c\nindex 9b2044de37..c83136cf70 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -96,12 +96,6 @@ static int check_and_freshen_source(struct odb_source *source,\n \treturn check_and_freshen_file(path.buf, freshen);\n }\n \n-int odb_source_loose_has_object(struct odb_source *source,\n-\t\t\t\tconst struct object_id *oid)\n-{\n-\treturn check_and_freshen_source(source, oid, 0);\n-}\n-\n int format_object_header(char *str, size_t size, enum object_type type,\n \t\t\t size_t objsize)\n {\n@@ -1000,9 +994,11 @@ int force_object_loose(struct odb_source *source,\n \tint hdrlen;\n \tint ret;\n \n-\tfor (struct odb_source *s = source->odb->sources; s; s = s->next)\n-\t\tif (odb_source_loose_has_object(s, oid))\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))\n \t\t\treturn 0;\n+\t}\n \n \toi.typep = &type;\n \toi.sizep = &len;\ndiff --git a/object-file.h b/object-file.h\nindex bc72d89f54..506ca6be40 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -23,14 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa\n struct object_info;\n struct odb_source;\n \n-/*\n- * Return true iff an object database source has a loose object\n- * with the specified name.  This function does not respect replace\n- * references.\n- */\n-int odb_source_loose_has_object(struct odb_source *source,\n-\t\t\t\tconst struct object_id *oid);\n-\n int odb_source_loose_freshen_object(struct odb_source *source,\n \t\t\t\t    const struct object_id *oid);\n \n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543799","messageId":"20260521-b4-pks-odb-source-loose-v1-12-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 12/18] odb/source-loose: wire up `freshen_object()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:32Z","receivedAt":"2026-05-21T08:23:02Z","isPatch":true,"body":"Move `odb_source_loose_freshen_object()` from \"object-file.c\" into\n\"odb/source-loose.c\" and wire it up as the `freshen_object()` callback\nof the loose source.\n\nAs part of the move, `check_and_freshen_source()` is inlined into the\ncallback function, as it has no other callers anymore.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 15 ---------------\n object-file.h      |  3 ---\n odb/source-files.c |  2 +-\n odb/source-loose.c |  9 +++++++++\n 4 files changed, 10 insertions(+), 19 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex c83136cf70..0689a4e67b 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -87,15 +87,6 @@ int check_and_freshen_file(const char *fn, int freshen)\n \treturn 1;\n }\n \n-static int check_and_freshen_source(struct odb_source *source,\n-\t\t\t\t    const struct object_id *oid,\n-\t\t\t\t    int freshen)\n-{\n-\tstatic struct strbuf path = STRBUF_INIT;\n-\todb_loose_path(source, &path, oid);\n-\treturn check_and_freshen_file(path.buf, freshen);\n-}\n-\n int format_object_header(char *str, size_t size, enum object_type type,\n \t\t\t size_t objsize)\n {\n@@ -815,12 +806,6 @@ static int write_loose_object(struct odb_source *source,\n \t\t\t\t\t  FOF_SKIP_COLLISION_CHECK);\n }\n \n-int odb_source_loose_freshen_object(struct odb_source *source,\n-\t\t\t\t    const struct object_id *oid)\n-{\n-\treturn !!check_and_freshen_source(source, oid, 1);\n-}\n-\n int odb_source_loose_write_stream(struct odb_source *source,\n \t\t\t\t  struct odb_write_stream *in_stream, size_t len,\n \t\t\t\t  struct object_id *oid)\ndiff --git a/object-file.h b/object-file.h\nindex 506ca6be40..1d90df9d98 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -23,9 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa\n struct object_info;\n struct odb_source;\n \n-int odb_source_loose_freshen_object(struct odb_source *source,\n-\t\t\t\t    const struct object_id *oid);\n-\n int odb_source_loose_write_object(struct odb_source *source,\n \t\t\t\t  const void *buf, unsigned long len,\n \t\t\t\t  enum object_type type, struct object_id *oid,\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex d5454e170d..ef548e6fe6 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -152,7 +152,7 @@ static int odb_source_files_freshen_object(struct odb_source *source,\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tif (packfile_store_freshen_object(files->packed, oid) ||\n-\t    odb_source_loose_freshen_object(source, oid))\n+\t    odb_source_freshen_object(&files->loose->base, oid))\n \t\treturn 1;\n \treturn 0;\n }\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 27be066327..e519365d23 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -580,6 +580,14 @@ static int odb_source_loose_count_objects(struct odb_source *source,\n \treturn ret;\n }\n \n+static int odb_source_loose_freshen_object(struct odb_source *source,\n+\t\t\t\t\t   const struct object_id *oid)\n+{\n+\tstatic struct strbuf path = STRBUF_INIT;\n+\todb_loose_path(source, &path, oid);\n+\treturn !!check_and_freshen_file(path.buf, 1);\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -638,6 +646,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.for_each_object = odb_source_loose_for_each_object;\n \tloose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;\n \tloose->base.count_objects = odb_source_loose_count_objects;\n+\tloose->base.freshen_object = odb_source_loose_freshen_object;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543800","messageId":"20260521-b4-pks-odb-source-loose-v1-13-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 13/18] loose: refactor object map to operate on `struct odb_source_loose`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:33Z","receivedAt":"2026-05-21T08:23:05Z","isPatch":true,"body":"While the loose object map functions in \"loose.c\" accept a generic\n`struct odb_source *`, they always expect this to be the \"files\"\nbackend. Furthermore, the subsystem doesn't even care about the \"files\"\nbackend, but only uses it as a stepping stone to get to the \"loose\"\nbackend.\n\nThis assumption is implicit and thus not immediately obvious. Refactor\nthe interfaces to instead operate on a `struct odb_source_loose`\ninstead, which eliminates the implicit dependency and unnecessary detour\nvia the \"files\" source.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n loose.c       | 45 ++++++++++++++++++++++-----------------------\n loose.h       |  4 ++--\n object-file.c |  9 ++++++---\n 3 files changed, 30 insertions(+), 28 deletions(-)\n\ndiff --git a/loose.c b/loose.c\nindex f7a3dd1a72..0b626c1b85 100644\n--- a/loose.c\n+++ b/loose.c\n@@ -46,38 +46,36 @@ static int insert_oid_pair(kh_oid_map_t *map, const struct object_id *key, const\n \treturn 1;\n }\n \n-static int insert_loose_map(struct odb_source *source,\n+static int insert_loose_map(struct odb_source_loose *loose,\n \t\t\t    const struct object_id *oid,\n \t\t\t    const struct object_id *compat_oid)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tstruct loose_object_map *map = files->loose->map;\n+\tstruct loose_object_map *map = loose->map;\n \tint inserted = 0;\n \n \tinserted |= insert_oid_pair(map->to_compat, oid, compat_oid);\n \tinserted |= insert_oid_pair(map->to_storage, compat_oid, oid);\n \tif (inserted)\n-\t\toidtree_insert(files->loose->cache, compat_oid, NULL);\n+\t\toidtree_insert(loose->cache, compat_oid, NULL);\n \n \treturn inserted;\n }\n \n-static int load_one_loose_object_map(struct repository *repo, struct odb_source *source)\n+static int load_one_loose_object_map(struct repository *repo, struct odb_source_loose *loose)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tstruct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;\n \tFILE *fp;\n \n-\tif (!files->loose->map)\n-\t\tloose_object_map_init(&files->loose->map);\n-\tif (!files->loose->cache) {\n-\t\tALLOC_ARRAY(files->loose->cache, 1);\n-\t\toidtree_init(files->loose->cache);\n+\tif (!loose->map)\n+\t\tloose_object_map_init(&loose->map);\n+\tif (!loose->cache) {\n+\t\tALLOC_ARRAY(loose->cache, 1);\n+\t\toidtree_init(loose->cache);\n \t}\n \n-\tinsert_loose_map(source, repo->hash_algo->empty_tree, repo->compat_hash_algo->empty_tree);\n-\tinsert_loose_map(source, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob);\n-\tinsert_loose_map(source, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid);\n+\tinsert_loose_map(loose, repo->hash_algo->empty_tree, repo->compat_hash_algo->empty_tree);\n+\tinsert_loose_map(loose, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob);\n+\tinsert_loose_map(loose, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid);\n \n \trepo_common_path_replace(repo, &path, \"objects/loose-object-idx\");\n \tfp = fopen(path.buf, \"rb\");\n@@ -97,7 +95,7 @@ static int load_one_loose_object_map(struct repository *repo, struct odb_source\n \t\t    parse_oid_hex_algop(p, &compat_oid, &p, repo->compat_hash_algo) ||\n \t\t    p != buf.buf + buf.len)\n \t\t\tgoto err;\n-\t\tinsert_loose_map(source, &oid, &compat_oid);\n+\t\tinsert_loose_map(loose, &oid, &compat_oid);\n \t}\n \n \tstrbuf_release(&buf);\n@@ -119,7 +117,8 @@ int repo_read_loose_object_map(struct repository *repo)\n \todb_prepare_alternates(repo->objects);\n \n \tfor (source = repo->objects->sources; source; source = source->next) {\n-\t\tif (load_one_loose_object_map(repo, source) < 0) {\n+\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n+\t\tif (load_one_loose_object_map(repo, files->loose) < 0) {\n \t\t\treturn -1;\n \t\t}\n \t}\n@@ -171,7 +170,7 @@ int repo_write_loose_object_map(struct repository *repo)\n \treturn -1;\n }\n \n-static int write_one_object(struct odb_source *source,\n+static int write_one_object(struct odb_source_loose *loose,\n \t\t\t    const struct object_id *oid,\n \t\t\t    const struct object_id *compat_oid)\n {\n@@ -180,7 +179,7 @@ static int write_one_object(struct odb_source *source,\n \tstruct stat st;\n \tstruct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;\n \n-\tstrbuf_addf(&path, \"%s/loose-object-idx\", source->path);\n+\tstrbuf_addf(&path, \"%s/loose-object-idx\", loose->base.path);\n \thold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);\n \n \tfd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666);\n@@ -196,7 +195,7 @@ static int write_one_object(struct odb_source *source,\n \t\tgoto errout;\n \tif (close(fd))\n \t\tgoto errout;\n-\tadjust_shared_perm(source->odb->repo, path.buf);\n+\tadjust_shared_perm(loose->base.odb->repo, path.buf);\n \trollback_lock_file(&lock);\n \tstrbuf_release(&buf);\n \tstrbuf_release(&path);\n@@ -210,18 +209,18 @@ static int write_one_object(struct odb_source *source,\n \treturn -1;\n }\n \n-int repo_add_loose_object_map(struct odb_source *source,\n+int repo_add_loose_object_map(struct odb_source_loose *loose,\n \t\t\t      const struct object_id *oid,\n \t\t\t      const struct object_id *compat_oid)\n {\n \tint inserted = 0;\n \n-\tif (!should_use_loose_object_map(source->odb->repo))\n+\tif (!should_use_loose_object_map(loose->base.odb->repo))\n \t\treturn 0;\n \n-\tinserted = insert_loose_map(source, oid, compat_oid);\n+\tinserted = insert_loose_map(loose, oid, compat_oid);\n \tif (inserted)\n-\t\treturn write_one_object(source, oid, compat_oid);\n+\t\treturn write_one_object(loose, oid, compat_oid);\n \treturn 0;\n }\n \ndiff --git a/loose.h b/loose.h\nindex 6af1702973..6c9b3f4571 100644\n--- a/loose.h\n+++ b/loose.h\n@@ -4,7 +4,7 @@\n #include \"khash.h\"\n \n struct repository;\n-struct odb_source;\n+struct odb_source_loose;\n \n struct loose_object_map {\n \tkh_oid_map_t *to_compat;\n@@ -17,7 +17,7 @@ int repo_loose_object_map_oid(struct repository *repo,\n \t\t\t      const struct object_id *src,\n \t\t\t      const struct git_hash_algo *dest_algo,\n \t\t\t      struct object_id *dest);\n-int repo_add_loose_object_map(struct odb_source *source,\n+int repo_add_loose_object_map(struct odb_source_loose *loose,\n \t\t\t      const struct object_id *oid,\n \t\t\t      const struct object_id *compat_oid);\n int repo_read_loose_object_map(struct repository *repo);\ndiff --git a/object-file.c b/object-file.c\nindex 0689a4e67b..fe24f00d1b 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -810,6 +810,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t\t\t\t  struct odb_write_stream *in_stream, size_t len,\n \t\t\t\t  struct object_id *oid)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n \tstruct object_id compat_oid;\n \tint fd, ret, err = 0, flush = 0;\n@@ -918,7 +919,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \terr = finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf,\n \t\t\t\t\t FOF_SKIP_COLLISION_CHECK);\n \tif (!err && compat)\n-\t\terr = repo_add_loose_object_map(source, oid, &compat_oid);\n+\t\terr = repo_add_loose_object_map(files->loose, oid, &compat_oid);\n cleanup:\n \tstrbuf_release(&tmp_file);\n \tstrbuf_release(&filename);\n@@ -931,6 +932,7 @@ int odb_source_loose_write_object(struct odb_source *source,\n \t\t\t\t  struct object_id *compat_oid_in,\n \t\t\t\t  enum odb_write_object_flags flags)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tconst struct git_hash_algo *algo = source->odb->repo->hash_algo;\n \tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n \tstruct object_id compat_oid;\n@@ -962,13 +964,14 @@ int odb_source_loose_write_object(struct odb_source *source,\n \tif (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))\n \t\treturn -1;\n \tif (compat)\n-\t\treturn repo_add_loose_object_map(source, oid, &compat_oid);\n+\t\treturn repo_add_loose_object_map(files->loose, oid, &compat_oid);\n \treturn 0;\n }\n \n int force_object_loose(struct odb_source *source,\n \t\t       const struct object_id *oid, time_t mtime)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n \tvoid *buf;\n \tunsigned long len;\n@@ -998,7 +1001,7 @@ int force_object_loose(struct odb_source *source,\n \thdrlen = format_object_header(hdr, sizeof(hdr), type, len);\n \tret = write_loose_object(source, oid, hdr, hdrlen, buf, len, mtime, 0);\n \tif (!ret && compat)\n-\t\tret = repo_add_loose_object_map(source, oid, &compat_oid);\n+\t\tret = repo_add_loose_object_map(files->loose, oid, &compat_oid);\n \tfree(buf);\n \n \treturn ret;\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543801","messageId":"20260521-b4-pks-odb-source-loose-v1-14-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 14/18] odb/source-loose: wire up `write_object()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:34Z","receivedAt":"2026-05-21T08:23:07Z","isPatch":true,"body":"Move `odb_source_loose_write_object()` from \"object-file.c\" into\n\"odb/source-loose.c\" and wire it up as the `write_object()` callback of\nthe loose source.\n\nAs in preceding commits, this requires us to expose a couple of generic\nfunctions from \"object-file.c\" as they are used in both subsystems now.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 58 ++++++++----------------------------------------------\n object-file.h      | 14 +++++++------\n odb/source-files.c |  5 +++--\n odb/source-loose.c | 44 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 63 insertions(+), 58 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex fe24f00d1b..7bb5b31bca 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -326,10 +326,10 @@ static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_c\n \tgit_hash_final_oid(oid, c);\n }\n \n-static void write_object_file_prepare(const struct git_hash_algo *algo,\n-\t\t\t\t      const void *buf, unsigned long len,\n-\t\t\t\t      enum object_type type, struct object_id *oid,\n-\t\t\t\t      char *hdr, int *hdrlen)\n+void write_object_file_prepare(const struct git_hash_algo *algo,\n+\t\t\t       const void *buf, unsigned long len,\n+\t\t\t       enum object_type type, struct object_id *oid,\n+\t\t\t       char *hdr, int *hdrlen)\n {\n \tstruct git_hash_ctx c;\n \n@@ -746,10 +746,10 @@ static int end_loose_object_common(struct odb_source *source,\n \treturn Z_OK;\n }\n \n-static int write_loose_object(struct odb_source *source,\n-\t\t\t      const struct object_id *oid, char *hdr,\n-\t\t\t      int hdrlen, const void *buf, unsigned long len,\n-\t\t\t      time_t mtime, unsigned flags)\n+int write_loose_object(struct odb_source *source,\n+\t\t       const struct object_id *oid, char *hdr,\n+\t\t       int hdrlen, const void *buf, unsigned long len,\n+\t\t       time_t mtime, unsigned flags)\n {\n \tint fd, ret;\n \tunsigned char compressed[4096];\n@@ -926,48 +926,6 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \treturn err;\n }\n \n-int odb_source_loose_write_object(struct odb_source *source,\n-\t\t\t\t  const void *buf, unsigned long len,\n-\t\t\t\t  enum object_type type, struct object_id *oid,\n-\t\t\t\t  struct object_id *compat_oid_in,\n-\t\t\t\t  enum odb_write_object_flags flags)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tconst struct git_hash_algo *algo = source->odb->repo->hash_algo;\n-\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n-\tstruct object_id compat_oid;\n-\tchar hdr[MAX_HEADER_LEN];\n-\tint hdrlen = sizeof(hdr);\n-\n-\t/* Generate compat_oid */\n-\tif (compat) {\n-\t\tif (compat_oid_in)\n-\t\t\toidcpy(&compat_oid, compat_oid_in);\n-\t\telse if (type == OBJ_BLOB)\n-\t\t\thash_object_file(compat, buf, len, type, &compat_oid);\n-\t\telse {\n-\t\t\tstruct strbuf converted = STRBUF_INIT;\n-\t\t\tconvert_object_file(source->odb->repo, &converted, algo, compat,\n-\t\t\t\t\t    buf, len, type, 0);\n-\t\t\thash_object_file(compat, converted.buf, converted.len,\n-\t\t\t\t\t type, &compat_oid);\n-\t\t\tstrbuf_release(&converted);\n-\t\t}\n-\t}\n-\n-\t/* Normally if we have it in the pack then we do not bother writing\n-\t * it out into .git/objects/??/?{38} file.\n-\t */\n-\twrite_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);\n-\tif (odb_freshen_object(source->odb, oid))\n-\t\treturn 0;\n-\tif (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))\n-\t\treturn -1;\n-\tif (compat)\n-\t\treturn repo_add_loose_object_map(files->loose, oid, &compat_oid);\n-\treturn 0;\n-}\n-\n int force_object_loose(struct odb_source *source,\n \t\t       const struct object_id *oid, time_t mtime)\n {\ndiff --git a/object-file.h b/object-file.h\nindex 1d90df9d98..2b32592de1 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -23,12 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa\n struct object_info;\n struct odb_source;\n \n-int odb_source_loose_write_object(struct odb_source *source,\n-\t\t\t\t  const void *buf, unsigned long len,\n-\t\t\t\t  enum object_type type, struct object_id *oid,\n-\t\t\t\t  struct object_id *compat_oid_in,\n-\t\t\t\t  enum odb_write_object_flags flags);\n-\n int odb_source_loose_write_stream(struct odb_source *source,\n \t\t\t\t  struct odb_write_stream *stream, size_t len,\n \t\t\t\t  struct object_id *oid);\n@@ -129,6 +123,14 @@ int finalize_object_file_flags(struct repository *repo,\n void hash_object_file(const struct git_hash_algo *algo, const void *buf,\n \t\t      unsigned long len, enum object_type type,\n \t\t      struct object_id *oid);\n+void write_object_file_prepare(const struct git_hash_algo *algo,\n+\t\t\t       const void *buf, unsigned long len,\n+\t\t\t       enum object_type type, struct object_id *oid,\n+\t\t\t       char *hdr, int *hdrlen);\n+int write_loose_object(struct odb_source *source,\n+\t\t       const struct object_id *oid, char *hdr,\n+\t\t       int hdrlen, const void *buf, unsigned long len,\n+\t\t       time_t mtime, unsigned flags);\n \n /* Helper to check and \"touch\" a file */\n int check_and_freshen_file(const char *fn, int freshen);\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex ef548e6fe6..52ba04237a 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -164,8 +164,9 @@ static int odb_source_files_write_object(struct odb_source *source,\n \t\t\t\t\t struct object_id *compat_oid,\n \t\t\t\t\t enum odb_write_object_flags flags)\n {\n-\treturn odb_source_loose_write_object(source, buf, len, type,\n-\t\t\t\t\t     oid, compat_oid, flags);\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n+\treturn odb_source_write_object(&files->loose->base, buf, len, type,\n+\t\t\t\t       oid, compat_oid, flags);\n }\n \n static int odb_source_files_write_object_stream(struct odb_source *source,\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex e519365d23..c91018109e 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -5,6 +5,7 @@\n #include \"hex.h\"\n #include \"loose.h\"\n #include \"object-file.h\"\n+#include \"object-file-convert.h\"\n #include \"odb.h\"\n #include \"odb/source-files.h\"\n #include \"odb/source-loose.h\"\n@@ -588,6 +589,48 @@ static int odb_source_loose_freshen_object(struct odb_source *source,\n \treturn !!check_and_freshen_file(path.buf, 1);\n }\n \n+static int odb_source_loose_write_object(struct odb_source *source,\n+\t\t\t\t\t const void *buf, unsigned long len,\n+\t\t\t\t\t enum object_type type, struct object_id *oid,\n+\t\t\t\t\t struct object_id *compat_oid_in,\n+\t\t\t\t\t enum odb_write_object_flags flags)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tconst struct git_hash_algo *algo = source->odb->repo->hash_algo;\n+\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n+\tstruct object_id compat_oid;\n+\tchar hdr[MAX_HEADER_LEN];\n+\tint hdrlen = sizeof(hdr);\n+\n+\t/* Generate compat_oid */\n+\tif (compat) {\n+\t\tif (compat_oid_in)\n+\t\t\toidcpy(&compat_oid, compat_oid_in);\n+\t\telse if (type == OBJ_BLOB)\n+\t\t\thash_object_file(compat, buf, len, type, &compat_oid);\n+\t\telse {\n+\t\t\tstruct strbuf converted = STRBUF_INIT;\n+\t\t\tconvert_object_file(source->odb->repo, &converted, algo, compat,\n+\t\t\t\t\t    buf, len, type, 0);\n+\t\t\thash_object_file(compat, converted.buf, converted.len,\n+\t\t\t\t\t type, &compat_oid);\n+\t\t\tstrbuf_release(&converted);\n+\t\t}\n+\t}\n+\n+\t/* Normally if we have it in the pack then we do not bother writing\n+\t * it out into .git/objects/??/?{38} file.\n+\t */\n+\twrite_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);\n+\tif (odb_freshen_object(source->odb, oid))\n+\t\treturn 0;\n+\tif (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))\n+\t\treturn -1;\n+\tif (compat)\n+\t\treturn repo_add_loose_object_map(loose, oid, &compat_oid);\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@@ -647,6 +690,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;\n \tloose->base.count_objects = odb_source_loose_count_objects;\n \tloose->base.freshen_object = odb_source_loose_freshen_object;\n+\tloose->base.write_object = odb_source_loose_write_object;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543802","messageId":"20260521-b4-pks-odb-source-loose-v1-15-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 15/18] object-file: refactor writing objects to use loose source","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:35Z","receivedAt":"2026-05-21T08:23:10Z","isPatch":true,"body":"The \"object-file\" subsystem still hosts the majority of logic used to\nwrite loose objects. Eventually, we'll want to move this logic into\n\"odb/source-loose.c\", but this isn't yet easily possible because a lot\nof the writing logic is still being shared with `force_object_loose()`.\n\nWe will eventually detangle this logic so that we can indeed move all of\nit into the \"loose\" source. Meanwhile though, refactor the code so that\nit operates on a `struct odb_source_loose` directly to already make the\ndependency explicit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n http-walker.c      |  3 ++-\n http.c             |  6 +++--\n object-file.c      | 75 +++++++++++++++++++++++++++---------------------------\n object-file.h      |  6 ++---\n odb/source-files.c |  3 ++-\n odb/source-loose.c |  9 ++++---\n 6 files changed, 53 insertions(+), 49 deletions(-)\n\ndiff --git a/http-walker.c b/http-walker.c\nindex 1b6d496548..435a726540 100644\n--- a/http-walker.c\n+++ b/http-walker.c\n@@ -539,8 +539,9 @@ 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 strbuf buf = STRBUF_INIT;\n-\t\todb_loose_path(the_repository->objects->sources, &buf, &req->oid);\n+\t\todb_loose_path(files->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 ea9b16861b..3fcc012233 100644\n--- a/http.c\n+++ b/http.c\n@@ -2826,6 +2826,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 \tchar *hex = oid_to_hex(oid);\n \tstruct strbuf filename = STRBUF_INIT;\n \tstruct strbuf prevfile = STRBUF_INIT;\n@@ -2840,7 +2841,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(the_repository->objects->sources, &filename, oid);\n+\todb_loose_path(files->loose, &filename, oid);\n \tstrbuf_addf(&freq->tmpfile, \"%s.temp\", filename.buf);\n \n \tstrbuf_addf(&prevfile, \"%s.prev\", filename.buf);\n@@ -2966,6 +2967,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 stat st;\n \tstruct strbuf filename = STRBUF_INIT;\n \n@@ -2992,7 +2994,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(the_repository->objects->sources, &filename, &freq->oid);\n+\todb_loose_path(files->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/object-file.c b/object-file.c\nindex 7bb5b31bca..bce941874e 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -54,14 +54,14 @@ static void fill_loose_path(struct strbuf *buf,\n \t}\n }\n \n-const char *odb_loose_path(struct odb_source *source,\n+const char *odb_loose_path(struct odb_source_loose *loose,\n \t\t\t   struct strbuf *buf,\n \t\t\t   const struct object_id *oid)\n {\n \tstrbuf_reset(buf);\n-\tstrbuf_addstr(buf, source->path);\n+\tstrbuf_addstr(buf, loose->base.path);\n \tstrbuf_addch(buf, '/');\n-\tfill_loose_path(buf, oid, source->odb->repo->hash_algo);\n+\tfill_loose_path(buf, oid, loose->base.odb->repo->hash_algo);\n \treturn buf->buf;\n }\n \n@@ -575,14 +575,14 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac\n }\n \n /* Finalize a file on disk, and close it. */\n-static void close_loose_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 (source->will_destroy)\n+\tif (loose->base.will_destroy)\n \t\tgoto out;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tfsync_loose_object_transaction(source->odb->transaction, fd, filename);\n+\t\tfsync_loose_object_transaction(loose->base.odb->transaction, fd, filename);\n \telse if (fsync_object_files > 0)\n \t\tfsync_or_die(fd, filename);\n \telse\n@@ -651,7 +651,7 @@ static int create_tmpfile(struct repository *repo,\n  * Returns a \"fd\", which should later be provided to\n  * end_loose_object_common().\n  */\n-static int start_loose_object_common(struct odb_source *source,\n+static int start_loose_object_common(struct odb_source_loose *loose,\n \t\t\t\t     struct strbuf *tmp_file,\n \t\t\t\t     const char *filename, unsigned flags,\n \t\t\t\t     git_zstream *stream,\n@@ -659,18 +659,18 @@ static int start_loose_object_common(struct odb_source *source,\n \t\t\t\t     struct git_hash_ctx *c, struct git_hash_ctx *compat_c,\n \t\t\t\t     char *hdr, int hdrlen)\n {\n-\tconst struct git_hash_algo *algo = source->odb->repo->hash_algo;\n-\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n+\tconst struct git_hash_algo *algo = loose->base.odb->repo->hash_algo;\n+\tconst struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;\n \tint fd;\n \n-\tfd = create_tmpfile(source->odb->repo, tmp_file, filename);\n+\tfd = create_tmpfile(loose->base.odb->repo, tmp_file, filename);\n \tif (fd < 0) {\n \t\tif (flags & ODB_WRITE_OBJECT_SILENT)\n \t\t\treturn -1;\n \t\telse if (errno == EACCES)\n \t\t\treturn error(_(\"insufficient permission for adding \"\n \t\t\t\t       \"an object to repository database %s\"),\n-\t\t\t\t     source->path);\n+\t\t\t\t     loose->base.path);\n \t\telse\n \t\t\treturn error_errno(\n \t\t\t\t_(\"unable to create temporary file\"));\n@@ -700,14 +700,14 @@ static int start_loose_object_common(struct odb_source *source,\n  * Common steps for the inner git_deflate() loop for writing loose\n  * objects. Returns what git_deflate() returns.\n  */\n-static int write_loose_object_common(struct odb_source *source,\n+static int write_loose_object_common(struct odb_source_loose *loose,\n \t\t\t\t     struct git_hash_ctx *c, struct git_hash_ctx *compat_c,\n \t\t\t\t     git_zstream *stream, const int flush,\n \t\t\t\t     unsigned char *in0, const int fd,\n \t\t\t\t     unsigned char *compressed,\n \t\t\t\t     const size_t compressed_len)\n {\n-\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n+\tconst struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;\n \tint ret;\n \n \tret = git_deflate(stream, flush ? Z_FINISH : 0);\n@@ -728,12 +728,12 @@ static int write_loose_object_common(struct odb_source *source,\n  * - End the compression of zlib stream.\n  * - Get the calculated oid to \"oid\".\n  */\n-static int end_loose_object_common(struct odb_source *source,\n+static int end_loose_object_common(struct odb_source_loose *loose,\n \t\t\t\t   struct git_hash_ctx *c, struct git_hash_ctx *compat_c,\n \t\t\t\t   git_zstream *stream, struct object_id *oid,\n \t\t\t\t   struct object_id *compat_oid)\n {\n-\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n+\tconst struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;\n \tint ret;\n \n \tret = git_deflate_end_gently(stream);\n@@ -746,7 +746,7 @@ static int end_loose_object_common(struct odb_source *source,\n \treturn Z_OK;\n }\n \n-int write_loose_object(struct odb_source *source,\n+int write_loose_object(struct odb_source_loose *loose,\n \t\t       const struct object_id *oid, char *hdr,\n \t\t       int hdrlen, const void *buf, unsigned long len,\n \t\t       time_t mtime, unsigned flags)\n@@ -760,11 +760,11 @@ int write_loose_object(struct odb_source *source,\n \tstatic struct strbuf filename = STRBUF_INIT;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(source->odb->transaction);\n+\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\n \n-\todb_loose_path(source, &filename, oid);\n+\todb_loose_path(loose, &filename, oid);\n \n-\tfd = start_loose_object_common(source, &tmp_file, filename.buf, flags,\n+\tfd = start_loose_object_common(loose, &tmp_file, filename.buf, flags,\n \t\t\t\t       &stream, compressed, sizeof(compressed),\n \t\t\t\t       &c, NULL, hdr, hdrlen);\n \tif (fd < 0)\n@@ -776,14 +776,14 @@ int write_loose_object(struct odb_source *source,\n \tdo {\n \t\tunsigned char *in0 = stream.next_in;\n \n-\t\tret = write_loose_object_common(source, &c, NULL, &stream, 1, in0, fd,\n+\t\tret = write_loose_object_common(loose, &c, NULL, &stream, 1, in0, fd,\n \t\t\t\t\t\tcompressed, sizeof(compressed));\n \t} while (ret == Z_OK);\n \n \tif (ret != Z_STREAM_END)\n \t\tdie(_(\"unable to deflate new object %s (%d)\"), oid_to_hex(oid),\n \t\t    ret);\n-\tret = end_loose_object_common(source, &c, NULL, &stream, &parano_oid, NULL);\n+\tret = end_loose_object_common(loose, &c, NULL, &stream, &parano_oid, NULL);\n \tif (ret != Z_OK)\n \t\tdie(_(\"deflateEnd on object %s failed (%d)\"), oid_to_hex(oid),\n \t\t    ret);\n@@ -791,7 +791,7 @@ int write_loose_object(struct odb_source *source,\n \t\tdie(_(\"confused by unstable object source data for %s\"),\n \t\t    oid_to_hex(oid));\n \n-\tclose_loose_object(source, fd, tmp_file.buf);\n+\tclose_loose_object(loose, fd, tmp_file.buf);\n \n \tif (mtime) {\n \t\tstruct utimbuf utb;\n@@ -802,16 +802,15 @@ int write_loose_object(struct odb_source *source,\n \t\t\twarning_errno(_(\"failed utime() on %s\"), tmp_file.buf);\n \t}\n \n-\treturn finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf,\n+\treturn finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,\n \t\t\t\t\t  FOF_SKIP_COLLISION_CHECK);\n }\n \n-int odb_source_loose_write_stream(struct odb_source *source,\n+int odb_source_loose_write_stream(struct odb_source_loose *loose,\n \t\t\t\t  struct odb_write_stream *in_stream, size_t len,\n \t\t\t\t  struct object_id *oid)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n+\tconst struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;\n \tstruct object_id compat_oid;\n \tint fd, ret, err = 0, flush = 0;\n \tunsigned char compressed[4096];\n@@ -825,10 +824,10 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \tint hdrlen;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(source->odb->transaction);\n+\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\n \n \t/* Since oid is not determined, save tmp file to odb path. */\n-\tstrbuf_addf(&filename, \"%s/\", source->path);\n+\tstrbuf_addf(&filename, \"%s/\", loose->base.path);\n \thdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len);\n \n \t/*\n@@ -839,7 +838,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t *  - Setup zlib stream for compression.\n \t *  - Start to feed header to zlib stream.\n \t */\n-\tfd = start_loose_object_common(source, &tmp_file, filename.buf, 0,\n+\tfd = start_loose_object_common(loose, &tmp_file, filename.buf, 0,\n \t\t\t\t       &stream, compressed, sizeof(compressed),\n \t\t\t\t       &c, &compat_c, hdr, hdrlen);\n \tif (fd < 0) {\n@@ -867,7 +866,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t\t\tif (in_stream->is_finished)\n \t\t\t\tflush = 1;\n \t\t}\n-\t\tret = write_loose_object_common(source, &c, &compat_c, &stream, flush, in0, fd,\n+\t\tret = write_loose_object_common(loose, &c, &compat_c, &stream, flush, in0, fd,\n \t\t\t\t\t\tcompressed, sizeof(compressed));\n \t\t/*\n \t\t * Unlike write_loose_object(), we do not have the entire\n@@ -890,16 +889,16 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t */\n \tif (ret != Z_STREAM_END)\n \t\tdie(_(\"unable to stream deflate new object (%d)\"), ret);\n-\tret = end_loose_object_common(source, &c, &compat_c, &stream, oid, &compat_oid);\n+\tret = end_loose_object_common(loose, &c, &compat_c, &stream, oid, &compat_oid);\n \tif (ret != Z_OK)\n \t\tdie(_(\"deflateEnd on stream object failed (%d)\"), ret);\n-\tclose_loose_object(source, fd, tmp_file.buf);\n+\tclose_loose_object(loose, fd, tmp_file.buf);\n \n-\tif (odb_freshen_object(source->odb, oid)) {\n+\tif (odb_freshen_object(loose->base.odb, oid)) {\n \t\tunlink_or_warn(tmp_file.buf);\n \t\tgoto cleanup;\n \t}\n-\todb_loose_path(source, &filename, oid);\n+\todb_loose_path(loose, &filename, oid);\n \n \t/* We finally know the object path, and create the missing dir. */\n \tdirlen = directory_size(filename.buf);\n@@ -907,7 +906,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t\tstruct strbuf dir = STRBUF_INIT;\n \t\tstrbuf_add(&dir, filename.buf, dirlen);\n \n-\t\tif (safe_create_dir_in_gitdir(source->odb->repo, dir.buf) &&\n+\t\tif (safe_create_dir_in_gitdir(loose->base.odb->repo, dir.buf) &&\n \t\t    errno != EEXIST) {\n \t\t\terr = error_errno(_(\"unable to create directory %s\"), dir.buf);\n \t\t\tstrbuf_release(&dir);\n@@ -916,10 +915,10 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t\tstrbuf_release(&dir);\n \t}\n \n-\terr = finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf,\n+\terr = finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,\n \t\t\t\t\t FOF_SKIP_COLLISION_CHECK);\n \tif (!err && compat)\n-\t\terr = repo_add_loose_object_map(files->loose, oid, &compat_oid);\n+\t\terr = repo_add_loose_object_map(loose, oid, &compat_oid);\n cleanup:\n \tstrbuf_release(&tmp_file);\n \tstrbuf_release(&filename);\n@@ -957,7 +956,7 @@ int force_object_loose(struct odb_source *source,\n \t\t\t\t     oid_to_hex(oid), compat->name);\n \t}\n \thdrlen = format_object_header(hdr, sizeof(hdr), type, len);\n-\tret = write_loose_object(source, oid, hdr, hdrlen, buf, len, mtime, 0);\n+\tret = write_loose_object(files->loose, oid, hdr, hdrlen, buf, len, mtime, 0);\n \tif (!ret && compat)\n \t\tret = repo_add_loose_object_map(files->loose, oid, &compat_oid);\n \tfree(buf);\ndiff --git a/object-file.h b/object-file.h\nindex 2b32592de1..d30f1b10b2 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -23,7 +23,7 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa\n struct object_info;\n struct odb_source;\n \n-int odb_source_loose_write_stream(struct odb_source *source,\n+int odb_source_loose_write_stream(struct odb_source_loose *loose,\n \t\t\t\t  struct odb_write_stream *stream, size_t len,\n \t\t\t\t  struct object_id *oid);\n \n@@ -31,7 +31,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n  * Put in `buf` the name of the file in the local object database that\n  * would be used to store a loose object with the specified oid.\n  */\n-const char *odb_loose_path(struct odb_source *source,\n+const char *odb_loose_path(struct odb_source_loose *source,\n \t\t\t   struct strbuf *buf,\n \t\t\t   const struct object_id *oid);\n \n@@ -127,7 +127,7 @@ void write_object_file_prepare(const struct git_hash_algo *algo,\n \t\t\t       const void *buf, unsigned long len,\n \t\t\t       enum object_type type, struct object_id *oid,\n \t\t\t       char *hdr, int *hdrlen);\n-int write_loose_object(struct odb_source *source,\n+int write_loose_object(struct odb_source_loose *loose,\n \t\t       const struct object_id *oid, char *hdr,\n \t\t       int hdrlen, const void *buf, unsigned long len,\n \t\t       time_t mtime, unsigned flags);\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 52ba04237a..2ba1def776 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -174,7 +174,8 @@ static int odb_source_files_write_object_stream(struct odb_source *source,\n \t\t\t\t\t\tsize_t len,\n \t\t\t\t\t\tstruct object_id *oid)\n {\n-\treturn odb_source_loose_write_stream(source, stream, len, oid);\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n+\treturn odb_source_loose_write_stream(files->loose, stream, len, oid);\n }\n \n static int odb_source_files_begin_transaction(struct odb_source *source,\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex c91018109e..da8a60dba1 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -220,7 +220,7 @@ static int odb_source_loose_read_object_info(struct odb_source *source,\n \tif (flags & OBJECT_INFO_SECOND_READ)\n \t\treturn -1;\n \n-\todb_loose_path(source, &buf, oid);\n+\todb_loose_path(loose, &buf, oid);\n \treturn read_object_info_from_path(loose, buf.buf, oid, oi, flags);\n }\n \n@@ -238,7 +238,7 @@ static int open_loose_object(struct odb_source_loose *loose,\n \tstatic struct strbuf buf = STRBUF_INIT;\n \tint fd;\n \n-\t*path = odb_loose_path(&loose->base, &buf, oid);\n+\t*path = odb_loose_path(loose, &buf, oid);\n \tfd = git_open(*path);\n \tif (fd >= 0)\n \t\treturn fd;\n@@ -584,8 +584,9 @@ static int odb_source_loose_count_objects(struct odb_source *source,\n static int odb_source_loose_freshen_object(struct odb_source *source,\n \t\t\t\t\t   const struct object_id *oid)\n {\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n \tstatic struct strbuf path = STRBUF_INIT;\n-\todb_loose_path(source, &path, oid);\n+\todb_loose_path(loose, &path, oid);\n \treturn !!check_and_freshen_file(path.buf, 1);\n }\n \n@@ -624,7 +625,7 @@ static int odb_source_loose_write_object(struct odb_source *source,\n \twrite_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);\n \tif (odb_freshen_object(source->odb, oid))\n \t\treturn 0;\n-\tif (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))\n+\tif (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags))\n \t\treturn -1;\n \tif (compat)\n \t\treturn repo_add_loose_object_map(loose, oid, &compat_oid);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543803","messageId":"20260521-b4-pks-odb-source-loose-v1-16-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 16/18] odb/source-loose: wire up `write_object_stream()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:36Z","receivedAt":"2026-05-21T08:23:13Z","isPatch":true,"body":"Wire up the `write_object_stream()` callback.\n\nNote that we don't move the implementation into \"odb/source-loose.c\".\nThis is because most of the logic to write loose objects is still\ncontained in \"object-file.c\", and detangling that requires us to do some\nrefactorings as explained in the preceding commit. So for now, the\nimplementation of writing an object stream is still located in\n\"object-file.c\".\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.h      | 12 +++++++++++-\n odb/source-files.c |  3 ++-\n odb/source-loose.c | 14 ++++++++++++++\n 3 files changed, 27 insertions(+), 2 deletions(-)\n\ndiff --git a/object-file.h b/object-file.h\nindex d30f1b10b2..b864351372 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -23,7 +23,17 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa\n struct object_info;\n struct odb_source;\n \n-int odb_source_loose_write_stream(struct odb_source_loose *loose,\n+/*\n+ * Write the given stream into the loose object source. The only difference to\n+ * the generic implementation of this function is that we don't perform an\n+ * object existence check here.\n+ *\n+ * TODO: We should stop exposing this function altogether and move it into\n+ * \"odb/source-loose.c\". This requires a couple of refactorings though to make\n+ * `force_object_loose()` generic and is thus postponed to a later point in\n+ * time.\n+ */\n+int odb_source_loose_write_stream(struct odb_source_loose *source,\n \t\t\t\t  struct odb_write_stream *stream, size_t len,\n \t\t\t\t  struct object_id *oid);\n \ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 2ba1def776..83f8066c67 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -7,6 +7,7 @@\n #include \"odb.h\"\n #include \"odb/source.h\"\n #include \"odb/source-files.h\"\n+#include \"odb/source-loose.h\"\n #include \"packfile.h\"\n #include \"strbuf.h\"\n #include \"write-or-die.h\"\n@@ -175,7 +176,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_loose_write_stream(files->loose, stream, len, oid);\n+\treturn odb_source_write_object_stream(&files->loose->base, stream, len, oid);\n }\n \n static int odb_source_files_begin_transaction(struct odb_source *source,\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex da8a60dba1..e52fc289a2 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -632,6 +632,19 @@ static int odb_source_loose_write_object(struct odb_source *source,\n \treturn 0;\n }\n \n+static int odb_source_loose_write_object_stream(struct odb_source *source,\n+\t\t\t\t\t\tstruct odb_write_stream *in_stream,\n+\t\t\t\t\t\tsize_t len,\n+\t\t\t\t\t\tstruct object_id *oid)\n+{\n+\t/*\n+\t * TODO: the implementation should be moved here, see the comment on\n+\t * the called function in \"object-file.h\".\n+\t */\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\treturn odb_source_loose_write_stream(loose, in_stream, len, oid);\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -692,6 +705,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.count_objects = odb_source_loose_count_objects;\n \tloose->base.freshen_object = odb_source_loose_freshen_object;\n \tloose->base.write_object = odb_source_loose_write_object;\n+\tloose->base.write_object_stream = odb_source_loose_write_object_stream;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543804","messageId":"20260521-b4-pks-odb-source-loose-v1-17-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 17/18] odb/source-loose: stub out remaining callbacks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:37Z","receivedAt":"2026-05-21T08:23:16Z","isPatch":true,"body":"Stub out remaining callback functions for the \"loose\" backend.\n\nNote that we also stub out transactions for loose objects. In fact, we\nalready have the infrastructure in place for those, and we could in\ntheory implement those, as well. But there are separate efforts ongoing\nto polish up transactional interfaces, and doing so now would likely\nresult in some messiness. This omission will thus be worked on in a\nsubsequent patch series, once the dust has settled.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb/source-loose.c | 22 ++++++++++++++++++++++\n 1 file changed, 22 insertions(+)\n\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex e52fc289a2..e174941318 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -645,6 +645,25 @@ static int odb_source_loose_write_object_stream(struct odb_source *source,\n \treturn odb_source_loose_write_stream(loose, in_stream, len, oid);\n }\n \n+static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED,\n+\t\t\t\t\t      struct odb_transaction **out UNUSED)\n+{\n+\t/* TODO: this is a known omission that we'll want to address eventually. */\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 int odb_source_loose_write_alternate(struct odb_source *source UNUSED,\n+\t\t\t\t\t    const char *alternate UNUSED)\n+{\n+\treturn error(\"loose source does not support alternates\");\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -706,6 +725,9 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.freshen_object = odb_source_loose_freshen_object;\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+\tloose->base.write_alternate = odb_source_loose_write_alternate;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543805","messageId":"20260521-b4-pks-odb-source-loose-v1-18-6553b399be2d@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH 18/18] odb/source-loose: drop pointer to the \"files\" source","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-21T08:22:38Z","receivedAt":"2026-05-21T08:23:18Z","isPatch":true,"body":"Now that all callbacks of the loose source operate on `struct\nodb_source_loose` directly we no longer have to reach into the \"files\"\nsource at all.\n\nDrop this field and update `odb_source_loose_new()` to instead accept\nall parameters required to initialize itself. This ensures that the\n\"loose\" backend is a fully standalone source.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb/source-files.c | 2 +-\n odb/source-loose.c | 8 ++++----\n odb/source-loose.h | 7 ++++---\n 3 files changed, 9 insertions(+), 8 deletions(-)\n\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 83f8066c67..5bdd042922 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -268,7 +268,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(files);\n+\tfiles->loose = odb_source_loose_new(odb, path, local);\n \tfiles->packed = packfile_store_new(&files->base);\n \n \tfiles->base.free = odb_source_files_free;\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex e174941318..7d7ea2fb84 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -705,14 +705,14 @@ static void odb_source_loose_free(struct odb_source *source)\n \tfree(loose);\n }\n \n-struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n+struct odb_source_loose *odb_source_loose_new(struct object_database *odb,\n+\t\t\t\t\t      const char *path,\n+\t\t\t\t\t      bool local)\n {\n \tstruct odb_source_loose *loose;\n \n \tCALLOC_ARRAY(loose, 1);\n-\todb_source_init(&loose->base, files->base.odb, ODB_SOURCE_LOOSE,\n-\t\t\tfiles->base.path, files->base.local);\n-\tloose->files = files;\n+\todb_source_init(&loose->base, odb, ODB_SOURCE_LOOSE, path, local);\n \n \tloose->base.free = odb_source_loose_free;\n \tloose->base.close = odb_source_loose_close;\ndiff --git a/odb/source-loose.h b/odb/source-loose.h\nindex 825e703072..fb75e3bbff 100644\n--- a/odb/source-loose.h\n+++ b/odb/source-loose.h\n@@ -9,11 +9,10 @@ struct oidtree;\n \n /*\n  * An object database source that stores its objects in loose format, one\n- * file per object. This source is part of the files source.\n+ * file per object.\n  */\n struct odb_source_loose {\n \tstruct odb_source base;\n-\tstruct odb_source_files *files;\n \n \t/*\n \t * Used to store the results of readdir(3) calls when we are OK\n@@ -31,7 +30,9 @@ struct odb_source_loose {\n \tstruct loose_object_map *map;\n };\n \n-struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files);\n+struct odb_source_loose *odb_source_loose_new(struct object_database *odb,\n+\t\t\t\t\t      const char *path,\n+\t\t\t\t\t      bool local);\n \n /*\n  * Cast the given object database source to the loose backend. This will cause\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"543834","messageId":"xmqqh5o0zrsr.fsf@gitster.g","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-3-6553b399be2d@pks.im","subject":"Re: [PATCH 03/18] odb/source-loose: start converting to a proper `struct odb_source`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-21T15:49:24Z","receivedAt":"2026-05-21T15:49:27Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Start converting `struct odb_source_loose` into a proper pluggable\n> `struct odb_source` by embedding the base struct and assigning it the\n> new `ODB_SOURCE_LOOSE` type. Furthermore, wire up lifecycle management\n> of this source by implementing the `free` callback and taking ownership\n> of the chdir notifications.\n>\n> Note that the loose source is not yet functional as a standalone `struct\n> odb_source`, as it's missing all of the callback implementations. These\n> will be wired up in subsequent commits.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  object-file.c      | 17 -----------------\n>  object-file.h      |  2 --\n>  odb/source-files.c |  2 +-\n>  odb/source-loose.c | 45 +++++++++++++++++++++++++++++++++++++++++++++\n>  odb/source-loose.h | 14 ++++++++++++++\n>  odb/source.h       |  3 +++\n>  6 files changed, 63 insertions(+), 20 deletions(-)\n>\n> diff --git a/object-file.c b/object-file.c\n> index 7a1908bfc0..977d959d33 100644\n> --- a/object-file.c\n> +++ b/object-file.c\n> @@ -2041,14 +2041,6 @@ static struct oidtree *odb_source_loose_cache(struct odb_source *source,\n>  \treturn files->loose->cache;\n>  }\n>  \n> -static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n> -{\n> -\toidtree_clear(loose->cache);\n> -\tFREE_AND_NULL(loose->cache);\n> -\tmemset(&loose->subdir_seen, 0,\n> -\t       sizeof(loose->subdir_seen));\n> -}\n> -\n>  void odb_source_loose_reprepare(struct odb_source *source)\n>  {\n>  \tstruct odb_source_files *files = odb_source_files_downcast(source);\n> @@ -2205,15 +2197,6 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n>  \treturn &transaction->base;\n>  }\n>  \n> -void odb_source_loose_free(struct odb_source_loose *loose)\n> -{\n> -\tif (!loose)\n> -\t\treturn;\n> -\todb_source_loose_clear_cache(loose);\n> -\tloose_object_map_clear(&loose->map);\n> -\tfree(loose);\n> -}\n> -\n>  struct odb_loose_read_stream {\n>  \tstruct odb_read_stream base;\n>  \tgit_zstream z;\n> diff --git a/object-file.h b/object-file.h\n> index 1d8312cf7f..02c9680980 100644\n> --- a/object-file.h\n> +++ b/object-file.h\n> @@ -21,8 +21,6 @@ struct object_info;\n>  struct odb_read_stream;\n>  struct odb_source;\n>  \n> -void odb_source_loose_free(struct odb_source_loose *loose);\n> -\n>  /* Reprepare the loose source by emptying the loose object cache. */\n>  void odb_source_loose_reprepare(struct odb_source *source);\n>  \n> diff --git a/odb/source-files.c b/odb/source-files.c\n> index 185cc6903e..ccc637311b 100644\n> --- a/odb/source-files.c\n> +++ b/odb/source-files.c\n> @@ -27,7 +27,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(NULL, odb_source_files_reparent, files);\n> -\todb_source_loose_free(files->loose);\n> +\todb_source_free(&files->loose->base);\n>  \tpackfile_store_free(files->packed);\n>  \todb_source_release(&files->base);\n>  \tfree(files);\n> diff --git a/odb/source-loose.c b/odb/source-loose.c\n> index c9e7414814..92e18f5adb 100644\n> --- a/odb/source-loose.c\n> +++ b/odb/source-loose.c\n> @@ -1,10 +1,55 @@\n>  #include \"git-compat-util.h\"\n> +#include \"abspath.h\"\n> +#include \"chdir-notify.h\"\n> +#include \"loose.h\"\n> +#include \"odb.h\"\n> +#include \"odb/source-files.h\"\n>  #include \"odb/source-loose.h\"\n> +#include \"oidtree.h\"\n> +\n> +void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n> +{\n> +\toidtree_clear(loose->cache);\n> +\tFREE_AND_NULL(loose->cache);\n> +\tmemset(&loose->subdir_seen, 0,\n> +\t       sizeof(loose->subdir_seen));\n> +}\n> +\n> +static void odb_source_loose_reparent(const char *name UNUSED,\n> +\t\t\t\t      const char *old_cwd,\n> +\t\t\t\t      const char *new_cwd,\n> +\t\t\t\t      void *cb_data)\n> +{\n> +\tstruct odb_source_loose *loose = cb_data;\n> +\tchar *path = reparent_relative_path(old_cwd, new_cwd,\n> +\t\t\t\t\t    loose->base.path);\n> +\tfree(loose->base.path);\n> +\tloose->base.path = path;\n> +}\n> +\n> +static void odb_source_loose_free(struct odb_source *source)\n> +{\n> +\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n> +\todb_source_loose_clear_cache(loose);\n> +\tloose_object_map_clear(&loose->map);\n> +\tchdir_notify_unregister(NULL, odb_source_loose_reparent, loose);\n> +\todb_source_release(&loose->base);\n> +\tfree(loose);\n> +}\n>  \n>  struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n>  {\n>  \tstruct odb_source_loose *loose;\n> +\n>  \tCALLOC_ARRAY(loose, 1);\n> +\todb_source_init(&loose->base, files->base.odb, ODB_SOURCE_LOOSE,\n> +\t\t\tfiles->base.path, files->base.local);\n>  \tloose->files = files;\n> +\n> +\tloose->base.free = odb_source_loose_free;\n> +\n> +\tif (!is_absolute_path(loose->base.path))\n> +\t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n> +\n>  \treturn loose;\n>  }\n> diff --git a/odb/source-loose.h b/odb/source-loose.h\n> index bf61e767c8..441da9e418 100644\n> --- a/odb/source-loose.h\n> +++ b/odb/source-loose.h\n> @@ -12,6 +12,7 @@ struct oidtree;\n>   * file per object. This source is part of the files source.\n>   */\n>  struct odb_source_loose {\n> +\tstruct odb_source base;\n>  \tstruct odb_source_files *files;\n>  \n>  \t/*\n> @@ -32,4 +33,17 @@ struct odb_source_loose {\n>  \n>  struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files);\n>  \n> +/*\n> + * Cast the given object database source to the loose backend. This will cause\n> + * a BUG in case the source uses doesn't use this backend.\n\n\"uses doesn't use\"???\n\n> + */\n> +static inline struct odb_source_loose *odb_source_loose_downcast(struct odb_source *source)\n> +{\n> +\tif (source->type != ODB_SOURCE_LOOSE)\n> +\t\tBUG(\"trying to downcast source of type '%d' to loose\", source->type);\n> +\treturn container_of(source, struct odb_source_loose, base);\n> +}\n> +\n> +void odb_source_loose_clear_cache(struct odb_source_loose *loose);\n> +\n>  #endif\n> diff --git a/odb/source.h b/odb/source.h\n> index 0a440884e4..8bcb67787e 100644\n> --- a/odb/source.h\n> +++ b/odb/source.h\n> @@ -14,6 +14,9 @@ enum odb_source_type {\n>  \t/* The \"files\" backend that uses loose objects and packfiles. */\n>  \tODB_SOURCE_FILES,\n>  \n> +\t/* The \"loose\" backend that uses loose objects, only. */\n> +\tODB_SOURCE_LOOSE,\n> +\n>  \t/* The \"in-memory\" backend that stores objects in memory. */\n>  \tODB_SOURCE_INMEMORY,\n>  };\n"},{"id":"543844","messageId":"xmqq8q9czm8r.fsf@gitster.g","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-16-6553b399be2d@pks.im","subject":"Re: [PATCH 16/18] odb/source-loose: wire up `write_object_stream()` callback","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-21T17:49:24Z","receivedAt":"2026-05-21T17:49:27Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> -int odb_source_loose_write_stream(struct odb_source_loose *loose,\n> +/*\n> + * Write the given stream into the loose object source. The only difference to\n> + * the generic implementation of this function is that we don't perform an\n\n\"difference to\" -> \"difference from\"???\n"},{"id":"543889","messageId":"ag_zwVpvjig6XbMW@pks.im","threadId":"65667","inReplyTo":"xmqq8q9czm8r.fsf@gitster.g","subject":"Re: [PATCH 16/18] odb/source-loose: wire up `write_object_stream()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-05-22T06:12:17Z","receivedAt":"2026-05-22T06:12:22Z","isPatch":true,"body":"On Fri, May 22, 2026 at 02:49:24AM +0900, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > -int odb_source_loose_write_stream(struct odb_source_loose *loose,\n> > +/*\n> > + * Write the given stream into the loose object source. The only difference to\n> > + * the generic implementation of this function is that we don't perform an\n> \n> \"difference to\" -> \"difference from\"???\n\nI guess this is a difference between American and British English. \"to\"\nis more popular in British English, but basically not used at all in\nAmerican English. Will adapt, thanks.\n\nPatrick\n"},{"id":"544355","messageId":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"[PATCH v2 00/18] odb: make loose object source a proper `struct odb_source`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:23Z","receivedAt":"2026-06-01T08:20:31Z","isPatch":true,"body":"Hi,\n\nthis patch series converts the loose object source into a proper `struct\nodb_source` so that it can be used via our generic interfaces.\n\nThe patch series is relatively straight-forward, as the source basically\nalready exists as such and the interfaces already match. So for most of\nthe part we are just moving around some code and converting functions\nthat were previously called directly into callbacks.\n\nI guess the only part that needs some attention is that there is some\nconfusion at first with the `struct odb_source_loose::source` parent\npointer that initially points at the owning `struct odb_source_files`.\nThis relationship doesn't make much sense, as a loose source can totally\nexist standalone without the files source.\n\nWe're thus getting rid of this relationship in this series, too. I found\nit quite hard to reason about which pointer one is holding at any point\nin time though, doubly so because the parent pointer was named \"source\",\nwhich is rather generic. The second commit thus renames the pointer to\n`files` and converts it into `struct odb_source_files` to make the\ntransition cleaner, but the whole pointer will be dropped at the end of\nthis series.\n\nThe series is built on top of aec3f58750 (Sync with 'maint', 2026-05-21)\nwith ps/odb-in-memory at d2902a4549 (t/unit-tests: add tests for the\nin-memory object source, 2026-04-10) merged into it.\n\nChanges in v2:\n  - Some smaller typo fixes.\n  - Link to v1: https://patch.msgid.link/20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (18):\n      odb/source-loose: move loose source into \"odb/\" subsystem\n      odb/source-loose: store pointer to \"files\" instead of generic source\n      odb/source-loose: start converting to a proper `struct odb_source`\n      odb/source-loose: wire up `reprepare()` callback\n      odb/source-loose: wire up `close()` callback\n      odb/source-loose: wire up `read_object_info()` callback\n      odb/source-loose: wire up `read_object_stream()` callback\n      odb/source-loose: wire up `for_each_object()` callback\n      odb/source-loose: wire up `find_abbrev_len()` callback\n      odb/source-loose: wire up `count_objects()` callback\n      odb/source-loose: drop `odb_source_loose_has_object()`\n      odb/source-loose: wire up `freshen_object()` callback\n      loose: refactor object map to operate on `struct odb_source_loose`\n      odb/source-loose: wire up `write_object()` callback\n      object-file: refactor writing objects to use loose source\n      odb/source-loose: wire up `write_object_stream()` callback\n      odb/source-loose: stub out remaining callbacks\n      odb/source-loose: drop pointer to the \"files\" source\n\n Makefile               |   1 +\n builtin/cat-file.c     |   5 +-\n builtin/gc.c           |   6 +-\n builtin/pack-objects.c |  12 +-\n http-walker.c          |   3 +-\n http.c                 |   6 +-\n loose.c                |  45 ++-\n loose.h                |   4 +-\n meson.build            |   1 +\n object-file.c          | 796 ++++---------------------------------------------\n object-file.h          | 149 ++++-----\n odb/source-files.c     |  28 +-\n odb/source-loose.c     | 736 +++++++++++++++++++++++++++++++++++++++++++++\n odb/source-loose.h     |  48 +++\n odb/source.h           |   3 +\n 15 files changed, 973 insertions(+), 870 deletions(-)\n\nRange-diff versus v1:\n\n 1:  f25aaf0889 =  1:  7c97c1687c odb/source-loose: move loose source into \"odb/\" subsystem\n 2:  0bfebeb0da =  2:  1e1e267b39 odb/source-loose: store pointer to \"files\" instead of generic source\n 3:  35787e6ca6 !  3:  847cb523ee odb/source-loose: start converting to a proper `struct odb_source`\n    @@ odb/source-loose.h: struct odb_source_loose {\n      \n     +/*\n     + * Cast the given object database source to the loose backend. This will cause\n    -+ * a BUG in case the source uses doesn't use this backend.\n    ++ * a BUG in case the source doesn't use this backend.\n     + */\n     +static inline struct odb_source_loose *odb_source_loose_downcast(struct odb_source *source)\n     +{\n 4:  392962c177 =  4:  af543598ee odb/source-loose: wire up `reprepare()` callback\n 5:  b4102668c3 =  5:  884f573f89 odb/source-loose: wire up `close()` callback\n 6:  63da6e4abb =  6:  de85ffb4a9 odb/source-loose: wire up `read_object_info()` callback\n 7:  12b0c5c32d =  7:  522aaa9c3d odb/source-loose: wire up `read_object_stream()` callback\n 8:  8df176e282 =  8:  75cf3f4428 odb/source-loose: wire up `for_each_object()` callback\n 9:  6199ae90e0 =  9:  87c1c9ae5e odb/source-loose: wire up `find_abbrev_len()` callback\n10:  d0b1ef48d4 = 10:  f6405c8070 odb/source-loose: wire up `count_objects()` callback\n11:  0476d8b0c4 = 11:  0e8d6b6487 odb/source-loose: drop `odb_source_loose_has_object()`\n12:  27bb7b0724 = 12:  58cc626dd1 odb/source-loose: wire up `freshen_object()` callback\n13:  f8ce6a169d = 13:  51a22e7400 loose: refactor object map to operate on `struct odb_source_loose`\n14:  7ab570b776 = 14:  a9a88d6200 odb/source-loose: wire up `write_object()` callback\n15:  8c9240aaa0 = 15:  9236d2fd26 object-file: refactor writing objects to use loose source\n16:  de69621fa1 ! 16:  6316efb890 odb/source-loose: wire up `write_object_stream()` callback\n    @@ object-file.h: int index_path(struct index_state *istate, struct object_id *oid,\n      \n     -int odb_source_loose_write_stream(struct odb_source_loose *loose,\n     +/*\n    -+ * Write the given stream into the loose object source. The only difference to\n    -+ * the generic implementation of this function is that we don't perform an\n    ++ * Write the given stream into the loose object source. The only difference\n    ++ * from the generic implementation of this function is that we don't perform an\n     + * object existence check here.\n     + *\n     + * TODO: We should stop exposing this function altogether and move it into\n17:  f2d45e1a56 = 17:  789ec50474 odb/source-loose: stub out remaining callbacks\n18:  070052fc22 = 18:  0a64d23377 odb/source-loose: drop pointer to the \"files\" source\n\n---\nbase-commit: 072edab49f312c80561b2899f03f361f74fc38e4\nchange-id: 20260413-b4-pks-odb-source-loose-4900c8ca91db\n\n"},{"id":"544356","messageId":"20260601-b4-pks-odb-source-loose-v2-1-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 01/18] odb/source-loose: move loose source into \"odb/\" subsystem","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:24Z","receivedAt":"2026-06-01T08:20:32Z","isPatch":true,"body":"In subsequent patches we'll be turning `struct odb_source_loose` into a\nproper `struct odb_source`. As a first step towards this goal, move its\nstruct out of \"object-file.c\" and into \"odb/source-loose.c\".\n\nThis detaches the implementation of the loose object source from the\ngeneric object file code, following the same convention already used by\nthe \"files\" and \"in-memory\" sources.\n\nNo functional changes are intended.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile           |  1 +\n meson.build        |  1 +\n object-file.c      |  8 --------\n object-file.h      | 21 +--------------------\n odb/source-loose.c | 10 ++++++++++\n odb/source-loose.h | 34 ++++++++++++++++++++++++++++++++++\n 6 files changed, 47 insertions(+), 28 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex a43b8ee067..01356235c3 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1217,6 +1217,7 @@ LIB_OBJS += odb.o\n LIB_OBJS += odb/source.o\n LIB_OBJS += odb/source-files.o\n LIB_OBJS += odb/source-inmemory.o\n+LIB_OBJS += odb/source-loose.o\n LIB_OBJS += odb/streaming.o\n LIB_OBJS += odb/transaction.o\n LIB_OBJS += oid-array.o\ndiff --git a/meson.build b/meson.build\nindex 664d831329..c85e598835 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -405,6 +405,7 @@ libgit_sources = [\n   'odb/source.c',\n   'odb/source-files.c',\n   'odb/source-inmemory.c',\n+  'odb/source-loose.c',\n   'odb/streaming.c',\n   'odb/transaction.c',\n   'oid-array.c',\ndiff --git a/object-file.c b/object-file.c\nindex 90f995d000..641bd9c079 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -2205,14 +2205,6 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n \treturn &transaction->base;\n }\n \n-struct odb_source_loose *odb_source_loose_new(struct odb_source *source)\n-{\n-\tstruct odb_source_loose *loose;\n-\tCALLOC_ARRAY(loose, 1);\n-\tloose->source = source;\n-\treturn loose;\n-}\n-\n void odb_source_loose_free(struct odb_source_loose *loose)\n {\n \tif (!loose)\ndiff --git a/object-file.h b/object-file.h\nindex 5241b8dd5c..1d8312cf7f 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -4,6 +4,7 @@\n #include \"git-zlib.h\"\n #include \"object.h\"\n #include \"odb.h\"\n+#include \"odb/source-loose.h\"\n \n struct index_state;\n \n@@ -20,26 +21,6 @@ struct object_info;\n struct odb_read_stream;\n struct odb_source;\n \n-struct odb_source_loose {\n-\tstruct odb_source *source;\n-\n-\t/*\n-\t * Used to store the results of readdir(3) calls when we are OK\n-\t * sacrificing accuracy due to races for speed. That includes\n-\t * object existence with OBJECT_INFO_QUICK, as well as\n-\t * our search for unique abbreviated hashes. Don't use it for tasks\n-\t * requiring greater accuracy!\n-\t *\n-\t * Be sure to call odb_load_loose_cache() before using.\n-\t */\n-\tuint32_t subdir_seen[8]; /* 256 bits */\n-\tstruct oidtree *cache;\n-\n-\t/* Map between object IDs for loose objects. */\n-\tstruct loose_object_map *map;\n-};\n-\n-struct odb_source_loose *odb_source_loose_new(struct odb_source *source);\n void odb_source_loose_free(struct odb_source_loose *loose);\n \n /* Reprepare the loose source by emptying the loose object cache. */\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nnew file mode 100644\nindex 0000000000..b944d21813\n--- /dev/null\n+++ b/odb/source-loose.c\n@@ -0,0 +1,10 @@\n+#include \"git-compat-util.h\"\n+#include \"odb/source-loose.h\"\n+\n+struct odb_source_loose *odb_source_loose_new(struct odb_source *source)\n+{\n+\tstruct odb_source_loose *loose;\n+\tCALLOC_ARRAY(loose, 1);\n+\tloose->source = source;\n+\treturn loose;\n+}\ndiff --git a/odb/source-loose.h b/odb/source-loose.h\nnew file mode 100644\nindex 0000000000..8b4bac77ea\n--- /dev/null\n+++ b/odb/source-loose.h\n@@ -0,0 +1,34 @@\n+#ifndef ODB_SOURCE_LOOSE_H\n+#define ODB_SOURCE_LOOSE_H\n+\n+#include \"odb/source.h\"\n+\n+struct object_database;\n+struct oidtree;\n+\n+/*\n+ * An object database source that stores its objects in loose format, one\n+ * file per object. This source is part of the files source.\n+ */\n+struct odb_source_loose {\n+\tstruct odb_source *source;\n+\n+\t/*\n+\t * Used to store the results of readdir(3) calls when we are OK\n+\t * sacrificing accuracy due to races for speed. That includes\n+\t * object existence with OBJECT_INFO_QUICK, as well as\n+\t * our search for unique abbreviated hashes. Don't use it for tasks\n+\t * requiring greater accuracy!\n+\t *\n+\t * Be sure to call odb_load_loose_cache() before using.\n+\t */\n+\tuint32_t subdir_seen[8]; /* 256 bits */\n+\tstruct oidtree *cache;\n+\n+\t/* Map between object IDs for loose objects. */\n+\tstruct loose_object_map *map;\n+};\n+\n+struct odb_source_loose *odb_source_loose_new(struct odb_source *source);\n+\n+#endif\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544357","messageId":"20260601-b4-pks-odb-source-loose-v2-2-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 02/18] odb/source-loose: store pointer to \"files\" instead of generic source","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:25Z","receivedAt":"2026-06-01T08:20:35Z","isPatch":true,"body":"The `struct odb_source_loose` holds a pointer to its owning parent\nsource. The way that Git is currently structured, this parent is always\nthe \"files\" source. In subsequent commits we're going to detangle that\nso that the \"loose\" source doesn't have any owning parent source at all\nso that it can be used as a completely standalone source.\n\nDetangling this mess is somewhat intricate though, and is made even more\nintricate because it's not always clear which kind of source one is\nholding at a specific point in time -- either the parent \"files\" source,\nor the child \"loose\" source.\n\nMake this relationship more explicit by storing a pointer to the \"files\"\nsource instead of storing a pointer to a generic `struct odb_source`.\nThis will help make subsequent steps a bit clearer.\n\nNote that this is a temporary step, only. At the end of this series\nwe will have dropped the parent pointer completely.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 4 ++--\n odb/source-files.c | 2 +-\n odb/source-loose.c | 4 ++--\n odb/source-loose.h | 5 +++--\n 4 files changed, 8 insertions(+), 7 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 641bd9c079..7a1908bfc0 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -178,7 +178,7 @@ static int open_loose_object(struct odb_source_loose *loose,\n \tstatic struct strbuf buf = STRBUF_INIT;\n \tint fd;\n \n-\t*path = odb_loose_path(loose->source, &buf, oid);\n+\t*path = odb_loose_path(&loose->files->base, &buf, oid);\n \tfd = git_open(*path);\n \tif (fd >= 0)\n \t\treturn fd;\n@@ -189,7 +189,7 @@ static int open_loose_object(struct odb_source_loose *loose,\n static int quick_has_loose(struct odb_source_loose *loose,\n \t\t\t   const struct object_id *oid)\n {\n-\treturn !!oidtree_contains(odb_source_loose_cache(loose->source, oid), oid);\n+\treturn !!oidtree_contains(odb_source_loose_cache(&loose->files->base, oid), oid);\n }\n \n /*\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex b5abd20e97..185cc6903e 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -264,7 +264,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(&files->base);\n+\tfiles->loose = odb_source_loose_new(files);\n \tfiles->packed = packfile_store_new(&files->base);\n \n \tfiles->base.free = odb_source_files_free;\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex b944d21813..c9e7414814 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -1,10 +1,10 @@\n #include \"git-compat-util.h\"\n #include \"odb/source-loose.h\"\n \n-struct odb_source_loose *odb_source_loose_new(struct odb_source *source)\n+struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n {\n \tstruct odb_source_loose *loose;\n \tCALLOC_ARRAY(loose, 1);\n-\tloose->source = source;\n+\tloose->files = files;\n \treturn loose;\n }\ndiff --git a/odb/source-loose.h b/odb/source-loose.h\nindex 8b4bac77ea..bf61e767c8 100644\n--- a/odb/source-loose.h\n+++ b/odb/source-loose.h\n@@ -3,6 +3,7 @@\n \n #include \"odb/source.h\"\n \n+struct odb_source_files;\n struct object_database;\n struct oidtree;\n \n@@ -11,7 +12,7 @@ struct oidtree;\n  * file per object. This source is part of the files source.\n  */\n struct odb_source_loose {\n-\tstruct odb_source *source;\n+\tstruct odb_source_files *files;\n \n \t/*\n \t * Used to store the results of readdir(3) calls when we are OK\n@@ -29,6 +30,6 @@ struct odb_source_loose {\n \tstruct loose_object_map *map;\n };\n \n-struct odb_source_loose *odb_source_loose_new(struct odb_source *source);\n+struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files);\n \n #endif\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544358","messageId":"20260601-b4-pks-odb-source-loose-v2-3-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 03/18] odb/source-loose: start converting to a proper `struct odb_source`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:26Z","receivedAt":"2026-06-01T08:20:37Z","isPatch":true,"body":"Start converting `struct odb_source_loose` into a proper pluggable\n`struct odb_source` by embedding the base struct and assigning it the\nnew `ODB_SOURCE_LOOSE` type. Furthermore, wire up lifecycle management\nof this source by implementing the `free` callback and taking ownership\nof the chdir notifications.\n\nNote that the loose source is not yet functional as a standalone `struct\nodb_source`, as it's missing all of the callback implementations. These\nwill be wired up in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 17 -----------------\n object-file.h      |  2 --\n odb/source-files.c |  2 +-\n odb/source-loose.c | 45 +++++++++++++++++++++++++++++++++++++++++++++\n odb/source-loose.h | 14 ++++++++++++++\n odb/source.h       |  3 +++\n 6 files changed, 63 insertions(+), 20 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 7a1908bfc0..977d959d33 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -2041,14 +2041,6 @@ static struct oidtree *odb_source_loose_cache(struct odb_source *source,\n \treturn files->loose->cache;\n }\n \n-static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n-{\n-\toidtree_clear(loose->cache);\n-\tFREE_AND_NULL(loose->cache);\n-\tmemset(&loose->subdir_seen, 0,\n-\t       sizeof(loose->subdir_seen));\n-}\n-\n void odb_source_loose_reprepare(struct odb_source *source)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n@@ -2205,15 +2197,6 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n \treturn &transaction->base;\n }\n \n-void odb_source_loose_free(struct odb_source_loose *loose)\n-{\n-\tif (!loose)\n-\t\treturn;\n-\todb_source_loose_clear_cache(loose);\n-\tloose_object_map_clear(&loose->map);\n-\tfree(loose);\n-}\n-\n struct odb_loose_read_stream {\n \tstruct odb_read_stream base;\n \tgit_zstream z;\ndiff --git a/object-file.h b/object-file.h\nindex 1d8312cf7f..02c9680980 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -21,8 +21,6 @@ struct object_info;\n struct odb_read_stream;\n struct odb_source;\n \n-void odb_source_loose_free(struct odb_source_loose *loose);\n-\n /* Reprepare the loose source by emptying the loose object cache. */\n void odb_source_loose_reprepare(struct odb_source *source);\n \ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 185cc6903e..ccc637311b 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -27,7 +27,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(NULL, odb_source_files_reparent, files);\n-\todb_source_loose_free(files->loose);\n+\todb_source_free(&files->loose->base);\n \tpackfile_store_free(files->packed);\n \todb_source_release(&files->base);\n \tfree(files);\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex c9e7414814..92e18f5adb 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -1,10 +1,55 @@\n #include \"git-compat-util.h\"\n+#include \"abspath.h\"\n+#include \"chdir-notify.h\"\n+#include \"loose.h\"\n+#include \"odb.h\"\n+#include \"odb/source-files.h\"\n #include \"odb/source-loose.h\"\n+#include \"oidtree.h\"\n+\n+void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n+{\n+\toidtree_clear(loose->cache);\n+\tFREE_AND_NULL(loose->cache);\n+\tmemset(&loose->subdir_seen, 0,\n+\t       sizeof(loose->subdir_seen));\n+}\n+\n+static void odb_source_loose_reparent(const char *name UNUSED,\n+\t\t\t\t      const char *old_cwd,\n+\t\t\t\t      const char *new_cwd,\n+\t\t\t\t      void *cb_data)\n+{\n+\tstruct odb_source_loose *loose = cb_data;\n+\tchar *path = reparent_relative_path(old_cwd, new_cwd,\n+\t\t\t\t\t    loose->base.path);\n+\tfree(loose->base.path);\n+\tloose->base.path = path;\n+}\n+\n+static void odb_source_loose_free(struct odb_source *source)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\todb_source_loose_clear_cache(loose);\n+\tloose_object_map_clear(&loose->map);\n+\tchdir_notify_unregister(NULL, odb_source_loose_reparent, loose);\n+\todb_source_release(&loose->base);\n+\tfree(loose);\n+}\n \n struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n {\n \tstruct odb_source_loose *loose;\n+\n \tCALLOC_ARRAY(loose, 1);\n+\todb_source_init(&loose->base, files->base.odb, ODB_SOURCE_LOOSE,\n+\t\t\tfiles->base.path, files->base.local);\n \tloose->files = files;\n+\n+\tloose->base.free = odb_source_loose_free;\n+\n+\tif (!is_absolute_path(loose->base.path))\n+\t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n+\n \treturn loose;\n }\ndiff --git a/odb/source-loose.h b/odb/source-loose.h\nindex bf61e767c8..bd989f0728 100644\n--- a/odb/source-loose.h\n+++ b/odb/source-loose.h\n@@ -12,6 +12,7 @@ struct oidtree;\n  * file per object. This source is part of the files source.\n  */\n struct odb_source_loose {\n+\tstruct odb_source base;\n \tstruct odb_source_files *files;\n \n \t/*\n@@ -32,4 +33,17 @@ struct odb_source_loose {\n \n struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files);\n \n+/*\n+ * Cast the given object database source to the loose backend. This will cause\n+ * a BUG in case the source doesn't use this backend.\n+ */\n+static inline struct odb_source_loose *odb_source_loose_downcast(struct odb_source *source)\n+{\n+\tif (source->type != ODB_SOURCE_LOOSE)\n+\t\tBUG(\"trying to downcast source of type '%d' to loose\", source->type);\n+\treturn container_of(source, struct odb_source_loose, base);\n+}\n+\n+void odb_source_loose_clear_cache(struct odb_source_loose *loose);\n+\n #endif\ndiff --git a/odb/source.h b/odb/source.h\nindex 0a440884e4..8bcb67787e 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -14,6 +14,9 @@ enum odb_source_type {\n \t/* The \"files\" backend that uses loose objects and packfiles. */\n \tODB_SOURCE_FILES,\n \n+\t/* The \"loose\" backend that uses loose objects, only. */\n+\tODB_SOURCE_LOOSE,\n+\n \t/* The \"in-memory\" backend that stores objects in memory. */\n \tODB_SOURCE_INMEMORY,\n };\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544359","messageId":"20260601-b4-pks-odb-source-loose-v2-4-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 04/18] odb/source-loose: wire up `reprepare()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:27Z","receivedAt":"2026-06-01T08:20:40Z","isPatch":true,"body":"Move `odb_source_loose_reprepare()` from \"object-file.c\" into\n\"odb/source-loose.c\" and wire it up as the `reprepare()` callback of the\nloose source.\n\nWhile at it, make `odb_source_loose_clear_cache()` static, as it is no\nlonger needed outside of its file.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 6 ------\n object-file.h      | 3 ---\n odb/source-files.c | 2 +-\n odb/source-loose.c | 9 ++++++++-\n odb/source-loose.h | 2 --\n 5 files changed, 9 insertions(+), 13 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 977d959d33..0f4f1e7bdc 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -2041,12 +2041,6 @@ static struct oidtree *odb_source_loose_cache(struct odb_source *source,\n \treturn files->loose->cache;\n }\n \n-void odb_source_loose_reprepare(struct odb_source *source)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\todb_source_loose_clear_cache(files->loose);\n-}\n-\n static int check_stream_oid(git_zstream *stream,\n \t\t\t    const char *hdr,\n \t\t\t    unsigned long size,\ndiff --git a/object-file.h b/object-file.h\nindex 02c9680980..420a0fff2e 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -21,9 +21,6 @@ struct object_info;\n struct odb_read_stream;\n struct odb_source;\n \n-/* Reprepare the loose source by emptying the loose object cache. */\n-void odb_source_loose_reprepare(struct odb_source *source);\n-\n int odb_source_loose_read_object_info(struct odb_source *source,\n \t\t\t\t      const struct object_id *oid,\n \t\t\t\t      struct object_info *oi,\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex ccc637311b..10832e81e4 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -42,7 +42,7 @@ static void odb_source_files_close(struct odb_source *source)\n static void odb_source_files_reprepare(struct odb_source *source)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\todb_source_loose_reprepare(&files->base);\n+\todb_source_reprepare(&files->loose->base);\n \tpackfile_store_reprepare(files->packed);\n }\n \ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 92e18f5adb..e0fe0d513d 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -7,7 +7,7 @@\n #include \"odb/source-loose.h\"\n #include \"oidtree.h\"\n \n-void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n+static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n \tFREE_AND_NULL(loose->cache);\n@@ -15,6 +15,12 @@ void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n \t       sizeof(loose->subdir_seen));\n }\n \n+static void odb_source_loose_reprepare(struct odb_source *source)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\todb_source_loose_clear_cache(loose);\n+}\n+\n static void odb_source_loose_reparent(const char *name UNUSED,\n \t\t\t\t      const char *old_cwd,\n \t\t\t\t      const char *new_cwd,\n@@ -47,6 +53,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->files = files;\n \n \tloose->base.free = odb_source_loose_free;\n+\tloose->base.reprepare = odb_source_loose_reprepare;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\ndiff --git a/odb/source-loose.h b/odb/source-loose.h\nindex bd989f0728..4dd4fd6ce3 100644\n--- a/odb/source-loose.h\n+++ b/odb/source-loose.h\n@@ -44,6 +44,4 @@ static inline struct odb_source_loose *odb_source_loose_downcast(struct odb_sour\n \treturn container_of(source, struct odb_source_loose, base);\n }\n \n-void odb_source_loose_clear_cache(struct odb_source_loose *loose);\n-\n #endif\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544360","messageId":"20260601-b4-pks-odb-source-loose-v2-5-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 05/18] odb/source-loose: wire up `close()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:28Z","receivedAt":"2026-06-01T08:20:43Z","isPatch":true,"body":"Wire up a new `close()` callback for the loose source and call it from\nthe \"files\" source via the generic `odb_source_close()` interface. The\ncallback itself is a no-op as the loose source has no resources that\nneed to be released on close.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb/source-files.c | 1 +\n odb/source-loose.c | 6 ++++++\n 2 files changed, 7 insertions(+)\n\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 10832e81e4..59e3a70d80 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -36,6 +36,7 @@ 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 \tpackfile_store_close(files->packed);\n }\n \ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex e0fe0d513d..65c1076659 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -21,6 +21,11 @@ static void odb_source_loose_reprepare(struct odb_source *source)\n \todb_source_loose_clear_cache(loose);\n }\n \n+static void odb_source_loose_close(struct odb_source *source UNUSED)\n+{\n+\t/* Nothing to do. */\n+}\n+\n static void odb_source_loose_reparent(const char *name UNUSED,\n \t\t\t\t      const char *old_cwd,\n \t\t\t\t      const char *new_cwd,\n@@ -53,6 +58,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->files = files;\n \n \tloose->base.free = odb_source_loose_free;\n+\tloose->base.close = odb_source_loose_close;\n \tloose->base.reprepare = odb_source_loose_reprepare;\n \n \tif (!is_absolute_path(loose->base.path))\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544361","messageId":"20260601-b4-pks-odb-source-loose-v2-6-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 06/18] odb/source-loose: wire up `read_object_info()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:29Z","receivedAt":"2026-06-01T08:20:45Z","isPatch":true,"body":"Move `odb_source_loose_read_object_info()` from \"object-file.c\" into\n\"odb/source-loose.c\" and wire it up as the `read_object_info()` callback\nof the loose source. Callers that previously invoked it directly now go\nthrough the generic `odb_source_read_object_info()` interface instead.\n\nThe function `read_object_info_from_path()` cannot be moved along with\nit because it is still called by `for_each_object_wrapper_cb()`. It is\ntherefore kept in place, but adjusted to take a loose source to clarify\nthat it's always operating on this structure.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 46 +++++++++++++---------------------------------\n object-file.h      | 11 ++++++-----\n odb/source-files.c |  2 +-\n odb/source-loose.c | 24 ++++++++++++++++++++++++\n 4 files changed, 44 insertions(+), 39 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 0f4f1e7bdc..fa174512a4 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -396,13 +396,12 @@ static int parse_loose_header(const char *hdr, struct object_info *oi)\n \treturn 0;\n }\n \n-static int read_object_info_from_path(struct odb_source *source,\n-\t\t\t\t      const char *path,\n-\t\t\t\t      const struct object_id *oid,\n-\t\t\t\t      struct object_info *oi,\n-\t\t\t\t      enum object_info_flags flags)\n+int read_object_info_from_path(struct odb_source_loose *loose,\n+\t\t\t       const char *path,\n+\t\t\t       const struct object_id *oid,\n+\t\t\t       struct object_info *oi,\n+\t\t\t       enum object_info_flags flags)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tint ret;\n \tint fd;\n \tunsigned long mapsize;\n@@ -425,7 +424,7 @@ static int read_object_info_from_path(struct odb_source *source,\n \t\tstruct stat st;\n \n \t\tif ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) {\n-\t\t\tret = quick_has_loose(files->loose, oid) ? 0 : -1;\n+\t\t\tret = quick_has_loose(loose, oid) ? 0 : -1;\n \t\t\tgoto out;\n \t\t}\n \n@@ -532,7 +531,7 @@ static int read_object_info_from_path(struct odb_source *source,\n \t\tif (oi->typep == &type_scratch)\n \t\t\toi->typep = NULL;\n \t\tif (oi->delta_base_oid)\n-\t\t\toidclr(oi->delta_base_oid, source->odb->repo->hash_algo);\n+\t\t\toidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo);\n \t\tif (!ret)\n \t\t\toi->whence = OI_LOOSE;\n \t}\n@@ -540,26 +539,6 @@ static int read_object_info_from_path(struct odb_source *source,\n \treturn ret;\n }\n \n-int odb_source_loose_read_object_info(struct odb_source *source,\n-\t\t\t\t      const struct object_id *oid,\n-\t\t\t\t      struct object_info *oi,\n-\t\t\t\t      enum object_info_flags flags)\n-{\n-\tstatic struct strbuf buf = STRBUF_INIT;\n-\n-\t/*\n-\t * The second read shouldn't cause new loose objects to show up, unless\n-\t * there was a race condition with a secondary process. We don't care\n-\t * about this case though, so we simply skip reading loose objects a\n-\t * second time.\n-\t */\n-\tif (flags & OBJECT_INFO_SECOND_READ)\n-\t\treturn -1;\n-\n-\todb_loose_path(source, &buf, oid);\n-\treturn read_object_info_from_path(source, buf.buf, oid, oi, flags);\n-}\n-\n static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c,\n \t\t\t     const void *buf, unsigned long len,\n \t\t\t     struct object_id *oid,\n@@ -1833,7 +1812,7 @@ int for_each_loose_file_in_source(struct odb_source *source,\n }\n \n struct for_each_object_wrapper_data {\n-\tstruct odb_source *source;\n+\tstruct odb_source_loose *loose;\n \tconst struct object_info *request;\n \todb_for_each_object_cb cb;\n \tvoid *cb_data;\n@@ -1848,7 +1827,7 @@ static int for_each_object_wrapper_cb(const struct object_id *oid,\n \tif (data->request) {\n \t\tstruct object_info oi = *data->request;\n \n-\t\tif (read_object_info_from_path(data->source, path, oid, &oi, 0) < 0)\n+\t\tif (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0)\n \t\t\treturn -1;\n \n \t\treturn data->cb(oid, &oi, data->cb_data);\n@@ -1865,8 +1844,8 @@ static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid,\n \tif (data->request) {\n \t\tstruct object_info oi = *data->request;\n \n-\t\tif (odb_source_loose_read_object_info(data->source,\n-\t\t\t\t\t\t      oid, &oi, 0) < 0)\n+\t\tif (odb_source_read_object_info(&data->loose->base,\n+\t\t\t\t\t\toid, &oi, 0) < 0)\n \t\t\treturn -1;\n \n \t\treturn data->cb(oid, &oi, data->cb_data);\n@@ -1881,8 +1860,9 @@ int odb_source_loose_for_each_object(struct odb_source *source,\n \t\t\t\t     void *cb_data,\n \t\t\t\t     const struct odb_for_each_object_options *opts)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tstruct for_each_object_wrapper_data data = {\n-\t\t.source = source,\n+\t\t.loose = files->loose,\n \t\t.request = request,\n \t\t.cb = cb,\n \t\t.cb_data = cb_data,\ndiff --git a/object-file.h b/object-file.h\nindex 420a0fff2e..8ac2832dac 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -21,11 +21,6 @@ struct object_info;\n struct odb_read_stream;\n struct odb_source;\n \n-int odb_source_loose_read_object_info(struct odb_source *source,\n-\t\t\t\t      const struct object_id *oid,\n-\t\t\t\t      struct object_info *oi,\n-\t\t\t\t      enum object_info_flags flags);\n-\n int odb_source_loose_read_object_stream(struct odb_read_stream **out,\n \t\t\t\t\tstruct odb_source *source,\n \t\t\t\t\tconst struct object_id *oid);\n@@ -198,6 +193,12 @@ int read_loose_object(struct repository *repo,\n \t\t      void **contents,\n \t\t      struct object_info *oi);\n \n+int read_object_info_from_path(struct odb_source_loose *loose,\n+\t\t\t       const char *path,\n+\t\t\t       const struct object_id *oid,\n+\t\t\t       struct object_info *oi,\n+\t\t\t       enum object_info_flags flags);\n+\n struct odb_transaction;\n \n /*\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 59e3a70d80..8d6924755f 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -55,7 +55,7 @@ static int odb_source_files_read_object_info(struct odb_source *source,\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \n \tif (!packfile_store_read_object_info(files->packed, oid, oi, flags) ||\n-\t    !odb_source_loose_read_object_info(source, oid, oi, flags))\n+\t    !odb_source_read_object_info(&files->loose->base, oid, oi, flags))\n \t\treturn 0;\n \n \treturn -1;\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 65c1076659..50f387ecf3 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -2,10 +2,33 @@\n #include \"abspath.h\"\n #include \"chdir-notify.h\"\n #include \"loose.h\"\n+#include \"object-file.h\"\n #include \"odb.h\"\n #include \"odb/source-files.h\"\n #include \"odb/source-loose.h\"\n #include \"oidtree.h\"\n+#include \"strbuf.h\"\n+\n+static int odb_source_loose_read_object_info(struct odb_source *source,\n+\t\t\t\t\t     const struct object_id *oid,\n+\t\t\t\t\t     struct object_info *oi,\n+\t\t\t\t\t     enum object_info_flags flags)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tstatic struct strbuf buf = STRBUF_INIT;\n+\n+\t/*\n+\t * The second read shouldn't cause new loose objects to show up, unless\n+\t * there was a race condition with a secondary process. We don't care\n+\t * about this case though, so we simply skip reading loose objects a\n+\t * second time.\n+\t */\n+\tif (flags & OBJECT_INFO_SECOND_READ)\n+\t\treturn -1;\n+\n+\todb_loose_path(source, &buf, oid);\n+\treturn read_object_info_from_path(loose, buf.buf, oid, oi, flags);\n+}\n \n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n@@ -60,6 +83,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.free = odb_source_loose_free;\n \tloose->base.close = odb_source_loose_close;\n \tloose->base.reprepare = odb_source_loose_reprepare;\n+\tloose->base.read_object_info = odb_source_loose_read_object_info;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544362","messageId":"20260601-b4-pks-odb-source-loose-v2-7-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 07/18] odb/source-loose: wire up `read_object_stream()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:30Z","receivedAt":"2026-06-01T08:20:48Z","isPatch":true,"body":"Move `odb_source_loose_read_object_stream()` and its associated helpers\nfrom \"object-file.c\" into \"odb/source-loose.c\" and wire it up as the\n`read_object_stream()` callback of the loose source.\n\nAs part of the move we are also forced to expose a couple of functions\nfrom \"object-file.h\" that parse object headers in a somewhat-generic\nway, as those functions are now used by both subsystems.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 200 ++---------------------------------------------------\n object-file.h      |  31 +++++++--\n odb/source-files.c |   2 +-\n odb/source-loose.c | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 222 insertions(+), 200 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex fa174512a4..adfb672493 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -164,28 +164,6 @@ int stream_object_signature(struct repository *r,\n \treturn !oideq(oid, &real_oid) ? -1 : 0;\n }\n \n-/*\n- * Find \"oid\" as a loose object in given source, open the object and return its\n- * file descriptor. Returns the file descriptor on success, negative on failure.\n- *\n- * The \"path\" out-parameter will give the path of the object we found (if any).\n- * Note that it may point to static storage and is only valid until another\n- * call to stat_loose_object().\n- */\n-static int open_loose_object(struct odb_source_loose *loose,\n-\t\t\t     const struct object_id *oid, const char **path)\n-{\n-\tstatic struct strbuf buf = STRBUF_INIT;\n-\tint fd;\n-\n-\t*path = odb_loose_path(&loose->files->base, &buf, oid);\n-\tfd = git_open(*path);\n-\tif (fd >= 0)\n-\t\treturn fd;\n-\n-\treturn -1;\n-}\n-\n static int quick_has_loose(struct odb_source_loose *loose,\n \t\t\t   const struct object_id *oid)\n {\n@@ -215,42 +193,11 @@ static void *map_fd(int fd, const char *path, unsigned long *size)\n \treturn map;\n }\n \n-static void *odb_source_loose_map_object(struct odb_source *source,\n-\t\t\t\t\t const struct object_id *oid,\n-\t\t\t\t\t unsigned long *size)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tconst char *p;\n-\tint fd = open_loose_object(files->loose, oid, &p);\n-\n-\tif (fd < 0)\n-\t\treturn NULL;\n-\treturn map_fd(fd, p, size);\n-}\n-\n-enum unpack_loose_header_result {\n-\tULHR_OK,\n-\tULHR_BAD,\n-\tULHR_TOO_LONG,\n-};\n-\n-/**\n- * unpack_loose_header() initializes the data stream needed to unpack\n- * a loose object header.\n- *\n- * Returns:\n- *\n- * - ULHR_OK on success\n- * - ULHR_BAD on error\n- * - ULHR_TOO_LONG if the header was too long\n- *\n- * It will only parse up to MAX_HEADER_LEN bytes.\n- */\n-static enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n-\t\t\t\t\t\t\t   unsigned char *map,\n-\t\t\t\t\t\t\t   unsigned long mapsize,\n-\t\t\t\t\t\t\t   void *buffer,\n-\t\t\t\t\t\t\t   unsigned long bufsiz)\n+enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n+\t\t\t\t\t\t    unsigned char *map,\n+\t\t\t\t\t\t    unsigned long mapsize,\n+\t\t\t\t\t\t    void *buffer,\n+\t\t\t\t\t\t    unsigned long bufsiz)\n {\n \tint status;\n \n@@ -340,7 +287,7 @@ static void *unpack_loose_rest(git_zstream *stream,\n  * too permissive for what we want to check. So do an anal\n  * object header parse by hand.\n  */\n-static int parse_loose_header(const char *hdr, struct object_info *oi)\n+int parse_loose_header(const char *hdr, struct object_info *oi)\n {\n \tconst char *type_buf = hdr;\n \tsize_t size;\n@@ -2170,138 +2117,3 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n \n \treturn &transaction->base;\n }\n-\n-struct odb_loose_read_stream {\n-\tstruct odb_read_stream base;\n-\tgit_zstream z;\n-\tenum {\n-\t\tODB_LOOSE_READ_STREAM_INUSE,\n-\t\tODB_LOOSE_READ_STREAM_DONE,\n-\t\tODB_LOOSE_READ_STREAM_ERROR,\n-\t} z_state;\n-\tvoid *mapped;\n-\tunsigned long mapsize;\n-\tchar hdr[32];\n-\tint hdr_avail;\n-\tint hdr_used;\n-};\n-\n-static ssize_t read_istream_loose(struct odb_read_stream *_st, char *buf, size_t sz)\n-{\n-\tstruct odb_loose_read_stream *st =\n-\t\tcontainer_of(_st, struct odb_loose_read_stream, base);\n-\tsize_t total_read = 0;\n-\n-\tswitch (st->z_state) {\n-\tcase ODB_LOOSE_READ_STREAM_DONE:\n-\t\treturn 0;\n-\tcase ODB_LOOSE_READ_STREAM_ERROR:\n-\t\treturn -1;\n-\tdefault:\n-\t\tbreak;\n-\t}\n-\n-\tif (st->hdr_used < st->hdr_avail) {\n-\t\tsize_t to_copy = st->hdr_avail - st->hdr_used;\n-\t\tif (sz < to_copy)\n-\t\t\tto_copy = sz;\n-\t\tmemcpy(buf, st->hdr + st->hdr_used, to_copy);\n-\t\tst->hdr_used += to_copy;\n-\t\ttotal_read += to_copy;\n-\t}\n-\n-\twhile (total_read < sz) {\n-\t\tint status;\n-\n-\t\tst->z.next_out = (unsigned char *)buf + total_read;\n-\t\tst->z.avail_out = sz - total_read;\n-\t\tstatus = git_inflate(&st->z, Z_FINISH);\n-\n-\t\ttotal_read = st->z.next_out - (unsigned char *)buf;\n-\n-\t\tif (status == Z_STREAM_END) {\n-\t\t\tgit_inflate_end(&st->z);\n-\t\t\tst->z_state = ODB_LOOSE_READ_STREAM_DONE;\n-\t\t\tbreak;\n-\t\t}\n-\t\tif (status != Z_OK && (status != Z_BUF_ERROR || total_read < sz)) {\n-\t\t\tgit_inflate_end(&st->z);\n-\t\t\tst->z_state = ODB_LOOSE_READ_STREAM_ERROR;\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-\treturn total_read;\n-}\n-\n-static int close_istream_loose(struct odb_read_stream *_st)\n-{\n-\tstruct odb_loose_read_stream *st =\n-\t\tcontainer_of(_st, struct odb_loose_read_stream, base);\n-\n-\tif (st->z_state == ODB_LOOSE_READ_STREAM_INUSE)\n-\t\tgit_inflate_end(&st->z);\n-\tmunmap(st->mapped, st->mapsize);\n-\treturn 0;\n-}\n-\n-int odb_source_loose_read_object_stream(struct odb_read_stream **out,\n-\t\t\t\t\tstruct odb_source *source,\n-\t\t\t\t\tconst struct object_id *oid)\n-{\n-\tstruct object_info oi = OBJECT_INFO_INIT;\n-\tstruct odb_loose_read_stream *st;\n-\tunsigned long mapsize;\n-\tunsigned long size_ul;\n-\tvoid *mapped;\n-\n-\tmapped = odb_source_loose_map_object(source, oid, &mapsize);\n-\tif (!mapped)\n-\t\treturn -1;\n-\n-\t/*\n-\t * Note: we must allocate this structure early even though we may still\n-\t * fail. This is because we need to initialize the zlib stream, and it\n-\t * is not possible to copy the stream around after the fact because it\n-\t * has self-referencing pointers.\n-\t */\n-\tCALLOC_ARRAY(st, 1);\n-\n-\tswitch (unpack_loose_header(&st->z, mapped, mapsize, st->hdr,\n-\t\t\t\t    sizeof(st->hdr))) {\n-\tcase ULHR_OK:\n-\t\tbreak;\n-\tcase ULHR_BAD:\n-\tcase ULHR_TOO_LONG:\n-\t\tgoto error;\n-\t}\n-\n-\t/*\n-\t * object_info.sizep is unsigned long* (32-bit on Windows), but\n-\t * st->base.size is size_t (64-bit). Use temporary variable.\n-\t * Note: loose objects >4GB would still truncate here, but such\n-\t * large loose objects are uncommon (they'd normally be packed).\n-\t */\n-\toi.sizep = &size_ul;\n-\toi.typep = &st->base.type;\n-\n-\tif (parse_loose_header(st->hdr, &oi) < 0 || st->base.type < 0)\n-\t\tgoto error;\n-\tst->base.size = size_ul;\n-\n-\tst->mapped = mapped;\n-\tst->mapsize = mapsize;\n-\tst->hdr_used = strlen(st->hdr) + 1;\n-\tst->hdr_avail = st->z.total_out;\n-\tst->z_state = ODB_LOOSE_READ_STREAM_INUSE;\n-\tst->base.close = close_istream_loose;\n-\tst->base.read = read_istream_loose;\n-\n-\t*out = &st->base;\n-\n-\treturn 0;\n-error:\n-\tgit_inflate_end(&st->z);\n-\tmunmap(mapped, mapsize);\n-\tfree(st);\n-\treturn -1;\n-}\ndiff --git a/object-file.h b/object-file.h\nindex 8ac2832dac..d93b7ffad7 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -18,13 +18,8 @@ int index_fd(struct index_state *istate, struct object_id *oid, int fd, struct s\n int index_path(struct index_state *istate, struct object_id *oid, const char *path, struct stat *st, unsigned flags);\n \n struct object_info;\n-struct odb_read_stream;\n struct odb_source;\n \n-int odb_source_loose_read_object_stream(struct odb_read_stream **out,\n-\t\t\t\t\tstruct odb_source *source,\n-\t\t\t\t\tconst struct object_id *oid);\n-\n /*\n  * Return true iff an object database source has a loose object\n  * with the specified name.  This function does not respect replace\n@@ -199,6 +194,32 @@ int read_object_info_from_path(struct odb_source_loose *loose,\n \t\t\t       struct object_info *oi,\n \t\t\t       enum object_info_flags flags);\n \n+enum unpack_loose_header_result {\n+\tULHR_OK,\n+\tULHR_BAD,\n+\tULHR_TOO_LONG,\n+};\n+\n+/**\n+ * unpack_loose_header() initializes the data stream needed to unpack\n+ * a loose object header.\n+ *\n+ * Returns:\n+ *\n+ * - ULHR_OK on success\n+ * - ULHR_BAD on error\n+ * - ULHR_TOO_LONG if the header was too long\n+ *\n+ * It will only parse up to MAX_HEADER_LEN bytes.\n+ */\n+enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n+\t\t\t\t\t\t    unsigned char *map,\n+\t\t\t\t\t\t    unsigned long mapsize,\n+\t\t\t\t\t\t    void *buffer,\n+\t\t\t\t\t\t    unsigned long bufsiz);\n+\n+int parse_loose_header(const char *hdr, struct object_info *oi);\n+\n struct odb_transaction;\n \n /*\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 8d6924755f..90806ddf86 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -67,7 +67,7 @@ static int odb_source_files_read_object_stream(struct odb_read_stream **out,\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tif (!packfile_store_read_object_stream(out, files->packed, oid) ||\n-\t    !odb_source_loose_read_object_stream(out, source, oid))\n+\t    !odb_source_read_object_stream(out, &files->loose->base, oid))\n \t\treturn 0;\n \treturn -1;\n }\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 50f387ecf3..4b82c6f316 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -1,11 +1,13 @@\n #include \"git-compat-util.h\"\n #include \"abspath.h\"\n #include \"chdir-notify.h\"\n+#include \"gettext.h\"\n #include \"loose.h\"\n #include \"object-file.h\"\n #include \"odb.h\"\n #include \"odb/source-files.h\"\n #include \"odb/source-loose.h\"\n+#include \"odb/streaming.h\"\n #include \"oidtree.h\"\n #include \"strbuf.h\"\n \n@@ -30,6 +32,192 @@ static int odb_source_loose_read_object_info(struct odb_source *source,\n \treturn read_object_info_from_path(loose, buf.buf, oid, oi, flags);\n }\n \n+/*\n+ * Find \"oid\" as a loose object in given source, open the object and return its\n+ * file descriptor. Returns the file descriptor on success, negative on failure.\n+ *\n+ * The \"path\" out-parameter will give the path of the object we found (if any).\n+ * Note that it may point to static storage and is only valid until another\n+ * call to open_loose_object().\n+ */\n+static int open_loose_object(struct odb_source_loose *loose,\n+\t\t\t     const struct object_id *oid, const char **path)\n+{\n+\tstatic struct strbuf buf = STRBUF_INIT;\n+\tint fd;\n+\n+\t*path = odb_loose_path(&loose->base, &buf, oid);\n+\tfd = git_open(*path);\n+\tif (fd >= 0)\n+\t\treturn fd;\n+\n+\treturn -1;\n+}\n+\n+static void *odb_source_loose_map_object(struct odb_source_loose *loose,\n+\t\t\t\t\t const struct object_id *oid,\n+\t\t\t\t\t unsigned long *size)\n+{\n+\tconst char *p;\n+\tint fd = open_loose_object(loose, oid, &p);\n+\tvoid *map = NULL;\n+\tstruct stat st;\n+\n+\tif (fd < 0)\n+\t\treturn NULL;\n+\n+\tif (!fstat(fd, &st)) {\n+\t\t*size = xsize_t(st.st_size);\n+\t\tif (!*size) {\n+\t\t\t/* mmap() is forbidden on empty files */\n+\t\t\terror(_(\"object file %s is empty\"), p);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tmap = xmmap(NULL, *size, PROT_READ, MAP_PRIVATE, fd, 0);\n+\t}\n+\n+out:\n+\tclose(fd);\n+\treturn map;\n+}\n+\n+struct odb_loose_read_stream {\n+\tstruct odb_read_stream base;\n+\tgit_zstream z;\n+\tenum {\n+\t\tODB_LOOSE_READ_STREAM_INUSE,\n+\t\tODB_LOOSE_READ_STREAM_DONE,\n+\t\tODB_LOOSE_READ_STREAM_ERROR,\n+\t} z_state;\n+\tvoid *mapped;\n+\tunsigned long mapsize;\n+\tchar hdr[32];\n+\tint hdr_avail;\n+\tint hdr_used;\n+};\n+\n+static ssize_t read_istream_loose(struct odb_read_stream *_st, char *buf, size_t sz)\n+{\n+\tstruct odb_loose_read_stream *st =\n+\t\tcontainer_of(_st, struct odb_loose_read_stream, base);\n+\tsize_t total_read = 0;\n+\n+\tswitch (st->z_state) {\n+\tcase ODB_LOOSE_READ_STREAM_DONE:\n+\t\treturn 0;\n+\tcase ODB_LOOSE_READ_STREAM_ERROR:\n+\t\treturn -1;\n+\tdefault:\n+\t\tbreak;\n+\t}\n+\n+\tif (st->hdr_used < st->hdr_avail) {\n+\t\tsize_t to_copy = st->hdr_avail - st->hdr_used;\n+\t\tif (sz < to_copy)\n+\t\t\tto_copy = sz;\n+\t\tmemcpy(buf, st->hdr + st->hdr_used, to_copy);\n+\t\tst->hdr_used += to_copy;\n+\t\ttotal_read += to_copy;\n+\t}\n+\n+\twhile (total_read < sz) {\n+\t\tint status;\n+\n+\t\tst->z.next_out = (unsigned char *)buf + total_read;\n+\t\tst->z.avail_out = sz - total_read;\n+\t\tstatus = git_inflate(&st->z, Z_FINISH);\n+\n+\t\ttotal_read = st->z.next_out - (unsigned char *)buf;\n+\n+\t\tif (status == Z_STREAM_END) {\n+\t\t\tgit_inflate_end(&st->z);\n+\t\t\tst->z_state = ODB_LOOSE_READ_STREAM_DONE;\n+\t\t\tbreak;\n+\t\t}\n+\t\tif (status != Z_OK && (status != Z_BUF_ERROR || total_read < sz)) {\n+\t\t\tgit_inflate_end(&st->z);\n+\t\t\tst->z_state = ODB_LOOSE_READ_STREAM_ERROR;\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\treturn total_read;\n+}\n+\n+static int close_istream_loose(struct odb_read_stream *_st)\n+{\n+\tstruct odb_loose_read_stream *st =\n+\t\tcontainer_of(_st, struct odb_loose_read_stream, base);\n+\n+\tif (st->z_state == ODB_LOOSE_READ_STREAM_INUSE)\n+\t\tgit_inflate_end(&st->z);\n+\tmunmap(st->mapped, st->mapsize);\n+\treturn 0;\n+}\n+\n+static int odb_source_loose_read_object_stream(struct odb_read_stream **out,\n+\t\t\t\t\t       struct odb_source *source,\n+\t\t\t\t\t       const struct object_id *oid)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tstruct object_info oi = OBJECT_INFO_INIT;\n+\tstruct odb_loose_read_stream *st;\n+\tunsigned long mapsize;\n+\tunsigned long size_ul;\n+\tvoid *mapped;\n+\n+\tmapped = odb_source_loose_map_object(loose, oid, &mapsize);\n+\tif (!mapped)\n+\t\treturn -1;\n+\n+\t/*\n+\t * Note: we must allocate this structure early even though we may still\n+\t * fail. This is because we need to initialize the zlib stream, and it\n+\t * is not possible to copy the stream around after the fact because it\n+\t * has self-referencing pointers.\n+\t */\n+\tCALLOC_ARRAY(st, 1);\n+\n+\tswitch (unpack_loose_header(&st->z, mapped, mapsize, st->hdr,\n+\t\t\t\t    sizeof(st->hdr))) {\n+\tcase ULHR_OK:\n+\t\tbreak;\n+\tcase ULHR_BAD:\n+\tcase ULHR_TOO_LONG:\n+\t\tgoto error;\n+\t}\n+\n+\t/*\n+\t * object_info.sizep is unsigned long* (32-bit on Windows), but\n+\t * st->base.size is size_t (64-bit). Use temporary variable.\n+\t * Note: loose objects >4GB would still truncate here, but such\n+\t * large loose objects are uncommon (they'd normally be packed).\n+\t */\n+\toi.sizep = &size_ul;\n+\toi.typep = &st->base.type;\n+\n+\tif (parse_loose_header(st->hdr, &oi) < 0 || st->base.type < 0)\n+\t\tgoto error;\n+\tst->base.size = size_ul;\n+\n+\tst->mapped = mapped;\n+\tst->mapsize = mapsize;\n+\tst->hdr_used = strlen(st->hdr) + 1;\n+\tst->hdr_avail = st->z.total_out;\n+\tst->z_state = ODB_LOOSE_READ_STREAM_INUSE;\n+\tst->base.close = close_istream_loose;\n+\tst->base.read = read_istream_loose;\n+\n+\t*out = &st->base;\n+\n+\treturn 0;\n+error:\n+\tgit_inflate_end(&st->z);\n+\tmunmap(mapped, mapsize);\n+\tfree(st);\n+\treturn -1;\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -84,6 +272,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.close = odb_source_loose_close;\n \tloose->base.reprepare = odb_source_loose_reprepare;\n \tloose->base.read_object_info = odb_source_loose_read_object_info;\n+\tloose->base.read_object_stream = odb_source_loose_read_object_stream;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544363","messageId":"20260601-b4-pks-odb-source-loose-v2-8-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 08/18] odb/source-loose: wire up `for_each_object()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:31Z","receivedAt":"2026-06-01T08:20:51Z","isPatch":true,"body":"Move `odb_source_loose_for_each_object()` and its associated helpers\nfrom \"object-file.c\" into \"odb/source-loose.c\" and wire it up as the\n`for_each_object()` callback of the loose source.\n\nAgain, as in the preceding commit, we are forced to expose a couple of\nfunctions from \"object-file.c\" that are now used by both subsystems.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/cat-file.c |   5 +-\n object-file.c      | 299 +++--------------------------------------------------\n object-file.h      |  32 +++---\n odb/source-files.c |   2 +-\n odb/source-loose.c | 264 ++++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 297 insertions(+), 305 deletions(-)\n\ndiff --git a/builtin/cat-file.c b/builtin/cat-file.c\nindex d9fbad5358..2958fc5357 100644\n--- a/builtin/cat-file.c\n+++ b/builtin/cat-file.c\n@@ -862,8 +862,9 @@ static void batch_each_object(struct batch_options *opt,\n \t */\n \todb_prepare_alternates(the_repository->objects);\n \tfor (source = the_repository->objects->sources; source; source = source->next) {\n-\t\tint ret = odb_source_loose_for_each_object(source, NULL, batch_one_object_oi,\n-\t\t\t\t\t\t\t   &payload, &opts);\n+\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n+\t\tint ret = odb_source_for_each_object(&files->loose->base, NULL, batch_one_object_oi,\n+\t\t\t\t\t\t     &payload, &opts);\n \t\tif (ret)\n \t\t\tbreak;\n \t}\ndiff --git a/object-file.c b/object-file.c\nindex adfb672493..157ecad3ea 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -22,7 +22,6 @@\n #include \"odb.h\"\n #include \"odb/streaming.h\"\n #include \"odb/transaction.h\"\n-#include \"oidtree.h\"\n #include \"pack.h\"\n #include \"packfile.h\"\n #include \"path.h\"\n@@ -31,12 +30,6 @@\n #include \"tempfile.h\"\n #include \"tmp-objdir.h\"\n \n-/* The maximum size for an object header. */\n-#define MAX_HEADER_LEN 32\n-\n-static struct oidtree *odb_source_loose_cache(struct odb_source *source,\n-\t\t\t\t\t      const struct object_id *oid);\n-\n static int get_conv_flags(unsigned flags)\n {\n \tif (flags & INDEX_RENORMALIZE)\n@@ -164,12 +157,6 @@ int stream_object_signature(struct repository *r,\n \treturn !oideq(oid, &real_oid) ? -1 : 0;\n }\n \n-static int quick_has_loose(struct odb_source_loose *loose,\n-\t\t\t   const struct object_id *oid)\n-{\n-\treturn !!oidtree_contains(odb_source_loose_cache(&loose->files->base, oid), oid);\n-}\n-\n /*\n  * Map and close the given loose object fd. The path argument is used for\n  * error reporting.\n@@ -227,9 +214,9 @@ enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n \treturn ULHR_TOO_LONG;\n }\n \n-static void *unpack_loose_rest(git_zstream *stream,\n-\t\t\t       void *buffer, unsigned long size,\n-\t\t\t       const struct object_id *oid)\n+void *unpack_loose_rest(git_zstream *stream,\n+\t\t\tvoid *buffer, unsigned long size,\n+\t\t\tconst struct object_id *oid)\n {\n \tsize_t bytes = strlen(buffer) + 1, n;\n \tunsigned char *buf = xmallocz(size);\n@@ -343,149 +330,6 @@ int parse_loose_header(const char *hdr, struct object_info *oi)\n \treturn 0;\n }\n \n-int read_object_info_from_path(struct odb_source_loose *loose,\n-\t\t\t       const char *path,\n-\t\t\t       const struct object_id *oid,\n-\t\t\t       struct object_info *oi,\n-\t\t\t       enum object_info_flags flags)\n-{\n-\tint ret;\n-\tint fd;\n-\tunsigned long mapsize;\n-\tvoid *map = NULL;\n-\tgit_zstream stream, *stream_to_end = NULL;\n-\tchar hdr[MAX_HEADER_LEN];\n-\tunsigned long size_scratch;\n-\tenum object_type type_scratch;\n-\tstruct stat st;\n-\n-\t/*\n-\t * If we don't care about type or size, then we don't\n-\t * need to look inside the object at all. Note that we\n-\t * do not optimize out the stat call, even if the\n-\t * caller doesn't care about the disk-size, since our\n-\t * return value implicitly indicates whether the\n-\t * object even exists.\n-\t */\n-\tif (!oi || (!oi->typep && !oi->sizep && !oi->contentp)) {\n-\t\tstruct stat st;\n-\n-\t\tif ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) {\n-\t\t\tret = quick_has_loose(loose, oid) ? 0 : -1;\n-\t\t\tgoto out;\n-\t\t}\n-\n-\t\tif (lstat(path, &st) < 0) {\n-\t\t\tret = -1;\n-\t\t\tgoto out;\n-\t\t}\n-\n-\t\tif (oi) {\n-\t\t\tif (oi->disk_sizep)\n-\t\t\t\t*oi->disk_sizep = st.st_size;\n-\t\t\tif (oi->mtimep)\n-\t\t\t\t*oi->mtimep = st.st_mtime;\n-\t\t}\n-\n-\t\tret = 0;\n-\t\tgoto out;\n-\t}\n-\n-\tfd = git_open(path);\n-\tif (fd < 0) {\n-\t\tif (errno != ENOENT)\n-\t\t\terror_errno(_(\"unable to open loose object %s\"), oid_to_hex(oid));\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\tif (fstat(fd, &st)) {\n-\t\tclose(fd);\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\tmapsize = xsize_t(st.st_size);\n-\tif (!mapsize) {\n-\t\tclose(fd);\n-\t\tret = error(_(\"object file %s is empty\"), path);\n-\t\tgoto out;\n-\t}\n-\n-\tmap = xmmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, fd, 0);\n-\tclose(fd);\n-\tif (!map) {\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\tif (oi->disk_sizep)\n-\t\t*oi->disk_sizep = mapsize;\n-\tif (oi->mtimep)\n-\t\t*oi->mtimep = st.st_mtime;\n-\n-\tstream_to_end = &stream;\n-\n-\tswitch (unpack_loose_header(&stream, map, mapsize, hdr, sizeof(hdr))) {\n-\tcase ULHR_OK:\n-\t\tif (!oi->sizep)\n-\t\t\toi->sizep = &size_scratch;\n-\t\tif (!oi->typep)\n-\t\t\toi->typep = &type_scratch;\n-\n-\t\tif (parse_loose_header(hdr, oi) < 0) {\n-\t\t\tret = error(_(\"unable to parse %s header\"), oid_to_hex(oid));\n-\t\t\tgoto corrupt;\n-\t\t}\n-\n-\t\tif (*oi->typep < 0)\n-\t\t\tdie(_(\"invalid object type\"));\n-\n-\t\tif (oi->contentp) {\n-\t\t\t*oi->contentp = unpack_loose_rest(&stream, hdr, *oi->sizep, oid);\n-\t\t\tif (!*oi->contentp) {\n-\t\t\t\tret = -1;\n-\t\t\t\tgoto corrupt;\n-\t\t\t}\n-\t\t}\n-\n-\t\tbreak;\n-\tcase ULHR_BAD:\n-\t\tret = error(_(\"unable to unpack %s header\"),\n-\t\t\t    oid_to_hex(oid));\n-\t\tgoto corrupt;\n-\tcase ULHR_TOO_LONG:\n-\t\tret = error(_(\"header for %s too long, exceeds %d bytes\"),\n-\t\t\t    oid_to_hex(oid), MAX_HEADER_LEN);\n-\t\tgoto corrupt;\n-\t}\n-\n-\tret = 0;\n-\n-corrupt:\n-\tif (ret && (flags & OBJECT_INFO_DIE_IF_CORRUPT))\n-\t\tdie(_(\"loose object %s (stored in %s) is corrupt\"),\n-\t\t    oid_to_hex(oid), path);\n-\n-out:\n-\tif (stream_to_end)\n-\t\tgit_inflate_end(stream_to_end);\n-\tif (map)\n-\t\tmunmap(map, mapsize);\n-\tif (oi) {\n-\t\tif (oi->sizep == &size_scratch)\n-\t\t\toi->sizep = NULL;\n-\t\tif (oi->typep == &type_scratch)\n-\t\t\toi->typep = NULL;\n-\t\tif (oi->delta_base_oid)\n-\t\t\toidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo);\n-\t\tif (!ret)\n-\t\t\toi->whence = OI_LOOSE;\n-\t}\n-\n-\treturn ret;\n-}\n-\n static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c,\n \t\t\t     const void *buf, unsigned long len,\n \t\t\t     struct object_id *oid,\n@@ -1667,13 +1511,13 @@ int read_pack_header(int fd, struct pack_header *header)\n \treturn 0;\n }\n \n-static int for_each_file_in_obj_subdir(unsigned int subdir_nr,\n-\t\t\t\t       struct strbuf *path,\n-\t\t\t\t       const struct git_hash_algo *algop,\n-\t\t\t\t       each_loose_object_fn obj_cb,\n-\t\t\t\t       each_loose_cruft_fn cruft_cb,\n-\t\t\t\t       each_loose_subdir_fn subdir_cb,\n-\t\t\t\t       void *data)\n+int for_each_file_in_obj_subdir(unsigned int subdir_nr,\n+\t\t\t\tstruct strbuf *path,\n+\t\t\t\tconst struct git_hash_algo *algop,\n+\t\t\t\teach_loose_object_fn obj_cb,\n+\t\t\t\teach_loose_cruft_fn cruft_cb,\n+\t\t\t\teach_loose_subdir_fn subdir_cb,\n+\t\t\t\tvoid *data)\n {\n \tsize_t origlen, baselen;\n \tDIR *dir;\n@@ -1758,78 +1602,6 @@ int for_each_loose_file_in_source(struct odb_source *source,\n \treturn r;\n }\n \n-struct for_each_object_wrapper_data {\n-\tstruct odb_source_loose *loose;\n-\tconst struct object_info *request;\n-\todb_for_each_object_cb cb;\n-\tvoid *cb_data;\n-};\n-\n-static int for_each_object_wrapper_cb(const struct object_id *oid,\n-\t\t\t\t      const char *path,\n-\t\t\t\t      void *cb_data)\n-{\n-\tstruct for_each_object_wrapper_data *data = cb_data;\n-\n-\tif (data->request) {\n-\t\tstruct object_info oi = *data->request;\n-\n-\t\tif (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0)\n-\t\t\treturn -1;\n-\n-\t\treturn data->cb(oid, &oi, data->cb_data);\n-\t} else {\n-\t\treturn data->cb(oid, NULL, data->cb_data);\n-\t}\n-}\n-\n-static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid,\n-\t\t\t\t\t       void *node_data UNUSED,\n-\t\t\t\t\t       void *cb_data)\n-{\n-\tstruct for_each_object_wrapper_data *data = cb_data;\n-\tif (data->request) {\n-\t\tstruct object_info oi = *data->request;\n-\n-\t\tif (odb_source_read_object_info(&data->loose->base,\n-\t\t\t\t\t\toid, &oi, 0) < 0)\n-\t\t\treturn -1;\n-\n-\t\treturn data->cb(oid, &oi, data->cb_data);\n-\t} else {\n-\t\treturn data->cb(oid, NULL, data->cb_data);\n-\t}\n-}\n-\n-int odb_source_loose_for_each_object(struct odb_source *source,\n-\t\t\t\t     const struct object_info *request,\n-\t\t\t\t     odb_for_each_object_cb cb,\n-\t\t\t\t     void *cb_data,\n-\t\t\t\t     const struct odb_for_each_object_options *opts)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tstruct for_each_object_wrapper_data data = {\n-\t\t.loose = files->loose,\n-\t\t.request = request,\n-\t\t.cb = cb,\n-\t\t.cb_data = cb_data,\n-\t};\n-\n-\t/* There are no loose promisor objects, so we can return immediately. */\n-\tif ((opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY))\n-\t\treturn 0;\n-\tif ((opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !source->local)\n-\t\treturn 0;\n-\n-\tif (opts->prefix)\n-\t\treturn oidtree_each(odb_source_loose_cache(source, opts->prefix),\n-\t\t\t\t    opts->prefix, opts->prefix_hex_len,\n-\t\t\t\t    for_each_prefixed_object_wrapper_cb, &data);\n-\n-\treturn for_each_loose_file_in_source(source, for_each_object_wrapper_cb,\n-\t\t\t\t\t     NULL, NULL, &data);\n-}\n-\n static int count_loose_object(const struct object_id *oid UNUSED,\n \t\t\t      struct object_info *oi UNUSED,\n \t\t\t      void *payload)\n@@ -1843,6 +1615,7 @@ int odb_source_loose_count_objects(struct odb_source *source,\n \t\t\t\t   enum odb_count_objects_flags flags,\n \t\t\t\t   unsigned long *out)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tconst unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2;\n \tchar *path = NULL;\n \tDIR *dir = NULL;\n@@ -1878,8 +1651,8 @@ int odb_source_loose_count_objects(struct odb_source *source,\n \t} else {\n \t\tstruct odb_for_each_object_options opts = { 0 };\n \t\t*out = 0;\n-\t\tret = odb_source_loose_for_each_object(source, NULL, count_loose_object,\n-\t\t\t\t\t\t       out, &opts);\n+\t\tret = odb_source_for_each_object(&files->loose->base, NULL, count_loose_object,\n+\t\t\t\t\t\t out, &opts);\n \t}\n \n out:\n@@ -1910,6 +1683,7 @@ int odb_source_loose_find_abbrev_len(struct odb_source *source,\n \t\t\t\t     unsigned min_len,\n \t\t\t\t     unsigned *out)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tstruct odb_for_each_object_options opts = {\n \t\t.prefix = oid,\n \t\t.prefix_hex_len = min_len,\n@@ -1920,54 +1694,13 @@ int odb_source_loose_find_abbrev_len(struct odb_source *source,\n \t};\n \tint ret;\n \n-\tret = odb_source_loose_for_each_object(source, NULL, find_abbrev_len_cb,\n-\t\t\t\t\t       &data, &opts);\n+\tret = odb_source_for_each_object(&files->loose->base, NULL, find_abbrev_len_cb,\n+\t\t\t\t\t &data, &opts);\n \t*out = data.len;\n \n \treturn ret;\n }\n \n-static int append_loose_object(const struct object_id *oid,\n-\t\t\t       const char *path UNUSED,\n-\t\t\t       void *data)\n-{\n-\toidtree_insert(data, oid, NULL);\n-\treturn 0;\n-}\n-\n-static struct oidtree *odb_source_loose_cache(struct odb_source *source,\n-\t\t\t\t\t      const struct object_id *oid)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tint subdir_nr = oid->hash[0];\n-\tstruct strbuf buf = STRBUF_INIT;\n-\tsize_t word_bits = bitsizeof(files->loose->subdir_seen[0]);\n-\tsize_t word_index = subdir_nr / word_bits;\n-\tsize_t mask = (size_t)1u << (subdir_nr % word_bits);\n-\tuint32_t *bitmap;\n-\n-\tif (subdir_nr < 0 ||\n-\t    (size_t) subdir_nr >= bitsizeof(files->loose->subdir_seen))\n-\t\tBUG(\"subdir_nr out of range\");\n-\n-\tbitmap = &files->loose->subdir_seen[word_index];\n-\tif (*bitmap & mask)\n-\t\treturn files->loose->cache;\n-\tif (!files->loose->cache) {\n-\t\tALLOC_ARRAY(files->loose->cache, 1);\n-\t\toidtree_init(files->loose->cache);\n-\t}\n-\tstrbuf_addstr(&buf, source->path);\n-\tfor_each_file_in_obj_subdir(subdir_nr, &buf,\n-\t\t\t\t    source->odb->repo->hash_algo,\n-\t\t\t\t    append_loose_object,\n-\t\t\t\t    NULL, NULL,\n-\t\t\t\t    files->loose->cache);\n-\t*bitmap |= mask;\n-\tstrbuf_release(&buf);\n-\treturn files->loose->cache;\n-}\n-\n static int check_stream_oid(git_zstream *stream,\n \t\t\t    const char *hdr,\n \t\t\t    unsigned long size,\ndiff --git a/object-file.h b/object-file.h\nindex d93b7ffad7..9ee5649220 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -6,6 +6,9 @@\n #include \"odb.h\"\n #include \"odb/source-loose.h\"\n \n+/* The maximum size for an object header. */\n+#define MAX_HEADER_LEN 32\n+\n struct index_state;\n \n enum {\n@@ -85,19 +88,13 @@ int for_each_loose_file_in_source(struct odb_source *source,\n \t\t\t\t  each_loose_cruft_fn cruft_cb,\n \t\t\t\t  each_loose_subdir_fn subdir_cb,\n \t\t\t\t  void *data);\n-\n-/*\n- * Iterate through all loose objects in the given object database source and\n- * invoke the callback function for each of them. If an object info request is\n- * given, then the object info will be read for every individual object and\n- * passed to the callback as if `odb_source_loose_read_object_info()` was\n- * called for the object.\n- */\n-int odb_source_loose_for_each_object(struct odb_source *source,\n-\t\t\t\t     const struct object_info *request,\n-\t\t\t\t     odb_for_each_object_cb cb,\n-\t\t\t\t     void *cb_data,\n-\t\t\t\t     const struct odb_for_each_object_options *opts);\n+int for_each_file_in_obj_subdir(unsigned int subdir_nr,\n+\t\t\t\tstruct strbuf *path,\n+\t\t\t\tconst struct git_hash_algo *algop,\n+\t\t\t\teach_loose_object_fn obj_cb,\n+\t\t\t\teach_loose_cruft_fn cruft_cb,\n+\t\t\t\teach_loose_subdir_fn subdir_cb,\n+\t\t\t\tvoid *data);\n \n /*\n  * Count the number of loose objects in this source.\n@@ -188,12 +185,6 @@ int read_loose_object(struct repository *repo,\n \t\t      void **contents,\n \t\t      struct object_info *oi);\n \n-int read_object_info_from_path(struct odb_source_loose *loose,\n-\t\t\t       const char *path,\n-\t\t\t       const struct object_id *oid,\n-\t\t\t       struct object_info *oi,\n-\t\t\t       enum object_info_flags flags);\n-\n enum unpack_loose_header_result {\n \tULHR_OK,\n \tULHR_BAD,\n@@ -217,6 +208,9 @@ enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n \t\t\t\t\t\t    unsigned long mapsize,\n \t\t\t\t\t\t    void *buffer,\n \t\t\t\t\t\t    unsigned long bufsiz);\n+void *unpack_loose_rest(git_zstream *stream,\n+\t\t\tvoid *buffer, unsigned long size,\n+\t\t\tconst struct object_id *oid);\n \n int parse_loose_header(const char *hdr, struct object_info *oi);\n \ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 90806ddf86..676a641739 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -82,7 +82,7 @@ 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_loose_for_each_object(source, request, cb, cb_data, opts);\n+\t\tret = odb_source_for_each_object(&files->loose->base, request, cb, cb_data, opts);\n \t\tif (ret)\n \t\t\treturn ret;\n \t}\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 4b82c6f316..4e8b923498 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -2,6 +2,7 @@\n #include \"abspath.h\"\n #include \"chdir-notify.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n #include \"loose.h\"\n #include \"object-file.h\"\n #include \"odb.h\"\n@@ -9,8 +10,198 @@\n #include \"odb/source-loose.h\"\n #include \"odb/streaming.h\"\n #include \"oidtree.h\"\n+#include \"repository.h\"\n #include \"strbuf.h\"\n \n+static int append_loose_object(const struct object_id *oid,\n+\t\t\t       const char *path UNUSED,\n+\t\t\t       void *data)\n+{\n+\toidtree_insert(data, oid, NULL);\n+\treturn 0;\n+}\n+\n+static struct oidtree *odb_source_loose_cache(struct odb_source_loose *loose,\n+\t\t\t\t\t      const struct object_id *oid)\n+{\n+\tint subdir_nr = oid->hash[0];\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tsize_t word_bits = bitsizeof(loose->subdir_seen[0]);\n+\tsize_t word_index = subdir_nr / word_bits;\n+\tsize_t mask = (size_t)1u << (subdir_nr % word_bits);\n+\tuint32_t *bitmap;\n+\n+\tif (subdir_nr < 0 ||\n+\t    (size_t) subdir_nr >= bitsizeof(loose->subdir_seen))\n+\t\tBUG(\"subdir_nr out of range\");\n+\n+\tbitmap = &loose->subdir_seen[word_index];\n+\tif (*bitmap & mask)\n+\t\treturn loose->cache;\n+\tif (!loose->cache) {\n+\t\tALLOC_ARRAY(loose->cache, 1);\n+\t\toidtree_init(loose->cache);\n+\t}\n+\tstrbuf_addstr(&buf, loose->base.path);\n+\tfor_each_file_in_obj_subdir(subdir_nr, &buf,\n+\t\t\t\t    loose->base.odb->repo->hash_algo,\n+\t\t\t\t    append_loose_object,\n+\t\t\t\t    NULL, NULL,\n+\t\t\t\t    loose->cache);\n+\t*bitmap |= mask;\n+\tstrbuf_release(&buf);\n+\treturn loose->cache;\n+}\n+\n+static int quick_has_loose(struct odb_source_loose *loose,\n+\t\t\t   const struct object_id *oid)\n+{\n+\treturn !!oidtree_contains(odb_source_loose_cache(loose, oid), oid);\n+}\n+\n+static int read_object_info_from_path(struct odb_source_loose *loose,\n+\t\t\t\t      const char *path,\n+\t\t\t\t      const struct object_id *oid,\n+\t\t\t\t      struct object_info *oi,\n+\t\t\t\t      enum object_info_flags flags)\n+{\n+\tint ret;\n+\tint fd;\n+\tunsigned long mapsize;\n+\tvoid *map = NULL;\n+\tgit_zstream stream, *stream_to_end = NULL;\n+\tchar hdr[MAX_HEADER_LEN];\n+\tunsigned long size_scratch;\n+\tenum object_type type_scratch;\n+\tstruct stat st;\n+\n+\t/*\n+\t * If we don't care about type or size, then we don't\n+\t * need to look inside the object at all. Note that we\n+\t * do not optimize out the stat call, even if the\n+\t * caller doesn't care about the disk-size, since our\n+\t * return value implicitly indicates whether the\n+\t * object even exists.\n+\t */\n+\tif (!oi || (!oi->typep && !oi->sizep && !oi->contentp)) {\n+\t\tstruct stat st;\n+\n+\t\tif ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) {\n+\t\t\tret = quick_has_loose(loose, oid) ? 0 : -1;\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (lstat(path, &st) < 0) {\n+\t\t\tret = -1;\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (oi) {\n+\t\t\tif (oi->disk_sizep)\n+\t\t\t\t*oi->disk_sizep = st.st_size;\n+\t\t\tif (oi->mtimep)\n+\t\t\t\t*oi->mtimep = st.st_mtime;\n+\t\t}\n+\n+\t\tret = 0;\n+\t\tgoto out;\n+\t}\n+\n+\tfd = git_open(path);\n+\tif (fd < 0) {\n+\t\tif (errno != ENOENT)\n+\t\t\terror_errno(_(\"unable to open loose object %s\"), oid_to_hex(oid));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tif (fstat(fd, &st)) {\n+\t\tclose(fd);\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tmapsize = xsize_t(st.st_size);\n+\tif (!mapsize) {\n+\t\tclose(fd);\n+\t\tret = error(_(\"object file %s is empty\"), path);\n+\t\tgoto out;\n+\t}\n+\n+\tmap = xmmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, fd, 0);\n+\tclose(fd);\n+\tif (!map) {\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tif (oi->disk_sizep)\n+\t\t*oi->disk_sizep = mapsize;\n+\tif (oi->mtimep)\n+\t\t*oi->mtimep = st.st_mtime;\n+\n+\tstream_to_end = &stream;\n+\n+\tswitch (unpack_loose_header(&stream, map, mapsize, hdr, sizeof(hdr))) {\n+\tcase ULHR_OK:\n+\t\tif (!oi->sizep)\n+\t\t\toi->sizep = &size_scratch;\n+\t\tif (!oi->typep)\n+\t\t\toi->typep = &type_scratch;\n+\n+\t\tif (parse_loose_header(hdr, oi) < 0) {\n+\t\t\tret = error(_(\"unable to parse %s header\"), oid_to_hex(oid));\n+\t\t\tgoto corrupt;\n+\t\t}\n+\n+\t\tif (*oi->typep < 0)\n+\t\t\tdie(_(\"invalid object type\"));\n+\n+\t\tif (oi->contentp) {\n+\t\t\t*oi->contentp = unpack_loose_rest(&stream, hdr, *oi->sizep, oid);\n+\t\t\tif (!*oi->contentp) {\n+\t\t\t\tret = -1;\n+\t\t\t\tgoto corrupt;\n+\t\t\t}\n+\t\t}\n+\n+\t\tbreak;\n+\tcase ULHR_BAD:\n+\t\tret = error(_(\"unable to unpack %s header\"),\n+\t\t\t    oid_to_hex(oid));\n+\t\tgoto corrupt;\n+\tcase ULHR_TOO_LONG:\n+\t\tret = error(_(\"header for %s too long, exceeds %d bytes\"),\n+\t\t\t    oid_to_hex(oid), MAX_HEADER_LEN);\n+\t\tgoto corrupt;\n+\t}\n+\n+\tret = 0;\n+\n+corrupt:\n+\tif (ret && (flags & OBJECT_INFO_DIE_IF_CORRUPT))\n+\t\tdie(_(\"loose object %s (stored in %s) is corrupt\"),\n+\t\t    oid_to_hex(oid), path);\n+\n+out:\n+\tif (stream_to_end)\n+\t\tgit_inflate_end(stream_to_end);\n+\tif (map)\n+\t\tmunmap(map, mapsize);\n+\tif (oi) {\n+\t\tif (oi->sizep == &size_scratch)\n+\t\t\toi->sizep = NULL;\n+\t\tif (oi->typep == &type_scratch)\n+\t\t\toi->typep = NULL;\n+\t\tif (oi->delta_base_oid)\n+\t\t\toidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo);\n+\t\tif (!ret)\n+\t\t\toi->whence = OI_LOOSE;\n+\t}\n+\n+\treturn ret;\n+}\n+\n static int odb_source_loose_read_object_info(struct odb_source *source,\n \t\t\t\t\t     const struct object_id *oid,\n \t\t\t\t\t     struct object_info *oi,\n@@ -218,6 +409,78 @@ static int odb_source_loose_read_object_stream(struct odb_read_stream **out,\n \treturn -1;\n }\n \n+struct for_each_object_wrapper_data {\n+\tstruct odb_source_loose *loose;\n+\tconst struct object_info *request;\n+\todb_for_each_object_cb cb;\n+\tvoid *cb_data;\n+};\n+\n+static int for_each_object_wrapper_cb(const struct object_id *oid,\n+\t\t\t\t      const char *path,\n+\t\t\t\t      void *cb_data)\n+{\n+\tstruct for_each_object_wrapper_data *data = cb_data;\n+\n+\tif (data->request) {\n+\t\tstruct object_info oi = *data->request;\n+\n+\t\tif (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0)\n+\t\t\treturn -1;\n+\n+\t\treturn data->cb(oid, &oi, data->cb_data);\n+\t} else {\n+\t\treturn data->cb(oid, NULL, data->cb_data);\n+\t}\n+}\n+\n+static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid,\n+\t\t\t\t\t       void *node_data UNUSED,\n+\t\t\t\t\t       void *cb_data)\n+{\n+\tstruct for_each_object_wrapper_data *data = cb_data;\n+\tif (data->request) {\n+\t\tstruct object_info oi = *data->request;\n+\n+\t\tif (odb_source_read_object_info(&data->loose->base,\n+\t\t\t\t\t\toid, &oi, 0) < 0)\n+\t\t\treturn -1;\n+\n+\t\treturn data->cb(oid, &oi, data->cb_data);\n+\t} else {\n+\t\treturn data->cb(oid, NULL, data->cb_data);\n+\t}\n+}\n+\n+static int odb_source_loose_for_each_object(struct odb_source *source,\n+\t\t\t\t\t    const struct object_info *request,\n+\t\t\t\t\t    odb_for_each_object_cb cb,\n+\t\t\t\t\t    void *cb_data,\n+\t\t\t\t\t    const struct odb_for_each_object_options *opts)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tstruct for_each_object_wrapper_data data = {\n+\t\t.loose = loose,\n+\t\t.request = request,\n+\t\t.cb = cb,\n+\t\t.cb_data = cb_data,\n+\t};\n+\n+\t/* There are no loose promisor objects, so we can return immediately. */\n+\tif ((opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY))\n+\t\treturn 0;\n+\tif ((opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !source->local)\n+\t\treturn 0;\n+\n+\tif (opts->prefix)\n+\t\treturn oidtree_each(odb_source_loose_cache(loose, opts->prefix),\n+\t\t\t\t    opts->prefix, opts->prefix_hex_len,\n+\t\t\t\t    for_each_prefixed_object_wrapper_cb, &data);\n+\n+\treturn for_each_loose_file_in_source(source, for_each_object_wrapper_cb,\n+\t\t\t\t\t     NULL, NULL, &data);\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -273,6 +536,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.reprepare = odb_source_loose_reprepare;\n \tloose->base.read_object_info = odb_source_loose_read_object_info;\n \tloose->base.read_object_stream = odb_source_loose_read_object_stream;\n+\tloose->base.for_each_object = odb_source_loose_for_each_object;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544364","messageId":"20260601-b4-pks-odb-source-loose-v2-9-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 09/18] odb/source-loose: wire up `find_abbrev_len()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:32Z","receivedAt":"2026-06-01T08:20:52Z","isPatch":true,"body":"Move `odb_source_loose_find_abbrev_len()` and its associated helpers\nfrom \"object-file.c\" into \"odb/source-loose.c\" and wire it up as the\n`find_abbrev_len` callback of the loose source.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 39 ---------------------------------------\n object-file.h      | 12 ------------\n odb/source-files.c |  2 +-\n odb/source-loose.c | 40 ++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 41 insertions(+), 52 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 157ecad3ea..11957aa44f 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1662,45 +1662,6 @@ int odb_source_loose_count_objects(struct odb_source *source,\n \treturn ret;\n }\n \n-struct find_abbrev_len_data {\n-\tconst struct object_id *oid;\n-\tunsigned len;\n-};\n-\n-static int find_abbrev_len_cb(const struct object_id *oid,\n-\t\t\t      struct object_info *oi UNUSED,\n-\t\t\t      void *cb_data)\n-{\n-\tstruct find_abbrev_len_data *data = cb_data;\n-\tunsigned len = oid_common_prefix_hexlen(oid, data->oid);\n-\tif (len != hash_algos[oid->algo].hexsz && len >= data->len)\n-\t\tdata->len = len + 1;\n-\treturn 0;\n-}\n-\n-int odb_source_loose_find_abbrev_len(struct odb_source *source,\n-\t\t\t\t     const struct object_id *oid,\n-\t\t\t\t     unsigned min_len,\n-\t\t\t\t     unsigned *out)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tstruct odb_for_each_object_options opts = {\n-\t\t.prefix = oid,\n-\t\t.prefix_hex_len = min_len,\n-\t};\n-\tstruct find_abbrev_len_data data = {\n-\t\t.oid = oid,\n-\t\t.len = min_len,\n-\t};\n-\tint ret;\n-\n-\tret = odb_source_for_each_object(&files->loose->base, NULL, find_abbrev_len_cb,\n-\t\t\t\t\t &data, &opts);\n-\t*out = data.len;\n-\n-\treturn ret;\n-}\n-\n static int check_stream_oid(git_zstream *stream,\n \t\t\t    const char *hdr,\n \t\t\t    unsigned long size,\ndiff --git a/object-file.h b/object-file.h\nindex 9ee5649220..96760db0e1 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -110,18 +110,6 @@ int odb_source_loose_count_objects(struct odb_source *source,\n \t\t\t\t   enum odb_count_objects_flags flags,\n \t\t\t\t   unsigned long *out);\n \n-/*\n- * Find the shortest unique prefix for the given object ID, where `min_len` is\n- * the minimum length that the prefix should have.\n- *\n- * Returns 0 on success, in which case the computed length will be written to\n- * `out`. Otherwise, a negative error code is returned.\n- */\n-int odb_source_loose_find_abbrev_len(struct odb_source *source,\n-\t\t\t\t     const struct object_id *oid,\n-\t\t\t\t     unsigned min_len,\n-\t\t\t\t     unsigned *out);\n-\n /**\n  * format_object_header() is a thin wrapper around s xsnprintf() that\n  * writes the initial \"<type> <obj-len>\" part of the loose object\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 676a641739..4a54b10e4a 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -136,7 +136,7 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source,\n \tif (ret < 0)\n \t\tgoto out;\n \n-\tret = odb_source_loose_find_abbrev_len(source, oid, len, &len);\n+\tret = odb_source_find_abbrev_len(&files->loose->base, oid, len, &len);\n \tif (ret < 0)\n \t\tgoto out;\n \ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 4e8b923498..4b8d10bc87 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -481,6 +481,45 @@ static int odb_source_loose_for_each_object(struct odb_source *source,\n \t\t\t\t\t     NULL, NULL, &data);\n }\n \n+struct find_abbrev_len_data {\n+\tconst struct object_id *oid;\n+\tunsigned len;\n+};\n+\n+static int find_abbrev_len_cb(const struct object_id *oid,\n+\t\t\t      struct object_info *oi UNUSED,\n+\t\t\t      void *cb_data)\n+{\n+\tstruct find_abbrev_len_data *data = cb_data;\n+\tunsigned len = oid_common_prefix_hexlen(oid, data->oid);\n+\tif (len != hash_algos[oid->algo].hexsz && len >= data->len)\n+\t\tdata->len = len + 1;\n+\treturn 0;\n+}\n+\n+static int odb_source_loose_find_abbrev_len(struct odb_source *source,\n+\t\t\t\t\t    const struct object_id *oid,\n+\t\t\t\t\t    unsigned min_len,\n+\t\t\t\t\t    unsigned *out)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tstruct odb_for_each_object_options opts = {\n+\t\t.prefix = oid,\n+\t\t.prefix_hex_len = min_len,\n+\t};\n+\tstruct find_abbrev_len_data data = {\n+\t\t.oid = oid,\n+\t\t.len = min_len,\n+\t};\n+\tint ret;\n+\n+\tret = odb_source_for_each_object(&loose->base, NULL, find_abbrev_len_cb,\n+\t\t\t\t\t &data, &opts);\n+\t*out = data.len;\n+\n+\treturn ret;\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -537,6 +576,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.read_object_info = odb_source_loose_read_object_info;\n \tloose->base.read_object_stream = odb_source_loose_read_object_stream;\n \tloose->base.for_each_object = odb_source_loose_for_each_object;\n+\tloose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544365","messageId":"20260601-b4-pks-odb-source-loose-v2-10-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 10/18] odb/source-loose: wire up `count_objects()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:33Z","receivedAt":"2026-06-01T08:20:55Z","isPatch":true,"body":"Move `odb_source_loose_count_objects()` and its associated helpers from\n\"object-file.c\" into \"odb/source-loose.c\" and wire it up as the\n`count_objects()` callback of the loose source.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/gc.c       |  6 +++---\n object-file.c      | 60 -----------------------------------------------------\n object-file.h      | 14 -------------\n odb/source-files.c |  2 +-\n odb/source-loose.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 65 insertions(+), 78 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 84a66d3240..c26c93ee0f 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -466,6 +466,7 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED)\n \n static int too_many_loose_objects(int limit)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources);\n \t/*\n \t * This is weird, but stems from legacy behaviour: the GC auto\n \t * threshold was always essentially interpreted as if it was rounded up\n@@ -474,9 +475,8 @@ static int too_many_loose_objects(int limit)\n \tint auto_threshold = DIV_ROUND_UP(limit, 256) * 256;\n \tunsigned long loose_count;\n \n-\tif (odb_source_loose_count_objects(the_repository->objects->sources,\n-\t\t\t\t\t   ODB_COUNT_OBJECTS_APPROXIMATE,\n-\t\t\t\t\t   &loose_count) < 0)\n+\tif (odb_source_count_objects(&files->loose->base, ODB_COUNT_OBJECTS_APPROXIMATE,\n+\t\t\t\t     &loose_count) < 0)\n \t\treturn 0;\n \n \treturn loose_count > auto_threshold;\ndiff --git a/object-file.c b/object-file.c\nindex 11957aa44f..9b2044de37 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1602,66 +1602,6 @@ int for_each_loose_file_in_source(struct odb_source *source,\n \treturn r;\n }\n \n-static int count_loose_object(const struct object_id *oid UNUSED,\n-\t\t\t      struct object_info *oi UNUSED,\n-\t\t\t      void *payload)\n-{\n-\tunsigned long *count = payload;\n-\t(*count)++;\n-\treturn 0;\n-}\n-\n-int odb_source_loose_count_objects(struct odb_source *source,\n-\t\t\t\t   enum odb_count_objects_flags flags,\n-\t\t\t\t   unsigned long *out)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tconst unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2;\n-\tchar *path = NULL;\n-\tDIR *dir = NULL;\n-\tint ret;\n-\n-\tif (flags & ODB_COUNT_OBJECTS_APPROXIMATE) {\n-\t\tunsigned long count = 0;\n-\t\tstruct dirent *ent;\n-\n-\t\tpath = xstrfmt(\"%s/17\", source->path);\n-\n-\t\tdir = opendir(path);\n-\t\tif (!dir) {\n-\t\t\tif (errno == ENOENT) {\n-\t\t\t\t*out = 0;\n-\t\t\t\tret = 0;\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\n-\t\t\tret = error_errno(\"cannot open object shard '%s'\", path);\n-\t\t\tgoto out;\n-\t\t}\n-\n-\t\twhile ((ent = readdir(dir)) != NULL) {\n-\t\t\tif (strspn(ent->d_name, \"0123456789abcdef\") != hexsz ||\n-\t\t\t    ent->d_name[hexsz] != '\\0')\n-\t\t\t\tcontinue;\n-\t\t\tcount++;\n-\t\t}\n-\n-\t\t*out = count * 256;\n-\t\tret = 0;\n-\t} else {\n-\t\tstruct odb_for_each_object_options opts = { 0 };\n-\t\t*out = 0;\n-\t\tret = odb_source_for_each_object(&files->loose->base, NULL, count_loose_object,\n-\t\t\t\t\t\t out, &opts);\n-\t}\n-\n-out:\n-\tif (dir)\n-\t\tclosedir(dir);\n-\tfree(path);\n-\treturn ret;\n-}\n-\n static int check_stream_oid(git_zstream *stream,\n \t\t\t    const char *hdr,\n \t\t\t    unsigned long size,\ndiff --git a/object-file.h b/object-file.h\nindex 96760db0e1..bc72d89f54 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -96,20 +96,6 @@ int for_each_file_in_obj_subdir(unsigned int subdir_nr,\n \t\t\t\teach_loose_subdir_fn subdir_cb,\n \t\t\t\tvoid *data);\n \n-/*\n- * Count the number of loose objects in this source.\n- *\n- * The object count is approximated by opening a single sharding directory for\n- * loose objects and scanning its contents. The result is then extrapolated by\n- * 256. This should generally work as a reasonable estimate given that the\n- * object hash is supposed to be indistinguishable from random.\n- *\n- * Returns 0 on success, a negative error code otherwise.\n- */\n-int odb_source_loose_count_objects(struct odb_source *source,\n-\t\t\t\t   enum odb_count_objects_flags flags,\n-\t\t\t\t   unsigned long *out);\n-\n /**\n  * format_object_header() is a thin wrapper around s xsnprintf() that\n  * writes the initial \"<type> <obj-len>\" part of the loose object\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 4a54b10e4a..d5454e170d 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -109,7 +109,7 @@ static int odb_source_files_count_objects(struct odb_source *source,\n \tif (!(flags & ODB_COUNT_OBJECTS_APPROXIMATE)) {\n \t\tunsigned long loose_count;\n \n-\t\tret = odb_source_loose_count_objects(source, flags, &loose_count);\n+\t\tret = odb_source_count_objects(&files->loose->base, flags, &loose_count);\n \t\tif (ret < 0)\n \t\t\tgoto out;\n \ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 4b8d10bc87..27be066327 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -520,6 +520,66 @@ static int odb_source_loose_find_abbrev_len(struct odb_source *source,\n \treturn ret;\n }\n \n+static int count_loose_object(const struct object_id *oid UNUSED,\n+\t\t\t      struct object_info *oi UNUSED,\n+\t\t\t      void *payload)\n+{\n+\tunsigned long *count = payload;\n+\t(*count)++;\n+\treturn 0;\n+}\n+\n+static int odb_source_loose_count_objects(struct odb_source *source,\n+\t\t\t\t\t  enum odb_count_objects_flags flags,\n+\t\t\t\t\t  unsigned long *out)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tconst unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2;\n+\tchar *path = NULL;\n+\tDIR *dir = NULL;\n+\tint ret;\n+\n+\tif (flags & ODB_COUNT_OBJECTS_APPROXIMATE) {\n+\t\tunsigned long count = 0;\n+\t\tstruct dirent *ent;\n+\n+\t\tpath = xstrfmt(\"%s/17\", source->path);\n+\n+\t\tdir = opendir(path);\n+\t\tif (!dir) {\n+\t\t\tif (errno == ENOENT) {\n+\t\t\t\t*out = 0;\n+\t\t\t\tret = 0;\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\n+\t\t\tret = error_errno(\"cannot open object shard '%s'\", path);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\twhile ((ent = readdir(dir)) != NULL) {\n+\t\t\tif (strspn(ent->d_name, \"0123456789abcdef\") != hexsz ||\n+\t\t\t    ent->d_name[hexsz] != '\\0')\n+\t\t\t\tcontinue;\n+\t\t\tcount++;\n+\t\t}\n+\n+\t\t*out = count * 256;\n+\t\tret = 0;\n+\t} else {\n+\t\tstruct odb_for_each_object_options opts = { 0 };\n+\t\t*out = 0;\n+\t\tret = odb_source_for_each_object(&loose->base, NULL, count_loose_object,\n+\t\t\t\t\t\t out, &opts);\n+\t}\n+\n+out:\n+\tif (dir)\n+\t\tclosedir(dir);\n+\tfree(path);\n+\treturn ret;\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -577,6 +637,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.read_object_stream = odb_source_loose_read_object_stream;\n \tloose->base.for_each_object = odb_source_loose_for_each_object;\n \tloose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;\n+\tloose->base.count_objects = odb_source_loose_count_objects;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544366","messageId":"20260601-b4-pks-odb-source-loose-v2-11-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 11/18] odb/source-loose: drop `odb_source_loose_has_object()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:34Z","receivedAt":"2026-06-01T08:20:58Z","isPatch":true,"body":"The function `odb_source_loose_has_object()` checks whether a specific\nobject exists as a loose object on disk by using lstat(3p). This\ninterface is somewhat redundant, as we typically check for object\nexistence in a generic way via `odb_source_read_object_info()`.\n\nIn fact, these two calls are redundant in case the latter is called in a\nspecific way: when called without an object info request and without the\n`OBJECT_INFO_QUICK` flag, then we will end up doing the same call to\nlstat(3p) in `read_object_info_from_path()`.\n\nDrop the function and adapt callers to instead use the generic\ninterface so that its calling conventions align with that of other\nsources.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/pack-objects.c | 12 ++++++++----\n object-file.c          | 12 ++++--------\n object-file.h          |  8 --------\n 3 files changed, 12 insertions(+), 20 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 480cc0bd8c..a6be3d659f 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -1750,9 +1750,11 @@ static int want_object_in_pack_mtime(const struct object_id *oid,\n \t\t * skip the local object source.\n \t\t */\n \t\tstruct odb_source *source = the_repository->objects->sources->next;\n-\t\tfor (; source; source = source->next)\n-\t\t\tif (odb_source_loose_has_object(source, oid))\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))\n \t\t\t\treturn 0;\n+\t\t}\n \t}\n \n \t/*\n@@ -4135,9 +4137,11 @@ static void add_cruft_object_entry(const struct object_id *oid, enum object_type\n \t\t\tstruct odb_source *source = the_repository->objects->sources;\n \t\t\tint found = 0;\n \n-\t\t\tfor (; !found && source; source = source->next)\n-\t\t\t\tif (odb_source_loose_has_object(source, oid))\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))\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\ndiff --git a/object-file.c b/object-file.c\nindex 9b2044de37..c83136cf70 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -96,12 +96,6 @@ static int check_and_freshen_source(struct odb_source *source,\n \treturn check_and_freshen_file(path.buf, freshen);\n }\n \n-int odb_source_loose_has_object(struct odb_source *source,\n-\t\t\t\tconst struct object_id *oid)\n-{\n-\treturn check_and_freshen_source(source, oid, 0);\n-}\n-\n int format_object_header(char *str, size_t size, enum object_type type,\n \t\t\t size_t objsize)\n {\n@@ -1000,9 +994,11 @@ int force_object_loose(struct odb_source *source,\n \tint hdrlen;\n \tint ret;\n \n-\tfor (struct odb_source *s = source->odb->sources; s; s = s->next)\n-\t\tif (odb_source_loose_has_object(s, oid))\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))\n \t\t\treturn 0;\n+\t}\n \n \toi.typep = &type;\n \toi.sizep = &len;\ndiff --git a/object-file.h b/object-file.h\nindex bc72d89f54..506ca6be40 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -23,14 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa\n struct object_info;\n struct odb_source;\n \n-/*\n- * Return true iff an object database source has a loose object\n- * with the specified name.  This function does not respect replace\n- * references.\n- */\n-int odb_source_loose_has_object(struct odb_source *source,\n-\t\t\t\tconst struct object_id *oid);\n-\n int odb_source_loose_freshen_object(struct odb_source *source,\n \t\t\t\t    const struct object_id *oid);\n \n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544367","messageId":"20260601-b4-pks-odb-source-loose-v2-12-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 12/18] odb/source-loose: wire up `freshen_object()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:35Z","receivedAt":"2026-06-01T08:21:00Z","isPatch":true,"body":"Move `odb_source_loose_freshen_object()` from \"object-file.c\" into\n\"odb/source-loose.c\" and wire it up as the `freshen_object()` callback\nof the loose source.\n\nAs part of the move, `check_and_freshen_source()` is inlined into the\ncallback function, as it has no other callers anymore.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 15 ---------------\n object-file.h      |  3 ---\n odb/source-files.c |  2 +-\n odb/source-loose.c |  9 +++++++++\n 4 files changed, 10 insertions(+), 19 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex c83136cf70..0689a4e67b 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -87,15 +87,6 @@ int check_and_freshen_file(const char *fn, int freshen)\n \treturn 1;\n }\n \n-static int check_and_freshen_source(struct odb_source *source,\n-\t\t\t\t    const struct object_id *oid,\n-\t\t\t\t    int freshen)\n-{\n-\tstatic struct strbuf path = STRBUF_INIT;\n-\todb_loose_path(source, &path, oid);\n-\treturn check_and_freshen_file(path.buf, freshen);\n-}\n-\n int format_object_header(char *str, size_t size, enum object_type type,\n \t\t\t size_t objsize)\n {\n@@ -815,12 +806,6 @@ static int write_loose_object(struct odb_source *source,\n \t\t\t\t\t  FOF_SKIP_COLLISION_CHECK);\n }\n \n-int odb_source_loose_freshen_object(struct odb_source *source,\n-\t\t\t\t    const struct object_id *oid)\n-{\n-\treturn !!check_and_freshen_source(source, oid, 1);\n-}\n-\n int odb_source_loose_write_stream(struct odb_source *source,\n \t\t\t\t  struct odb_write_stream *in_stream, size_t len,\n \t\t\t\t  struct object_id *oid)\ndiff --git a/object-file.h b/object-file.h\nindex 506ca6be40..1d90df9d98 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -23,9 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa\n struct object_info;\n struct odb_source;\n \n-int odb_source_loose_freshen_object(struct odb_source *source,\n-\t\t\t\t    const struct object_id *oid);\n-\n int odb_source_loose_write_object(struct odb_source *source,\n \t\t\t\t  const void *buf, unsigned long len,\n \t\t\t\t  enum object_type type, struct object_id *oid,\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex d5454e170d..ef548e6fe6 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -152,7 +152,7 @@ static int odb_source_files_freshen_object(struct odb_source *source,\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tif (packfile_store_freshen_object(files->packed, oid) ||\n-\t    odb_source_loose_freshen_object(source, oid))\n+\t    odb_source_freshen_object(&files->loose->base, oid))\n \t\treturn 1;\n \treturn 0;\n }\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 27be066327..e519365d23 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -580,6 +580,14 @@ static int odb_source_loose_count_objects(struct odb_source *source,\n \treturn ret;\n }\n \n+static int odb_source_loose_freshen_object(struct odb_source *source,\n+\t\t\t\t\t   const struct object_id *oid)\n+{\n+\tstatic struct strbuf path = STRBUF_INIT;\n+\todb_loose_path(source, &path, oid);\n+\treturn !!check_and_freshen_file(path.buf, 1);\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -638,6 +646,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.for_each_object = odb_source_loose_for_each_object;\n \tloose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;\n \tloose->base.count_objects = odb_source_loose_count_objects;\n+\tloose->base.freshen_object = odb_source_loose_freshen_object;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544368","messageId":"20260601-b4-pks-odb-source-loose-v2-13-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 13/18] loose: refactor object map to operate on `struct odb_source_loose`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:36Z","receivedAt":"2026-06-01T08:21:03Z","isPatch":true,"body":"While the loose object map functions in \"loose.c\" accept a generic\n`struct odb_source *`, they always expect this to be the \"files\"\nbackend. Furthermore, the subsystem doesn't even care about the \"files\"\nbackend, but only uses it as a stepping stone to get to the \"loose\"\nbackend.\n\nThis assumption is implicit and thus not immediately obvious. Refactor\nthe interfaces to instead operate on a `struct odb_source_loose`\ninstead, which eliminates the implicit dependency and unnecessary detour\nvia the \"files\" source.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n loose.c       | 45 ++++++++++++++++++++++-----------------------\n loose.h       |  4 ++--\n object-file.c |  9 ++++++---\n 3 files changed, 30 insertions(+), 28 deletions(-)\n\ndiff --git a/loose.c b/loose.c\nindex f7a3dd1a72..0b626c1b85 100644\n--- a/loose.c\n+++ b/loose.c\n@@ -46,38 +46,36 @@ static int insert_oid_pair(kh_oid_map_t *map, const struct object_id *key, const\n \treturn 1;\n }\n \n-static int insert_loose_map(struct odb_source *source,\n+static int insert_loose_map(struct odb_source_loose *loose,\n \t\t\t    const struct object_id *oid,\n \t\t\t    const struct object_id *compat_oid)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tstruct loose_object_map *map = files->loose->map;\n+\tstruct loose_object_map *map = loose->map;\n \tint inserted = 0;\n \n \tinserted |= insert_oid_pair(map->to_compat, oid, compat_oid);\n \tinserted |= insert_oid_pair(map->to_storage, compat_oid, oid);\n \tif (inserted)\n-\t\toidtree_insert(files->loose->cache, compat_oid, NULL);\n+\t\toidtree_insert(loose->cache, compat_oid, NULL);\n \n \treturn inserted;\n }\n \n-static int load_one_loose_object_map(struct repository *repo, struct odb_source *source)\n+static int load_one_loose_object_map(struct repository *repo, struct odb_source_loose *loose)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tstruct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;\n \tFILE *fp;\n \n-\tif (!files->loose->map)\n-\t\tloose_object_map_init(&files->loose->map);\n-\tif (!files->loose->cache) {\n-\t\tALLOC_ARRAY(files->loose->cache, 1);\n-\t\toidtree_init(files->loose->cache);\n+\tif (!loose->map)\n+\t\tloose_object_map_init(&loose->map);\n+\tif (!loose->cache) {\n+\t\tALLOC_ARRAY(loose->cache, 1);\n+\t\toidtree_init(loose->cache);\n \t}\n \n-\tinsert_loose_map(source, repo->hash_algo->empty_tree, repo->compat_hash_algo->empty_tree);\n-\tinsert_loose_map(source, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob);\n-\tinsert_loose_map(source, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid);\n+\tinsert_loose_map(loose, repo->hash_algo->empty_tree, repo->compat_hash_algo->empty_tree);\n+\tinsert_loose_map(loose, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob);\n+\tinsert_loose_map(loose, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid);\n \n \trepo_common_path_replace(repo, &path, \"objects/loose-object-idx\");\n \tfp = fopen(path.buf, \"rb\");\n@@ -97,7 +95,7 @@ static int load_one_loose_object_map(struct repository *repo, struct odb_source\n \t\t    parse_oid_hex_algop(p, &compat_oid, &p, repo->compat_hash_algo) ||\n \t\t    p != buf.buf + buf.len)\n \t\t\tgoto err;\n-\t\tinsert_loose_map(source, &oid, &compat_oid);\n+\t\tinsert_loose_map(loose, &oid, &compat_oid);\n \t}\n \n \tstrbuf_release(&buf);\n@@ -119,7 +117,8 @@ int repo_read_loose_object_map(struct repository *repo)\n \todb_prepare_alternates(repo->objects);\n \n \tfor (source = repo->objects->sources; source; source = source->next) {\n-\t\tif (load_one_loose_object_map(repo, source) < 0) {\n+\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n+\t\tif (load_one_loose_object_map(repo, files->loose) < 0) {\n \t\t\treturn -1;\n \t\t}\n \t}\n@@ -171,7 +170,7 @@ int repo_write_loose_object_map(struct repository *repo)\n \treturn -1;\n }\n \n-static int write_one_object(struct odb_source *source,\n+static int write_one_object(struct odb_source_loose *loose,\n \t\t\t    const struct object_id *oid,\n \t\t\t    const struct object_id *compat_oid)\n {\n@@ -180,7 +179,7 @@ static int write_one_object(struct odb_source *source,\n \tstruct stat st;\n \tstruct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;\n \n-\tstrbuf_addf(&path, \"%s/loose-object-idx\", source->path);\n+\tstrbuf_addf(&path, \"%s/loose-object-idx\", loose->base.path);\n \thold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);\n \n \tfd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666);\n@@ -196,7 +195,7 @@ static int write_one_object(struct odb_source *source,\n \t\tgoto errout;\n \tif (close(fd))\n \t\tgoto errout;\n-\tadjust_shared_perm(source->odb->repo, path.buf);\n+\tadjust_shared_perm(loose->base.odb->repo, path.buf);\n \trollback_lock_file(&lock);\n \tstrbuf_release(&buf);\n \tstrbuf_release(&path);\n@@ -210,18 +209,18 @@ static int write_one_object(struct odb_source *source,\n \treturn -1;\n }\n \n-int repo_add_loose_object_map(struct odb_source *source,\n+int repo_add_loose_object_map(struct odb_source_loose *loose,\n \t\t\t      const struct object_id *oid,\n \t\t\t      const struct object_id *compat_oid)\n {\n \tint inserted = 0;\n \n-\tif (!should_use_loose_object_map(source->odb->repo))\n+\tif (!should_use_loose_object_map(loose->base.odb->repo))\n \t\treturn 0;\n \n-\tinserted = insert_loose_map(source, oid, compat_oid);\n+\tinserted = insert_loose_map(loose, oid, compat_oid);\n \tif (inserted)\n-\t\treturn write_one_object(source, oid, compat_oid);\n+\t\treturn write_one_object(loose, oid, compat_oid);\n \treturn 0;\n }\n \ndiff --git a/loose.h b/loose.h\nindex 6af1702973..6c9b3f4571 100644\n--- a/loose.h\n+++ b/loose.h\n@@ -4,7 +4,7 @@\n #include \"khash.h\"\n \n struct repository;\n-struct odb_source;\n+struct odb_source_loose;\n \n struct loose_object_map {\n \tkh_oid_map_t *to_compat;\n@@ -17,7 +17,7 @@ int repo_loose_object_map_oid(struct repository *repo,\n \t\t\t      const struct object_id *src,\n \t\t\t      const struct git_hash_algo *dest_algo,\n \t\t\t      struct object_id *dest);\n-int repo_add_loose_object_map(struct odb_source *source,\n+int repo_add_loose_object_map(struct odb_source_loose *loose,\n \t\t\t      const struct object_id *oid,\n \t\t\t      const struct object_id *compat_oid);\n int repo_read_loose_object_map(struct repository *repo);\ndiff --git a/object-file.c b/object-file.c\nindex 0689a4e67b..fe24f00d1b 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -810,6 +810,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t\t\t\t  struct odb_write_stream *in_stream, size_t len,\n \t\t\t\t  struct object_id *oid)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n \tstruct object_id compat_oid;\n \tint fd, ret, err = 0, flush = 0;\n@@ -918,7 +919,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \terr = finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf,\n \t\t\t\t\t FOF_SKIP_COLLISION_CHECK);\n \tif (!err && compat)\n-\t\terr = repo_add_loose_object_map(source, oid, &compat_oid);\n+\t\terr = repo_add_loose_object_map(files->loose, oid, &compat_oid);\n cleanup:\n \tstrbuf_release(&tmp_file);\n \tstrbuf_release(&filename);\n@@ -931,6 +932,7 @@ int odb_source_loose_write_object(struct odb_source *source,\n \t\t\t\t  struct object_id *compat_oid_in,\n \t\t\t\t  enum odb_write_object_flags flags)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tconst struct git_hash_algo *algo = source->odb->repo->hash_algo;\n \tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n \tstruct object_id compat_oid;\n@@ -962,13 +964,14 @@ int odb_source_loose_write_object(struct odb_source *source,\n \tif (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))\n \t\treturn -1;\n \tif (compat)\n-\t\treturn repo_add_loose_object_map(source, oid, &compat_oid);\n+\t\treturn repo_add_loose_object_map(files->loose, oid, &compat_oid);\n \treturn 0;\n }\n \n int force_object_loose(struct odb_source *source,\n \t\t       const struct object_id *oid, time_t mtime)\n {\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n \tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n \tvoid *buf;\n \tunsigned long len;\n@@ -998,7 +1001,7 @@ int force_object_loose(struct odb_source *source,\n \thdrlen = format_object_header(hdr, sizeof(hdr), type, len);\n \tret = write_loose_object(source, oid, hdr, hdrlen, buf, len, mtime, 0);\n \tif (!ret && compat)\n-\t\tret = repo_add_loose_object_map(source, oid, &compat_oid);\n+\t\tret = repo_add_loose_object_map(files->loose, oid, &compat_oid);\n \tfree(buf);\n \n \treturn ret;\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544369","messageId":"20260601-b4-pks-odb-source-loose-v2-14-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 14/18] odb/source-loose: wire up `write_object()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:37Z","receivedAt":"2026-06-01T08:21:06Z","isPatch":true,"body":"Move `odb_source_loose_write_object()` from \"object-file.c\" into\n\"odb/source-loose.c\" and wire it up as the `write_object()` callback of\nthe loose source.\n\nAs in preceding commits, this requires us to expose a couple of generic\nfunctions from \"object-file.c\" as they are used in both subsystems now.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 58 ++++++++----------------------------------------------\n object-file.h      | 14 +++++++------\n odb/source-files.c |  5 +++--\n odb/source-loose.c | 44 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 63 insertions(+), 58 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex fe24f00d1b..7bb5b31bca 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -326,10 +326,10 @@ static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_c\n \tgit_hash_final_oid(oid, c);\n }\n \n-static void write_object_file_prepare(const struct git_hash_algo *algo,\n-\t\t\t\t      const void *buf, unsigned long len,\n-\t\t\t\t      enum object_type type, struct object_id *oid,\n-\t\t\t\t      char *hdr, int *hdrlen)\n+void write_object_file_prepare(const struct git_hash_algo *algo,\n+\t\t\t       const void *buf, unsigned long len,\n+\t\t\t       enum object_type type, struct object_id *oid,\n+\t\t\t       char *hdr, int *hdrlen)\n {\n \tstruct git_hash_ctx c;\n \n@@ -746,10 +746,10 @@ static int end_loose_object_common(struct odb_source *source,\n \treturn Z_OK;\n }\n \n-static int write_loose_object(struct odb_source *source,\n-\t\t\t      const struct object_id *oid, char *hdr,\n-\t\t\t      int hdrlen, const void *buf, unsigned long len,\n-\t\t\t      time_t mtime, unsigned flags)\n+int write_loose_object(struct odb_source *source,\n+\t\t       const struct object_id *oid, char *hdr,\n+\t\t       int hdrlen, const void *buf, unsigned long len,\n+\t\t       time_t mtime, unsigned flags)\n {\n \tint fd, ret;\n \tunsigned char compressed[4096];\n@@ -926,48 +926,6 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \treturn err;\n }\n \n-int odb_source_loose_write_object(struct odb_source *source,\n-\t\t\t\t  const void *buf, unsigned long len,\n-\t\t\t\t  enum object_type type, struct object_id *oid,\n-\t\t\t\t  struct object_id *compat_oid_in,\n-\t\t\t\t  enum odb_write_object_flags flags)\n-{\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tconst struct git_hash_algo *algo = source->odb->repo->hash_algo;\n-\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n-\tstruct object_id compat_oid;\n-\tchar hdr[MAX_HEADER_LEN];\n-\tint hdrlen = sizeof(hdr);\n-\n-\t/* Generate compat_oid */\n-\tif (compat) {\n-\t\tif (compat_oid_in)\n-\t\t\toidcpy(&compat_oid, compat_oid_in);\n-\t\telse if (type == OBJ_BLOB)\n-\t\t\thash_object_file(compat, buf, len, type, &compat_oid);\n-\t\telse {\n-\t\t\tstruct strbuf converted = STRBUF_INIT;\n-\t\t\tconvert_object_file(source->odb->repo, &converted, algo, compat,\n-\t\t\t\t\t    buf, len, type, 0);\n-\t\t\thash_object_file(compat, converted.buf, converted.len,\n-\t\t\t\t\t type, &compat_oid);\n-\t\t\tstrbuf_release(&converted);\n-\t\t}\n-\t}\n-\n-\t/* Normally if we have it in the pack then we do not bother writing\n-\t * it out into .git/objects/??/?{38} file.\n-\t */\n-\twrite_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);\n-\tif (odb_freshen_object(source->odb, oid))\n-\t\treturn 0;\n-\tif (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))\n-\t\treturn -1;\n-\tif (compat)\n-\t\treturn repo_add_loose_object_map(files->loose, oid, &compat_oid);\n-\treturn 0;\n-}\n-\n int force_object_loose(struct odb_source *source,\n \t\t       const struct object_id *oid, time_t mtime)\n {\ndiff --git a/object-file.h b/object-file.h\nindex 1d90df9d98..2b32592de1 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -23,12 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa\n struct object_info;\n struct odb_source;\n \n-int odb_source_loose_write_object(struct odb_source *source,\n-\t\t\t\t  const void *buf, unsigned long len,\n-\t\t\t\t  enum object_type type, struct object_id *oid,\n-\t\t\t\t  struct object_id *compat_oid_in,\n-\t\t\t\t  enum odb_write_object_flags flags);\n-\n int odb_source_loose_write_stream(struct odb_source *source,\n \t\t\t\t  struct odb_write_stream *stream, size_t len,\n \t\t\t\t  struct object_id *oid);\n@@ -129,6 +123,14 @@ int finalize_object_file_flags(struct repository *repo,\n void hash_object_file(const struct git_hash_algo *algo, const void *buf,\n \t\t      unsigned long len, enum object_type type,\n \t\t      struct object_id *oid);\n+void write_object_file_prepare(const struct git_hash_algo *algo,\n+\t\t\t       const void *buf, unsigned long len,\n+\t\t\t       enum object_type type, struct object_id *oid,\n+\t\t\t       char *hdr, int *hdrlen);\n+int write_loose_object(struct odb_source *source,\n+\t\t       const struct object_id *oid, char *hdr,\n+\t\t       int hdrlen, const void *buf, unsigned long len,\n+\t\t       time_t mtime, unsigned flags);\n \n /* Helper to check and \"touch\" a file */\n int check_and_freshen_file(const char *fn, int freshen);\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex ef548e6fe6..52ba04237a 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -164,8 +164,9 @@ static int odb_source_files_write_object(struct odb_source *source,\n \t\t\t\t\t struct object_id *compat_oid,\n \t\t\t\t\t enum odb_write_object_flags flags)\n {\n-\treturn odb_source_loose_write_object(source, buf, len, type,\n-\t\t\t\t\t     oid, compat_oid, flags);\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n+\treturn odb_source_write_object(&files->loose->base, buf, len, type,\n+\t\t\t\t       oid, compat_oid, flags);\n }\n \n static int odb_source_files_write_object_stream(struct odb_source *source,\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex e519365d23..c91018109e 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -5,6 +5,7 @@\n #include \"hex.h\"\n #include \"loose.h\"\n #include \"object-file.h\"\n+#include \"object-file-convert.h\"\n #include \"odb.h\"\n #include \"odb/source-files.h\"\n #include \"odb/source-loose.h\"\n@@ -588,6 +589,48 @@ static int odb_source_loose_freshen_object(struct odb_source *source,\n \treturn !!check_and_freshen_file(path.buf, 1);\n }\n \n+static int odb_source_loose_write_object(struct odb_source *source,\n+\t\t\t\t\t const void *buf, unsigned long len,\n+\t\t\t\t\t enum object_type type, struct object_id *oid,\n+\t\t\t\t\t struct object_id *compat_oid_in,\n+\t\t\t\t\t enum odb_write_object_flags flags)\n+{\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\tconst struct git_hash_algo *algo = source->odb->repo->hash_algo;\n+\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n+\tstruct object_id compat_oid;\n+\tchar hdr[MAX_HEADER_LEN];\n+\tint hdrlen = sizeof(hdr);\n+\n+\t/* Generate compat_oid */\n+\tif (compat) {\n+\t\tif (compat_oid_in)\n+\t\t\toidcpy(&compat_oid, compat_oid_in);\n+\t\telse if (type == OBJ_BLOB)\n+\t\t\thash_object_file(compat, buf, len, type, &compat_oid);\n+\t\telse {\n+\t\t\tstruct strbuf converted = STRBUF_INIT;\n+\t\t\tconvert_object_file(source->odb->repo, &converted, algo, compat,\n+\t\t\t\t\t    buf, len, type, 0);\n+\t\t\thash_object_file(compat, converted.buf, converted.len,\n+\t\t\t\t\t type, &compat_oid);\n+\t\t\tstrbuf_release(&converted);\n+\t\t}\n+\t}\n+\n+\t/* Normally if we have it in the pack then we do not bother writing\n+\t * it out into .git/objects/??/?{38} file.\n+\t */\n+\twrite_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);\n+\tif (odb_freshen_object(source->odb, oid))\n+\t\treturn 0;\n+\tif (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))\n+\t\treturn -1;\n+\tif (compat)\n+\t\treturn repo_add_loose_object_map(loose, oid, &compat_oid);\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@@ -647,6 +690,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.find_abbrev_len = odb_source_loose_find_abbrev_len;\n \tloose->base.count_objects = odb_source_loose_count_objects;\n \tloose->base.freshen_object = odb_source_loose_freshen_object;\n+\tloose->base.write_object = odb_source_loose_write_object;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544370","messageId":"20260601-b4-pks-odb-source-loose-v2-15-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 15/18] object-file: refactor writing objects to use loose source","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:38Z","receivedAt":"2026-06-01T08:21:07Z","isPatch":true,"body":"The \"object-file\" subsystem still hosts the majority of logic used to\nwrite loose objects. Eventually, we'll want to move this logic into\n\"odb/source-loose.c\", but this isn't yet easily possible because a lot\nof the writing logic is still being shared with `force_object_loose()`.\n\nWe will eventually detangle this logic so that we can indeed move all of\nit into the \"loose\" source. Meanwhile though, refactor the code so that\nit operates on a `struct odb_source_loose` directly to already make the\ndependency explicit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n http-walker.c      |  3 ++-\n http.c             |  6 +++--\n object-file.c      | 75 +++++++++++++++++++++++++++---------------------------\n object-file.h      |  6 ++---\n odb/source-files.c |  3 ++-\n odb/source-loose.c |  9 ++++---\n 6 files changed, 53 insertions(+), 49 deletions(-)\n\ndiff --git a/http-walker.c b/http-walker.c\nindex 1b6d496548..435a726540 100644\n--- a/http-walker.c\n+++ b/http-walker.c\n@@ -539,8 +539,9 @@ 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 strbuf buf = STRBUF_INIT;\n-\t\todb_loose_path(the_repository->objects->sources, &buf, &req->oid);\n+\t\todb_loose_path(files->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 ea9b16861b..3fcc012233 100644\n--- a/http.c\n+++ b/http.c\n@@ -2826,6 +2826,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 \tchar *hex = oid_to_hex(oid);\n \tstruct strbuf filename = STRBUF_INIT;\n \tstruct strbuf prevfile = STRBUF_INIT;\n@@ -2840,7 +2841,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(the_repository->objects->sources, &filename, oid);\n+\todb_loose_path(files->loose, &filename, oid);\n \tstrbuf_addf(&freq->tmpfile, \"%s.temp\", filename.buf);\n \n \tstrbuf_addf(&prevfile, \"%s.prev\", filename.buf);\n@@ -2966,6 +2967,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 stat st;\n \tstruct strbuf filename = STRBUF_INIT;\n \n@@ -2992,7 +2994,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(the_repository->objects->sources, &filename, &freq->oid);\n+\todb_loose_path(files->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/object-file.c b/object-file.c\nindex 7bb5b31bca..bce941874e 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -54,14 +54,14 @@ static void fill_loose_path(struct strbuf *buf,\n \t}\n }\n \n-const char *odb_loose_path(struct odb_source *source,\n+const char *odb_loose_path(struct odb_source_loose *loose,\n \t\t\t   struct strbuf *buf,\n \t\t\t   const struct object_id *oid)\n {\n \tstrbuf_reset(buf);\n-\tstrbuf_addstr(buf, source->path);\n+\tstrbuf_addstr(buf, loose->base.path);\n \tstrbuf_addch(buf, '/');\n-\tfill_loose_path(buf, oid, source->odb->repo->hash_algo);\n+\tfill_loose_path(buf, oid, loose->base.odb->repo->hash_algo);\n \treturn buf->buf;\n }\n \n@@ -575,14 +575,14 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac\n }\n \n /* Finalize a file on disk, and close it. */\n-static void close_loose_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 (source->will_destroy)\n+\tif (loose->base.will_destroy)\n \t\tgoto out;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tfsync_loose_object_transaction(source->odb->transaction, fd, filename);\n+\t\tfsync_loose_object_transaction(loose->base.odb->transaction, fd, filename);\n \telse if (fsync_object_files > 0)\n \t\tfsync_or_die(fd, filename);\n \telse\n@@ -651,7 +651,7 @@ static int create_tmpfile(struct repository *repo,\n  * Returns a \"fd\", which should later be provided to\n  * end_loose_object_common().\n  */\n-static int start_loose_object_common(struct odb_source *source,\n+static int start_loose_object_common(struct odb_source_loose *loose,\n \t\t\t\t     struct strbuf *tmp_file,\n \t\t\t\t     const char *filename, unsigned flags,\n \t\t\t\t     git_zstream *stream,\n@@ -659,18 +659,18 @@ static int start_loose_object_common(struct odb_source *source,\n \t\t\t\t     struct git_hash_ctx *c, struct git_hash_ctx *compat_c,\n \t\t\t\t     char *hdr, int hdrlen)\n {\n-\tconst struct git_hash_algo *algo = source->odb->repo->hash_algo;\n-\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n+\tconst struct git_hash_algo *algo = loose->base.odb->repo->hash_algo;\n+\tconst struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;\n \tint fd;\n \n-\tfd = create_tmpfile(source->odb->repo, tmp_file, filename);\n+\tfd = create_tmpfile(loose->base.odb->repo, tmp_file, filename);\n \tif (fd < 0) {\n \t\tif (flags & ODB_WRITE_OBJECT_SILENT)\n \t\t\treturn -1;\n \t\telse if (errno == EACCES)\n \t\t\treturn error(_(\"insufficient permission for adding \"\n \t\t\t\t       \"an object to repository database %s\"),\n-\t\t\t\t     source->path);\n+\t\t\t\t     loose->base.path);\n \t\telse\n \t\t\treturn error_errno(\n \t\t\t\t_(\"unable to create temporary file\"));\n@@ -700,14 +700,14 @@ static int start_loose_object_common(struct odb_source *source,\n  * Common steps for the inner git_deflate() loop for writing loose\n  * objects. Returns what git_deflate() returns.\n  */\n-static int write_loose_object_common(struct odb_source *source,\n+static int write_loose_object_common(struct odb_source_loose *loose,\n \t\t\t\t     struct git_hash_ctx *c, struct git_hash_ctx *compat_c,\n \t\t\t\t     git_zstream *stream, const int flush,\n \t\t\t\t     unsigned char *in0, const int fd,\n \t\t\t\t     unsigned char *compressed,\n \t\t\t\t     const size_t compressed_len)\n {\n-\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n+\tconst struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;\n \tint ret;\n \n \tret = git_deflate(stream, flush ? Z_FINISH : 0);\n@@ -728,12 +728,12 @@ static int write_loose_object_common(struct odb_source *source,\n  * - End the compression of zlib stream.\n  * - Get the calculated oid to \"oid\".\n  */\n-static int end_loose_object_common(struct odb_source *source,\n+static int end_loose_object_common(struct odb_source_loose *loose,\n \t\t\t\t   struct git_hash_ctx *c, struct git_hash_ctx *compat_c,\n \t\t\t\t   git_zstream *stream, struct object_id *oid,\n \t\t\t\t   struct object_id *compat_oid)\n {\n-\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n+\tconst struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;\n \tint ret;\n \n \tret = git_deflate_end_gently(stream);\n@@ -746,7 +746,7 @@ static int end_loose_object_common(struct odb_source *source,\n \treturn Z_OK;\n }\n \n-int write_loose_object(struct odb_source *source,\n+int write_loose_object(struct odb_source_loose *loose,\n \t\t       const struct object_id *oid, char *hdr,\n \t\t       int hdrlen, const void *buf, unsigned long len,\n \t\t       time_t mtime, unsigned flags)\n@@ -760,11 +760,11 @@ int write_loose_object(struct odb_source *source,\n \tstatic struct strbuf filename = STRBUF_INIT;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(source->odb->transaction);\n+\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\n \n-\todb_loose_path(source, &filename, oid);\n+\todb_loose_path(loose, &filename, oid);\n \n-\tfd = start_loose_object_common(source, &tmp_file, filename.buf, flags,\n+\tfd = start_loose_object_common(loose, &tmp_file, filename.buf, flags,\n \t\t\t\t       &stream, compressed, sizeof(compressed),\n \t\t\t\t       &c, NULL, hdr, hdrlen);\n \tif (fd < 0)\n@@ -776,14 +776,14 @@ int write_loose_object(struct odb_source *source,\n \tdo {\n \t\tunsigned char *in0 = stream.next_in;\n \n-\t\tret = write_loose_object_common(source, &c, NULL, &stream, 1, in0, fd,\n+\t\tret = write_loose_object_common(loose, &c, NULL, &stream, 1, in0, fd,\n \t\t\t\t\t\tcompressed, sizeof(compressed));\n \t} while (ret == Z_OK);\n \n \tif (ret != Z_STREAM_END)\n \t\tdie(_(\"unable to deflate new object %s (%d)\"), oid_to_hex(oid),\n \t\t    ret);\n-\tret = end_loose_object_common(source, &c, NULL, &stream, &parano_oid, NULL);\n+\tret = end_loose_object_common(loose, &c, NULL, &stream, &parano_oid, NULL);\n \tif (ret != Z_OK)\n \t\tdie(_(\"deflateEnd on object %s failed (%d)\"), oid_to_hex(oid),\n \t\t    ret);\n@@ -791,7 +791,7 @@ int write_loose_object(struct odb_source *source,\n \t\tdie(_(\"confused by unstable object source data for %s\"),\n \t\t    oid_to_hex(oid));\n \n-\tclose_loose_object(source, fd, tmp_file.buf);\n+\tclose_loose_object(loose, fd, tmp_file.buf);\n \n \tif (mtime) {\n \t\tstruct utimbuf utb;\n@@ -802,16 +802,15 @@ int write_loose_object(struct odb_source *source,\n \t\t\twarning_errno(_(\"failed utime() on %s\"), tmp_file.buf);\n \t}\n \n-\treturn finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf,\n+\treturn finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,\n \t\t\t\t\t  FOF_SKIP_COLLISION_CHECK);\n }\n \n-int odb_source_loose_write_stream(struct odb_source *source,\n+int odb_source_loose_write_stream(struct odb_source_loose *loose,\n \t\t\t\t  struct odb_write_stream *in_stream, size_t len,\n \t\t\t\t  struct object_id *oid)\n {\n-\tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tconst struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;\n+\tconst struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;\n \tstruct object_id compat_oid;\n \tint fd, ret, err = 0, flush = 0;\n \tunsigned char compressed[4096];\n@@ -825,10 +824,10 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \tint hdrlen;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(source->odb->transaction);\n+\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\n \n \t/* Since oid is not determined, save tmp file to odb path. */\n-\tstrbuf_addf(&filename, \"%s/\", source->path);\n+\tstrbuf_addf(&filename, \"%s/\", loose->base.path);\n \thdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len);\n \n \t/*\n@@ -839,7 +838,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t *  - Setup zlib stream for compression.\n \t *  - Start to feed header to zlib stream.\n \t */\n-\tfd = start_loose_object_common(source, &tmp_file, filename.buf, 0,\n+\tfd = start_loose_object_common(loose, &tmp_file, filename.buf, 0,\n \t\t\t\t       &stream, compressed, sizeof(compressed),\n \t\t\t\t       &c, &compat_c, hdr, hdrlen);\n \tif (fd < 0) {\n@@ -867,7 +866,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t\t\tif (in_stream->is_finished)\n \t\t\t\tflush = 1;\n \t\t}\n-\t\tret = write_loose_object_common(source, &c, &compat_c, &stream, flush, in0, fd,\n+\t\tret = write_loose_object_common(loose, &c, &compat_c, &stream, flush, in0, fd,\n \t\t\t\t\t\tcompressed, sizeof(compressed));\n \t\t/*\n \t\t * Unlike write_loose_object(), we do not have the entire\n@@ -890,16 +889,16 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t */\n \tif (ret != Z_STREAM_END)\n \t\tdie(_(\"unable to stream deflate new object (%d)\"), ret);\n-\tret = end_loose_object_common(source, &c, &compat_c, &stream, oid, &compat_oid);\n+\tret = end_loose_object_common(loose, &c, &compat_c, &stream, oid, &compat_oid);\n \tif (ret != Z_OK)\n \t\tdie(_(\"deflateEnd on stream object failed (%d)\"), ret);\n-\tclose_loose_object(source, fd, tmp_file.buf);\n+\tclose_loose_object(loose, fd, tmp_file.buf);\n \n-\tif (odb_freshen_object(source->odb, oid)) {\n+\tif (odb_freshen_object(loose->base.odb, oid)) {\n \t\tunlink_or_warn(tmp_file.buf);\n \t\tgoto cleanup;\n \t}\n-\todb_loose_path(source, &filename, oid);\n+\todb_loose_path(loose, &filename, oid);\n \n \t/* We finally know the object path, and create the missing dir. */\n \tdirlen = directory_size(filename.buf);\n@@ -907,7 +906,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t\tstruct strbuf dir = STRBUF_INIT;\n \t\tstrbuf_add(&dir, filename.buf, dirlen);\n \n-\t\tif (safe_create_dir_in_gitdir(source->odb->repo, dir.buf) &&\n+\t\tif (safe_create_dir_in_gitdir(loose->base.odb->repo, dir.buf) &&\n \t\t    errno != EEXIST) {\n \t\t\terr = error_errno(_(\"unable to create directory %s\"), dir.buf);\n \t\t\tstrbuf_release(&dir);\n@@ -916,10 +915,10 @@ int odb_source_loose_write_stream(struct odb_source *source,\n \t\tstrbuf_release(&dir);\n \t}\n \n-\terr = finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf,\n+\terr = finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,\n \t\t\t\t\t FOF_SKIP_COLLISION_CHECK);\n \tif (!err && compat)\n-\t\terr = repo_add_loose_object_map(files->loose, oid, &compat_oid);\n+\t\terr = repo_add_loose_object_map(loose, oid, &compat_oid);\n cleanup:\n \tstrbuf_release(&tmp_file);\n \tstrbuf_release(&filename);\n@@ -957,7 +956,7 @@ int force_object_loose(struct odb_source *source,\n \t\t\t\t     oid_to_hex(oid), compat->name);\n \t}\n \thdrlen = format_object_header(hdr, sizeof(hdr), type, len);\n-\tret = write_loose_object(source, oid, hdr, hdrlen, buf, len, mtime, 0);\n+\tret = write_loose_object(files->loose, oid, hdr, hdrlen, buf, len, mtime, 0);\n \tif (!ret && compat)\n \t\tret = repo_add_loose_object_map(files->loose, oid, &compat_oid);\n \tfree(buf);\ndiff --git a/object-file.h b/object-file.h\nindex 2b32592de1..d30f1b10b2 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -23,7 +23,7 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa\n struct object_info;\n struct odb_source;\n \n-int odb_source_loose_write_stream(struct odb_source *source,\n+int odb_source_loose_write_stream(struct odb_source_loose *loose,\n \t\t\t\t  struct odb_write_stream *stream, size_t len,\n \t\t\t\t  struct object_id *oid);\n \n@@ -31,7 +31,7 @@ int odb_source_loose_write_stream(struct odb_source *source,\n  * Put in `buf` the name of the file in the local object database that\n  * would be used to store a loose object with the specified oid.\n  */\n-const char *odb_loose_path(struct odb_source *source,\n+const char *odb_loose_path(struct odb_source_loose *source,\n \t\t\t   struct strbuf *buf,\n \t\t\t   const struct object_id *oid);\n \n@@ -127,7 +127,7 @@ void write_object_file_prepare(const struct git_hash_algo *algo,\n \t\t\t       const void *buf, unsigned long len,\n \t\t\t       enum object_type type, struct object_id *oid,\n \t\t\t       char *hdr, int *hdrlen);\n-int write_loose_object(struct odb_source *source,\n+int write_loose_object(struct odb_source_loose *loose,\n \t\t       const struct object_id *oid, char *hdr,\n \t\t       int hdrlen, const void *buf, unsigned long len,\n \t\t       time_t mtime, unsigned flags);\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 52ba04237a..2ba1def776 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -174,7 +174,8 @@ static int odb_source_files_write_object_stream(struct odb_source *source,\n \t\t\t\t\t\tsize_t len,\n \t\t\t\t\t\tstruct object_id *oid)\n {\n-\treturn odb_source_loose_write_stream(source, stream, len, oid);\n+\tstruct odb_source_files *files = odb_source_files_downcast(source);\n+\treturn odb_source_loose_write_stream(files->loose, stream, len, oid);\n }\n \n static int odb_source_files_begin_transaction(struct odb_source *source,\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex c91018109e..da8a60dba1 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -220,7 +220,7 @@ static int odb_source_loose_read_object_info(struct odb_source *source,\n \tif (flags & OBJECT_INFO_SECOND_READ)\n \t\treturn -1;\n \n-\todb_loose_path(source, &buf, oid);\n+\todb_loose_path(loose, &buf, oid);\n \treturn read_object_info_from_path(loose, buf.buf, oid, oi, flags);\n }\n \n@@ -238,7 +238,7 @@ static int open_loose_object(struct odb_source_loose *loose,\n \tstatic struct strbuf buf = STRBUF_INIT;\n \tint fd;\n \n-\t*path = odb_loose_path(&loose->base, &buf, oid);\n+\t*path = odb_loose_path(loose, &buf, oid);\n \tfd = git_open(*path);\n \tif (fd >= 0)\n \t\treturn fd;\n@@ -584,8 +584,9 @@ static int odb_source_loose_count_objects(struct odb_source *source,\n static int odb_source_loose_freshen_object(struct odb_source *source,\n \t\t\t\t\t   const struct object_id *oid)\n {\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n \tstatic struct strbuf path = STRBUF_INIT;\n-\todb_loose_path(source, &path, oid);\n+\todb_loose_path(loose, &path, oid);\n \treturn !!check_and_freshen_file(path.buf, 1);\n }\n \n@@ -624,7 +625,7 @@ static int odb_source_loose_write_object(struct odb_source *source,\n \twrite_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);\n \tif (odb_freshen_object(source->odb, oid))\n \t\treturn 0;\n-\tif (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags))\n+\tif (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags))\n \t\treturn -1;\n \tif (compat)\n \t\treturn repo_add_loose_object_map(loose, oid, &compat_oid);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544371","messageId":"20260601-b4-pks-odb-source-loose-v2-16-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 16/18] odb/source-loose: wire up `write_object_stream()` callback","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:39Z","receivedAt":"2026-06-01T08:21:10Z","isPatch":true,"body":"Wire up the `write_object_stream()` callback.\n\nNote that we don't move the implementation into \"odb/source-loose.c\".\nThis is because most of the logic to write loose objects is still\ncontained in \"object-file.c\", and detangling that requires us to do some\nrefactorings as explained in the preceding commit. So for now, the\nimplementation of writing an object stream is still located in\n\"object-file.c\".\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.h      | 12 +++++++++++-\n odb/source-files.c |  3 ++-\n odb/source-loose.c | 14 ++++++++++++++\n 3 files changed, 27 insertions(+), 2 deletions(-)\n\ndiff --git a/object-file.h b/object-file.h\nindex d30f1b10b2..528c4e6e69 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -23,7 +23,17 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa\n struct object_info;\n struct odb_source;\n \n-int odb_source_loose_write_stream(struct odb_source_loose *loose,\n+/*\n+ * Write the given stream into the loose object source. The only difference\n+ * from the generic implementation of this function is that we don't perform an\n+ * object existence check here.\n+ *\n+ * TODO: We should stop exposing this function altogether and move it into\n+ * \"odb/source-loose.c\". This requires a couple of refactorings though to make\n+ * `force_object_loose()` generic and is thus postponed to a later point in\n+ * time.\n+ */\n+int odb_source_loose_write_stream(struct odb_source_loose *source,\n \t\t\t\t  struct odb_write_stream *stream, size_t len,\n \t\t\t\t  struct object_id *oid);\n \ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 2ba1def776..83f8066c67 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -7,6 +7,7 @@\n #include \"odb.h\"\n #include \"odb/source.h\"\n #include \"odb/source-files.h\"\n+#include \"odb/source-loose.h\"\n #include \"packfile.h\"\n #include \"strbuf.h\"\n #include \"write-or-die.h\"\n@@ -175,7 +176,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_loose_write_stream(files->loose, stream, len, oid);\n+\treturn odb_source_write_object_stream(&files->loose->base, stream, len, oid);\n }\n \n static int odb_source_files_begin_transaction(struct odb_source *source,\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex da8a60dba1..e52fc289a2 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -632,6 +632,19 @@ static int odb_source_loose_write_object(struct odb_source *source,\n \treturn 0;\n }\n \n+static int odb_source_loose_write_object_stream(struct odb_source *source,\n+\t\t\t\t\t\tstruct odb_write_stream *in_stream,\n+\t\t\t\t\t\tsize_t len,\n+\t\t\t\t\t\tstruct object_id *oid)\n+{\n+\t/*\n+\t * TODO: the implementation should be moved here, see the comment on\n+\t * the called function in \"object-file.h\".\n+\t */\n+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n+\treturn odb_source_loose_write_stream(loose, in_stream, len, oid);\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -692,6 +705,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.count_objects = odb_source_loose_count_objects;\n \tloose->base.freshen_object = odb_source_loose_freshen_object;\n \tloose->base.write_object = odb_source_loose_write_object;\n+\tloose->base.write_object_stream = odb_source_loose_write_object_stream;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544372","messageId":"20260601-b4-pks-odb-source-loose-v2-17-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 17/18] odb/source-loose: stub out remaining callbacks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:40Z","receivedAt":"2026-06-01T08:21:13Z","isPatch":true,"body":"Stub out remaining callback functions for the \"loose\" backend.\n\nNote that we also stub out transactions for loose objects. In fact, we\nalready have the infrastructure in place for those, and we could in\ntheory implement those, as well. But there are separate efforts ongoing\nto polish up transactional interfaces, and doing so now would likely\nresult in some messiness. This omission will thus be worked on in a\nsubsequent patch series, once the dust has settled.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb/source-loose.c | 22 ++++++++++++++++++++++\n 1 file changed, 22 insertions(+)\n\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex e52fc289a2..e174941318 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -645,6 +645,25 @@ static int odb_source_loose_write_object_stream(struct odb_source *source,\n \treturn odb_source_loose_write_stream(loose, in_stream, len, oid);\n }\n \n+static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED,\n+\t\t\t\t\t      struct odb_transaction **out UNUSED)\n+{\n+\t/* TODO: this is a known omission that we'll want to address eventually. */\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 int odb_source_loose_write_alternate(struct odb_source *source UNUSED,\n+\t\t\t\t\t    const char *alternate UNUSED)\n+{\n+\treturn error(\"loose source does not support alternates\");\n+}\n+\n static void odb_source_loose_clear_cache(struct odb_source_loose *loose)\n {\n \toidtree_clear(loose->cache);\n@@ -706,6 +725,9 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n \tloose->base.freshen_object = odb_source_loose_freshen_object;\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+\tloose->base.write_alternate = odb_source_loose_write_alternate;\n \n \tif (!is_absolute_path(loose->base.path))\n \t\tchdir_notify_register(NULL, odb_source_loose_reparent, loose);\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544373","messageId":"20260601-b4-pks-odb-source-loose-v2-18-90ff159430af@pks.im","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-0-90ff159430af@pks.im","subject":"[PATCH v2 18/18] odb/source-loose: drop pointer to the \"files\" source","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-01T08:20:41Z","receivedAt":"2026-06-01T08:21:15Z","isPatch":true,"body":"Now that all callbacks of the loose source operate on `struct\nodb_source_loose` directly we no longer have to reach into the \"files\"\nsource at all.\n\nDrop this field and update `odb_source_loose_new()` to instead accept\nall parameters required to initialize itself. This ensures that the\n\"loose\" backend is a fully standalone source.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb/source-files.c | 2 +-\n odb/source-loose.c | 8 ++++----\n odb/source-loose.h | 7 ++++---\n 3 files changed, 9 insertions(+), 8 deletions(-)\n\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 83f8066c67..5bdd042922 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -268,7 +268,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(files);\n+\tfiles->loose = odb_source_loose_new(odb, path, local);\n \tfiles->packed = packfile_store_new(&files->base);\n \n \tfiles->base.free = odb_source_files_free;\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex e174941318..7d7ea2fb84 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -705,14 +705,14 @@ static void odb_source_loose_free(struct odb_source *source)\n \tfree(loose);\n }\n \n-struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files)\n+struct odb_source_loose *odb_source_loose_new(struct object_database *odb,\n+\t\t\t\t\t      const char *path,\n+\t\t\t\t\t      bool local)\n {\n \tstruct odb_source_loose *loose;\n \n \tCALLOC_ARRAY(loose, 1);\n-\todb_source_init(&loose->base, files->base.odb, ODB_SOURCE_LOOSE,\n-\t\t\tfiles->base.path, files->base.local);\n-\tloose->files = files;\n+\todb_source_init(&loose->base, odb, ODB_SOURCE_LOOSE, path, local);\n \n \tloose->base.free = odb_source_loose_free;\n \tloose->base.close = odb_source_loose_close;\ndiff --git a/odb/source-loose.h b/odb/source-loose.h\nindex 4dd4fd6ce3..6070aaf3ce 100644\n--- a/odb/source-loose.h\n+++ b/odb/source-loose.h\n@@ -9,11 +9,10 @@ struct oidtree;\n \n /*\n  * An object database source that stores its objects in loose format, one\n- * file per object. This source is part of the files source.\n+ * file per object.\n  */\n struct odb_source_loose {\n \tstruct odb_source base;\n-\tstruct odb_source_files *files;\n \n \t/*\n \t * Used to store the results of readdir(3) calls when we are OK\n@@ -31,7 +30,9 @@ struct odb_source_loose {\n \tstruct loose_object_map *map;\n };\n \n-struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files);\n+struct odb_source_loose *odb_source_loose_new(struct object_database *odb,\n+\t\t\t\t\t      const char *path,\n+\t\t\t\t\t      bool local);\n \n /*\n  * Cast the given object database source to the loose backend. This will cause\n\n-- \n2.54.0.926.g75ba10bac6.dirty\n\n"},{"id":"544423","messageId":"xmqqh5nm3q09.fsf@gitster.g","threadId":"65667","inReplyTo":"20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im","subject":"Re: [PATCH 00/18] odb: make loose object source a proper `struct odb_source`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-01T21:33:10Z","receivedAt":"2026-06-01T21:33:13Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Hi,\n>\n> this patch series converts the loose object source into a proper `struct\n> odb_source` so that it can be used via our generic interfaces.\n>\n> The patch series is relatively straight-forward, as the source basically\n> already exists as such and the interfaces already match. So for most of\n> the part we are just moving around some code and converting functions\n> that were previously called directly into callbacks.\n>\n> I guess the only part that needs some attention is that there is some\n> confusion at first with the `struct odb_source_loose::source` parent\n> pointer that initially points at the owning `struct odb_source_files`.\n> This relationship doesn't make much sense, as a loose source can totally\n> exist standalone without the files source.\n\nNo significant comments came in the past week or so on these\npatches.  Should we declare victory, and mark it for 'next'?  I can\nlocally amend a typo in [3/18] (<xmqqh5o0zrsr.fsf@gitster.g>).\n"},{"id":"544433","messageId":"xmqqqzmp3o3q.fsf@gitster.g","threadId":"65667","inReplyTo":"xmqqh5nm3q09.fsf@gitster.g","subject":"Re: [PATCH 00/18] odb: make loose object source a proper `struct odb_source`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-01T22:14:17Z","receivedAt":"2026-06-01T22:14:20Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> Hi,\n>>\n>> this patch series converts the loose object source into a proper `struct\n>> odb_source` so that it can be used via our generic interfaces.\n>>\n>> The patch series is relatively straight-forward, as the source basically\n>> already exists as such and the interfaces already match. So for most of\n>> the part we are just moving around some code and converting functions\n>> that were previously called directly into callbacks.\n>>\n>> I guess the only part that needs some attention is that there is some\n>> confusion at first with the `struct odb_source_loose::source` parent\n>> pointer that initially points at the owning `struct odb_source_files`.\n>> This relationship doesn't make much sense, as a loose source can totally\n>> exist standalone without the files source.\n>\n> No significant comments came in the past week or so on these\n> patches.  Should we declare victory, and mark it for 'next'?  I can\n> locally amend a typo in [3/18] (<xmqqh5o0zrsr.fsf@gitster.g>).\n\nAh, I see your reroll.  Perfect.  Let me mark the topic for 'next'\nthen.\n"},{"id":"544614","messageId":"CAOLa=ZSRQpAMGDwfP8vAiJi+G=WPW=YPrrs21pVt1O4j2Uh-zQ@mail.gmail.com","threadId":"65667","inReplyTo":"20260601-b4-pks-odb-source-loose-v2-1-90ff159430af@pks.im","subject":"Re: [PATCH v2 01/18] odb/source-loose: move loose source into \"odb/\" subsystem","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-06-03T13:07:14Z","receivedAt":"2026-06-03T13:07:16Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> In subsequent patches we'll be turning `struct odb_source_loose` into a\n> proper `struct odb_source`. As a first step towards this goal, move its\n\ns/its/this?\n\n> struct out of \"object-file.c\" and into \"odb/source-loose.c\".\n>\n> This detaches the implementation of the loose object source from the\n> generic object file code, following the same convention already used by\n> the \"files\" and \"in-memory\" sources.\n>\n> No functional changes are intended.\n>\n\n[snip]\n"},{"id":"544635","messageId":"aiBTKKtj-evY4Xzx@pks.im","threadId":"65667","inReplyTo":"CAOLa=ZSRQpAMGDwfP8vAiJi+G=WPW=YPrrs21pVt1O4j2Uh-zQ@mail.gmail.com","subject":"Re: [PATCH v2 01/18] odb/source-loose: move loose source into \"odb/\" subsystem","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-03T16:15:36Z","receivedAt":"2026-06-03T16:15:41Z","isPatch":true,"body":"On Wed, Jun 03, 2026 at 06:07:14AM -0700, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > In subsequent patches we'll be turning `struct odb_source_loose` into a\n> > proper `struct odb_source`. As a first step towards this goal, move its\n> \n> s/its/this?\n> \n> > struct out of \"object-file.c\" and into \"odb/source-loose.c\".\n\nHm. I agree it would read more naturally with \"this\" instead of \"its\",\nbut I'm not even sure whether it's wrong. In any case, I'd prefer to not\nreroll this topic for this one nit, if that's alright with you.\n\nThanks!\n\nPatrick\n"},{"id":"544643","messageId":"CAOLa=ZTfL+MO_uF4SF-iRHbtwpk8TGU2CH1kHX59XuBeKbcyHQ@mail.gmail.com","threadId":"65667","inReplyTo":"CAOLa=ZSRQpAMGDwfP8vAiJi+G=WPW=YPrrs21pVt1O4j2Uh-zQ@mail.gmail.com","subject":"Re: [PATCH v2 01/18] odb/source-loose: move loose source into \"odb/\" subsystem","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-06-03T20:04:28Z","receivedAt":"2026-06-03T20:04:30Z","isPatch":true,"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> In subsequent patches we'll be turning `struct odb_source_loose` into a\n>> proper `struct odb_source`. As a first step towards this goal, move its\n>\n> s/its/this?\n>\n\nPost reading it again, seems like 'its' fits all along.\n\n>> struct out of \"object-file.c\" and into \"odb/source-loose.c\".\n>>\n>> This detaches the implementation of the loose object source from the\n>> generic object file code, following the same convention already used by\n>> the \"files\" and \"in-memory\" sources.\n>>\n>> No functional changes are intended.\n>>\n>\n> [snip]\n"}]}