{"thread":{"id":"66027","subject":"[PATCH 0/9] object-file: move writing of loose objects into \"loose\" source","startedAt":"2026-07-17T09:32:19Z","lastAt":"2026-07-28T22:10:44Z","messageCount":17,"participants":["Patrick Steinhardt","SZEDER Gábor","Junio C Hamano","Toon Claes","Justin Tobler"],"isPatch":true,"patchVersion":1,"patchTotal":9},"messages":[{"id":"548485","messageId":"20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im","threadId":"66027","inReplyTo":null,"subject":"[PATCH 0/9] object-file: move writing of loose objects into \"loose\" source","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-17T09:32:08Z","receivedAt":"2026-07-17T09:32:19Z","isPatch":true,"body":"Hi,\n\nthis patch series refactors \"object-file.c\" so that we can move the\nlogic that writes loose objects into \"odb/source-loose.c\". This ensures\nthat the logic to read and write loose objects is mostly self-contained\nwithin that source's implementation now.\n\nTo achieve that, this series first refactors `force_object_loose()` so\nthat it can work on top of the generic `odb_source_write_object()`\ninstead of having to call into `write_loose_object()`. This is the bulk\nof the patch series.\n\nThis patch series is built on top of 44de1520f0 (Merge branch 'master'\nof https://github.com/j6t/git-gui, 2026-07-16) with the following two\ntopics merged into it:\n\n  - jt/receive-pack-use-odb-transaction at bdee7b3013\n    (builtin/receive-pack: stage incoming objects via ODB transactions,\n    2026-07-10).\n\n  - jk/git-hash-cleanups at 9e396aa553 (hash: check ctx->active flag in\n    all wrapper functions, 2026-07-07).\n\nNote that jt/receive-pack-use-odb-transaction requires an evil merge:\n\ndiff --git a/odb/source-packed.c b/odb/source-packed.c\nindex 06b31dd743..cbb06da038 100644\n--- a/odb/source-packed.c\n+++ b/odb/source-packed.c\n@@ -545,7 +545,8 @@ static int odb_source_packed_write_object_stream(struct odb_source *source UNUSE\n }\n\n static int odb_source_packed_begin_transaction(struct odb_source *source UNUSED,\n-                                              struct odb_transaction **out UNUSED)\n+                                              struct odb_transaction **out UNUSED,\n+                                              enum odb_transaction_flags flags UNUSED)\n {\n        return error(\"packed backend cannot begin transactions\");\n }\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (9):\n      odb: compute compat object ID in `odb_write_object_ext()`\n      t/u-odb-inmemory: implement wrapper for writing objects\n      odb: compute object hash in `odb_write_object_ext()`\n      odb: lift object existence check out of the \"loose\" backend\n      odb: support setting mtime when writing objects\n      object-file: fix memory leak in `force_object_loose()`\n      object-file: force objects loose via generic interface\n      object-file: move `force_object_loose()`\n      object-file: move logic to write loose objects\n\n builtin/pack-objects.c        |  48 ++++-\n object-file.c                 | 449 +++---------------------------------------\n object-file.h                 |  32 +--\n odb.c                         |  39 +++-\n odb.h                         |  10 +-\n odb/source-files.c            |  14 +-\n odb/source-inmemory.c         |  14 +-\n odb/source-loose.c            | 402 +++++++++++++++++++++++++++++++++----\n odb/source-packed.c           |  17 +-\n odb/source.h                  |  20 +-\n read-cache.c                  |   2 +-\n t/unit-tests/u-odb-inmemory.c |  53 +++--\n 12 files changed, 551 insertions(+), 549 deletions(-)\n\n\n---\nbase-commit: 8061f1ea286318fbf976f58df3985bd60237b8a7\nchange-id: 20260703-pks-odb-move-loose-object-writing-8bcf1f51b04d\n\n"},{"id":"548486","messageId":"20260717-pks-odb-move-loose-object-writing-v1-1-46446a3cb5b7@pks.im","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im","subject":"[PATCH 1/9] odb: compute compat object ID in `odb_write_object_ext()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-17T09:32:09Z","receivedAt":"2026-07-17T09:32:21Z","isPatch":true,"body":"Repositories can have a compatibility hash configured, which means that\nsuch a repository is expected to maintain a mapping between canonical\nand compatibility object hashes. Maintaining this mapping is the\nresponsibility of the object database sources, where we either store\nthem as part of the loose objects map or in packfile indices v3 (once we\ngain support for this feature).\n\nBut besides storing these compatibility hashes, the sources are also\nresponsible for generating the compatibility hash in the first place.\nThis is somewhat unnecessary though, as the compatibility hash should be\ncomputed the same no matter which source is being used. The consequence\nis that we need to duplicate this functionality across the different\nbackends, which does not make a lot of sense.\n\nRefactor the code so that we instead compute the compatibility hash in\n`odb_write_object_ext()` and then pass the computed value to the\nsources. No callers need adjustment as there are none that write objects\nvia the source interfaces directly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb.c                 | 26 ++++++++++++++++++++++++--\n odb.h                 | 10 ++++++----\n odb/source-files.c    |  2 +-\n odb/source-inmemory.c |  2 +-\n odb/source-loose.c    | 24 +++---------------------\n odb/source-packed.c   |  2 +-\n odb/source.h          |  4 ++--\n 7 files changed, 38 insertions(+), 32 deletions(-)\n\ndiff --git a/odb.c b/odb.c\nindex cf6e7938c0..1d6538163b 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -989,11 +989,33 @@ int odb_write_object_ext(struct object_database *odb,\n \t\t\t const void *buf, unsigned long len,\n \t\t\t enum object_type type,\n \t\t\t struct object_id *oid,\n-\t\t\t struct object_id *compat_oid,\n+\t\t\t const struct object_id *compat_oid_in,\n \t\t\t enum odb_write_object_flags flags)\n {\n+\tconst struct git_hash_algo *compat = odb->repo->compat_hash_algo;\n+\tstruct object_id compat_oid, *compat_oid_p = NULL;\n+\n+\tif (compat) {\n+\t\tconst struct git_hash_algo *algo = odb->repo->hash_algo;\n+\n+\t\tif (compat_oid_in) {\n+\t\t\toidcpy(&compat_oid, compat_oid_in);\n+\t\t} else if (type == OBJ_BLOB) {\n+\t\t\thash_object_file(compat, buf, len, type, &compat_oid);\n+\t\t} else {\n+\t\t\tstruct strbuf converted = STRBUF_INIT;\n+\t\t\tconvert_object_file(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+\n+\t\tcompat_oid_p = &compat_oid;\n+\t}\n+\n \treturn odb_source_write_object(odb->sources, buf, len, type,\n-\t\t\t\t       oid, compat_oid, flags);\n+\t\t\t\t       oid, compat_oid_p, flags);\n }\n \n int odb_write_object_stream(struct object_database *odb,\ndiff --git a/odb.h b/odb.h\nindex 94754643d2..066560113e 100644\n--- a/odb.h\n+++ b/odb.h\n@@ -585,9 +585,11 @@ enum odb_write_object_flags {\n \n /*\n  * Write an object into the object database. The object is being written into\n- * the local alternate of the repository. If provided, the converted object ID\n- * as well as the compatibility object ID are written to the respective\n- * pointers.\n+ * the local alternate of the repository. If provided, the object ID of the\n+ * final object is written into `oid`.\n+ *\n+ * If the caller provides a `compat_oid`, then this compatibility object hash\n+ * will be stored instead of computing the compatibility hash ad-hoc.\n  *\n  * Returns 0 on success, a negative error code otherwise.\n  */\n@@ -595,7 +597,7 @@ int odb_write_object_ext(struct object_database *odb,\n \t\t\t const void *buf, unsigned long len,\n \t\t\t enum object_type type,\n \t\t\t struct object_id *oid,\n-\t\t\t struct object_id *compat_oid,\n+\t\t\t const struct object_id *compat_oid,\n \t\t\t enum odb_write_object_flags flags);\n \n static inline int odb_write_object(struct object_database *odb,\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 4138758511..3d9f5eca32 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -163,7 +163,7 @@ static int odb_source_files_write_object(struct odb_source *source,\n \t\t\t\t\t const void *buf, size_t len,\n \t\t\t\t\t enum object_type type,\n \t\t\t\t\t struct object_id *oid,\n-\t\t\t\t\t struct object_id *compat_oid,\n+\t\t\t\t\t const struct object_id *compat_oid,\n \t\t\t\t\t enum odb_write_object_flags flags)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\ndiff --git a/odb/source-inmemory.c b/odb/source-inmemory.c\nindex e47bfd8fcc..e727aba427 100644\n--- a/odb/source-inmemory.c\n+++ b/odb/source-inmemory.c\n@@ -231,7 +231,7 @@ static int odb_source_inmemory_write_object(struct odb_source *source,\n \t\t\t\t\t    const void *buf, size_t len,\n \t\t\t\t\t    enum object_type type,\n \t\t\t\t\t    struct object_id *oid,\n-\t\t\t\t\t    struct object_id *compat_oid UNUSED,\n+\t\t\t\t\t    const struct object_id *compat_oid UNUSED,\n \t\t\t\t\t    enum odb_write_object_flags flags UNUSED)\n {\n \tstruct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source);\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 3f7d04a56e..ca223109cd 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -585,32 +585,14 @@ static int odb_source_loose_freshen_object(struct odb_source *source,\n static int odb_source_loose_write_object(struct odb_source *source,\n \t\t\t\t\t const void *buf, size_t 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 const struct object_id *compat_oid,\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 \tsize_t 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@@ -619,8 +601,8 @@ static int odb_source_loose_write_object(struct odb_source *source,\n \t\treturn 0;\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+\tif (compat_oid)\n+\t\treturn repo_add_loose_object_map(loose, oid, compat_oid);\n \treturn 0;\n }\n \ndiff --git a/odb/source-packed.c b/odb/source-packed.c\nindex 8d9ce197cc..af0d533375 100644\n--- a/odb/source-packed.c\n+++ b/odb/source-packed.c\n@@ -530,7 +530,7 @@ static int odb_source_packed_write_object(struct odb_source *source UNUSED,\n \t\t\t\t\t  size_t len UNUSED,\n \t\t\t\t\t  enum object_type type UNUSED,\n \t\t\t\t\t  struct object_id *oid UNUSED,\n-\t\t\t\t\t  struct object_id *compat_oid UNUSED,\n+\t\t\t\t\t  const struct object_id *compat_oid UNUSED,\n \t\t\t\t\t  unsigned flags UNUSED)\n {\n \treturn error(\"packed backend cannot write objects\");\ndiff --git a/odb/source.h b/odb/source.h\nindex cd63dba91f..b3c1ca3a66 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -207,7 +207,7 @@ struct odb_source {\n \t\t\t    const void *buf, size_t len,\n \t\t\t    enum object_type type,\n \t\t\t    struct object_id *oid,\n-\t\t\t    struct object_id *compat_oid,\n+\t\t\t    const struct object_id *compat_oid,\n \t\t\t    enum odb_write_object_flags flags);\n \n \t/*\n@@ -417,7 +417,7 @@ static inline int odb_source_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,\n \t\t\t\t\t  struct object_id *oid,\n-\t\t\t\t\t  struct object_id *compat_oid,\n+\t\t\t\t\t  const struct object_id *compat_oid,\n \t\t\t\t\t  enum odb_write_object_flags flags)\n {\n \treturn source->write_object(source, buf, len, type, oid,\n\n-- \n2.55.0.407.g700c83d4f3.dirty\n\n"},{"id":"548487","messageId":"20260717-pks-odb-move-loose-object-writing-v1-2-46446a3cb5b7@pks.im","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im","subject":"[PATCH 2/9] t/u-odb-inmemory: implement wrapper for writing objects","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-17T09:32:10Z","receivedAt":"2026-07-17T09:32:24Z","isPatch":true,"body":"In the next commit we're about to change how objects are being written\ninto the object database source. Prepare for this refactoring by\nintroducing a wrapper function into our unit tests so that we don't have\nto adjust all callsites.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/unit-tests/u-odb-inmemory.c | 48 +++++++++++++++++++++----------------------\n 1 file changed, 23 insertions(+), 25 deletions(-)\n\ndiff --git a/t/unit-tests/u-odb-inmemory.c b/t/unit-tests/u-odb-inmemory.c\nindex 6844bfc37c..2dbc3ab1df 100644\n--- a/t/unit-tests/u-odb-inmemory.c\n+++ b/t/unit-tests/u-odb-inmemory.c\n@@ -1,5 +1,6 @@\n #include \"unit-test.h\"\n #include \"hex.h\"\n+#include \"object-file.h\"\n #include \"odb/source-inmemory.h\"\n #include \"odb/streaming.h\"\n #include \"oidset.h\"\n@@ -36,6 +37,16 @@ static void cl_assert_object_info(struct odb_source_inmemory *source,\n \tfree(actual_content);\n }\n \n+static void cl_assert_write_object(struct odb_source_inmemory *source,\n+\t\t\t\t   const char *content,\n+\t\t\t\t   enum object_type type,\n+\t\t\t\t   struct object_id *oid)\n+{\n+\tsize_t content_len = strlen(content);\n+\tcl_must_pass(odb_source_write_object(&source->base, content, content_len,\n+\t\t\t\t\t     type, oid, NULL, 0));\n+}\n+\n void test_odb_inmemory__initialize(void)\n {\n \todb = odb_new(&repo, \"\", \"\");\n@@ -78,8 +89,7 @@ void test_odb_inmemory__read_written_object(void)\n \tconst char data[] = \"foobar\";\n \tstruct object_id written_oid;\n \n-\tcl_must_pass(odb_source_write_object(&source->base, data, strlen(data),\n-\t\t\t\t\t     OBJ_BLOB, &written_oid, NULL, 0));\n+\tcl_assert_write_object(source, data, OBJ_BLOB, &written_oid);\n \tcl_assert_equal_s(oid_to_hex(&written_oid), FOOBAR_OID);\n \tcl_assert_object_info(source, &written_oid, OBJ_BLOB, \"foobar\");\n \n@@ -94,8 +104,7 @@ void test_odb_inmemory__read_stream_object(void)\n \tconst char data[] = \"foobar\";\n \tchar buf[3] = { 0 };\n \n-\tcl_must_pass(odb_source_write_object(&source->base, data, strlen(data),\n-\t\t\t\t\t     OBJ_BLOB, &written_oid, NULL, 0));\n+\tcl_assert_write_object(source, data, OBJ_BLOB, &written_oid);\n \n \tcl_must_pass(odb_source_read_object_stream(&stream, &source->base,\n \t\t\t\t\t\t   &written_oid));\n@@ -141,8 +150,7 @@ void test_odb_inmemory__for_each_object(void)\n \t\tstrbuf_reset(&buf);\n \t\tstrbuf_addf(&buf, \"%d\", i);\n \n-\t\tcl_must_pass(odb_source_write_object(&source->base, buf.buf, buf.len,\n-\t\t\t\t\t\t     OBJ_BLOB, &written_oid, NULL, 0));\n+\t\tcl_assert_write_object(source, buf.buf, OBJ_BLOB, &written_oid);\n \t\tcl_must_pass(oidset_insert(&expected_oids, &written_oid));\n \t}\n \n@@ -174,12 +182,9 @@ void test_odb_inmemory__for_each_object_can_abort_iteration(void)\n \tstruct object_id written_oid;\n \tunsigned counter = 0;\n \n-\tcl_must_pass(odb_source_write_object(&source->base, \"1\", 1,\n-\t\t\t\t\t     OBJ_BLOB, &written_oid, NULL, 0));\n-\tcl_must_pass(odb_source_write_object(&source->base, \"2\", 1,\n-\t\t\t\t\t     OBJ_BLOB, &written_oid, NULL, 0));\n-\tcl_must_pass(odb_source_write_object(&source->base, \"3\", 1,\n-\t\t\t\t\t     OBJ_BLOB, &written_oid, NULL, 0));\n+\tcl_assert_write_object(source, \"1\", OBJ_BLOB, &written_oid);\n+\tcl_assert_write_object(source, \"2\", OBJ_BLOB, &written_oid);\n+\tcl_assert_write_object(source, \"3\", OBJ_BLOB, &written_oid);\n \n \tcl_assert_equal_i(odb_source_for_each_object(&source->base, NULL,\n \t\t\t\t\t\t     abort_after_two_objects,\n@@ -199,12 +204,9 @@ void test_odb_inmemory__count_objects(void)\n \tcl_must_pass(odb_source_count_objects(&source->base, 0, &count));\n \tcl_assert_equal_u(count, 0);\n \n-\tcl_must_pass(odb_source_write_object(&source->base, \"1\", 1,\n-\t\t\t\t\t     OBJ_BLOB, &written_oid, NULL, 0));\n-\tcl_must_pass(odb_source_write_object(&source->base, \"2\", 1,\n-\t\t\t\t\t     OBJ_BLOB, &written_oid, NULL, 0));\n-\tcl_must_pass(odb_source_write_object(&source->base, \"3\", 1,\n-\t\t\t\t\t     OBJ_BLOB, &written_oid, NULL, 0));\n+\tcl_assert_write_object(source, \"1\", OBJ_BLOB, &written_oid);\n+\tcl_assert_write_object(source, \"2\", OBJ_BLOB, &written_oid);\n+\tcl_assert_write_object(source, \"3\", OBJ_BLOB, &written_oid);\n \n \tcl_must_pass(odb_source_count_objects(&source->base, 0, &count));\n \tcl_assert_equal_u(count, 3);\n@@ -228,8 +230,7 @@ void test_odb_inmemory__find_abbrev_len(void)\n \t *\n \t * With only one blob written we expect a length of 4.\n \t */\n-\tcl_must_pass(odb_source_write_object(&source->base, \"368317\", strlen(\"368317\"),\n-\t\t\t\t\t     OBJ_BLOB, &oid1, NULL, 0));\n+\tcl_assert_write_object(source, \"368317\", OBJ_BLOB, &oid1);\n \tcl_must_pass(odb_source_find_abbrev_len(&source->base, &oid1, 4,\n \t\t\t\t\t\t&abbrev_len));\n \tcl_assert_equal_u(abbrev_len, 4);\n@@ -238,8 +239,7 @@ void test_odb_inmemory__find_abbrev_len(void)\n \t * With both objects present, the shared 10-character prefix means we\n \t * need at least 11 characters to uniquely identify either object.\n \t */\n-\tcl_must_pass(odb_source_write_object(&source->base, \"514796\", strlen(\"514796\"),\n-\t\t\t\t\t     OBJ_BLOB, &oid2, NULL, 0));\n+\tcl_assert_write_object(source, \"514796\", OBJ_BLOB, &oid2);\n \tcl_must_pass(odb_source_find_abbrev_len(&source->base, &oid1, 4,\n \t\t\t\t\t\t&abbrev_len));\n \tcl_assert_equal_u(abbrev_len, 11);\n@@ -257,9 +257,7 @@ void test_odb_inmemory__freshen_object(void)\n \tcl_must_pass(parse_oid_hex_algop(RANDOM_OID, &oid, &end, repo.hash_algo));\n \tcl_assert_equal_i(odb_source_freshen_object(&source->base, &oid), 0);\n \n-\tcl_must_pass(odb_source_write_object(&source->base, \"foobar\",\n-\t\t\t\t\t     strlen(\"foobar\"), OBJ_BLOB,\n-\t\t\t\t\t     &written_oid, NULL, 0));\n+\tcl_assert_write_object(source, \"foobar\", OBJ_BLOB, &written_oid);\n \tcl_assert_equal_i(odb_source_freshen_object(&source->base,\n \t\t\t\t\t\t    &written_oid), 1);\n \n\n-- \n2.55.0.407.g700c83d4f3.dirty\n\n"},{"id":"548488","messageId":"20260717-pks-odb-move-loose-object-writing-v1-3-46446a3cb5b7@pks.im","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im","subject":"[PATCH 3/9] odb: compute object hash in `odb_write_object_ext()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-17T09:32:11Z","receivedAt":"2026-07-17T09:32:26Z","isPatch":true,"body":"Same as in a preceding commit, compute the object hash in\n`odb_write_object_ext()` so that we can unify this logic.\n\nBesides unification, this change also allows us to lift the object\nexistence check out of the \"loose\" backend into the generic layer, which\nwill happen in the next commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c                 | 35 ++++++++---------------------------\n object-file.h                 |  4 ----\n odb.c                         |  2 ++\n odb/source-files.c            |  2 +-\n odb/source-inmemory.c         |  6 +++---\n odb/source-loose.c            | 12 +++++++-----\n odb/source-packed.c           |  2 +-\n odb/source.h                  |  4 ++--\n t/unit-tests/u-odb-inmemory.c |  1 +\n 9 files changed, 25 insertions(+), 43 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 5283292f1e..9ca14f484d 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -316,31 +316,6 @@ int parse_loose_header(const char *hdr, struct object_info *oi)\n \treturn 0;\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, size_t len,\n-\t\t\t     struct object_id *oid,\n-\t\t\t     char *hdr, size_t *hdrlen)\n-{\n-\tgit_hash_init(c, algo);\n-\tgit_hash_update(c, hdr, *hdrlen);\n-\tgit_hash_update(c, buf, len);\n-\tgit_hash_final_oid(oid, c);\n-}\n-\n-void write_object_file_prepare(const struct git_hash_algo *algo,\n-\t\t\t       const void *buf, size_t len,\n-\t\t\t       enum object_type type, struct object_id *oid,\n-\t\t\t       char *hdr, size_t *hdrlen)\n-{\n-\tstruct git_hash_ctx c;\n-\n-\t/* Generate the header */\n-\t*hdrlen = format_object_header(hdr, *hdrlen, type, len);\n-\n-\t/* Hash (function pointers) computation */\n-\thash_object_body(algo, &c, buf, len, oid, hdr, hdrlen);\n-}\n-\n #define CHECK_COLLISION_DEST_VANISHED -2\n \n static int check_collision(const char *source, const char *dest)\n@@ -476,10 +451,16 @@ void hash_object_file(const struct git_hash_algo *algo, const void *buf,\n \t\t      size_t len, enum object_type type,\n \t\t      struct object_id *oid)\n {\n+\tstruct git_hash_ctx c;\n \tchar hdr[MAX_HEADER_LEN];\n-\tsize_t hdrlen = sizeof(hdr);\n+\tint hdrlen;\n+\n+\thdrlen = format_object_header(hdr, sizeof(hdr), type, len);\n \n-\twrite_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);\n+\tgit_hash_init(&c, algo);\n+\tgit_hash_update(&c, hdr, hdrlen);\n+\tgit_hash_update(&c, buf, len);\n+\tgit_hash_final_oid(oid, &c);\n }\n \n struct transaction_packfile {\ndiff --git a/object-file.h b/object-file.h\nindex d04ffa6493..08aafcda0d 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -134,10 +134,6 @@ 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      size_t 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, size_t len,\n-\t\t\t       enum object_type type, struct object_id *oid,\n-\t\t\t       char *hdr, size_t *hdrlen);\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,\ndiff --git a/odb.c b/odb.c\nindex 1d6538163b..4adbdf8a64 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -995,6 +995,8 @@ int odb_write_object_ext(struct object_database *odb,\n \tconst struct git_hash_algo *compat = odb->repo->compat_hash_algo;\n \tstruct object_id compat_oid, *compat_oid_p = NULL;\n \n+\thash_object_file(odb->repo->hash_algo, buf, len, type, oid);\n+\n \tif (compat) {\n \t\tconst struct git_hash_algo *algo = odb->repo->hash_algo;\n \ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 3d9f5eca32..06dfc8dd78 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -162,7 +162,7 @@ static int odb_source_files_freshen_object(struct odb_source *source,\n static int odb_source_files_write_object(struct odb_source *source,\n \t\t\t\t\t const void *buf, size_t len,\n \t\t\t\t\t enum object_type type,\n-\t\t\t\t\t struct object_id *oid,\n+\t\t\t\t\t const struct object_id *oid,\n \t\t\t\t\t const struct object_id *compat_oid,\n \t\t\t\t\t enum odb_write_object_flags flags)\n {\ndiff --git a/odb/source-inmemory.c b/odb/source-inmemory.c\nindex e727aba427..963d520317 100644\n--- a/odb/source-inmemory.c\n+++ b/odb/source-inmemory.c\n@@ -230,15 +230,13 @@ static int odb_source_inmemory_count_objects(struct odb_source *source,\n static int odb_source_inmemory_write_object(struct odb_source *source,\n \t\t\t\t\t    const void *buf, size_t len,\n \t\t\t\t\t    enum object_type type,\n-\t\t\t\t\t    struct object_id *oid,\n+\t\t\t\t\t    const struct object_id *oid,\n \t\t\t\t\t    const struct object_id *compat_oid UNUSED,\n \t\t\t\t\t    enum odb_write_object_flags flags UNUSED)\n {\n \tstruct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source);\n \tstruct inmemory_object *object;\n \n-\thash_object_file(source->odb->repo->hash_algo, buf, len, type, oid);\n-\n \tif (!inmemory->objects) {\n \t\tCALLOC_ARRAY(inmemory->objects, 1);\n \t\toidtree_init(inmemory->objects);\n@@ -285,6 +283,8 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source,\n \t\tgoto out;\n \t}\n \n+\thash_object_file(source->odb->repo->hash_algo, data, total_read, OBJ_BLOB, oid);\n+\n \tret = odb_source_inmemory_write_object(source, data, len, OBJ_BLOB, oid,\n \t\t\t\t\t       NULL, 0);\n \tif (ret < 0)\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex ca223109cd..d4715da6d1 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -584,19 +584,21 @@ static int odb_source_loose_freshen_object(struct odb_source *source,\n \n static int odb_source_loose_write_object(struct odb_source *source,\n \t\t\t\t\t const void *buf, size_t len,\n-\t\t\t\t\t enum object_type type, struct object_id *oid,\n+\t\t\t\t\t enum object_type type,\n+\t\t\t\t\t const struct object_id *oid,\n \t\t\t\t\t const struct object_id *compat_oid,\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 \tchar hdr[MAX_HEADER_LEN];\n-\tsize_t hdrlen = sizeof(hdr);\n+\tint hdrlen;\n+\n+\thdrlen = format_object_header(hdr, sizeof(hdr), type, len);\n \n-\t/* Normally if we have it in the pack then we do not bother writing\n+\t/*\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(loose, oid, hdr, hdrlen, buf, len, 0, flags))\ndiff --git a/odb/source-packed.c b/odb/source-packed.c\nindex af0d533375..f7f1706447 100644\n--- a/odb/source-packed.c\n+++ b/odb/source-packed.c\n@@ -529,7 +529,7 @@ static int odb_source_packed_write_object(struct odb_source *source UNUSED,\n \t\t\t\t\t  const void *buf UNUSED,\n \t\t\t\t\t  size_t len UNUSED,\n \t\t\t\t\t  enum object_type type UNUSED,\n-\t\t\t\t\t  struct object_id *oid UNUSED,\n+\t\t\t\t\t  const struct object_id *oid UNUSED,\n \t\t\t\t\t  const struct object_id *compat_oid UNUSED,\n \t\t\t\t\t  unsigned flags UNUSED)\n {\ndiff --git a/odb/source.h b/odb/source.h\nindex b3c1ca3a66..c4e94c9d0d 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -206,7 +206,7 @@ struct odb_source {\n \tint (*write_object)(struct odb_source *source,\n \t\t\t    const void *buf, size_t len,\n \t\t\t    enum object_type type,\n-\t\t\t    struct object_id *oid,\n+\t\t\t    const struct object_id *oid,\n \t\t\t    const struct object_id *compat_oid,\n \t\t\t    enum odb_write_object_flags flags);\n \n@@ -416,7 +416,7 @@ static inline int odb_source_freshen_object(struct odb_source *source,\n static inline int odb_source_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,\n-\t\t\t\t\t  struct object_id *oid,\n+\t\t\t\t\t  const struct object_id *oid,\n \t\t\t\t\t  const struct object_id *compat_oid,\n \t\t\t\t\t  enum odb_write_object_flags flags)\n {\ndiff --git a/t/unit-tests/u-odb-inmemory.c b/t/unit-tests/u-odb-inmemory.c\nindex 2dbc3ab1df..28a69fc244 100644\n--- a/t/unit-tests/u-odb-inmemory.c\n+++ b/t/unit-tests/u-odb-inmemory.c\n@@ -43,6 +43,7 @@ static void cl_assert_write_object(struct odb_source_inmemory *source,\n \t\t\t\t   struct object_id *oid)\n {\n \tsize_t content_len = strlen(content);\n+\thash_object_file(repo.hash_algo, content, content_len, type, oid);\n \tcl_must_pass(odb_source_write_object(&source->base, content, content_len,\n \t\t\t\t\t     type, oid, NULL, 0));\n }\n\n-- \n2.55.0.407.g700c83d4f3.dirty\n\n"},{"id":"548489","messageId":"20260717-pks-odb-move-loose-object-writing-v1-4-46446a3cb5b7@pks.im","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im","subject":"[PATCH 4/9] odb: lift object existence check out of the \"loose\" backend","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-17T09:32:12Z","receivedAt":"2026-07-17T09:32:29Z","isPatch":true,"body":"Before writing a new loose object we first check whether the object\nalready exists in any of the sources attached to the object database.\nThis results in a couple of issues:\n\n  - We have a layering violation, where the source needs to be aware of\n    objects stored in any of the other sources.\n\n  - Every backend would have to reimplement this check, which feels\n    somewhat pointless.\n\n  - It is not possible to easily write an object into a source in case\n    the same object already exists in another source.\n\nRefactor the code and lift up the object existence check from the\n\"loose\" backend into the generic ODB layer. No callers need adjustment\nas none of them write via a specific source, but via the ODB layer.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n odb.c              | 7 +++++++\n odb/source-loose.c | 8 ++------\n 2 files changed, 9 insertions(+), 6 deletions(-)\n\ndiff --git a/odb.c b/odb.c\nindex 4adbdf8a64..bfeca76f4e 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -997,6 +997,13 @@ int odb_write_object_ext(struct object_database *odb,\n \n \thash_object_file(odb->repo->hash_algo, buf, len, type, oid);\n \n+\t/*\n+\t * We can skip the write in case we already have the object available.\n+\t * In that case, we only freshen its mtime.\n+\t */\n+\tif (odb_freshen_object(odb, oid))\n+\t\treturn 0;\n+\n \tif (compat) {\n \t\tconst struct git_hash_algo *algo = odb->repo->hash_algo;\n \ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex d4715da6d1..04af1a54a3 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -595,16 +595,12 @@ static int odb_source_loose_write_object(struct odb_source *source,\n \n \thdrlen = format_object_header(hdr, sizeof(hdr), type, len);\n \n-\t/*\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-\tif (odb_freshen_object(source->odb, oid))\n-\t\treturn 0;\n \tif (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags))\n \t\treturn -1;\n+\n \tif (compat_oid)\n \t\treturn repo_add_loose_object_map(loose, oid, compat_oid);\n+\n \treturn 0;\n }\n \n\n-- \n2.55.0.407.g700c83d4f3.dirty\n\n"},{"id":"548490","messageId":"20260717-pks-odb-move-loose-object-writing-v1-5-46446a3cb5b7@pks.im","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im","subject":"[PATCH 5/9] odb: support setting mtime when writing objects","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-17T09:32:13Z","receivedAt":"2026-07-17T09:32:31Z","isPatch":true,"body":"The function `force_object_loose()` is used to loosen packed objects\nbefore repacking. It passes the pack's mtime along so that the newly\nwritten loose object inherits the same timestamp. This matters for\nobject pruning, which uses the mtime to determine whether an object is\nold enough to be pruned.\n\nIn a subsequent commit, `force_object_loose()` will be converted to use\nthe generic `odb_source_write_object()` interface instead of calling\n`write_loose_object()` directly. But the generic interface doesn't yet\nsupport setting a specific mtime, which makes it impossible to implement\nthe logic as of now.\n\nPrepare for the change by introducing a new `mtime` parameter to this\nfunction that we plumb through the stack. If set, the backends are\ninstructed to set the object's mtime accordingly. If unset, the backends\nare expected to use the current time instead.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/pack-objects.c        |  2 +-\n object-file.c                 | 29 ++++++++++++++++++++---------\n object-file.h                 |  8 +++++---\n odb.c                         |  6 +++---\n odb/source-files.c            | 10 ++++++----\n odb/source-inmemory.c         |  6 ++++--\n odb/source-loose.c            |  8 +++++---\n odb/source-packed.c           | 13 +++++++++++--\n odb/source.h                  | 12 ++++++++----\n read-cache.c                  |  2 +-\n t/unit-tests/u-odb-inmemory.c |  6 +++---\n 11 files changed, 67 insertions(+), 35 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex ea5eab4cf8..e64a96f1a7 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -4642,7 +4642,7 @@ static void loosen_unused_packed_objects(void)\n \t\t\t    !has_sha1_pack_kept_or_nonlocal(&oid) &&\n \t\t\t    !loosened_object_can_be_discarded(&oid, p->mtime)) {\n \t\t\t\tif (force_object_loose(the_repository->objects->sources,\n-\t\t\t\t\t\t       &oid, p->mtime))\n+\t\t\t\t\t\t       &oid, &p->mtime))\n \t\t\t\t\tdie(_(\"unable to force loose object\"));\n \t\t\t\tloosened_objects_nr++;\n \t\t\t}\ndiff --git a/object-file.c b/object-file.c\nindex 9ca14f484d..5b07530950 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -67,9 +67,17 @@ const char *odb_loose_path(struct odb_source_loose *loose,\n }\n \n /* Returns 1 if we have successfully freshened the file, 0 otherwise. */\n-static int freshen_file(const char *fn)\n+static int freshen_file(const char *fn, const time_t *mtime)\n {\n-\treturn !utime(fn, NULL);\n+\tstruct utimbuf times, *timesp = NULL;\n+\n+\tif (mtime) {\n+\t\ttimes.actime = *mtime;\n+\t\ttimes.modtime = *mtime;\n+\t\ttimesp = &times;\n+\t}\n+\n+\treturn !utime(fn, timesp);\n }\n \n /*\n@@ -79,11 +87,12 @@ static int freshen_file(const char *fn)\n  * either does not exist on disk, or has a stale mtime and may be subject to\n  * pruning).\n  */\n-int check_and_freshen_file(const char *fn, int freshen)\n+int check_and_freshen_file(const char *fn, int freshen,\n+\t\t\t   const time_t *mtime)\n {\n \tif (access(fn, F_OK))\n \t\treturn 0;\n-\tif (freshen && !freshen_file(fn))\n+\tif (freshen && !freshen_file(fn, mtime))\n \t\treturn 0;\n \treturn 1;\n }\n@@ -706,7 +715,7 @@ static int end_loose_object_common(struct odb_source_loose *loose,\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+\t\t       const time_t *mtime, unsigned flags)\n {\n \tint fd, ret;\n \tunsigned char compressed[4096];\n@@ -751,9 +760,11 @@ int write_loose_object(struct odb_source_loose *loose,\n \tclose_loose_object(loose, fd, tmp_file.buf);\n \n \tif (mtime) {\n-\t\tstruct utimbuf utb;\n-\t\tutb.actime = mtime;\n-\t\tutb.modtime = mtime;\n+\t\tstruct utimbuf utb = {\n+\t\t\t.actime = *mtime,\n+\t\t\t.modtime = *mtime,\n+\t\t};\n+\n \t\tif (utime(tmp_file.buf, &utb) < 0 &&\n \t\t    !(flags & ODB_WRITE_OBJECT_SILENT))\n \t\t\twarning_errno(_(\"failed utime() on %s\"), tmp_file.buf);\n@@ -883,7 +894,7 @@ int odb_source_loose_write_stream(struct odb_source_loose *loose,\n }\n \n int force_object_loose(struct odb_source *source,\n-\t\t       const struct object_id *oid, time_t mtime)\n+\t\t       const struct object_id *oid, const 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;\ndiff --git a/object-file.h b/object-file.h\nindex 08aafcda0d..9fd540afb6 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -99,7 +99,8 @@ int format_object_header(char *str, size_t size, enum object_type type,\n \t\t\t size_t objsize);\n \n int force_object_loose(struct odb_source *source,\n-\t\t       const struct object_id *oid, time_t mtime);\n+\t\t       const struct object_id *oid,\n+\t\t       const time_t *mtime);\n \n /**\n  * With in-core object data in \"buf\", rehash it to make sure the\n@@ -137,10 +138,11 @@ void hash_object_file(const struct git_hash_algo *algo, const void *buf,\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+\t\t       const 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);\n+int check_and_freshen_file(const char *fn, int freshen,\n+\t\t\t   const time_t *mtime);\n \n /*\n  * Open the loose object at path, check its hash, and return the contents,\ndiff --git a/odb.c b/odb.c\nindex bfeca76f4e..dabd481f57 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -738,7 +738,7 @@ int odb_pretend_object(struct object_database *odb,\n \t\treturn 0;\n \n \treturn odb_source_write_object(odb->inmemory_objects,\n-\t\t\t\t       buf, len, type, oid, NULL, 0);\n+\t\t\t\t       buf, len, type, oid, NULL, NULL, 0);\n }\n \n void *odb_read_object(struct object_database *odb,\n@@ -829,7 +829,7 @@ int odb_freshen_object(struct object_database *odb,\n \tstruct odb_source *source;\n \todb_prepare_alternates(odb);\n \tfor (source = odb->sources; source; source = source->next)\n-\t\tif (odb_source_freshen_object(source, oid))\n+\t\tif (odb_source_freshen_object(source, oid, NULL))\n \t\t\treturn 1;\n \treturn 0;\n }\n@@ -1024,7 +1024,7 @@ int odb_write_object_ext(struct object_database *odb,\n \t}\n \n \treturn odb_source_write_object(odb->sources, buf, len, type,\n-\t\t\t\t       oid, compat_oid_p, flags);\n+\t\t\t\t       oid, compat_oid_p, NULL, flags);\n }\n \n int odb_write_object_stream(struct object_database *odb,\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 06dfc8dd78..4df4e1af6c 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -150,11 +150,12 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source,\n }\n \n static int odb_source_files_freshen_object(struct odb_source *source,\n-\t\t\t\t\t   const struct object_id *oid)\n+\t\t\t\t\t   const struct object_id *oid,\n+\t\t\t\t\t   const time_t *mtime)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n-\tif (odb_source_freshen_object(&files->packed->base, oid) ||\n-\t    odb_source_freshen_object(&files->loose->base, oid))\n+\tif (odb_source_freshen_object(&files->packed->base, oid, mtime) ||\n+\t    odb_source_freshen_object(&files->loose->base, oid, mtime))\n \t\treturn 1;\n \treturn 0;\n }\n@@ -164,11 +165,12 @@ static int odb_source_files_write_object(struct odb_source *source,\n \t\t\t\t\t enum object_type type,\n \t\t\t\t\t const struct object_id *oid,\n \t\t\t\t\t const struct object_id *compat_oid,\n+\t\t\t\t\t const time_t *mtime,\n \t\t\t\t\t enum odb_write_object_flags flags)\n {\n \tstruct odb_source_files *files = odb_source_files_downcast(source);\n \treturn odb_source_write_object(&files->loose->base, buf, len, type,\n-\t\t\t\t       oid, compat_oid, flags);\n+\t\t\t\t       oid, compat_oid, mtime, flags);\n }\n \n static int odb_source_files_write_object_stream(struct odb_source *source,\ndiff --git a/odb/source-inmemory.c b/odb/source-inmemory.c\nindex 963d520317..3e71611b8e 100644\n--- a/odb/source-inmemory.c\n+++ b/odb/source-inmemory.c\n@@ -232,6 +232,7 @@ static int odb_source_inmemory_write_object(struct odb_source *source,\n \t\t\t\t\t    enum object_type type,\n \t\t\t\t\t    const struct object_id *oid,\n \t\t\t\t\t    const struct object_id *compat_oid UNUSED,\n+\t\t\t\t\t    const time_t *mtime UNUSED,\n \t\t\t\t\t    enum odb_write_object_flags flags UNUSED)\n {\n \tstruct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source);\n@@ -286,7 +287,7 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source,\n \thash_object_file(source->odb->repo->hash_algo, data, total_read, OBJ_BLOB, oid);\n \n \tret = odb_source_inmemory_write_object(source, data, len, OBJ_BLOB, oid,\n-\t\t\t\t\t       NULL, 0);\n+\t\t\t\t\t       NULL, NULL, 0);\n \tif (ret < 0)\n \t\tgoto out;\n \n@@ -296,7 +297,8 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source,\n }\n \n static int odb_source_inmemory_freshen_object(struct odb_source *source,\n-\t\t\t\t\t      const struct object_id *oid)\n+\t\t\t\t\t      const struct object_id *oid,\n+\t\t\t\t\t      const time_t *mtime UNUSED)\n {\n \tstruct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source);\n \tif (find_cached_object(inmemory, oid))\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 04af1a54a3..520a30157c 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -574,12 +574,13 @@ static int odb_source_loose_count_objects(struct odb_source *source,\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+\t\t\t\t\t   const struct object_id *oid,\n+\t\t\t\t\t   const time_t *mtime)\n {\n \tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\n \tstatic struct strbuf path = STRBUF_INIT;\n \todb_loose_path(loose, &path, oid);\n-\treturn !!check_and_freshen_file(path.buf, 1);\n+\treturn !!check_and_freshen_file(path.buf, 1, mtime);\n }\n \n static int odb_source_loose_write_object(struct odb_source *source,\n@@ -587,6 +588,7 @@ static int odb_source_loose_write_object(struct odb_source *source,\n \t\t\t\t\t enum object_type type,\n \t\t\t\t\t const struct object_id *oid,\n \t\t\t\t\t const struct object_id *compat_oid,\n+\t\t\t\t\t const time_t *mtime,\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@@ -595,7 +597,7 @@ static int odb_source_loose_write_object(struct odb_source *source,\n \n \thdrlen = format_object_header(hdr, sizeof(hdr), type, len);\n \n-\tif (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags))\n+\tif (write_loose_object(loose, oid, hdr, hdrlen, buf, len, mtime, flags))\n \t\treturn -1;\n \n \tif (compat_oid)\ndiff --git a/odb/source-packed.c b/odb/source-packed.c\nindex f7f1706447..5e5da9bc54 100644\n--- a/odb/source-packed.c\n+++ b/odb/source-packed.c\n@@ -507,18 +507,26 @@ static int odb_source_packed_find_abbrev_len(struct odb_source *source,\n }\n \n static int odb_source_packed_freshen_object(struct odb_source *source,\n-\t\t\t\t\t    const struct object_id *oid)\n+\t\t\t\t\t    const struct object_id *oid,\n+\t\t\t\t\t    const time_t *mtime)\n {\n \tstruct odb_source_packed *packed = odb_source_packed_downcast(source);\n+\tstruct utimbuf times, *timesp = NULL;\n \tstruct pack_entry e;\n \n+\tif (mtime) {\n+\t\ttimes.actime = *mtime;\n+\t\ttimes.modtime = *mtime;\n+\t\ttimesp = &times;\n+\t}\n+\n \tif (!find_pack_entry(packed, oid, &e))\n \t\treturn 0;\n \tif (e.p->is_cruft)\n \t\treturn 0;\n \tif (e.p->freshened)\n \t\treturn 1;\n-\tif (utime(e.p->pack_name, NULL))\n+\tif (utime(e.p->pack_name, timesp))\n \t\treturn 0;\n \te.p->freshened = 1;\n \n@@ -531,6 +539,7 @@ static int odb_source_packed_write_object(struct odb_source *source UNUSED,\n \t\t\t\t\t  enum object_type type UNUSED,\n \t\t\t\t\t  const struct object_id *oid UNUSED,\n \t\t\t\t\t  const struct object_id *compat_oid UNUSED,\n+\t\t\t\t\t  const time_t *mtime UNUSED,\n \t\t\t\t\t  unsigned flags UNUSED)\n {\n \treturn error(\"packed backend cannot write objects\");\ndiff --git a/odb/source.h b/odb/source.h\nindex c4e94c9d0d..fc04dd5cda 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -190,7 +190,8 @@ struct odb_source {\n \t * has been freshened.\n \t */\n \tint (*freshen_object)(struct odb_source *source,\n-\t\t\t      const struct object_id *oid);\n+\t\t\t      const struct object_id *oid,\n+\t\t\t      const time_t *mtime);\n \n \t/*\n \t * This callback is expected to persist the given object into the\n@@ -208,6 +209,7 @@ struct odb_source {\n \t\t\t    enum object_type type,\n \t\t\t    const struct object_id *oid,\n \t\t\t    const struct object_id *compat_oid,\n+\t\t\t    const time_t *mtime,\n \t\t\t    enum odb_write_object_flags flags);\n \n \t/*\n@@ -403,9 +405,10 @@ static inline int odb_source_find_abbrev_len(struct odb_source *source,\n  * not exist.\n  */\n static inline int odb_source_freshen_object(struct odb_source *source,\n-\t\t\t\t\t    const struct object_id *oid)\n+\t\t\t\t\t    const struct object_id *oid,\n+\t\t\t\t\t    const time_t *mtime)\n {\n-\treturn source->freshen_object(source, oid);\n+\treturn source->freshen_object(source, oid, mtime);\n }\n \n /*\n@@ -418,10 +421,11 @@ static inline int odb_source_write_object(struct odb_source *source,\n \t\t\t\t\t  enum object_type type,\n \t\t\t\t\t  const struct object_id *oid,\n \t\t\t\t\t  const struct object_id *compat_oid,\n+\t\t\t\t\t  const time_t *mtime,\n \t\t\t\t\t  enum odb_write_object_flags flags)\n {\n \treturn source->write_object(source, buf, len, type, oid,\n-\t\t\t\t    compat_oid, flags);\n+\t\t\t\t    compat_oid, mtime, flags);\n }\n \n /*\ndiff --git a/read-cache.c b/read-cache.c\nindex 3510b49edf..c67930177f 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -2342,7 +2342,7 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist)\n  */\n static void freshen_shared_index(const char *shared_index, int warn)\n {\n-\tif (!check_and_freshen_file(shared_index, 1) && warn)\n+\tif (!check_and_freshen_file(shared_index, 1, NULL) && warn)\n \t\twarning(_(\"could not freshen shared index '%s'\"), shared_index);\n }\n \ndiff --git a/t/unit-tests/u-odb-inmemory.c b/t/unit-tests/u-odb-inmemory.c\nindex 28a69fc244..ddf2db5c81 100644\n--- a/t/unit-tests/u-odb-inmemory.c\n+++ b/t/unit-tests/u-odb-inmemory.c\n@@ -45,7 +45,7 @@ static void cl_assert_write_object(struct odb_source_inmemory *source,\n \tsize_t content_len = strlen(content);\n \thash_object_file(repo.hash_algo, content, content_len, type, oid);\n \tcl_must_pass(odb_source_write_object(&source->base, content, content_len,\n-\t\t\t\t\t     type, oid, NULL, 0));\n+\t\t\t\t\t     type, oid, NULL, NULL, 0));\n }\n \n void test_odb_inmemory__initialize(void)\n@@ -256,11 +256,11 @@ void test_odb_inmemory__freshen_object(void)\n \tconst char *end;\n \n \tcl_must_pass(parse_oid_hex_algop(RANDOM_OID, &oid, &end, repo.hash_algo));\n-\tcl_assert_equal_i(odb_source_freshen_object(&source->base, &oid), 0);\n+\tcl_assert_equal_i(odb_source_freshen_object(&source->base, &oid, NULL), 0);\n \n \tcl_assert_write_object(source, \"foobar\", OBJ_BLOB, &written_oid);\n \tcl_assert_equal_i(odb_source_freshen_object(&source->base,\n-\t\t\t\t\t\t    &written_oid), 1);\n+\t\t\t\t\t\t    &written_oid, NULL), 1);\n \n \todb_source_free(&source->base);\n }\n\n-- \n2.55.0.407.g700c83d4f3.dirty\n\n"},{"id":"548491","messageId":"20260717-pks-odb-move-loose-object-writing-v1-6-46446a3cb5b7@pks.im","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im","subject":"[PATCH 6/9] object-file: fix memory leak in `force_object_loose()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-17T09:32:14Z","receivedAt":"2026-07-17T09:32:34Z","isPatch":true,"body":"We return an error when converting the given object to the compatibility\nhash algorithm fails. This early return causes a memory leak though,\nbecause we don't free the content buffer that we've already read before\nvia `odb_read_object_info_extended()`.\n\nPlug the memory leak by creating a common exit path where the buffer\ngets free'd.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c | 26 ++++++++++++++++++--------\n 1 file changed, 18 insertions(+), 8 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 5b07530950..067a63a4f1 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -898,7 +898,7 @@ int force_object_loose(struct odb_source *source,\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+\tvoid *buf = NULL;\n \tsize_t len;\n \tstruct object_info oi = OBJECT_INFO_INIT;\n \tstruct object_id compat_oid;\n@@ -916,19 +916,29 @@ int force_object_loose(struct odb_source *source,\n \toi.typep = &type;\n \toi.sizep = &len;\n \toi.contentp = &buf;\n-\tif (odb_read_object_info_extended(source->odb, oid, &oi, 0))\n-\t\treturn error(_(\"cannot read object for %s\"), oid_to_hex(oid));\n+\tif (odb_read_object_info_extended(source->odb, oid, &oi, 0)) {\n+\t\tret = error(_(\"cannot read object for %s\"), oid_to_hex(oid));\n+\t\tgoto out;\n+\t}\n+\n \tif (compat) {\n-\t\tif (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid))\n-\t\t\treturn error(_(\"cannot map object %s to %s\"),\n-\t\t\t\t     oid_to_hex(oid), compat->name);\n+\t\tif (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid)) {\n+\t\t\tret = error(_(\"cannot map object %s to %s\"),\n+\t\t\t\t    oid_to_hex(oid), compat->name);\n+\t\t\tgoto out;\n+\t\t}\n \t}\n+\n \thdrlen = format_object_header(hdr, sizeof(hdr), type, len);\n \tret = write_loose_object(files->loose, oid, hdr, hdrlen, buf, len, mtime, 0);\n-\tif (!ret && compat)\n+\tif (ret)\n+\t\tgoto out;\n+\n+\tif (compat)\n \t\tret = repo_add_loose_object_map(files->loose, oid, &compat_oid);\n-\tfree(buf);\n \n+out:\n+\tfree(buf);\n \treturn ret;\n }\n \n\n-- \n2.55.0.407.g700c83d4f3.dirty\n\n"},{"id":"548492","messageId":"20260717-pks-odb-move-loose-object-writing-v1-7-46446a3cb5b7@pks.im","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im","subject":"[PATCH 7/9] object-file: force objects loose via generic interface","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-17T09:32:15Z","receivedAt":"2026-07-17T09:32:36Z","isPatch":true,"body":"When repacking objects we may end up \"loosening\" objects via\n`force_objects_loose()`. The implementation of this logic still sits\nwith \"object-file.c\" even though it is ultimately an implementation\ndetail of the \"files\" backend.\n\nMoving this logic around is non-trivial though as we depend on\n`write_loose_object()`, which is an internal implementation detail of\nhow we write loose objects. Until now it wasn't possible to use the\ngeneric function `odb_source_write_object()` though, because the \"loose\"\nimplementation thereof would skip writing the object in case it already\nexists in any other source.\n\nThis restriction was lifted over the preceding commits though, where\nthis object existence check is now handled on the object database level\nand not on the individual source level anymore. Consequently, it is now\npossible to use generic interfaces.\n\nRefactor the code accordingly so that we can move the logic around in a\nsubsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c | 19 +++++++------------\n 1 file changed, 7 insertions(+), 12 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 067a63a4f1..89825feed0 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -898,13 +898,11 @@ int force_object_loose(struct odb_source *source,\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 = NULL;\n-\tsize_t len;\n \tstruct object_info oi = OBJECT_INFO_INIT;\n-\tstruct object_id compat_oid;\n+\tstruct object_id compat_oid, *compat_oid_p = NULL;\n \tenum object_type type;\n-\tchar hdr[MAX_HEADER_LEN];\n-\tint hdrlen;\n+\tvoid *buf = NULL;\n+\tsize_t len;\n \tint ret;\n \n \tfor (struct odb_source *s = source->odb->sources; s; s = s->next) {\n@@ -927,15 +925,12 @@ int force_object_loose(struct odb_source *source,\n \t\t\t\t    oid_to_hex(oid), compat->name);\n \t\t\tgoto out;\n \t\t}\n-\t}\n \n-\thdrlen = format_object_header(hdr, sizeof(hdr), type, len);\n-\tret = write_loose_object(files->loose, oid, hdr, hdrlen, buf, len, mtime, 0);\n-\tif (ret)\n-\t\tgoto out;\n+\t\tcompat_oid_p = &compat_oid;\n+\t}\n \n-\tif (compat)\n-\t\tret = repo_add_loose_object_map(files->loose, oid, &compat_oid);\n+\tret = odb_source_write_object(&files->loose->base, buf, len, type, oid,\n+\t\t\t\t      compat_oid_p, mtime, 0);\n \n out:\n \tfree(buf);\n\n-- \n2.55.0.407.g700c83d4f3.dirty\n\n"},{"id":"548493","messageId":"20260717-pks-odb-move-loose-object-writing-v1-8-46446a3cb5b7@pks.im","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im","subject":"[PATCH 8/9] object-file: move `force_object_loose()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-17T09:32:16Z","receivedAt":"2026-07-17T09:32:38Z","isPatch":true,"body":"In the preceding commits we have refactored `force_object_loose()` to\nnot call internal functions anymore for writing the object. Instead, it\nnow only uses generic functions that are accessible to all callers.\n\nConsequently, we can now easily move the function to its only caller,\nwhich is git-pack-objects(1). Do so.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/pack-objects.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++\n object-file.c          | 44 --------------------------------------------\n object-file.h          |  4 ----\n 3 files changed, 46 insertions(+), 48 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex e64a96f1a7..bb3bc486e8 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -32,6 +32,7 @@\n #include \"list.h\"\n #include \"packfile.h\"\n #include \"object-file.h\"\n+#include \"object-file-convert.h\"\n #include \"odb.h\"\n #include \"odb/streaming.h\"\n #include \"replace-object.h\"\n@@ -4622,6 +4623,51 @@ static int loosened_object_can_be_discarded(const struct object_id *oid,\n \treturn 1;\n }\n \n+static int force_object_loose(struct odb_source *source,\n+\t\t\t      const struct object_id *oid,\n+\t\t\t      const 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+\tstruct object_info oi = OBJECT_INFO_INIT;\n+\tstruct object_id compat_oid, *compat_oid_p = NULL;\n+\tenum object_type type;\n+\tvoid *buf = NULL;\n+\tsize_t len;\n+\tint ret;\n+\n+\tfor (struct odb_source *s = source->odb->sources; s; s = s->next) {\n+\t\tstruct odb_source_files *files = odb_source_files_downcast(s);\n+\t\tif (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0))\n+\t\t\treturn 0;\n+\t}\n+\n+\toi.typep = &type;\n+\toi.sizep = &len;\n+\toi.contentp = &buf;\n+\tif (odb_read_object_info_extended(source->odb, oid, &oi, 0)) {\n+\t\tret = error(_(\"cannot read object for %s\"), oid_to_hex(oid));\n+\t\tgoto out;\n+\t}\n+\n+\tif (compat) {\n+\t\tif (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid)) {\n+\t\t\tret = error(_(\"cannot map object %s to %s\"),\n+\t\t\t\t    oid_to_hex(oid), compat->name);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tcompat_oid_p = &compat_oid;\n+\t}\n+\n+\tret = odb_source_write_object(&files->loose->base, buf, len, type, oid,\n+\t\t\t\t      compat_oid_p, mtime, 0);\n+\n+out:\n+\tfree(buf);\n+\treturn ret;\n+}\n+\n static void loosen_unused_packed_objects(void)\n {\n \tstruct packed_git *p;\ndiff --git a/object-file.c b/object-file.c\nindex 89825feed0..b867d8d9de 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -893,50 +893,6 @@ int odb_source_loose_write_stream(struct odb_source_loose *loose,\n \treturn err;\n }\n \n-int force_object_loose(struct odb_source *source,\n-\t\t       const struct object_id *oid, const 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-\tstruct object_info oi = OBJECT_INFO_INIT;\n-\tstruct object_id compat_oid, *compat_oid_p = NULL;\n-\tenum object_type type;\n-\tvoid *buf = NULL;\n-\tsize_t len;\n-\tint ret;\n-\n-\tfor (struct odb_source *s = source->odb->sources; s; s = s->next) {\n-\t\tstruct odb_source_files *files = odb_source_files_downcast(s);\n-\t\tif (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0))\n-\t\t\treturn 0;\n-\t}\n-\n-\toi.typep = &type;\n-\toi.sizep = &len;\n-\toi.contentp = &buf;\n-\tif (odb_read_object_info_extended(source->odb, oid, &oi, 0)) {\n-\t\tret = error(_(\"cannot read object for %s\"), oid_to_hex(oid));\n-\t\tgoto out;\n-\t}\n-\n-\tif (compat) {\n-\t\tif (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid)) {\n-\t\t\tret = error(_(\"cannot map object %s to %s\"),\n-\t\t\t\t    oid_to_hex(oid), compat->name);\n-\t\t\tgoto out;\n-\t\t}\n-\n-\t\tcompat_oid_p = &compat_oid;\n-\t}\n-\n-\tret = odb_source_write_object(&files->loose->base, buf, len, type, oid,\n-\t\t\t\t      compat_oid_p, mtime, 0);\n-\n-out:\n-\tfree(buf);\n-\treturn ret;\n-}\n-\n /*\n  * We can't use the normal fsck_error_function() for index_mem(),\n  * because we don't yet have a valid oid for it to report. Instead,\ndiff --git a/object-file.h b/object-file.h\nindex 9fd540afb6..31781a9c53 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -98,10 +98,6 @@ int for_each_file_in_obj_subdir(unsigned int subdir_nr,\n int format_object_header(char *str, size_t size, enum object_type type,\n \t\t\t size_t objsize);\n \n-int force_object_loose(struct odb_source *source,\n-\t\t       const struct object_id *oid,\n-\t\t       const time_t *mtime);\n-\n /**\n  * With in-core object data in \"buf\", rehash it to make sure the\n  * object name actually matches \"oid\" to detect object corruption.\n\n-- \n2.55.0.407.g700c83d4f3.dirty\n\n"},{"id":"548494","messageId":"20260717-pks-odb-move-loose-object-writing-v1-9-46446a3cb5b7@pks.im","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im","subject":"[PATCH 9/9] object-file: move logic to write loose objects","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-17T09:32:17Z","receivedAt":"2026-07-17T09:32:42Z","isPatch":true,"body":"The logic to write loose objects is split up across \"object-file.c\" and\n\"odb/source-loose.c\". This split is somewhat weird, but it is the result\nof two things:\n\n  - `force_object_loose()` used to reach into internals of how exactly\n    we write objects.\n\n  - The logic of writing objects is intertwined with potentially\n    starting a transaction.\n\nWe have refactored `force_object_loose()` over preceding commits to work\nvia generic interfaces now, so this reason doesn't exist anymore. But\nthe second reason still does, as our management of \"files\" transactions\nand their ad-hoc creation is still very messy. This area definitely\nrequires further work, and that work is indeed ongoing.\n\nThat being said, we can already move the writing logic into the \"loose\"\nbackend rather easily. All we have to do is to expose two functions that\nrelate to the transactions.\n\nExpose these two functions and move the writing logic into the \"loose\"\nbackend accordingly so that it becomes more self-contained. Note that\nthis requires us to drop a reference to `the_repository` in favor of\nusing the source's repository in `start_loose_object_common()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n object-file.c      | 360 +----------------------------------------------------\n object-file.h      |  22 +---\n odb/source-loose.c | 354 +++++++++++++++++++++++++++++++++++++++++++++++++++-\n 3 files changed, 357 insertions(+), 379 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex b867d8d9de..bdc97d7943 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -491,7 +491,7 @@ struct odb_transaction_files {\n \tconst char *prefix;\n };\n \n-static int odb_transaction_files_prepare(struct odb_transaction *base)\n+int odb_transaction_files_prepare(struct odb_transaction *base)\n {\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of_or_null(base, struct odb_transaction_files, base);\n@@ -514,8 +514,8 @@ static int odb_transaction_files_prepare(struct odb_transaction *base)\n \treturn 0;\n }\n \n-static void odb_transaction_files_fsync(struct odb_transaction *base,\n-\t\t\t\t\tint fd, const char *filename)\n+void odb_transaction_files_fsync(struct odb_transaction *base,\n+\t\t\t\t int fd, const char *filename)\n {\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of_or_null(base, struct odb_transaction_files, base);\n@@ -539,360 +539,6 @@ static void odb_transaction_files_fsync(struct odb_transaction *base,\n \t}\n }\n \n-/* Finalize a file on disk, and close it. */\n-static void close_loose_object(struct odb_source_loose *loose,\n-\t\t\t       int fd, const char *filename)\n-{\n-\tif (loose->base.will_destroy)\n-\t\tgoto out;\n-\n-\tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\todb_transaction_files_fsync(loose->base.odb->transaction, fd, filename);\n-\telse if (fsync_object_files > 0)\n-\t\tfsync_or_die(fd, filename);\n-\telse\n-\t\tfsync_component_or_die(FSYNC_COMPONENT_LOOSE_OBJECT, fd,\n-\t\t\t\t       filename);\n-\n-out:\n-\tif (close(fd) != 0)\n-\t\tdie_errno(_(\"error when closing loose object file\"));\n-}\n-\n-/* Size of directory component, including the ending '/' */\n-static inline int directory_size(const char *filename)\n-{\n-\tconst char *s = strrchr(filename, '/');\n-\tif (!s)\n-\t\treturn 0;\n-\treturn s - filename + 1;\n-}\n-\n-/*\n- * This creates a temporary file in the same directory as the final\n- * 'filename'\n- *\n- * We want to avoid cross-directory filename renames, because those\n- * can have problems on various filesystems (FAT, NFS, Coda).\n- */\n-static int create_tmpfile(struct repository *repo,\n-\t\t\t  struct strbuf *tmp, const char *filename)\n-{\n-\tint fd, dirlen = directory_size(filename);\n-\n-\tstrbuf_reset(tmp);\n-\tstrbuf_add(tmp, filename, dirlen);\n-\tstrbuf_addstr(tmp, \"tmp_obj_XXXXXX\");\n-\tfd = git_mkstemp_mode(tmp->buf, 0444);\n-\tif (fd < 0 && dirlen && errno == ENOENT) {\n-\t\t/*\n-\t\t * Make sure the directory exists; note that the contents\n-\t\t * of the buffer are undefined after mkstemp returns an\n-\t\t * error, so we have to rewrite the whole buffer from\n-\t\t * scratch.\n-\t\t */\n-\t\tstrbuf_reset(tmp);\n-\t\tstrbuf_add(tmp, filename, dirlen - 1);\n-\t\tif (mkdir(tmp->buf, 0777) && errno != EEXIST)\n-\t\t\treturn -1;\n-\t\tif (adjust_shared_perm(repo, tmp->buf))\n-\t\t\treturn -1;\n-\n-\t\t/* Try again */\n-\t\tstrbuf_addstr(tmp, \"/tmp_obj_XXXXXX\");\n-\t\tfd = git_mkstemp_mode(tmp->buf, 0444);\n-\t}\n-\treturn fd;\n-}\n-\n-/**\n- * Common steps for loose object writers to start writing loose\n- * objects:\n- *\n- * - Create tmpfile for the loose object.\n- * - Setup zlib stream for compression.\n- * - Start to feed header to zlib stream.\n- *\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_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-\t\t\t\t     unsigned char *buf, size_t buflen,\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 = loose->base.odb->repo->hash_algo;\n-\tconst struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;\n-\tint fd;\n-\tstruct repo_config_values *cfg = repo_config_values(the_repository);\n-\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     loose->base.path);\n-\t\telse\n-\t\t\treturn error_errno(\n-\t\t\t\t_(\"unable to create temporary file\"));\n-\t}\n-\n-\t/*  Setup zlib stream for compression */\n-\tgit_deflate_init(stream, cfg->zlib_compression_level);\n-\tstream->next_out = buf;\n-\tstream->avail_out = buflen;\n-\tgit_hash_init(c, algo);\n-\tif (compat && compat_c)\n-\t\tgit_hash_init(compat_c, compat);\n-\n-\t/*  Start to feed header to zlib stream */\n-\tstream->next_in = (unsigned char *)hdr;\n-\tstream->avail_in = hdrlen;\n-\twhile (git_deflate(stream, 0) == Z_OK)\n-\t\t; /* nothing */\n-\tgit_hash_update(c, hdr, hdrlen);\n-\tif (compat && compat_c)\n-\t\tgit_hash_update(compat_c, hdr, hdrlen);\n-\n-\treturn fd;\n-}\n-\n-/**\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_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 = loose->base.odb->repo->compat_hash_algo;\n-\tint ret;\n-\n-\tret = git_deflate(stream, flush ? Z_FINISH : 0);\n-\tgit_hash_update(c, in0, stream->next_in - in0);\n-\tif (compat && compat_c)\n-\t\tgit_hash_update(compat_c, in0, stream->next_in - in0);\n-\tif (write_in_full(fd, compressed, stream->next_out - compressed) < 0)\n-\t\tdie_errno(_(\"unable to write loose object file\"));\n-\tstream->next_out = compressed;\n-\tstream->avail_out = compressed_len;\n-\n-\treturn ret;\n-}\n-\n-/**\n- * Common steps for loose object writers to end writing loose objects:\n- *\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_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 = loose->base.odb->repo->compat_hash_algo;\n-\tint ret;\n-\n-\tret = git_deflate_end_gently(stream);\n-\tif (ret != Z_OK)\n-\t\treturn ret;\n-\tgit_hash_final_oid(oid, c);\n-\tif (compat && compat_c)\n-\t\tgit_hash_final_oid(compat_oid, compat_c);\n-\n-\treturn Z_OK;\n-}\n-\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       const time_t *mtime, unsigned flags)\n-{\n-\tint fd, ret;\n-\tunsigned char compressed[4096];\n-\tgit_zstream stream;\n-\tstruct git_hash_ctx c;\n-\tstruct object_id parano_oid;\n-\tstatic struct strbuf tmp_file = STRBUF_INIT;\n-\tstatic struct strbuf filename = STRBUF_INIT;\n-\n-\tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\todb_transaction_files_prepare(loose->base.odb->transaction);\n-\n-\todb_loose_path(loose, &filename, oid);\n-\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-\t\treturn -1;\n-\n-\t/* Then the data itself.. */\n-\tstream.next_in = (void *)buf;\n-\tstream.avail_in = len;\n-\tdo {\n-\t\tunsigned char *in0 = stream.next_in;\n-\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(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-\tif (!oideq(oid, &parano_oid))\n-\t\tdie(_(\"confused by unstable object source data for %s\"),\n-\t\t    oid_to_hex(oid));\n-\n-\tclose_loose_object(loose, fd, tmp_file.buf);\n-\n-\tif (mtime) {\n-\t\tstruct utimbuf utb = {\n-\t\t\t.actime = *mtime,\n-\t\t\t.modtime = *mtime,\n-\t\t};\n-\n-\t\tif (utime(tmp_file.buf, &utb) < 0 &&\n-\t\t    !(flags & ODB_WRITE_OBJECT_SILENT))\n-\t\t\twarning_errno(_(\"failed utime() on %s\"), tmp_file.buf);\n-\t}\n-\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_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-\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-\tgit_zstream stream;\n-\tstruct git_hash_ctx c, compat_c;\n-\tstruct strbuf tmp_file = STRBUF_INIT;\n-\tstruct strbuf filename = STRBUF_INIT;\n-\tunsigned char buf[8192];\n-\tint dirlen;\n-\tchar hdr[MAX_HEADER_LEN];\n-\tint hdrlen;\n-\n-\tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\todb_transaction_files_prepare(loose->base.odb->transaction);\n-\n-\t/* Since oid is not determined, save tmp file to odb path. */\n-\tstrbuf_addf(&filename, \"%s/\", loose->base.path);\n-\thdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len);\n-\n-\t/*\n-\t * Common steps for write_loose_object and stream_loose_object to\n-\t * start writing loose objects:\n-\t *\n-\t *  - Create tmpfile for the loose object.\n-\t *  - Setup zlib stream for compression.\n-\t *  - Start to feed header to zlib stream.\n-\t */\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-\t\terr = -1;\n-\t\tgoto cleanup;\n-\t}\n-\n-\t/* Then the data itself.. */\n-\tdo {\n-\t\tunsigned char *in0 = stream.next_in;\n-\n-\t\tif (!stream.avail_in && !in_stream->is_finished) {\n-\t\t\tssize_t read_len = odb_write_stream_read(in_stream, buf,\n-\t\t\t\t\t\t\t\t sizeof(buf));\n-\t\t\tif (read_len < 0) {\n-\t\t\t\tclose(fd);\n-\t\t\t\terr = -1;\n-\t\t\t\tgoto cleanup;\n-\t\t\t}\n-\n-\t\t\tstream.avail_in = read_len;\n-\t\t\tstream.next_in = buf;\n-\t\t\tin0 = buf;\n-\t\t\t/* All data has been read. */\n-\t\t\tif (in_stream->is_finished)\n-\t\t\t\tflush = 1;\n-\t\t}\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-\t\t * buffer. If we get Z_BUF_ERROR due to too few input bytes,\n-\t\t * then we'll replenish them in the next input_stream->read()\n-\t\t * call when we loop.\n-\t\t */\n-\t} while (ret == Z_OK || ret == Z_BUF_ERROR);\n-\n-\tif (stream.total_in != len + hdrlen)\n-\t\tdie(_(\"write stream object %\"PRIuMAX\" != %\"PRIuMAX), (uintmax_t)stream.total_in,\n-\t\t    (uintmax_t)len + hdrlen);\n-\n-\t/*\n-\t * Common steps for write_loose_object and stream_loose_object to\n-\t * end writing loose object:\n-\t *\n-\t *  - End the compression of zlib stream.\n-\t *  - Get the calculated oid.\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(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(loose, fd, tmp_file.buf);\n-\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(loose, &filename, oid);\n-\n-\t/* We finally know the object path, and create the missing dir. */\n-\tdirlen = directory_size(filename.buf);\n-\tif (dirlen) {\n-\t\tstruct strbuf dir = STRBUF_INIT;\n-\t\tstrbuf_add(&dir, filename.buf, dirlen);\n-\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-\t\t\tgoto cleanup;\n-\t\t}\n-\t\tstrbuf_release(&dir);\n-\t}\n-\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(loose, oid, &compat_oid);\n-cleanup:\n-\tstrbuf_release(&tmp_file);\n-\tstrbuf_release(&filename);\n-\treturn err;\n-}\n-\n /*\n  * We can't use the normal fsck_error_function() for index_mem(),\n  * because we don't yet have a valid oid for it to report. Instead,\ndiff --git a/object-file.h b/object-file.h\nindex 31781a9c53..805f2cfa28 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -24,20 +24,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- * 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-\n /*\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@@ -131,10 +117,6 @@ 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      size_t len, enum object_type type,\n \t\t      struct object_id *oid);\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       const 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,\n@@ -195,4 +177,8 @@ int odb_transaction_files_begin(struct odb_source *source,\n \t\t\t\tstruct odb_transaction **out,\n \t\t\t\tenum odb_transaction_flags flags);\n \n+int odb_transaction_files_prepare(struct odb_transaction *base);\n+void odb_transaction_files_fsync(struct odb_transaction *base,\n+\t\t\t\t int fd, const char *filename);\n+\n #endif /* OBJECT_FILE_H */\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 520a30157c..ef0e919277 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -11,8 +11,11 @@\n #include \"odb/source-loose.h\"\n #include \"odb/streaming.h\"\n #include \"oidtree.h\"\n+#include \"path.h\"\n #include \"repository.h\"\n #include \"strbuf.h\"\n+#include \"tempfile.h\"\n+#include \"write-or-die.h\"\n \n static int append_loose_object(const struct object_id *oid,\n \t\t\t       const char *path UNUSED,\n@@ -583,6 +586,241 @@ static int odb_source_loose_freshen_object(struct odb_source *source,\n \treturn !!check_and_freshen_file(path.buf, 1, mtime);\n }\n \n+/* Finalize a file on disk, and close it. */\n+static void close_loose_object(struct odb_source_loose *loose,\n+\t\t\t       int fd, const char *filename)\n+{\n+\tif (loose->base.will_destroy)\n+\t\tgoto out;\n+\n+\tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n+\t\todb_transaction_files_fsync(loose->base.odb->transaction, fd, filename);\n+\telse if (fsync_object_files > 0)\n+\t\tfsync_or_die(fd, filename);\n+\telse\n+\t\tfsync_component_or_die(FSYNC_COMPONENT_LOOSE_OBJECT, fd,\n+\t\t\t\t       filename);\n+\n+out:\n+\tif (close(fd) != 0)\n+\t\tdie_errno(_(\"error when closing loose object file\"));\n+}\n+\n+/* Size of directory component, including the ending '/' */\n+static inline int directory_size(const char *filename)\n+{\n+\tconst char *s = strrchr(filename, '/');\n+\tif (!s)\n+\t\treturn 0;\n+\treturn s - filename + 1;\n+}\n+\n+/*\n+ * This creates a temporary file in the same directory as the final\n+ * 'filename'\n+ *\n+ * We want to avoid cross-directory filename renames, because those\n+ * can have problems on various filesystems (FAT, NFS, Coda).\n+ */\n+static int create_tmpfile(struct repository *repo,\n+\t\t\t  struct strbuf *tmp, const char *filename)\n+{\n+\tint fd, dirlen = directory_size(filename);\n+\n+\tstrbuf_reset(tmp);\n+\tstrbuf_add(tmp, filename, dirlen);\n+\tstrbuf_addstr(tmp, \"tmp_obj_XXXXXX\");\n+\tfd = git_mkstemp_mode(tmp->buf, 0444);\n+\tif (fd < 0 && dirlen && errno == ENOENT) {\n+\t\t/*\n+\t\t * Make sure the directory exists; note that the contents\n+\t\t * of the buffer are undefined after mkstemp returns an\n+\t\t * error, so we have to rewrite the whole buffer from\n+\t\t * scratch.\n+\t\t */\n+\t\tstrbuf_reset(tmp);\n+\t\tstrbuf_add(tmp, filename, dirlen - 1);\n+\t\tif (mkdir(tmp->buf, 0777) && errno != EEXIST)\n+\t\t\treturn -1;\n+\t\tif (adjust_shared_perm(repo, tmp->buf))\n+\t\t\treturn -1;\n+\n+\t\t/* Try again */\n+\t\tstrbuf_addstr(tmp, \"/tmp_obj_XXXXXX\");\n+\t\tfd = git_mkstemp_mode(tmp->buf, 0444);\n+\t}\n+\treturn fd;\n+}\n+\n+/**\n+ * Common steps for loose object writers to start writing loose\n+ * objects:\n+ *\n+ * - Create tmpfile for the loose object.\n+ * - Setup zlib stream for compression.\n+ * - Start to feed header to zlib stream.\n+ *\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_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+\t\t\t\t     unsigned char *buf, size_t buflen,\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 = loose->base.odb->repo->hash_algo;\n+\tconst struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;\n+\tint fd;\n+\tstruct repo_config_values *cfg = repo_config_values(loose->base.odb->repo);\n+\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     loose->base.path);\n+\t\telse\n+\t\t\treturn error_errno(\n+\t\t\t\t_(\"unable to create temporary file\"));\n+\t}\n+\n+\t/*  Setup zlib stream for compression */\n+\tgit_deflate_init(stream, cfg->zlib_compression_level);\n+\tstream->next_out = buf;\n+\tstream->avail_out = buflen;\n+\tgit_hash_init(c, algo);\n+\tif (compat && compat_c)\n+\t\tgit_hash_init(compat_c, compat);\n+\n+\t/*  Start to feed header to zlib stream */\n+\tstream->next_in = (unsigned char *)hdr;\n+\tstream->avail_in = hdrlen;\n+\twhile (git_deflate(stream, 0) == Z_OK)\n+\t\t; /* nothing */\n+\tgit_hash_update(c, hdr, hdrlen);\n+\tif (compat && compat_c)\n+\t\tgit_hash_update(compat_c, hdr, hdrlen);\n+\n+\treturn fd;\n+}\n+\n+/**\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_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 = loose->base.odb->repo->compat_hash_algo;\n+\tint ret;\n+\n+\tret = git_deflate(stream, flush ? Z_FINISH : 0);\n+\tgit_hash_update(c, in0, stream->next_in - in0);\n+\tif (compat && compat_c)\n+\t\tgit_hash_update(compat_c, in0, stream->next_in - in0);\n+\tif (write_in_full(fd, compressed, stream->next_out - compressed) < 0)\n+\t\tdie_errno(_(\"unable to write loose object file\"));\n+\tstream->next_out = compressed;\n+\tstream->avail_out = compressed_len;\n+\n+\treturn ret;\n+}\n+\n+/**\n+ * Common steps for loose object writers to end writing loose objects:\n+ *\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_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 = loose->base.odb->repo->compat_hash_algo;\n+\tint ret;\n+\n+\tret = git_deflate_end_gently(stream);\n+\tif (ret != Z_OK)\n+\t\treturn ret;\n+\tgit_hash_final_oid(oid, c);\n+\tif (compat && compat_c)\n+\t\tgit_hash_final_oid(compat_oid, compat_c);\n+\n+\treturn Z_OK;\n+}\n+\n+static int write_loose_object(struct odb_source_loose *loose,\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      const time_t *mtime, unsigned flags)\n+{\n+\tint fd, ret;\n+\tunsigned char compressed[4096];\n+\tgit_zstream stream;\n+\tstruct git_hash_ctx c;\n+\tstruct object_id parano_oid;\n+\tstatic struct strbuf tmp_file = STRBUF_INIT;\n+\tstatic struct strbuf filename = STRBUF_INIT;\n+\n+\tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n+\t\todb_transaction_files_prepare(loose->base.odb->transaction);\n+\n+\todb_loose_path(loose, &filename, oid);\n+\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+\t\treturn -1;\n+\n+\t/* Then the data itself.. */\n+\tstream.next_in = (void *)buf;\n+\tstream.avail_in = len;\n+\tdo {\n+\t\tunsigned char *in0 = stream.next_in;\n+\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(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+\tif (!oideq(oid, &parano_oid))\n+\t\tdie(_(\"confused by unstable object source data for %s\"),\n+\t\t    oid_to_hex(oid));\n+\n+\tclose_loose_object(loose, fd, tmp_file.buf);\n+\n+\tif (mtime) {\n+\t\tstruct utimbuf utb = {\n+\t\t\t.actime = *mtime,\n+\t\t\t.modtime = *mtime,\n+\t\t};\n+\n+\t\tif (utime(tmp_file.buf, &utb) < 0 &&\n+\t\t    !(flags & ODB_WRITE_OBJECT_SILENT))\n+\t\t\twarning_errno(_(\"failed utime() on %s\"), tmp_file.buf);\n+\t}\n+\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 static int odb_source_loose_write_object(struct odb_source *source,\n \t\t\t\t\t const void *buf, size_t len,\n \t\t\t\t\t enum object_type type,\n@@ -611,12 +849,120 @@ static int odb_source_loose_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+\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\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+\tgit_zstream stream;\n+\tstruct git_hash_ctx c, compat_c;\n+\tstruct strbuf tmp_file = STRBUF_INIT;\n+\tstruct strbuf filename = STRBUF_INIT;\n+\tunsigned char buf[8192];\n+\tint dirlen;\n+\tchar hdr[MAX_HEADER_LEN];\n+\tint hdrlen;\n+\n+\tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n+\t\todb_transaction_files_prepare(loose->base.odb->transaction);\n+\n+\t/* Since oid is not determined, save tmp file to odb path. */\n+\tstrbuf_addf(&filename, \"%s/\", loose->base.path);\n+\thdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len);\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 * Common steps for write_loose_object and stream_loose_object to\n+\t * start writing loose objects:\n+\t *\n+\t *  - Create tmpfile for the loose object.\n+\t *  - Setup zlib stream for compression.\n+\t *  - Start to feed header to zlib stream.\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+\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+\t\terr = -1;\n+\t\tgoto cleanup;\n+\t}\n+\n+\t/* Then the data itself.. */\n+\tdo {\n+\t\tunsigned char *in0 = stream.next_in;\n+\n+\t\tif (!stream.avail_in && !in_stream->is_finished) {\n+\t\t\tssize_t read_len = odb_write_stream_read(in_stream, buf,\n+\t\t\t\t\t\t\t\t sizeof(buf));\n+\t\t\tif (read_len < 0) {\n+\t\t\t\tclose(fd);\n+\t\t\t\terr = -1;\n+\t\t\t\tgoto cleanup;\n+\t\t\t}\n+\n+\t\t\tstream.avail_in = read_len;\n+\t\t\tstream.next_in = buf;\n+\t\t\tin0 = buf;\n+\t\t\t/* All data has been read. */\n+\t\t\tif (in_stream->is_finished)\n+\t\t\t\tflush = 1;\n+\t\t}\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+\t\t * buffer. If we get Z_BUF_ERROR due to too few input bytes,\n+\t\t * then we'll replenish them in the next input_stream->read()\n+\t\t * call when we loop.\n+\t\t */\n+\t} while (ret == Z_OK || ret == Z_BUF_ERROR);\n+\n+\tif (stream.total_in != len + hdrlen)\n+\t\tdie(_(\"write stream object %\"PRIuMAX\" != %\"PRIuMAX), (uintmax_t)stream.total_in,\n+\t\t    (uintmax_t)len + hdrlen);\n+\n+\t/*\n+\t * Common steps for write_loose_object and stream_loose_object to\n+\t * end writing loose object:\n+\t *\n+\t *  - End the compression of zlib stream.\n+\t *  - Get the calculated oid.\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(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(loose, fd, tmp_file.buf);\n+\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(loose, &filename, oid);\n+\n+\t/* We finally know the object path, and create the missing dir. */\n+\tdirlen = directory_size(filename.buf);\n+\tif (dirlen) {\n+\t\tstruct strbuf dir = STRBUF_INIT;\n+\t\tstrbuf_add(&dir, filename.buf, dirlen);\n+\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+\t\t\tgoto cleanup;\n+\t\t}\n+\t\tstrbuf_release(&dir);\n+\t}\n+\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(loose, oid, &compat_oid);\n+cleanup:\n+\tstrbuf_release(&tmp_file);\n+\tstrbuf_release(&filename);\n+\treturn err;\n }\n \n static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED,\n\n-- \n2.55.0.407.g700c83d4f3.dirty\n\n"},{"id":"548595","messageId":"alvWfOJb6vAsusai@szeder.dev","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im","subject":"Re: [PATCH 0/9] object-file: move writing of loose objects into \"loose\" source","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2026-07-18T19:39:40Z","receivedAt":"2026-07-18T19:39:43Z","isPatch":true,"body":"Hi Junio,\n\n\nOn Fri, Jul 17, 2026 at 11:32:08AM +0200, Patrick Steinhardt wrote:\n> Hi,\n> \n> this patch series refactors \"object-file.c\" so that we can move the\n> logic that writes loose objects into \"odb/source-loose.c\". This ensures\n> that the logic to read and write loose objects is mostly self-contained\n> within that source's implementation now.\n> \n> To achieve that, this series first refactors `force_object_loose()` so\n> that it can work on top of the generic `odb_source_write_object()`\n> instead of having to call into `write_loose_object()`. This is the bulk\n> of the patch series.\n> \n> This patch series is built on top of 44de1520f0 (Merge branch 'master'\n> of https://github.com/j6t/git-gui, 2026-07-16) with the following two\n> topics merged into it:\n> \n>   - jt/receive-pack-use-odb-transaction at bdee7b3013\n>     (builtin/receive-pack: stage incoming objects via ODB transactions,\n>     2026-07-10).\n> \n>   - jk/git-hash-cleanups at 9e396aa553 (hash: check ctx->active flag in\n>     all wrapper functions, 2026-07-07).\n> \n> Note that jt/receive-pack-use-odb-transaction requires an evil merge:\n> \n> diff --git a/odb/source-packed.c b/odb/source-packed.c\n> index 06b31dd743..cbb06da038 100644\n> --- a/odb/source-packed.c\n> +++ b/odb/source-packed.c\n> @@ -545,7 +545,8 @@ static int odb_source_packed_write_object_stream(struct odb_source *source UNUSE\n>  }\n> \n>  static int odb_source_packed_begin_transaction(struct odb_source *source UNUSED,\n> -                                              struct odb_transaction **out UNUSED)\n> +                                              struct odb_transaction **out UNUSED,\n> +                                              enum odb_transaction_flags flags UNUSED)\n>  {\n>         return error(\"packed backend cannot begin transactions\");\n>  }\n\nIt seems that you performed this evil merge when merging the topic\njt/receive-pack-use-odb-transaction into jch as 9727bd8447 (Merge\nbranch 'jt/receive-pack-use-odb-transactions' into jch, 2026-07-17),\nbut forgot to do so when creating the base for this patch series as\n1d64e64326 (Merge branch 'jt/receive-pack-use-odb-transactions' into\nps/odb-move-loose-object-writing, 2026-07-17).  Consequently, neither\n1d64e64326 nor any of the the commits of this patch series can be\nbuilt because of the mismatching function signature:\n\n  GIT_VERSION=2.55.0.256.g1d64e64326\n  odb/source-packed.c: In function ‘odb_source_packed_new’:\n  odb/source-packed.c:778:40: error: assignment to ‘int (*)(struct odb_source *, struct odb_transaction **, enum odb_transaction_flags)’ from incompatible pointer type ‘int (*)(struct odb_source *, struct odb_transaction **)’ [-Werror=incompatible-pointer-types]\n    778 |         packed->base.begin_transaction = odb_source_packed_begin_transaction;\n        |                                        ^\n  cc1: all warnings being treated as errors\n  make: *** [Makefile:2924: odb/source-packed.o] Error 1\n\n\n"},{"id":"548613","messageId":"xmqq5x2brdqj.fsf@gitster.g","threadId":"66027","inReplyTo":"alvWfOJb6vAsusai@szeder.dev","subject":"Re: [PATCH 0/9] object-file: move writing of loose objects into \"loose\" source","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-19T01:04:52Z","receivedAt":"2026-07-19T01:04:54Z","isPatch":true,"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> Hi Junio,\n> ...\n>> Note that jt/receive-pack-use-odb-transaction requires an evil merge:\n>> \n>> diff --git a/odb/source-packed.c b/odb/source-packed.c\n>> index 06b31dd743..cbb06da038 100644\n>> --- a/odb/source-packed.c\n>> +++ b/odb/source-packed.c\n>> @@ -545,7 +545,8 @@ static int odb_source_packed_write_object_stream(struct odb_source *source UNUSE\n>>  }\n>> \n>>  static int odb_source_packed_begin_transaction(struct odb_source *source UNUSED,\n>> -                                              struct odb_transaction **out UNUSED)\n>> +                                              struct odb_transaction **out UNUSED,\n>> +                                              enum odb_transaction_flags flags UNUSED)\n>>  {\n>>         return error(\"packed backend cannot begin transactions\");\n>>  }\n>\n> It seems that you performed this evil merge when merging the topic\n> jt/receive-pack-use-odb-transaction into jch as 9727bd8447 (Merge\n> branch 'jt/receive-pack-use-odb-transactions' into jch, 2026-07-17),\n> but forgot to do so when creating the base for this patch series as\n> 1d64e64326 (Merge branch 'jt/receive-pack-use-odb-transactions' into\n> ps/odb-move-loose-object-writing, 2026-07-17).  Consequently, neither\n> 1d64e64326 nor any of the the commits of this patch series can be\n> built because of the mismatching function signature:\n\nThanks for noticing.\nVery much appreciated.\n\nWill fix-up.\n"},{"id":"548620","messageId":"xmqqecgzpm0y.fsf@gitster.g","threadId":"66027","inReplyTo":"xmqq5x2brdqj.fsf@gitster.g","subject":"Re: [PATCH 0/9] object-file: move writing of loose objects into \"loose\" source","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-19T05:48:45Z","receivedAt":"2026-07-19T05:48:47Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> SZEDER Gábor <szeder.dev@gmail.com> writes:\n>\n>> It seems that you performed this evil merge when merging the topic\n>> jt/receive-pack-use-odb-transaction into jch as 9727bd8447 (Merge\n>> branch 'jt/receive-pack-use-odb-transactions' into jch, 2026-07-17),\n>> but forgot to do so when creating the base for this patch series as\n>> 1d64e64326 (Merge branch 'jt/receive-pack-use-odb-transactions' into\n>> ps/odb-move-loose-object-writing, 2026-07-17).  Consequently, neither\n>> 1d64e64326 nor any of the the commits of this patch series can be\n>> built because of the mismatching function signature:\n>\n> Thanks for noticing.\n> Very much appreciated.\n>\n> Will fix-up.\n\nI've rebuilt the topic in question and pushed the results out.  I\nalso added some more automation to catch this kind of mistakes early\nto make it less likely to happen again.\n\nThanks.\n"},{"id":"548777","messageId":"87jyqnp365.fsf@emacs.iotcl.com","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-4-46446a3cb5b7@pks.im","subject":"Re: [PATCH 4/9] odb: lift object existence check out of the \"loose\" backend","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-07-22T13:25:06Z","receivedAt":"2026-07-22T13:25:22Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Before writing a new loose object we first check whether the object\n> already exists in any of the sources attached to the object database.\n> This results in a couple of issues:\n>\n>   - We have a layering violation, where the source needs to be aware of\n>     objects stored in any of the other sources.\n>\n>   - Every backend would have to reimplement this check, which feels\n>     somewhat pointless.\n>\n>   - It is not possible to easily write an object into a source in case\n>     the same object already exists in another source.\n>\n> Refactor the code and lift up the object existence check from the\n> \"loose\" backend into the generic ODB layer. No callers need adjustment\n> as none of them write via a specific source, but via the ODB layer.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  odb.c              | 7 +++++++\n>  odb/source-loose.c | 8 ++------\n>  2 files changed, 9 insertions(+), 6 deletions(-)\n>\n> diff --git a/odb.c b/odb.c\n> index 4adbdf8a64..bfeca76f4e 100644\n> --- a/odb.c\n> +++ b/odb.c\n> @@ -997,6 +997,13 @@ int odb_write_object_ext(struct object_database *odb,\n>  \n>  \thash_object_file(odb->repo->hash_algo, buf, len, type, oid);\n>  \n> +\t/*\n> +\t * We can skip the write in case we already have the object available.\n> +\t * In that case, we only freshen its mtime.\n> +\t */\n> +\tif (odb_freshen_object(odb, oid))\n> +\t\treturn 0;\n> +\n\nFor the loose backend, this calls check_and_freshen_file(), which code\ncomment says:\n\n    /*\n     * All of the check_and_freshen functions return 1 if the file exists and was\n     * freshened (if freshening was requested), 0 otherwise. If they return\n     * 0, you should not assume that it is safe to skip a write of the object (it\n     * either does not exist on disk, or has a stale mtime and may be subject to\n     * pruning).\n     */\n\nAnd I see similar logic in the packed backend. So a non-zero value skips\nthe write and the file was freshened. Makes sense.\n\n-- \nCheers,\nToon\n"},{"id":"548781","messageId":"87fr1bp0bi.fsf@emacs.iotcl.com","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-9-46446a3cb5b7@pks.im","subject":"Re: [PATCH 9/9] object-file: move logic to write loose objects","fromName":"Toon Claes","fromEmail":"toon@iotcl.com","sentAt":"2026-07-22T14:26:41Z","receivedAt":"2026-07-22T14:26:53Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The logic to write loose objects is split up across \"object-file.c\" and\n> \"odb/source-loose.c\". This split is somewhat weird, but it is the result\n> of two things:\n>\n>   - `force_object_loose()` used to reach into internals of how exactly\n>     we write objects.\n>\n>   - The logic of writing objects is intertwined with potentially\n>     starting a transaction.\n>\n> We have refactored `force_object_loose()` over preceding commits to work\n> via generic interfaces now, so this reason doesn't exist anymore. But\n> the second reason still does, as our management of \"files\" transactions\n> and their ad-hoc creation is still very messy. This area definitely\n> requires further work, and that work is indeed ongoing.\n>\n> That being said, we can already move the writing logic into the \"loose\"\n> backend rather easily. All we have to do is to expose two functions that\n> relate to the transactions.\n\nI'm a bit on the fence that should have gone in a separte commit, but\nit's fine.\n\n> Expose these two functions and move the writing logic into the \"loose\"\n> backend accordingly so that it becomes more self-contained. Note that\n> this requires us to drop a reference to `the_repository` in favor of\n> using the source's repository in `start_loose_object_common()`.\n\nYay! Thanks for calling that out, it standed out in the zebra diff.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  object-file.c      | 360 +----------------------------------------------------\n>  object-file.h      |  22 +---\n>  odb/source-loose.c | 354 +++++++++++++++++++++++++++++++++++++++++++++++++++-\n\nThat's a pretty large diff, but luckily the zebra diff helps a lot.\n\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       const time_t *mtime, unsigned flags)\n\nThis line is not colored being moved because it was made static, which\nmakes sense.\n\n> diff --git a/object-file.h b/object-file.h\n> index 31781a9c53..805f2cfa28 100644\n> --- a/object-file.h\n> +++ b/object-file.h\n> @@ -24,20 +24,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> - * 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\nThis was added by you on 2026-06-01, so thanks for addressing this.\n\n> @@ -611,12 +849,120 @@ static int odb_source_loose_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> +\tstruct odb_source_loose *loose = odb_source_loose_downcast(source);\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> +\tgit_zstream stream;\n> +\tstruct git_hash_ctx c, compat_c;\n> +\tstruct strbuf tmp_file = STRBUF_INIT;\n> +\tstruct strbuf filename = STRBUF_INIT;\n> +\tunsigned char buf[8192];\n> +\tint dirlen;\n> +\tchar hdr[MAX_HEADER_LEN];\n> +\tint hdrlen;\n> +\n> +\tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n> +\t\todb_transaction_files_prepare(loose->base.odb->transaction);\n> +\n> +\t/* Since oid is not determined, save tmp file to odb path. */\n> +\tstrbuf_addf(&filename, \"%s/\", loose->base.path);\n> +\thdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len);\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\nSo this is what you did, as suggested, by yourself.\n\n> +\t * Common steps for write_loose_object and stream_loose_object to\n> +\t * start writing loose objects:\n> +\t *\n> +\t *  - Create tmpfile for the loose object.\n> +\t *  - Setup zlib stream for compression.\n> +\t *  - Start to feed header to zlib stream.\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\nThis line is marked as removed in the zebra diff, but that's because the\ncode is being inlined into this odb_source_loose_write_object_stream()\nfunction.\n\nAll good.\n\n\n-- \nCheers,\nToon\n"},{"id":"549173","messageId":"amkk_0C8joQKH43M@denethor","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-1-46446a3cb5b7@pks.im","subject":"Re: [PATCH 1/9] odb: compute compat object ID in `odb_write_object_ext()`","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-28T22:02:13Z","receivedAt":"2026-07-28T22:02:20Z","isPatch":true,"body":"On 26/07/17 11:32AM, Patrick Steinhardt wrote:\n> Repositories can have a compatibility hash configured, which means that\n> such a repository is expected to maintain a mapping between canonical\n> and compatibility object hashes. Maintaining this mapping is the\n> responsibility of the object database sources, where we either store\n> them as part of the loose objects map or in packfile indices v3 (once we\n> gain support for this feature).\n\nMakes sense. Each ODB source should be responsible to tracking how an\nobjects maps from one hash to another for compatibility.\n\n> But besides storing these compatibility hashes, the sources are also\n> responsible for generating the compatibility hash in the first place.\n> This is somewhat unnecessary though, as the compatibility hash should be\n> computed the same no matter which source is being used. The consequence\n> is that we need to duplicate this functionality across the different\n> backends, which does not make a lot of sense.\n\nAgreed, there is no need to duplicate logic as the hashes that get\ngenerated should be the same regardless of the backend.\n\n> Refactor the code so that we instead compute the compatibility hash in\n> `odb_write_object_ext()` and then pass the computed value to the\n> sources. No callers need adjustment as there are none that write objects\n> via the source interfaces directly.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  odb.c                 | 26 ++++++++++++++++++++++++--\n>  odb.h                 | 10 ++++++----\n>  odb/source-files.c    |  2 +-\n>  odb/source-inmemory.c |  2 +-\n>  odb/source-loose.c    | 24 +++---------------------\n>  odb/source-packed.c   |  2 +-\n>  odb/source.h          |  4 ++--\n>  7 files changed, 38 insertions(+), 32 deletions(-)\n> \n> diff --git a/odb.c b/odb.c\n> index cf6e7938c0..1d6538163b 100644\n> --- a/odb.c\n> +++ b/odb.c\n> @@ -989,11 +989,33 @@ int odb_write_object_ext(struct object_database *odb,\n>  \t\t\t const void *buf, unsigned long len,\n>  \t\t\t enum object_type type,\n>  \t\t\t struct object_id *oid,\n> -\t\t\t struct object_id *compat_oid,\n> +\t\t\t const struct object_id *compat_oid_in,\n>  \t\t\t enum odb_write_object_flags flags)\n>  {\n> +\tconst struct git_hash_algo *compat = odb->repo->compat_hash_algo;\n> +\tstruct object_id compat_oid, *compat_oid_p = NULL;\n> +\n> +\tif (compat) {\n> +\t\tconst struct git_hash_algo *algo = odb->repo->hash_algo;\n> +\n> +\t\tif (compat_oid_in) {\n> +\t\t\toidcpy(&compat_oid, compat_oid_in);\n> +\t\t} else if (type == OBJ_BLOB) {\n> +\t\t\thash_object_file(compat, buf, len, type, &compat_oid);\n> +\t\t} else {\n> +\t\t\tstruct strbuf converted = STRBUF_INIT;\n> +\t\t\tconvert_object_file(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> +\n> +\t\tcompat_oid_p = &compat_oid;\n> +\t}\n\nHere we lift up the logic to gnerate the compat hash out of the backend\nand into `odb_write_object_ext()` so the resulting hash can be wired to\nthe ODB source callback to write the object. The logic itself is the\nsame and looks good.\n\nThe rest of this patch is mainly just updating the callsites accordingly\nand also looks good.\n\n-Justin\n"},{"id":"549174","messageId":"amkoeX3HeUWqwqeu@denethor","threadId":"66027","inReplyTo":"20260717-pks-odb-move-loose-object-writing-v1-2-46446a3cb5b7@pks.im","subject":"Re: [PATCH 2/9] t/u-odb-inmemory: implement wrapper for writing objects","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-28T22:10:40Z","receivedAt":"2026-07-28T22:10:44Z","isPatch":true,"body":"On 26/07/17 11:32AM, Patrick Steinhardt wrote:\n> In the next commit we're about to change how objects are being written\n> into the object database source. Prepare for this refactoring by\n> introducing a wrapper function into our unit tests so that we don't have\n> to adjust all callsites.\n\nOk, I assume future patches will extend the `cl_assert_write_object()`\nwrapper function logic which would make sense. The patch itself looks\ntrivially correct.\n\n-Justin\n"}]}