{"thread":{"id":"65861","subject":"[PATCH 0/6] receive-pack: use ODB transactions to stage object writes","startedAt":"2026-06-24T04:19:27Z","lastAt":"2026-07-13T05:19:01Z","messageCount":90,"participants":["Justin Tobler","Patrick Steinhardt","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":6},"messages":[{"id":"546256","messageId":"20260624041920.2601961-1-jltobler@gmail.com","threadId":"65861","inReplyTo":null,"subject":"[PATCH 0/6] receive-pack: use ODB transactions to stage object writes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-24T04:19:14Z","receivedAt":"2026-06-24T04:19:27Z","isPatch":true,"body":"Greetings,\n\nThis patch series replaces direct usage of the `tmp_objdir` interfaces\nin git-receive-pack(1) to instead use the `odb_transaction` interfaces\nto create/manage a staging area to write objects to. The purpose of this\nchange is to get git-receive-pack(1) one step closer to being ODB\nbackend agnostic. For now, the object writes themselves are still\n\"files\" backend specific due to being handled by the git-index-pack(1)\nand git-unpack-objects(1) child processes. This will be tackled in a\nseparate series though.\n\nThanks,\n-Justin\n\nJustin Tobler (6):\n  object-file: rename files transaction prepare function\n  object-file: propagate files transaction errors\n  odb/transaction: propagate begin errors\n  odb/transaction: propagate commit errors\n  odb/transaction: add transaction env interface\n  builtin/receive-pack: stage incoming objects via ODB transactions\n\n builtin/add.c            |  2 +-\n builtin/receive-pack.c   | 46 ++++++++++--------------\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  2 +-\n cache-tree.c             |  2 +-\n object-file.c            | 77 +++++++++++++++++++++++++++++++---------\n object-file.h            |  7 ++--\n odb/source-files.c       |  9 ++---\n odb/source-inmemory.c    |  3 +-\n odb/source-loose.c       |  3 +-\n odb/source.h             |  9 +++--\n odb/transaction.c        | 38 +++++++++++++++-----\n odb/transaction.h        | 49 +++++++++++++++++++++----\n read-cache.c             |  2 +-\n 14 files changed, 173 insertions(+), 78 deletions(-)\n\n\nbase-commit: ab776a62a78576513ee121424adb19597fbb7613\n-- \n2.54.0.105.g59ff4886a5\n\n"},{"id":"546257","messageId":"20260624041920.2601961-2-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260624041920.2601961-1-jltobler@gmail.com","subject":"[PATCH 1/6] object-file: rename files transaction prepare function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-24T04:19:15Z","receivedAt":"2026-06-24T04:19:29Z","isPatch":true,"body":"The \"files\" ODB transaction backend lazily creates a temporary object\ndirectory when the first loose object is written to the transaction via\n`prepare_loose_object_transaction()`. In a subsequent commit, the\ntemporary directory is used to also write packfiles to.\n\nRename the function to `odb_transaction_files_prepare()` accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex e3d92bbda2..a3eb8d71dd 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -499,7 +499,7 @@ struct odb_transaction_files {\n \tstruct transaction_packfile packfile;\n };\n \n-static void prepare_loose_object_transaction(struct odb_transaction *base)\n+static void 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@@ -761,7 +761,7 @@ int write_loose_object(struct odb_source_loose *loose,\n \tstatic struct strbuf filename = STRBUF_INIT;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\n+\t\todb_transaction_files_prepare(loose->base.odb->transaction);\n \n \todb_loose_path(loose, &filename, oid);\n \n@@ -825,7 +825,7 @@ int odb_source_loose_write_stream(struct odb_source_loose *loose,\n \tint hdrlen;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\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-- \n2.54.0.105.g59ff4886a5\n\n"},{"id":"546258","messageId":"20260624041920.2601961-3-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260624041920.2601961-1-jltobler@gmail.com","subject":"[PATCH 2/6] object-file: propagate files transaction errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-24T04:19:16Z","receivedAt":"2026-06-24T04:19:30Z","isPatch":true,"body":"The \"files\" transaction backend may encounter errors related to managing\nthe temporary directory used to stage objects, but silently ignores\nthese errors. Instead return errors encountered in the\n`odb_transaction_files_{prepare,begin,commit}()` interfaces to allow\ncallers to handle as needed.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c      | 41 ++++++++++++++++++++++++++++-------------\n object-file.h      |  5 +++--\n odb/source-files.c |  6 +-----\n odb/transaction.h  |  2 +-\n 4 files changed, 33 insertions(+), 21 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex a3eb8d71dd..18c2df75fb 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -499,7 +499,7 @@ struct odb_transaction_files {\n \tstruct transaction_packfile packfile;\n };\n \n-static void odb_transaction_files_prepare(struct odb_transaction *base)\n+static 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@@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n \t * added at the time they call odb_transaction_files_begin.\n \t */\n \tif (!transaction || transaction->objdir)\n-\t\treturn;\n+\t\treturn 0;\n \n \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n-\tif (transaction->objdir)\n-\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n+\tif (!transaction->objdir)\n+\t\treturn -1;\n+\n+\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n+\n+\treturn 0;\n }\n \n static void fsync_loose_object_transaction(struct odb_transaction *base,\n@@ -542,13 +546,13 @@ static void fsync_loose_object_transaction(struct odb_transaction *base,\n /*\n  * Cleanup after batch-mode fsync_object_files.\n  */\n-static void flush_loose_object_transaction(struct odb_transaction_files *transaction)\n+static int flush_loose_object_transaction(struct odb_transaction_files *transaction)\n {\n \tstruct strbuf temp_path = STRBUF_INIT;\n \tstruct tempfile *temp;\n \n \tif (!transaction->objdir)\n-\t\treturn;\n+\t\treturn 0;\n \n \t/*\n \t * Issue a full hardware flush against a temporary file to ensure\n@@ -570,8 +574,12 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac\n \t * Make the object files visible in the primary ODB after their data is\n \t * fully durable.\n \t */\n-\ttmp_objdir_migrate(transaction->objdir);\n+\tif (tmp_objdir_migrate(transaction->objdir))\n+\t\treturn -1;\n+\n \ttransaction->objdir = NULL;\n+\n+\treturn 0;\n }\n \n /* Finalize a file on disk, and close it. */\n@@ -1670,27 +1678,34 @@ int read_loose_object(struct repository *repo,\n \treturn ret;\n }\n \n-static void odb_transaction_files_commit(struct odb_transaction *base)\n+static int odb_transaction_files_commit(struct odb_transaction *base)\n {\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of(base, struct odb_transaction_files, base);\n \n-\tflush_loose_object_transaction(transaction);\n+\tif (flush_loose_object_transaction(transaction))\n+\t\treturn -1;\n \tflush_packfile_transaction(transaction);\n+\n+\treturn 0;\n }\n \n-struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n+int odb_transaction_files_begin(struct odb_source *source,\n+\t\t\t\tstruct odb_transaction **out)\n {\n \tstruct odb_transaction_files *transaction;\n \tstruct object_database *odb = source->odb;\n \n-\tif (odb->transaction)\n-\t\treturn NULL;\n+\tif (odb->transaction) {\n+\t\t*out = NULL;\n+\t\treturn 0;\n+\t}\n \n \ttransaction = xcalloc(1, sizeof(*transaction));\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\t*out = &transaction->base;\n \n-\treturn &transaction->base;\n+\treturn 0;\n }\ndiff --git a/object-file.h b/object-file.h\nindex 528c4e6e69..ac927fec07 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -195,8 +195,9 @@ struct odb_transaction;\n  * Tell the object database to optimize for adding\n  * multiple objects. odb_transaction_files_commit must be called\n  * to make new objects visible. If a transaction is already\n- * pending, NULL is returned.\n+ * pending, out is set to NULL.\n  */\n-struct odb_transaction *odb_transaction_files_begin(struct odb_source *source);\n+int odb_transaction_files_begin(struct odb_source *source,\n+\t\t\t\tstruct odb_transaction **out);\n \n #endif /* OBJECT_FILE_H */\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 5bdd042922..2545bd81d4 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -182,11 +182,7 @@ static int odb_source_files_write_object_stream(struct odb_source *source,\n static int odb_source_files_begin_transaction(struct odb_source *source,\n \t\t\t\t\t      struct odb_transaction **out)\n {\n-\tstruct odb_transaction *tx = odb_transaction_files_begin(source);\n-\tif (!tx)\n-\t\treturn -1;\n-\t*out = tx;\n-\treturn 0;\n+\treturn odb_transaction_files_begin(source, out);\n }\n \n static int odb_source_files_read_alternates(struct odb_source *source,\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 854fda06f5..f4c1ebfaaa 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -17,7 +17,7 @@ struct odb_transaction {\n \tstruct odb_source *source;\n \n \t/* The ODB source specific callback invoked to commit a transaction. */\n-\tvoid (*commit)(struct odb_transaction *transaction);\n+\tint (*commit)(struct odb_transaction *transaction);\n \n \t/*\n \t * This callback is expected to write the given object stream into\n-- \n2.54.0.105.g59ff4886a5\n\n"},{"id":"546259","messageId":"20260624041920.2601961-4-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260624041920.2601961-1-jltobler@gmail.com","subject":"[PATCH 3/6] odb/transaction: propagate begin errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-24T04:19:17Z","receivedAt":"2026-06-24T04:19:30Z","isPatch":true,"body":"When `odb_transaction_begin()` is invoked, the function returns the\ntransaction pointer directly. There is no way for the backend to\nsignal that it failed to set up its state, such as when creating the\ntemporary object directory backing the transaction.\n\nIn a subsequent commit, git-receive-pack(1) starts using ODB\ntransactions and needs to be able to report such failures rather\nthan silently ignore them. Refactor `odb_transaction_begin()` to\nreturn an int error code and write the resulting transaction into an\nout parameter. Also introduce `odb_transaction_begin_or_die()` as a\nconvenience for callsites that do not need to handle errors\nexplicitly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  2 +-\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  2 +-\n cache-tree.c             |  2 +-\n object-file.c            |  3 ++-\n odb/transaction.c        | 16 +++++++++++-----\n odb/transaction.h        | 19 +++++++++++++++----\n read-cache.c             |  2 +-\n 8 files changed, 33 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex c859f66519..3d5d9cfdb9 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -581,7 +581,7 @@ int cmd_add(int argc,\n \t\tstring_list_clear(&only_match_skip_worktree, 0);\n \t}\n \n-\ttransaction = odb_transaction_begin(repo->objects);\n+\todb_transaction_begin_or_die(repo->objects, &transaction);\n \n \tps_matched = xcalloc(pathspec.nr, 1);\n \tif (add_renormalize)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex f3849bb654..d0136cdd99 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -598,7 +598,7 @@ static void unpack_all(void)\n \t\tprogress = start_progress(the_repository,\n \t\t\t\t\t  _(\"Unpacking objects\"), nr_objects);\n \tCALLOC_ARRAY(obj_list, nr_objects);\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \tfor (i = 0; i < nr_objects; i++) {\n \t\tunpack_one(i);\n \t\tdisplay_progress(progress, i + 1);\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 3d6646c318..17f3ea284c 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1124,7 +1124,7 @@ int cmd_update_index(int argc,\n \t * Allow the object layer to optimize adding multiple objects in\n \t * a batch.\n \t */\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \twhile (ctx.argc) {\n \t\tif (parseopt_state != PARSE_OPT_DONE)\n \t\t\tparseopt_state = parse_options_step(&ctx, options,\ndiff --git a/cache-tree.c b/cache-tree.c\nindex 184f7e2635..1a7dfed9cf 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -490,7 +490,7 @@ int cache_tree_update(struct index_state *istate, int flags)\n \n \ttrace_performance_enter();\n \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n \t\t       \"\", 0, &skip, flags);\n \todb_transaction_commit(transaction);\ndiff --git a/object-file.c b/object-file.c\nindex 18c2df75fb..696f05dc2d 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1389,8 +1389,9 @@ int index_fd(struct index_state *istate, struct object_id *oid,\n \n \t\tif (flags & INDEX_WRITE_OBJECT) {\n \t\t\tstruct object_database *odb = the_repository->objects;\n-\t\t\tstruct odb_transaction *transaction = odb_transaction_begin(odb);\n+\t\t\tstruct odb_transaction *transaction;\n \n+\t\t\todb_transaction_begin_or_die(odb, &transaction);\n \t\t\tret = odb_transaction_write_object_stream(odb->transaction,\n \t\t\t\t\t\t\t\t  &stream,\n \t\t\t\t\t\t\t\t  xsize_t(st->st_size),\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex b16e07aebf..d3de01db50 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -2,14 +2,20 @@\n #include \"odb/source.h\"\n #include \"odb/transaction.h\"\n \n-struct odb_transaction *odb_transaction_begin(struct object_database *odb)\n+int odb_transaction_begin(struct object_database *odb,\n+\t\t\t  struct odb_transaction **out)\n {\n-\tif (odb->transaction)\n-\t\treturn NULL;\n+\tint ret;\n \n-\todb_source_begin_transaction(odb->sources, &odb->transaction);\n+\tif (odb->transaction) {\n+\t\t*out = NULL;\n+\t\treturn 0;\n+\t}\n \n-\treturn odb->transaction;\n+\tret = odb_source_begin_transaction(odb->sources, out);\n+\todb->transaction = *out;\n+\n+\treturn ret;\n }\n \n void odb_transaction_commit(struct odb_transaction *transaction)\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex f4c1ebfaaa..cd6d50f2e5 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -1,6 +1,8 @@\n #ifndef ODB_TRANSACTION_H\n #define ODB_TRANSACTION_H\n \n+#include \"git-compat-util.h\"\n+#include \"gettext.h\"\n #include \"odb.h\"\n #include \"odb/source.h\"\n \n@@ -33,11 +35,20 @@ struct odb_transaction {\n };\n \n /*\n- * Starts an ODB transaction. Subsequent objects are written to the transaction\n- * and not committed until odb_transaction_commit() is invoked on the\n- * transaction. If the ODB already has a pending transaction, NULL is returned.\n+ * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n+ * written to the transaction and not committed until odb_transaction_commit()\n+ * is invoked on the transaction. Returns 0 on success and a negative value on\n+ * error. If the ODB already has a pending transaction, `out` is set to NULL.\n  */\n-struct odb_transaction *odb_transaction_begin(struct object_database *odb);\n+int odb_transaction_begin(struct object_database *odb,\n+\t\t\t  struct odb_transaction **out);\n+\n+static inline void odb_transaction_begin_or_die(struct object_database *odb,\n+\t\t\t\t\t\tstruct odb_transaction **out)\n+{\n+\tif (odb_transaction_begin(odb, out))\n+\t\tdie(_(\"failed to start ODB transaction\"));\n+}\n \n /*\n  * Commits an ODB transaction making the written objects visible. If the\ndiff --git a/read-cache.c b/read-cache.c\nindex 21ca58beea..db0bfa60fe 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4042,7 +4042,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \t * This function is invoked from commands other than 'add', which\n \t * may not have their own transaction active.\n \t */\n-\ttransaction = odb_transaction_begin(repo->objects);\n+\todb_transaction_begin_or_die(repo->objects, &transaction);\n \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n \todb_transaction_commit(transaction);\n \n-- \n2.54.0.105.g59ff4886a5\n\n"},{"id":"546260","messageId":"20260624041920.2601961-6-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260624041920.2601961-1-jltobler@gmail.com","subject":"[PATCH 5/6] odb/transaction: add transaction env interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-24T04:19:19Z","receivedAt":"2026-06-24T04:19:33Z","isPatch":true,"body":"The ODB transaction backend is responsible for creating/managing its own\nstaging area for writing objects. Other child processes spawned by Git\nmay need to access to uncommitted objects or write new objects in the\nstaging area though.\n\nIntroduce `odb_transaction_env()` which is expected to provide the set\nof environment variables needed by a child process to access the\ntransaction staging area.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c     | 11 +++++++++++\n odb/transaction.c |  8 ++++++++\n odb/transaction.h | 19 +++++++++++++++++++\n 3 files changed, 38 insertions(+)\n\ndiff --git a/object-file.c b/object-file.c\nindex 696f05dc2d..14064d188a 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1691,6 +1691,16 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n \treturn 0;\n }\n \n+static const char **odb_transaction_files_env(struct odb_transaction *base)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\n+\todb_transaction_files_prepare(&transaction->base);\n+\n+\treturn tmp_objdir_env(transaction->objdir);\n+}\n+\n int odb_transaction_files_begin(struct odb_source *source,\n \t\t\t\tstruct odb_transaction **out)\n {\n@@ -1706,6 +1716,7 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\ttransaction->base.env = odb_transaction_files_env;\n \t*out = &transaction->base;\n \n \treturn 0;\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex b20d6a16f8..20d3f43f54 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -46,3 +46,11 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n {\n \treturn transaction->write_object_stream(transaction, stream, len, oid);\n }\n+\n+const char **odb_transaction_env(struct odb_transaction *transaction)\n+{\n+\tif (!transaction)\n+\t\treturn NULL;\n+\n+\treturn transaction->env(transaction);\n+}\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 7898770071..536458297b 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -32,6 +32,16 @@ struct odb_transaction {\n \tint (*write_object_stream)(struct odb_transaction *transaction,\n \t\t\t\t   struct odb_write_stream *stream, size_t len,\n \t\t\t\t   struct object_id *oid);\n+\n+\t/*\n+\t * This callback is expected to return a NULL-terminated array of\n+\t * environment variables that a child process should inherit so\n+\t * that its object writes participate in the transaction. The\n+\t * returned array is owned by the backend and remains valid until\n+\t * the transaction ends. May return NULL when the backend does not\n+\t * need to expose any state to child processes.\n+\t */\n+\tconst char **(*env)(struct odb_transaction *transaction);\n };\n \n /*\n@@ -65,4 +75,13 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n \t\t\t\t\tstruct odb_write_stream *stream,\n \t\t\t\t\tsize_t len, struct object_id *oid);\n \n+/*\n+ * Returns a NULL-terminated array of environment variables that a child\n+ * process should inherit so that its object writes participate in the\n+ * transaction, suitable for passing via child_process.env. Returns NULL if\n+ * the transaction is NULL or the backend does not expose any state to child\n+ * processes.\n+ */\n+const char **odb_transaction_env(struct odb_transaction *transaction);\n+\n #endif\n-- \n2.54.0.105.g59ff4886a5\n\n"},{"id":"546261","messageId":"20260624041920.2601961-7-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260624041920.2601961-1-jltobler@gmail.com","subject":"[PATCH 6/6] builtin/receive-pack: stage incoming objects via ODB transactions","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-24T04:19:20Z","receivedAt":"2026-06-24T04:19:34Z","isPatch":true,"body":"Objects received by git-receive-pack(1) are quarantined in a temporary\n\"incoming\" directory and migrated into the object database prior to the\nreference updates. The quarantine is currently managed through\n`tmp_objdir` directly. In a pluggable ODB future, how exactly an object\ngets written to a transaction may vary for a given ODB source. Refactor\ngit-receive-pack(1) to use the ODB transaction interfaces to manage the\nobject staging area in a more agnostic manner accordingly.\n\nNote that the temporary directory created for git-receive-pack(1) is\neagerly created and uses a different prefix name. This behavior is\nspecial cased in the \"files\" backend by having `odb_transaction_begin()`\ncallers that require this behavior provide an `ODB_TRANSACTION_RECEIVE`\nflag.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  2 +-\n builtin/receive-pack.c   | 46 ++++++++++++++++------------------------\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  2 +-\n cache-tree.c             |  2 +-\n object-file.c            | 22 ++++++++++++++++---\n object-file.h            |  4 +++-\n odb/source-files.c       |  5 +++--\n odb/source-inmemory.c    |  3 ++-\n odb/source-loose.c       |  3 ++-\n odb/source.h             |  9 +++++---\n odb/transaction.c        |  5 +++--\n odb/transaction.h        | 13 ++++++++----\n read-cache.c             |  2 +-\n 14 files changed, 70 insertions(+), 50 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 3d5d9cfdb9..60ffbede2b 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -581,7 +581,7 @@ int cmd_add(int argc,\n \t\tstring_list_clear(&only_match_skip_worktree, 0);\n \t}\n \n-\todb_transaction_begin_or_die(repo->objects, &transaction);\n+\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \n \tps_matched = xcalloc(pathspec.nr, 1);\n \tif (add_renormalize)\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 19eb6a1b61..ee8e03e2ab 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -112,8 +112,6 @@ static enum {\n } use_keepalive;\n static int keepalive_in_sec = 5;\n \n-static struct tmp_objdir *tmp_objdir;\n-\n static struct proc_receive_ref {\n \tunsigned int want_add:1,\n \t\t     want_delete:1,\n@@ -959,8 +957,8 @@ static int run_receive_hook(struct command *commands,\n \t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n \t}\n \n-\tif (tmp_objdir)\n-\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n+\tif (the_repository->objects->transaction)\n+\t\tstrvec_pushv(&opt.env, odb_transaction_env(the_repository->objects->transaction));\n \n \tprepare_push_cert_sha1(&opt);\n \n@@ -1363,7 +1361,7 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)\n \t\t    !delayed_reachability_test(si, i))\n \t\t\toid_array_append(&extra, &si->shallow->oid[i]);\n \n-\topt.env = tmp_objdir_env(tmp_objdir);\n+\topt.env = odb_transaction_env(the_repository->objects->transaction);\n \tsetup_alternate_shallow(&shallow_lock, &opt.shallow_file, &extra);\n \tif (check_connected(command_singleton_iterator, cmd, &opt)) {\n \t\trollback_shallow_file(the_repository, &shallow_lock);\n@@ -1802,7 +1800,7 @@ static void set_connectivity_errors(struct command *commands,\n \t\t\t/* to be checked in update_shallow_ref() */\n \t\t\tcontinue;\n \n-\t\topt.env = tmp_objdir_env(tmp_objdir);\n+\t\topt.env = odb_transaction_env(the_repository->objects->transaction);\n \t\tif (!check_connected(command_singleton_iterator, &singleton,\n \t\t\t\t     &opt))\n \t\t\tcontinue;\n@@ -2057,7 +2055,7 @@ static void execute_commands(struct command *commands,\n \t\tdata.si = si;\n \t\topt.err_fd = err_fd;\n \t\topt.progress = err_fd && !quiet;\n-\t\topt.env = tmp_objdir_env(tmp_objdir);\n+\t\topt.env = odb_transaction_env(the_repository->objects->transaction);\n \t\topt.exclude_hidden_refs_section = \"receive\";\n \n \t\tif (check_connected(iterate_receive_command_list, &data, &opt))\n@@ -2106,14 +2104,13 @@ static void execute_commands(struct command *commands,\n \t * Now we'll start writing out refs, which means the objects need\n \t * to be in their final positions so that other processes can see them.\n \t */\n-\tif (tmp_objdir_migrate(tmp_objdir) < 0) {\n+\tif (odb_transaction_commit(the_repository->objects->transaction)) {\n \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\t\tif (!cmd->error_string)\n \t\t\t\tcmd->error_string = \"unable to migrate objects to permanent storage\";\n \t\t}\n \t\treturn;\n \t}\n-\ttmp_objdir = NULL;\n \n \tcheck_aliased_updates(commands);\n \n@@ -2326,7 +2323,8 @@ static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n \t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n }\n \n-static const char *unpack(int err_fd, struct shallow_info *si)\n+static const char *unpack(int err_fd, struct shallow_info *si,\n+\t\t\t  struct odb_transaction *transaction)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2351,20 +2349,7 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n \t\tstrvec_push(&child.args, alt_shallow_file);\n \t}\n \n-\ttmp_objdir = tmp_objdir_create(the_repository, \"incoming\");\n-\tif (!tmp_objdir) {\n-\t\tif (err_fd > 0)\n-\t\t\tclose(err_fd);\n-\t\treturn \"unable to create temporary object directory\";\n-\t}\n-\tstrvec_pushv(&child.env, tmp_objdir_env(tmp_objdir));\n-\n-\t/*\n-\t * Normally we just pass the tmp_objdir environment to the child\n-\t * processes that do the heavy lifting, but we may need to see these\n-\t * objects ourselves to set up shallow information.\n-\t */\n-\ttmp_objdir_add_as_alternate(tmp_objdir);\n+\tstrvec_pushv(&child.env, odb_transaction_env(transaction));\n \n \tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n@@ -2431,13 +2416,14 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n \treturn NULL;\n }\n \n-static const char *unpack_with_sideband(struct shallow_info *si)\n+static const char *unpack_with_sideband(struct shallow_info *si,\n+\t\t\t\t\tstruct odb_transaction *transaction)\n {\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(0, si);\n+\t\treturn unpack(0, si, transaction);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2446,7 +2432,7 @@ static const char *unpack_with_sideband(struct shallow_info *si)\n \tif (start_async(&muxer))\n \t\treturn NULL;\n \n-\tret = unpack(muxer.in, si);\n+\tret = unpack(muxer.in, si, transaction);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2623,6 +2609,7 @@ int cmd_receive_pack(int argc,\n \tstruct oid_array ref = OID_ARRAY_INIT;\n \tstruct shallow_info si;\n \tstruct packet_reader reader;\n+\tstruct odb_transaction *transaction = NULL;\n \n \tstruct option options[] = {\n \t\tOPT__QUIET(&quiet, N_(\"quiet\")),\n@@ -2707,7 +2694,10 @@ int cmd_receive_pack(int argc,\n \t\tif (!si.nr_ours && !si.nr_theirs)\n \t\t\tshallow_update = 0;\n \t\tif (!delete_only(commands)) {\n-\t\t\tunpack_status = unpack_with_sideband(&si);\n+\t\t\tif (odb_transaction_begin(the_repository->objects, &transaction, ODB_TRANSACTION_RECEIVE))\n+\t\t\t\tunpack_status = \"unable to start ODB transaction\";\n+\t\t\telse\n+\t\t\t\tunpack_status = unpack_with_sideband(&si, transaction);\n \t\t\tupdate_shallow_info(commands, &si, &ref);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex d0136cdd99..c3d0fc7507 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -598,7 +598,7 @@ static void unpack_all(void)\n \t\tprogress = start_progress(the_repository,\n \t\t\t\t\t  _(\"Unpacking objects\"), nr_objects);\n \tCALLOC_ARRAY(obj_list, nr_objects);\n-\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \tfor (i = 0; i < nr_objects; i++) {\n \t\tunpack_one(i);\n \t\tdisplay_progress(progress, i + 1);\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 17f3ea284c..bf6ea60ef4 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1124,7 +1124,7 @@ int cmd_update_index(int argc,\n \t * Allow the object layer to optimize adding multiple objects in\n \t * a batch.\n \t */\n-\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \twhile (ctx.argc) {\n \t\tif (parseopt_state != PARSE_OPT_DONE)\n \t\t\tparseopt_state = parse_options_step(&ctx, options,\ndiff --git a/cache-tree.c b/cache-tree.c\nindex 1a7dfed9cf..ed05acc4c7 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -490,7 +490,7 @@ int cache_tree_update(struct index_state *istate, int flags)\n \n \ttrace_performance_enter();\n \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n-\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n \t\t       \"\", 0, &skip, flags);\n \todb_transaction_commit(transaction);\ndiff --git a/object-file.c b/object-file.c\nindex 14064d188a..e7958753ec 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -497,6 +497,7 @@ struct odb_transaction_files {\n \n \tstruct tmp_objdir *objdir;\n \tstruct transaction_packfile packfile;\n+\tconst char *prefix;\n };\n \n static int odb_transaction_files_prepare(struct odb_transaction *base)\n@@ -513,7 +514,7 @@ static int odb_transaction_files_prepare(struct odb_transaction *base)\n \tif (!transaction || transaction->objdir)\n \t\treturn 0;\n \n-\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n+\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, transaction->prefix);\n \tif (!transaction->objdir)\n \t\treturn -1;\n \n@@ -1391,7 +1392,7 @@ int index_fd(struct index_state *istate, struct object_id *oid,\n \t\t\tstruct object_database *odb = the_repository->objects;\n \t\t\tstruct odb_transaction *transaction;\n \n-\t\t\todb_transaction_begin_or_die(odb, &transaction);\n+\t\t\todb_transaction_begin_or_die(odb, &transaction, 0);\n \t\t\tret = odb_transaction_write_object_stream(odb->transaction,\n \t\t\t\t\t\t\t\t  &stream,\n \t\t\t\t\t\t\t\t  xsize_t(st->st_size),\n@@ -1702,7 +1703,8 @@ static const char **odb_transaction_files_env(struct odb_transaction *base)\n }\n \n int odb_transaction_files_begin(struct odb_source *source,\n-\t\t\t\tstruct odb_transaction **out)\n+\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\tenum odb_transaction_flags flags)\n {\n \tstruct odb_transaction_files *transaction;\n \tstruct object_database *odb = source->odb;\n@@ -1717,6 +1719,20 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n \ttransaction->base.env = odb_transaction_files_env;\n+\n+\ttransaction->prefix = \"bulk-fsync\";\n+\tif (flags & ODB_TRANSACTION_RECEIVE) {\n+\t\t/*\n+\t\t * ODB transactions for git-receive-pack(1) eagerly create a\n+\t\t * temporary directory and use a different prefix.\n+\t\t */\n+\t\ttransaction->prefix = \"incoming\";\n+\t\tif (odb_transaction_files_prepare(&transaction->base)) {\n+\t\t\tfree(transaction);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\n \t*out = &transaction->base;\n \n \treturn 0;\ndiff --git a/object-file.h b/object-file.h\nindex ac927fec07..fe098d54cb 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -5,6 +5,7 @@\n #include \"object.h\"\n #include \"odb.h\"\n #include \"odb/source-loose.h\"\n+#include \"odb/transaction.h\"\n \n /* The maximum size for an object header. */\n #define MAX_HEADER_LEN 32\n@@ -198,6 +199,7 @@ struct odb_transaction;\n  * pending, out is set to NULL.\n  */\n int odb_transaction_files_begin(struct odb_source *source,\n-\t\t\t\tstruct odb_transaction **out);\n+\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\tenum odb_transaction_flags flags);\n \n #endif /* OBJECT_FILE_H */\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 2545bd81d4..534f48aad9 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -180,9 +180,10 @@ static int odb_source_files_write_object_stream(struct odb_source *source,\n }\n \n static int odb_source_files_begin_transaction(struct odb_source *source,\n-\t\t\t\t\t      struct odb_transaction **out)\n+\t\t\t\t\t      struct odb_transaction **out,\n+\t\t\t\t\t      enum odb_transaction_flags flags)\n {\n-\treturn odb_transaction_files_begin(source, out);\n+\treturn odb_transaction_files_begin(source, out, flags);\n }\n \n static int odb_source_files_read_alternates(struct odb_source *source,\ndiff --git a/odb/source-inmemory.c b/odb/source-inmemory.c\nindex e004566d76..9644d9d474 100644\n--- a/odb/source-inmemory.c\n+++ b/odb/source-inmemory.c\n@@ -304,7 +304,8 @@ static int odb_source_inmemory_freshen_object(struct odb_source *source,\n }\n \n static int odb_source_inmemory_begin_transaction(struct odb_source *source UNUSED,\n-\t\t\t\t\t\t struct odb_transaction **out UNUSED)\n+\t\t\t\t\t\t struct odb_transaction **out UNUSED,\n+\t\t\t\t\t\t enum odb_transaction_flags flags UNUSED)\n {\n \treturn error(\"in-memory source does not support transactions\");\n }\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 66e6bb8d3f..57c91986b4 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -638,7 +638,8 @@ static int odb_source_loose_write_object_stream(struct odb_source *source,\n }\n \n static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED,\n-\t\t\t\t\t      struct odb_transaction **out UNUSED)\n+\t\t\t\t\t      struct odb_transaction **out UNUSED,\n+\t\t\t\t\t      enum odb_transaction_flags flags UNUSED)\n {\n \t/* TODO: this is a known omission that we'll want to address eventually. */\n \treturn error(\"loose source does not support transactions\");\ndiff --git a/odb/source.h b/odb/source.h\nindex 2192a101b8..3790d03ff2 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -3,6 +3,7 @@\n \n #include \"object.h\"\n #include \"odb.h\"\n+#include \"odb/transaction.h\"\n \n enum odb_source_type {\n \t/*\n@@ -228,7 +229,8 @@ struct odb_source {\n \t * negative error code otherwise.\n \t */\n \tint (*begin_transaction)(struct odb_source *source,\n-\t\t\t\t struct odb_transaction **out);\n+\t\t\t\t struct odb_transaction **out,\n+\t\t\t\t enum odb_transaction_flags flags);\n \n \t/*\n \t * This callback is expected to read the list of alternate object\n@@ -467,9 +469,10 @@ static inline int odb_source_write_alternate(struct odb_source *source,\n  * Returns 0 on success, a negative error code otherwise.\n  */\n static inline int odb_source_begin_transaction(struct odb_source *source,\n-\t\t\t\t\t       struct odb_transaction **out)\n+\t\t\t\t\t       struct odb_transaction **out,\n+\t\t\t\t\t       enum odb_transaction_flags flags)\n {\n-\treturn source->begin_transaction(source, out);\n+\treturn source->begin_transaction(source, out, flags);\n }\n \n #endif\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex 20d3f43f54..34c212020c 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -3,7 +3,8 @@\n #include \"odb/transaction.h\"\n \n int odb_transaction_begin(struct object_database *odb,\n-\t\t\t  struct odb_transaction **out)\n+\t\t\t  struct odb_transaction **out,\n+\t\t\t  enum odb_transaction_flags flags)\n {\n \tint ret;\n \n@@ -12,7 +13,7 @@ int odb_transaction_begin(struct object_database *odb,\n \t\treturn 0;\n \t}\n \n-\tret = odb_source_begin_transaction(odb->sources, out);\n+\tret = odb_source_begin_transaction(odb->sources, out, flags);\n \todb->transaction = *out;\n \n \treturn ret;\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 536458297b..78392ff13d 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -4,7 +4,6 @@\n #include \"git-compat-util.h\"\n #include \"gettext.h\"\n #include \"odb.h\"\n-#include \"odb/source.h\"\n \n /*\n  * A transaction may be started for an object database prior to writing new\n@@ -44,6 +43,10 @@ struct odb_transaction {\n \tconst char **(*env)(struct odb_transaction *transaction);\n };\n \n+enum odb_transaction_flags {\n+\tODB_TRANSACTION_RECEIVE = (1 << 0),\n+};\n+\n /*\n  * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n  * written to the transaction and not committed until odb_transaction_commit()\n@@ -51,12 +54,14 @@ struct odb_transaction {\n  * error. If the ODB already has a pending transaction, `out` is set to NULL.\n  */\n int odb_transaction_begin(struct object_database *odb,\n-\t\t\t  struct odb_transaction **out);\n+\t\t\t  struct odb_transaction **out,\n+\t\t\t  enum odb_transaction_flags flags);\n \n static inline void odb_transaction_begin_or_die(struct object_database *odb,\n-\t\t\t\t\t\tstruct odb_transaction **out)\n+\t\t\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\t\t\tenum odb_transaction_flags flags)\n {\n-\tif (odb_transaction_begin(odb, out))\n+\tif (odb_transaction_begin(odb, out, flags))\n \t\tdie(_(\"failed to start ODB transaction\"));\n }\n \ndiff --git a/read-cache.c b/read-cache.c\nindex db0bfa60fe..35bfb25576 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4042,7 +4042,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \t * This function is invoked from commands other than 'add', which\n \t * may not have their own transaction active.\n \t */\n-\todb_transaction_begin_or_die(repo->objects, &transaction);\n+\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n \todb_transaction_commit(transaction);\n \n-- \n2.54.0.105.g59ff4886a5\n\n"},{"id":"546262","messageId":"20260624041920.2601961-5-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260624041920.2601961-1-jltobler@gmail.com","subject":"[PATCH 4/6] odb/transaction: propagate commit errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-24T04:19:18Z","receivedAt":"2026-06-24T04:19:36Z","isPatch":true,"body":"When `odb_transaction_commit()` is invoked, the return value of the\nbackend commit callback is silently discarded. A backend has no way\nto signal that committing failed, such as when the \"files\" backend\ncannot migrate its temporary object directory into the permanent\nODB.\n\nIn a subsequent commit, git-receive-pack(1) starts using ODB transaction\nto stage objects and consequently cares about such failures so it can\nhandle the error appropriately. Change the commit callback signature to\nreturn an int error code and have `odb_transaction_commit()` forward it\naccordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n odb/transaction.c | 13 ++++++++++---\n odb/transaction.h |  2 +-\n 2 files changed, 11 insertions(+), 4 deletions(-)\n\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex d3de01db50..b20d6a16f8 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -18,19 +18,26 @@ int odb_transaction_begin(struct object_database *odb,\n \treturn ret;\n }\n \n-void odb_transaction_commit(struct odb_transaction *transaction)\n+int odb_transaction_commit(struct odb_transaction *transaction)\n {\n+\tint ret;\n+\n \tif (!transaction)\n-\t\treturn;\n+\t\treturn 0;\n \n \t/*\n \t * Ensure the transaction ending matches the pending transaction.\n \t */\n \tASSERT(transaction == transaction->source->odb->transaction);\n \n-\ttransaction->commit(transaction);\n+\tret = transaction->commit(transaction);\n+\tif (ret)\n+\t\treturn ret;\n+\n \ttransaction->source->odb->transaction = NULL;\n \tfree(transaction);\n+\n+\treturn 0;\n }\n \n int odb_transaction_write_object_stream(struct odb_transaction *transaction,\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex cd6d50f2e5..7898770071 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -54,7 +54,7 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n  * Commits an ODB transaction making the written objects visible. If the\n  * specified transaction is NULL, the function is a no-op.\n  */\n-void odb_transaction_commit(struct odb_transaction *transaction);\n+int odb_transaction_commit(struct odb_transaction *transaction);\n \n /*\n  * Writes the object in the provided stream into the transaction. The resulting\n-- \n2.54.0.105.g59ff4886a5\n\n"},{"id":"546290","messageId":"aju-7Z-ecJG_ORow@pks.im","threadId":"65861","inReplyTo":"20260624041920.2601961-3-jltobler@gmail.com","subject":"Re: [PATCH 2/6] object-file: propagate files transaction errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-24T11:26:37Z","receivedAt":"2026-06-24T11:26:43Z","isPatch":true,"body":"On Tue, Jun 23, 2026 at 11:19:16PM -0500, Justin Tobler wrote:\n> The \"files\" transaction backend may encounter errors related to managing\n> the temporary directory used to stage objects, but silently ignores\n> these errors. Instead return errors encountered in the\n> `odb_transaction_files_{prepare,begin,commit}()` interfaces to allow\n> callers to handle as needed.\n\nMissing a then? \"to handle as needed\" -> \"to handle them as needed\"\n\nMakes sense. It always felt a bit off that those functions didn't have a\nway to signal errors to the caller.\n\n> diff --git a/object-file.c b/object-file.c\n> index a3eb8d71dd..18c2df75fb 100644\n> --- a/object-file.c\n> +++ b/object-file.c\n> @@ -499,7 +499,7 @@ struct odb_transaction_files {\n>  \tstruct transaction_packfile packfile;\n>  };\n>  \n> -static void odb_transaction_files_prepare(struct odb_transaction *base)\n> +static 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\nBy the way, is there any reason why those functions are still hosted in\n\"object-file.c\" instead of in \"odb/source-files.c\"? I should probably\nknow, but I forgot.\n\n> @@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n>  \t * added at the time they call odb_transaction_files_begin.\n>  \t */\n>  \tif (!transaction || transaction->objdir)\n> -\t\treturn;\n> +\t\treturn 0;\n>  \n>  \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n> -\tif (transaction->objdir)\n> -\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n> +\tif (!transaction->objdir)\n> +\t\treturn -1;\n\nHuh. So previously we just didn't handle this error at all and just\ncontinued to tag along? Did that result in anything sensible or was this\njust YOLOing it?\n\n> @@ -542,13 +546,13 @@ static void fsync_loose_object_transaction(struct odb_transaction *base,\n>  /*\n>   * Cleanup after batch-mode fsync_object_files.\n>   */\n> -static void flush_loose_object_transaction(struct odb_transaction_files *transaction)\n> +static int flush_loose_object_transaction(struct odb_transaction_files *transaction)\n\nFeels like this function should've been renamed in the preceding commit,\nas well.\n\n>  {\n>  \tstruct strbuf temp_path = STRBUF_INIT;\n>  \tstruct tempfile *temp;\n>  \n>  \tif (!transaction->objdir)\n> -\t\treturn;\n> +\t\treturn 0;\n>  \n>  \t/*\n>  \t * Issue a full hardware flush against a temporary file to ensure\n> @@ -570,8 +574,12 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac\n\nThere is a call to `xmks_tempfile()` hidden that can fail, but that\nfailure is already handled in that function itself by dying.\n\n>  \t * Make the object files visible in the primary ODB after their data is\n>  \t * fully durable.\n>  \t */\n> -\ttmp_objdir_migrate(transaction->objdir);\n> +\tif (tmp_objdir_migrate(transaction->objdir))\n> +\t\treturn -1;\n\nFeels like another case of YOLOing it. The migration could have failed,\nbut we just ignored that failure and never told the user about it. The\nresult may be silent corruption, I assume?\n\n> @@ -1670,27 +1678,34 @@ int read_loose_object(struct repository *repo,\n>  \treturn ret;\n>  }\n>  \n> -static void odb_transaction_files_commit(struct odb_transaction *base)\n> +static int odb_transaction_files_commit(struct odb_transaction *base)\n>  {\n>  \tstruct odb_transaction_files *transaction =\n>  \t\tcontainer_of(base, struct odb_transaction_files, base);\n>  \n> -\tflush_loose_object_transaction(transaction);\n> +\tif (flush_loose_object_transaction(transaction))\n> +\t\treturn -1;\n>  \tflush_packfile_transaction(transaction);\n> +\n> +\treturn 0;\n>  }\n>  \n> -struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n> +int odb_transaction_files_begin(struct odb_source *source,\n> +\t\t\t\tstruct odb_transaction **out)\n>  {\n>  \tstruct odb_transaction_files *transaction;\n>  \tstruct object_database *odb = source->odb;\n>  \n> -\tif (odb->transaction)\n> -\t\treturn NULL;\n> +\tif (odb->transaction) {\n> +\t\t*out = NULL;\n> +\t\treturn 0;\n> +\t}\n>  \n>  \ttransaction = xcalloc(1, sizeof(*transaction));\n>  \ttransaction->base.source = source;\n>  \ttransaction->base.commit = odb_transaction_files_commit;\n>  \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n> +\t*out = &transaction->base;\n>  \n> -\treturn &transaction->base;\n> +\treturn 0;\n>  }\n\nIt's still somewhat fishy that we have this ODB-level transaction, but\nthat's a preexisting issue and thus outside the scope of this patch\nseries. Ideally though, it would be possible for there to be multiple\ntransactions, and it would be the caller's responsibility for juggling\nthese transactions. Just as it happens with reference transactions.\n\n> diff --git a/odb/transaction.h b/odb/transaction.h\n> index 854fda06f5..f4c1ebfaaa 100644\n> --- a/odb/transaction.h\n> +++ b/odb/transaction.h\n> @@ -17,7 +17,7 @@ struct odb_transaction {\n>  \tstruct odb_source *source;\n>  \n>  \t/* The ODB source specific callback invoked to commit a transaction. */\n> -\tvoid (*commit)(struct odb_transaction *transaction);\n> +\tint (*commit)(struct odb_transaction *transaction);\n\nWe might want to document the returned error code here.\n\nPatrick\n"},{"id":"546291","messageId":"aju-8hUeuyL6gnNU@pks.im","threadId":"65861","inReplyTo":"20260624041920.2601961-4-jltobler@gmail.com","subject":"Re: [PATCH 3/6] odb/transaction: propagate begin errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-24T11:26:42Z","receivedAt":"2026-06-24T11:26:53Z","isPatch":true,"body":"On Tue, Jun 23, 2026 at 11:19:17PM -0500, Justin Tobler wrote:\n> diff --git a/odb/transaction.c b/odb/transaction.c\n> index b16e07aebf..d3de01db50 100644\n> --- a/odb/transaction.c\n> +++ b/odb/transaction.c\n> @@ -2,14 +2,20 @@\n>  #include \"odb/source.h\"\n>  #include \"odb/transaction.h\"\n>  \n> -struct odb_transaction *odb_transaction_begin(struct object_database *odb)\n> +int odb_transaction_begin(struct object_database *odb,\n> +\t\t\t  struct odb_transaction **out)\n>  {\n> -\tif (odb->transaction)\n> -\t\treturn NULL;\n> +\tint ret;\n>  \n> -\todb_source_begin_transaction(odb->sources, &odb->transaction);\n> +\tif (odb->transaction) {\n> +\t\t*out = NULL;\n> +\t\treturn 0;\n> +\t}\n\nHm. So we may return successful, but not set the `out` pointer to a\ntransaction. And...\n\n> diff --git a/odb/transaction.h b/odb/transaction.h\n> index f4c1ebfaaa..cd6d50f2e5 100644\n> --- a/odb/transaction.h\n> +++ b/odb/transaction.h\n> @@ -33,11 +35,20 @@ struct odb_transaction {\n>  };\n>  \n>  /*\n> - * Starts an ODB transaction. Subsequent objects are written to the transaction\n> - * and not committed until odb_transaction_commit() is invoked on the\n> - * transaction. If the ODB already has a pending transaction, NULL is returned.\n> + * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n> + * written to the transaction and not committed until odb_transaction_commit()\n> + * is invoked on the transaction. Returns 0 on success and a negative value on\n> + * error. If the ODB already has a pending transaction, `out` is set to NULL.\n>   */\n> -struct odb_transaction *odb_transaction_begin(struct object_database *odb);\n> +int odb_transaction_begin(struct object_database *odb,\n> +\t\t\t  struct odb_transaction **out);\n> +\n> +static inline void odb_transaction_begin_or_die(struct object_database *odb,\n> +\t\t\t\t\t\tstruct odb_transaction **out)\n> +{\n> +\tif (odb_transaction_begin(odb, out))\n> +\t\tdie(_(\"failed to start ODB transaction\"));\n> +}\n\n... we don't special-case that here, either. So a caller may invoke the\nfunction, not die, but it might still not have a valid transaction. That\nfeels wrong to me.\n\nPatrick\n"},{"id":"546292","messageId":"aju-90Uayxwsevm7@pks.im","threadId":"65861","inReplyTo":"20260624041920.2601961-5-jltobler@gmail.com","subject":"Re: [PATCH 4/6] odb/transaction: propagate commit errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-24T11:26:47Z","receivedAt":"2026-06-24T11:26:54Z","isPatch":true,"body":"On Tue, Jun 23, 2026 at 11:19:18PM -0500, Justin Tobler wrote:\n> diff --git a/odb/transaction.h b/odb/transaction.h\n> index cd6d50f2e5..7898770071 100644\n> --- a/odb/transaction.h\n> +++ b/odb/transaction.h\n> @@ -54,7 +54,7 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n>   * Commits an ODB transaction making the written objects visible. If the\n>   * specified transaction is NULL, the function is a no-op.\n>   */\n> -void odb_transaction_commit(struct odb_transaction *transaction);\n> +int odb_transaction_commit(struct odb_transaction *transaction);\n\nShould the function comment be amended, as well? We should definitely\npoint out that calling this with a NULL transaction also returns\nsuccess.\n\nPatrick\n"},{"id":"546293","messageId":"aju-_Nf3kmoIidue@pks.im","threadId":"65861","inReplyTo":"20260624041920.2601961-6-jltobler@gmail.com","subject":"Re: [PATCH 5/6] odb/transaction: add transaction env interface","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-24T11:26:52Z","receivedAt":"2026-06-24T11:26:56Z","isPatch":true,"body":"On Tue, Jun 23, 2026 at 11:19:19PM -0500, Justin Tobler wrote:\n> The ODB transaction backend is responsible for creating/managing its own\n> staging area for writing objects. Other child processes spawned by Git\n> may need to access to uncommitted objects or write new objects in the\n\ns/may need to access to/may need access to/\n\n> staging area though.\n> \n> Introduce `odb_transaction_env()` which is expected to provide the set\n> of environment variables needed by a child process to access the\n> transaction staging area.\n\nPossessive s is missing, I think.\n\n> diff --git a/object-file.c b/object-file.c\n> index 696f05dc2d..14064d188a 100644\n> --- a/object-file.c\n> +++ b/object-file.c\n> @@ -1691,6 +1691,16 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n>  \treturn 0;\n>  }\n>  \n> +static const char **odb_transaction_files_env(struct odb_transaction *base)\n> +{\n> +\tstruct odb_transaction_files *transaction =\n> +\t\tcontainer_of(base, struct odb_transaction_files, base);\n> +\n> +\todb_transaction_files_prepare(&transaction->base);\n> +\n> +\treturn tmp_objdir_env(transaction->objdir);\n> +}\n> +\n>  int odb_transaction_files_begin(struct odb_source *source,\n>  \t\t\t\tstruct odb_transaction **out)\n>  {\n\nMakes sense. Transactions may have a different way to quarantine the\nwrite than using a quarantine directory. So making this functionality\npluggable so that backends may expose a separate set of environment\nvariables feels sensible.\n\n> diff --git a/odb/transaction.h b/odb/transaction.h\n> index 7898770071..536458297b 100644\n> --- a/odb/transaction.h\n> +++ b/odb/transaction.h\n> @@ -32,6 +32,16 @@ struct odb_transaction {\n>  \tint (*write_object_stream)(struct odb_transaction *transaction,\n>  \t\t\t\t   struct odb_write_stream *stream, size_t len,\n>  \t\t\t\t   struct object_id *oid);\n> +\n> +\t/*\n> +\t * This callback is expected to return a NULL-terminated array of\n> +\t * environment variables that a child process should inherit so\n> +\t * that its object writes participate in the transaction. The\n> +\t * returned array is owned by the backend and remains valid until\n> +\t * the transaction ends. May return NULL when the backend does not\n> +\t * need to expose any state to child processes.\n> +\t */\n> +\tconst char **(*env)(struct odb_transaction *transaction);\n\nWould it make more sense to adapt this function so that:\n\n  - It receives a `struct strvec` as input that the environment\n    variables are to be amended to.\n\n  - It returns a normal error code to indicate errors?\n\nPatrick\n"},{"id":"546294","messageId":"aju_AmlKVi5UZaiQ@pks.im","threadId":"65861","inReplyTo":"20260624041920.2601961-7-jltobler@gmail.com","subject":"Re: [PATCH 6/6] builtin/receive-pack: stage incoming objects via ODB transactions","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-24T11:26:58Z","receivedAt":"2026-06-24T11:27:03Z","isPatch":true,"body":"On Tue, Jun 23, 2026 at 11:19:20PM -0500, Justin Tobler wrote:\n> Objects received by git-receive-pack(1) are quarantined in a temporary\n> \"incoming\" directory and migrated into the object database prior to the\n> reference updates. The quarantine is currently managed through\n> `tmp_objdir` directly. In a pluggable ODB future, how exactly an object\n> gets written to a transaction may vary for a given ODB source. Refactor\n> git-receive-pack(1) to use the ODB transaction interfaces to manage the\n> object staging area in a more agnostic manner accordingly.\n> \n> Note that the temporary directory created for git-receive-pack(1) is\n> eagerly created and uses a different prefix name. This behavior is\n\nA different prefix name compared to what?\n\n> special cased in the \"files\" backend by having `odb_transaction_begin()`\n> callers that require this behavior provide an `ODB_TRANSACTION_RECEIVE`\n> flag.\n\nOkay. I guess this is to retain existing behaviour where the temporary\ndirectory is created lazily everywhere else. Makes me wonder whether we\nshould eventually change this to just unconditionally create the\ndirectory in all cases so that we can drop this new flag.\n\nIt might've also made sense to split this commit up into two: one to\nintroduce the flag parameter, and then one to do the changes to\ngit-receive-pack(1).\n\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index 19eb6a1b61..ee8e03e2ab 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -112,8 +112,6 @@ static enum {\n>  } use_keepalive;\n>  static int keepalive_in_sec = 5;\n>  \n> -static struct tmp_objdir *tmp_objdir;\n> -\n>  static struct proc_receive_ref {\n>  \tunsigned int want_add:1,\n>  \t\t     want_delete:1,\n\nI assume the goal is that we convert all other users of the tmp-objdir\nsubsystem to also use transactions eventually, so that this becomes an\nimplementation detail fo the files transaction?\n\n> @@ -2106,14 +2104,13 @@ static void execute_commands(struct command *commands,\n>  \t * Now we'll start writing out refs, which means the objects need\n>  \t * to be in their final positions so that other processes can see them.\n>  \t */\n> -\tif (tmp_objdir_migrate(tmp_objdir) < 0) {\n> +\tif (odb_transaction_commit(the_repository->objects->transaction)) {\n>  \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n>  \t\t\tif (!cmd->error_string)\n>  \t\t\t\tcmd->error_string = \"unable to migrate objects to permanent storage\";\n>  \t\t}\n>  \t\treturn;\n>  \t}\n> -\ttmp_objdir = NULL;\n\nWe don't need to unset the transaction because that's what\n`odb_transaction_commit()` already does for us, I assume?\n\n> @@ -2326,7 +2323,8 @@ static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n>  \t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n>  }\n>  \n> -static const char *unpack(int err_fd, struct shallow_info *si)\n> +static const char *unpack(int err_fd, struct shallow_info *si,\n> +\t\t\t  struct odb_transaction *transaction)\n>  {\n>  \tstruct pack_header hdr;\n>  \tconst char *hdr_err;\n\nIt feels a bit weird that we sometimes pass the transaction as\nparameter, whereas othertimes we access it via `the_repository`.\n\n> @@ -2351,20 +2349,7 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n>  \t\tstrvec_push(&child.args, alt_shallow_file);\n>  \t}\n>  \n> -\ttmp_objdir = tmp_objdir_create(the_repository, \"incoming\");\n> -\tif (!tmp_objdir) {\n> -\t\tif (err_fd > 0)\n> -\t\t\tclose(err_fd);\n> -\t\treturn \"unable to create temporary object directory\";\n> -\t}\n> -\tstrvec_pushv(&child.env, tmp_objdir_env(tmp_objdir));\n> -\n> -\t/*\n> -\t * Normally we just pass the tmp_objdir environment to the child\n> -\t * processes that do the heavy lifting, but we may need to see these\n> -\t * objects ourselves to set up shallow information.\n> -\t */\n> -\ttmp_objdir_add_as_alternate(tmp_objdir);\n> +\tstrvec_pushv(&child.env, odb_transaction_env(transaction));\n\nInteresting, this here seems like a change in behaviour. Previously we\nadded the transactions as an alternate, but now we only propagate it via\nthe environment. I didn't see this mentioned in the commit message.\n\n> @@ -2707,7 +2694,10 @@ int cmd_receive_pack(int argc,\n>  \t\tif (!si.nr_ours && !si.nr_theirs)\n>  \t\t\tshallow_update = 0;\n>  \t\tif (!delete_only(commands)) {\n> -\t\t\tunpack_status = unpack_with_sideband(&si);\n> +\t\t\tif (odb_transaction_begin(the_repository->objects, &transaction, ODB_TRANSACTION_RECEIVE))\n> +\t\t\t\tunpack_status = \"unable to start ODB transaction\";\n\ns/ODB/object/\n\nThis may be visible to the user, and \"ODB\" may mean nothing to them.\n\n> diff --git a/object-file.c b/object-file.c\n> index 14064d188a..e7958753ec 100644\n> --- a/object-file.c\n> +++ b/object-file.c\n> @@ -1702,7 +1703,8 @@ static const char **odb_transaction_files_env(struct odb_transaction *base)\n>  }\n>  \n>  int odb_transaction_files_begin(struct odb_source *source,\n> -\t\t\t\tstruct odb_transaction **out)\n> +\t\t\t\tstruct odb_transaction **out,\n> +\t\t\t\tenum odb_transaction_flags flags)\n>  {\n>  \tstruct odb_transaction_files *transaction;\n>  \tstruct object_database *odb = source->odb;\n> @@ -1717,6 +1719,20 @@ int odb_transaction_files_begin(struct odb_source *source,\n>  \ttransaction->base.commit = odb_transaction_files_commit;\n>  \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n>  \ttransaction->base.env = odb_transaction_files_env;\n> +\n> +\ttransaction->prefix = \"bulk-fsync\";\n> +\tif (flags & ODB_TRANSACTION_RECEIVE) {\n> +\t\t/*\n> +\t\t * ODB transactions for git-receive-pack(1) eagerly create a\n> +\t\t * temporary directory and use a different prefix.\n> +\t\t */\n> +\t\ttransaction->prefix = \"incoming\";\n> +\t\tif (odb_transaction_files_prepare(&transaction->base)) {\n> +\t\t\tfree(transaction);\n> +\t\t\treturn -1;\n> +\t\t}\n> +\t}\n> +\n\nOkay, makes sense. I really wonder whether we need to insist this much\non the exact name used by this, but better be safe than sorry for now I\nguess.\n\nAnd as mentioned before, I also wonder whether it really makes sense to\nhave the lazy creation of the tmp-objdir. Maybe add a NEEDSWORK item\nhere that mentions we want to investigate whether this is even needed at\nall?\n\n> diff --git a/odb/transaction.h b/odb/transaction.h\n> index 536458297b..78392ff13d 100644\n> --- a/odb/transaction.h\n> +++ b/odb/transaction.h\n> @@ -44,6 +43,10 @@ struct odb_transaction {\n>  \tconst char **(*env)(struct odb_transaction *transaction);\n>  };\n>  \n> +enum odb_transaction_flags {\n> +\tODB_TRANSACTION_RECEIVE = (1 << 0),\n> +};\n\nIt's not clear at all what this flag does based on its name, so we\nshould have documentation for it.\n\nPatrick\n"},{"id":"546295","messageId":"aju_CWdiXnRsu688@pks.im","threadId":"65861","inReplyTo":"20260624041920.2601961-1-jltobler@gmail.com","subject":"Re: [PATCH 0/6] receive-pack: use ODB transactions to stage object writes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-24T11:27:05Z","receivedAt":"2026-06-24T11:27:09Z","isPatch":true,"body":"On Tue, Jun 23, 2026 at 11:19:14PM -0500, Justin Tobler wrote:\n> Greetings,\n> \n> This patch series replaces direct usage of the `tmp_objdir` interfaces\n> in git-receive-pack(1) to instead use the `odb_transaction` interfaces\n> to create/manage a staging area to write objects to. The purpose of this\n> change is to get git-receive-pack(1) one step closer to being ODB\n> backend agnostic. For now, the object writes themselves are still\n> \"files\" backend specific due to being handled by the git-index-pack(1)\n> and git-unpack-objects(1) child processes. This will be tackled in a\n> separate series though.\n\nThanks, this was a pleasant read. I've got a bunch of comments, but\noverall I really like the direction of this patch series.\n\nPatrick\n"},{"id":"546340","messageId":"xmqqse6biyma.fsf@gitster.g","threadId":"65861","inReplyTo":"20260624041920.2601961-2-jltobler@gmail.com","subject":"Re: [PATCH 1/6] object-file: rename files transaction prepare function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-24T18:26:05Z","receivedAt":"2026-06-24T18:26:07Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> The \"files\" ODB transaction backend lazily creates a temporary object\n> directory when the first loose object is written to the transaction via\n> `prepare_loose_object_transaction()`. In a subsequent commit, the\n> temporary directory is used to also write packfiles to.\n>\n> Rename the function to `odb_transaction_files_prepare()` accordingly.\n\nTaken by itself this renaming does make sense, but there are many\nother function that follow the historical naming convention, like\n{fsync,flush}_loose_object_transaction().  Should we rename them for\nconsistency with the new naming scheme, not necessarily as part of\nthis series but with a todo comment to do so once the dust settles,\nor something?\n"},{"id":"546341","messageId":"xmqqjyrniy6r.fsf@gitster.g","threadId":"65861","inReplyTo":"20260624041920.2601961-3-jltobler@gmail.com","subject":"Re: [PATCH 2/6] object-file: propagate files transaction errors","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-24T18:35:24Z","receivedAt":"2026-06-24T18:35:27Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> The \"files\" transaction backend may encounter errors related to managing\n> the temporary directory used to stage objects, but silently ignores\n> these errors. Instead return errors encountered in the\n> `odb_transaction_files_{prepare,begin,commit}()` interfaces to allow\n> callers to handle as needed.\n\n\"handle them as needed\", perhaps.\n\n> -static void odb_transaction_files_prepare(struct odb_transaction *base)\n> +static 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> @@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n>  \t * added at the time they call odb_transaction_files_begin.\n>  \t */\n>  \tif (!transaction || transaction->objdir)\n> -\t\treturn;\n> +\t\treturn 0;\n>  \n>  \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n\nIf this fails, NULL is returned, and ...\n\n> -\tif (transaction->objdir)\n> -\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n> +\tif (!transaction->objdir)\n> +\t\treturn -1;\n\n... we return -1 from here to signal an error now.\n\nBut callers of this function in write_loose_object(), and\nodb_source_loose_write_stream() are not prepared to react to such an\nerror.\n\nI guess this is nothing new.  The callers ignored such an error from here\nin the original and proceeded writing the primary ODB anyway, and we\ncontinue to do so after this step.\n\n> @@ -542,13 +546,13 @@ static void fsync_loose_object_transaction(struct odb_transaction *base,\n>  /*\n>   * Cleanup after batch-mode fsync_object_files.\n>   */\n> -static void flush_loose_object_transaction(struct odb_transaction_files *transaction)\n> +static int flush_loose_object_transaction(struct odb_transaction_files *transaction)\n>  {\n>  \tstruct strbuf temp_path = STRBUF_INIT;\n>  \tstruct tempfile *temp;\n>  \n>  \tif (!transaction->objdir)\n> -\t\treturn;\n> +\t\treturn 0;\n>  \n>  \t/*\n>  \t * Issue a full hardware flush against a temporary file to ensure\n> @@ -570,8 +574,12 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac\n>  \t * Make the object files visible in the primary ODB after their data is\n>  \t * fully durable.\n>  \t */\n> -\ttmp_objdir_migrate(transaction->objdir);\n> +\tif (tmp_objdir_migrate(transaction->objdir))\n> +\t\treturn -1;\n> +\n>  \ttransaction->objdir = NULL;\n> +\n> +\treturn 0;\n>  }\n\nThe caller of this function does react to a failure of it, ...\n\n> @@ -1670,27 +1678,34 @@ int read_loose_object(struct repository *repo,\n>  \treturn ret;\n>  }\n>  \n> -static void odb_transaction_files_commit(struct odb_transaction *base)\n> +static int odb_transaction_files_commit(struct odb_transaction *base)\n>  {\n>  \tstruct odb_transaction_files *transaction =\n>  \t\tcontainer_of(base, struct odb_transaction_files, base);\n>  \n> -\tflush_loose_object_transaction(transaction);\n> +\tif (flush_loose_object_transaction(transaction))\n> +\t\treturn -1;\n>  \tflush_packfile_transaction(transaction);\n> +\n> +\treturn 0;\n>  }\n\n... like this, which is good.  Do we need an explicit \"abort-transaction\",\nor is that implicit?\n\nThanks.\n"},{"id":"546343","messageId":"xmqqechvitu3.fsf@gitster.g","threadId":"65861","inReplyTo":"20260624041920.2601961-1-jltobler@gmail.com","subject":"Re: [PATCH 0/6] receive-pack: use ODB transactions to stage object writes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-24T20:09:24Z","receivedAt":"2026-06-24T20:09:27Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> Greetings,\n>\n> This patch series replaces direct usage of the `tmp_objdir` interfaces\n> in git-receive-pack(1) to instead use the `odb_transaction` interfaces\n> to create/manage a staging area to write objects to. The purpose of this\n> change is to get git-receive-pack(1) one step closer to being ODB\n> backend agnostic. For now, the object writes themselves are still\n> \"files\" backend specific due to being handled by the git-index-pack(1)\n> and git-unpack-objects(1) child processes. This will be tackled in a\n> separate series though.\n>\n> Thanks,\n> -Justin\n\nThe integration cycle this morning was somehow more painful than\nother cycles.  \n\nFYI, this topic had some interactions with ps/odb-source-packed and\nps/refs-avoid-chdir-notify-reparent and needed the following\nevil-merge fix to make it build.\n\ncommit 721c15c1d5ef8f8aab8b9f50463e979e890b1ab6\nAuthor: Junio C Hamano <gitster@pobox.com>\nDate:   Wed Jun 24 12:45:35 2026 -0700\n\n    merge-fix/jt/receive-pack-use-odb-transactions\n    \n    conflict with ps/odb-source-packed and ps/refs-avoid-chdir-notify-reparent\n\ndiff --git a/odb/source-packed.c b/odb/source-packed.c\nindex c3c26fb53e..b9231a8da0 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-\t\t\t\t\t       struct odb_transaction **out UNUSED)\n+\t\t\t\t\t       struct odb_transaction **out UNUSED,\n+\t\t\t\t\t       enum odb_transaction_flags flags UNUSED)\n {\n \treturn error(\"packed backend cannot begin transactions\");\n }\ndiff --git a/t/unit-tests/u-reftable-table.c b/t/unit-tests/u-reftable-table.c\nindex a35bbc8004..dd5db06534 100644\n--- a/t/unit-tests/u-reftable-table.c\n+++ b/t/unit-tests/u-reftable-table.c\n@@ -235,7 +235,8 @@ void test_reftable_table__seek_invalid_log_offset(void)\n \tuint8_t *footer;\n \n \tcl_reftable_write_to_buf(&buf, refs, ARRAY_SIZE(refs),\n-\t\t\t\t logs, ARRAY_SIZE(logs), NULL);\n+\t\t\t\t logs, ARRAY_SIZE(logs),\n+\t\t\t\t REFTABLE_HASH_SHA1, NULL);\n \n \t/*\n \t * Corrupt the log section offset stored in the footer so that it points\n@@ -278,7 +279,8 @@ void test_reftable_table__new_with_truncated_table(void)\n \tstruct reftable_table *table;\n \tstruct reftable_buf buf = REFTABLE_BUF_INIT;\n \n-\tcl_reftable_write_to_buf(&buf, refs, ARRAY_SIZE(refs), NULL, 0, NULL);\n+\tcl_reftable_write_to_buf(&buf, refs, ARRAY_SIZE(refs), NULL, 0,\n+\t\t\t\t REFTABLE_HASH_SHA1, NULL);\n \n \t/*\n \t * Truncate the table so that it is large enough to read the header, but\n"},{"id":"546694","messageId":"akK05yZ6843K8Vdd@denethor","threadId":"65861","inReplyTo":"xmqqse6biyma.fsf@gitster.g","subject":"Re: [PATCH 1/6] object-file: rename files transaction prepare function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-29T18:11:53Z","receivedAt":"2026-06-29T18:11:58Z","isPatch":true,"body":"On 26/06/24 11:26AM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > The \"files\" ODB transaction backend lazily creates a temporary object\n> > directory when the first loose object is written to the transaction via\n> > `prepare_loose_object_transaction()`. In a subsequent commit, the\n> > temporary directory is used to also write packfiles to.\n> >\n> > Rename the function to `odb_transaction_files_prepare()` accordingly.\n> \n> Taken by itself this renaming does make sense, but there are many\n> other function that follow the historical naming convention, like\n> {fsync,flush}_loose_object_transaction().  Should we rename them for\n> consistency with the new naming scheme, not necessarily as part of\n> this series but with a todo comment to do so once the dust settles,\n> or something?\n\nYa, both {fsync,flush}_loose_object_transaction() are probably good\ncandidates to be renamed to odb_transaction_files_{flush,flush} also. In\nthe next version, I'll probably add another patch to do so accordingly. \n\n-Justin\n"},{"id":"546698","messageId":"akK1roQJknYstX0u@denethor","threadId":"65861","inReplyTo":"aju-7Z-ecJG_ORow@pks.im","subject":"Re: [PATCH 2/6] object-file: propagate files transaction errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-29T18:58:54Z","receivedAt":"2026-06-29T18:58:58Z","isPatch":true,"body":"On 26/06/24 01:26PM, Patrick Steinhardt wrote:\n> On Tue, Jun 23, 2026 at 11:19:16PM -0500, Justin Tobler wrote:\n> > The \"files\" transaction backend may encounter errors related to managing\n> > the temporary directory used to stage objects, but silently ignores\n> > these errors. Instead return errors encountered in the\n> > `odb_transaction_files_{prepare,begin,commit}()` interfaces to allow\n> > callers to handle as needed.\n> \n> Missing a then? \"to handle as needed\" -> \"to handle them as needed\"\n\nWill fix.\n\n[snip]\n> > diff --git a/object-file.c b/object-file.c\n> > index a3eb8d71dd..18c2df75fb 100644\n> > --- a/object-file.c\n> > +++ b/object-file.c\n> > @@ -499,7 +499,7 @@ struct odb_transaction_files {\n> >  \tstruct transaction_packfile packfile;\n> >  };\n> >  \n> > -static void odb_transaction_files_prepare(struct odb_transaction *base)\n> > +static 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> \n> By the way, is there any reason why those functions are still hosted in\n> \"object-file.c\" instead of in \"odb/source-files.c\"? I should probably\n> know, but I forgot.\n\nThere are currently a couple spots in the \"files\" object write path in\n\"object-file.c\" that still reach into some of these transaction function\nthat are not part of the generic ODB transaction interface. I'm hoping\nin a future followup series to detangle this a bit and be able to get\nall the \"files\" ODB transaction related code moved into\n\"odb/source-files.c\".\n\n> > @@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n> >  \t * added at the time they call odb_transaction_files_begin.\n> >  \t */\n> >  \tif (!transaction || transaction->objdir)\n> > -\t\treturn;\n> > +\t\treturn 0;\n> >  \n> >  \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n> > -\tif (transaction->objdir)\n> > -\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n> > +\tif (!transaction->objdir)\n> > +\t\treturn -1;\n> \n> Huh. So previously we just didn't handle this error at all and just\n> continued to tag along? Did that result in anything sensible or was this\n> just YOLOing it?\n\nGood question. Previously if there was an error, we wouldn't end up\ncreating any tmpdir and would instead continue to use the primary ODB to\nwrite objects in. This change would make it a hard error if we fail to\ncreate the temp dir. This matches the behavior that git-receive-pack(1)\nexpects, but I didn't consider that the existing callers could\ntransparently handle there being no temp dir.\n\nI suspect we may want existing ODB transaction users to continue being\nresilient in the same manner. In the next version, I'll maintain the\nsame behavior.\n\n> > @@ -542,13 +546,13 @@ static void fsync_loose_object_transaction(struct odb_transaction *base,\n> >  /*\n> >   * Cleanup after batch-mode fsync_object_files.\n> >   */\n> > -static void flush_loose_object_transaction(struct odb_transaction_files *transaction)\n> > +static int flush_loose_object_transaction(struct odb_transaction_files *transaction)\n> \n> Feels like this function should've been renamed in the preceding commit,\n> as well.\n\nYa I agree, will address in the next version.\n\n> >  {\n> >  \tstruct strbuf temp_path = STRBUF_INIT;\n> >  \tstruct tempfile *temp;\n> >  \n> >  \tif (!transaction->objdir)\n> > -\t\treturn;\n> > +\t\treturn 0;\n> >  \n> >  \t/*\n> >  \t * Issue a full hardware flush against a temporary file to ensure\n> > @@ -570,8 +574,12 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac\n> \n> There is a call to `xmks_tempfile()` hidden that can fail, but that\n> failure is already handled in that function itself by dying.\n> \n> >  \t * Make the object files visible in the primary ODB after their data is\n> >  \t * fully durable.\n> >  \t */\n> > -\ttmp_objdir_migrate(transaction->objdir);\n> > +\tif (tmp_objdir_migrate(transaction->objdir))\n> > +\t\treturn -1;\n> \n> Feels like another case of YOLOing it. The migration could have failed,\n> but we just ignored that failure and never told the user about it. The\n> result may be silent corruption, I assume?\n\nYa in this case, if the migration failed, we would just continue on\nwhich would likely result in silent corruption.\n\n> \n> > @@ -1670,27 +1678,34 @@ int read_loose_object(struct repository *repo,\n> >  \treturn ret;\n> >  }\n> >  \n> > -static void odb_transaction_files_commit(struct odb_transaction *base)\n> > +static int odb_transaction_files_commit(struct odb_transaction *base)\n> >  {\n> >  \tstruct odb_transaction_files *transaction =\n> >  \t\tcontainer_of(base, struct odb_transaction_files, base);\n> >  \n> > -\tflush_loose_object_transaction(transaction);\n> > +\tif (flush_loose_object_transaction(transaction))\n> > +\t\treturn -1;\n> >  \tflush_packfile_transaction(transaction);\n> > +\n> > +\treturn 0;\n> >  }\n> >  \n> > -struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n> > +int odb_transaction_files_begin(struct odb_source *source,\n> > +\t\t\t\tstruct odb_transaction **out)\n> >  {\n> >  \tstruct odb_transaction_files *transaction;\n> >  \tstruct object_database *odb = source->odb;\n> >  \n> > -\tif (odb->transaction)\n> > -\t\treturn NULL;\n> > +\tif (odb->transaction) {\n> > +\t\t*out = NULL;\n> > +\t\treturn 0;\n> > +\t}\n> >  \n> >  \ttransaction = xcalloc(1, sizeof(*transaction));\n> >  \ttransaction->base.source = source;\n> >  \ttransaction->base.commit = odb_transaction_files_commit;\n> >  \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n> > +\t*out = &transaction->base;\n> >  \n> > -\treturn &transaction->base;\n> > +\treturn 0;\n> >  }\n> \n> It's still somewhat fishy that we have this ODB-level transaction, but\n> that's a preexisting issue and thus outside the scope of this patch\n> series. Ideally though, it would be possible for there to be multiple\n> transactions, and it would be the caller's responsibility for juggling\n> these transactions. Just as it happens with reference transactions.\n\nI completely agree. One of the current problems preventing this is that\nonly a single instance of tmp_objdir is allowed and stored globally.\nThis is done to keep atexit() cleanup simple.\n\nMy plan is to eventually convert all existing tmp_objdir callsites to\nuse ODB transactions which should hopefully allow us to remove the need\nfor a separate tmp_objdir system. At that point, we can also work to\nchange how temp dir cleanup is handled at exit.\n\nAnother problem is that there are also a couple of ODB transaction\ncallsites that require to know if there is already a pending transaction\nfor the repository and the transaction has not been wired down to these\ncallsites. My hope is that this can be addressed though as we expand ODB\ntransaction usage for object writes.\n\n> > diff --git a/odb/transaction.h b/odb/transaction.h\n> > index 854fda06f5..f4c1ebfaaa 100644\n> > --- a/odb/transaction.h\n> > +++ b/odb/transaction.h\n> > @@ -17,7 +17,7 @@ struct odb_transaction {\n> >  \tstruct odb_source *source;\n> >  \n> >  \t/* The ODB source specific callback invoked to commit a transaction. */\n> > -\tvoid (*commit)(struct odb_transaction *transaction);\n> > +\tint (*commit)(struct odb_transaction *transaction);\n> \n> We might want to document the returned error code here.\n\nWill do in the next version.\n\nThanks,\n-Justin\n"},{"id":"546699","messageId":"akLBFaTfBEq8vHUr@denethor","threadId":"65861","inReplyTo":"akK1roQJknYstX0u@denethor","subject":"Re: [PATCH 2/6] object-file: propagate files transaction errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-29T19:04:08Z","receivedAt":"2026-06-29T19:04:09Z","isPatch":true,"body":"On 26/06/29 01:58PM, Justin Tobler wrote:\n> On 26/06/24 01:26PM, Patrick Steinhardt wrote:\n> > On Tue, Jun 23, 2026 at 11:19:16PM -0500, Justin Tobler wrote:\n> > > @@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n> > >  \t * added at the time they call odb_transaction_files_begin.\n> > >  \t */\n> > >  \tif (!transaction || transaction->objdir)\n> > > -\t\treturn;\n> > > +\t\treturn 0;\n> > >  \n> > >  \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n> > > -\tif (transaction->objdir)\n> > > -\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n> > > +\tif (!transaction->objdir)\n> > > +\t\treturn -1;\n> > \n> > Huh. So previously we just didn't handle this error at all and just\n> > continued to tag along? Did that result in anything sensible or was this\n> > just YOLOing it?\n> \n> Good question. Previously if there was an error, we wouldn't end up\n> creating any tmpdir and would instead continue to use the primary ODB to\n> write objects in. This change would make it a hard error if we fail to\n> create the temp dir. This matches the behavior that git-receive-pack(1)\n> expects, but I didn't consider that the existing callers could\n> transparently handle there being no temp dir.\n> \n> I suspect we may want existing ODB transaction users to continue being\n> resilient in the same manner. In the next version, I'll maintain the\n> same behavior.\n\nI think I got a bit ahead of myself. The existing callers of\nodb_transaction_files_prepare() still continue to ignore this error. So\nthe behavior already does remain the same here.\n\n-Justin\n"},{"id":"546700","messageId":"akLByeT2no922sBX@denethor","threadId":"65861","inReplyTo":"xmqqjyrniy6r.fsf@gitster.g","subject":"Re: [PATCH 2/6] object-file: propagate files transaction errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-29T19:10:37Z","receivedAt":"2026-06-29T19:10:40Z","isPatch":true,"body":"On 26/06/24 11:35AM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > The \"files\" transaction backend may encounter errors related to managing\n> > the temporary directory used to stage objects, but silently ignores\n> > these errors. Instead return errors encountered in the\n> > `odb_transaction_files_{prepare,begin,commit}()` interfaces to allow\n> > callers to handle as needed.\n> \n> \"handle them as needed\", perhaps.\n\nWill fix, thanks\n\n[snip]\n> The caller of this function does react to a failure of it, ...\n> \n> > @@ -1670,27 +1678,34 @@ int read_loose_object(struct repository *repo,\n> >  \treturn ret;\n> >  }\n> >  \n> > -static void odb_transaction_files_commit(struct odb_transaction *base)\n> > +static int odb_transaction_files_commit(struct odb_transaction *base)\n> >  {\n> >  \tstruct odb_transaction_files *transaction =\n> >  \t\tcontainer_of(base, struct odb_transaction_files, base);\n> >  \n> > -\tflush_loose_object_transaction(transaction);\n> > +\tif (flush_loose_object_transaction(transaction))\n> > +\t\treturn -1;\n> >  \tflush_packfile_transaction(transaction);\n> > +\n> > +\treturn 0;\n> >  }\n> \n> ... like this, which is good.  Do we need an explicit \"abort-transaction\",\n> or is that implicit?\n\nSo this is currently handled implicitly via\n`tmp-objdir.c:remove_tmp_objdir()` which gets registered as an atexit()\nhandler. As long as the tmp_objdir global remains set, it will\nautomatically get cleaned up.\n\nIn a subsequent series, I do plan to add `odb_transaction_abort()` to\nthe transaction interface. It may make sense to also use that here to\nmake the cleanup a bit more explicit though.\n\n-Justin\n"},{"id":"546701","messageId":"akLDimUfWnCtRL6e@denethor","threadId":"65861","inReplyTo":"aju-8hUeuyL6gnNU@pks.im","subject":"Re: [PATCH 3/6] odb/transaction: propagate begin errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-29T19:15:14Z","receivedAt":"2026-06-29T19:15:15Z","isPatch":true,"body":"On 26/06/24 01:26PM, Patrick Steinhardt wrote:\n> On Tue, Jun 23, 2026 at 11:19:17PM -0500, Justin Tobler wrote:\n> > diff --git a/odb/transaction.c b/odb/transaction.c\n> > index b16e07aebf..d3de01db50 100644\n> > --- a/odb/transaction.c\n> > +++ b/odb/transaction.c\n> > @@ -2,14 +2,20 @@\n> >  #include \"odb/source.h\"\n> >  #include \"odb/transaction.h\"\n> >  \n> > -struct odb_transaction *odb_transaction_begin(struct object_database *odb)\n> > +int odb_transaction_begin(struct object_database *odb,\n> > +\t\t\t  struct odb_transaction **out)\n> >  {\n> > -\tif (odb->transaction)\n> > -\t\treturn NULL;\n> > +\tint ret;\n> >  \n> > -\todb_source_begin_transaction(odb->sources, &odb->transaction);\n> > +\tif (odb->transaction) {\n> > +\t\t*out = NULL;\n> > +\t\treturn 0;\n> > +\t}\n> \n> Hm. So we may return successful, but not set the `out` pointer to a\n> transaction. And...\n> \n> > diff --git a/odb/transaction.h b/odb/transaction.h\n> > index f4c1ebfaaa..cd6d50f2e5 100644\n> > --- a/odb/transaction.h\n> > +++ b/odb/transaction.h\n> > @@ -33,11 +35,20 @@ struct odb_transaction {\n> >  };\n> >  \n> >  /*\n> > - * Starts an ODB transaction. Subsequent objects are written to the transaction\n> > - * and not committed until odb_transaction_commit() is invoked on the\n> > - * transaction. If the ODB already has a pending transaction, NULL is returned.\n> > + * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n> > + * written to the transaction and not committed until odb_transaction_commit()\n> > + * is invoked on the transaction. Returns 0 on success and a negative value on\n> > + * error. If the ODB already has a pending transaction, `out` is set to NULL.\n> >   */\n> > -struct odb_transaction *odb_transaction_begin(struct object_database *odb);\n> > +int odb_transaction_begin(struct object_database *odb,\n> > +\t\t\t  struct odb_transaction **out);\n> > +\n> > +static inline void odb_transaction_begin_or_die(struct object_database *odb,\n> > +\t\t\t\t\t\tstruct odb_transaction **out)\n> > +{\n> > +\tif (odb_transaction_begin(odb, out))\n> > +\t\tdie(_(\"failed to start ODB transaction\"));\n> > +}\n> \n> ... we don't special-case that here, either. So a caller may invoke the\n> function, not die, but it might still not have a valid transaction. That\n> feels wrong to me.\n\nYa, the original idea for a NULL transaction to signal that there is\nalready a transaction in flight and nested ODB transaction users to use\nthat to determine whether they needed to start a new transaction or not.\nI completely agree thought that this is rather ackward.\n\nInstead we could just make the callers that are worried about\npotentially nested transactions handle this explicitly. I'll do that in\nthe next version.\n\n-Justin\n"},{"id":"546702","messageId":"akLEigsjg3TaIEcb@denethor","threadId":"65861","inReplyTo":"aju-90Uayxwsevm7@pks.im","subject":"Re: [PATCH 4/6] odb/transaction: propagate commit errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-29T19:16:46Z","receivedAt":"2026-06-29T19:16:48Z","isPatch":true,"body":"On 26/06/24 01:26PM, Patrick Steinhardt wrote:\n> On Tue, Jun 23, 2026 at 11:19:18PM -0500, Justin Tobler wrote:\n> > diff --git a/odb/transaction.h b/odb/transaction.h\n> > index cd6d50f2e5..7898770071 100644\n> > --- a/odb/transaction.h\n> > +++ b/odb/transaction.h\n> > @@ -54,7 +54,7 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n> >   * Commits an ODB transaction making the written objects visible. If the\n> >   * specified transaction is NULL, the function is a no-op.\n> >   */\n> > -void odb_transaction_commit(struct odb_transaction *transaction);\n> > +int odb_transaction_commit(struct odb_transaction *transaction);\n> \n> Should the function comment be amended, as well? We should definitely\n> point out that calling this with a NULL transaction also returns\n> success.\n\nWill do in the next version.\n\nThanks,\n-Justin\n"},{"id":"546703","messageId":"akLE1DhWGaOWhvQ4@denethor","threadId":"65861","inReplyTo":"aju-_Nf3kmoIidue@pks.im","subject":"Re: [PATCH 5/6] odb/transaction: add transaction env interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-29T19:20:03Z","receivedAt":"2026-06-29T19:20:05Z","isPatch":true,"body":"On 26/06/24 01:26PM, Patrick Steinhardt wrote:\n> On Tue, Jun 23, 2026 at 11:19:19PM -0500, Justin Tobler wrote:\n> > The ODB transaction backend is responsible for creating/managing its own\n> > staging area for writing objects. Other child processes spawned by Git\n> > may need to access to uncommitted objects or write new objects in the\n> \n> s/may need to access to/may need access to/\n\nWill fix.\n\n> > staging area though.\n> > \n> > Introduce `odb_transaction_env()` which is expected to provide the set\n> > of environment variables needed by a child process to access the\n> > transaction staging area.\n> \n> Possessive s is missing, I think.\n\nYes. :)\n\n[snip]\n> > +\n> > +\t/*\n> > +\t * This callback is expected to return a NULL-terminated array of\n> > +\t * environment variables that a child process should inherit so\n> > +\t * that its object writes participate in the transaction. The\n> > +\t * returned array is owned by the backend and remains valid until\n> > +\t * the transaction ends. May return NULL when the backend does not\n> > +\t * need to expose any state to child processes.\n> > +\t */\n> > +\tconst char **(*env)(struct odb_transaction *transaction);\n> \n> Would it make more sense to adapt this function so that:\n> \n>   - It receives a `struct strvec` as input that the environment\n>     variables are to be amended to.\n> \n>   - It returns a normal error code to indicate errors?\n\nYa, that is probably a more sensible interface as it would be nice to be\nable to signal an error. Will do in the next version.\n\nThanks,\n-Justin\n"},{"id":"546706","messageId":"akLLB_J-pvJ7iR7c@denethor","threadId":"65861","inReplyTo":"aju_AmlKVi5UZaiQ@pks.im","subject":"Re: [PATCH 6/6] builtin/receive-pack: stage incoming objects via ODB transactions","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-29T20:25:18Z","receivedAt":"2026-06-29T20:25:23Z","isPatch":true,"body":"On 26/06/24 01:26PM, Patrick Steinhardt wrote:\n> On Tue, Jun 23, 2026 at 11:19:20PM -0500, Justin Tobler wrote:\n> > Objects received by git-receive-pack(1) are quarantined in a temporary\n> > \"incoming\" directory and migrated into the object database prior to the\n> > reference updates. The quarantine is currently managed through\n> > `tmp_objdir` directly. In a pluggable ODB future, how exactly an object\n> > gets written to a transaction may vary for a given ODB source. Refactor\n> > git-receive-pack(1) to use the ODB transaction interfaces to manage the\n> > object staging area in a more agnostic manner accordingly.\n> > \n> > Note that the temporary directory created for git-receive-pack(1) is\n> > eagerly created and uses a different prefix name. This behavior is\n> \n> A different prefix name compared to what?\n\nCurrently the temporary directories created for ODB transactions all use\nthe prefix \"bulk-fsync\". The temp dir created by git-receive-pack(1) is\nexpected to have the prefix \"incoming\".\n\n> > special cased in the \"files\" backend by having `odb_transaction_begin()`\n> > callers that require this behavior provide an `ODB_TRANSACTION_RECEIVE`\n> > flag.\n> \n> Okay. I guess this is to retain existing behaviour where the temporary\n> directory is created lazily everywhere else. Makes me wonder whether we\n> should eventually change this to just unconditionally create the\n> directory in all cases so that we can drop this new flag.\n\nIt would be nice to not have to have a flag here, but if we want to also\nkeep the existing temp dir prefixes, we would also need to keep the\nflags.\n\n> It might've also made sense to split this commit up into two: one to\n> introduce the flag parameter, and then one to do the changes to\n> git-receive-pack(1).\n\nYa, I think you are right. I actually originally had it this way but\nsquashed these two commits together for some reason. I'll split them\nback up in the next version.\n\n> > diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> > index 19eb6a1b61..ee8e03e2ab 100644\n> > --- a/builtin/receive-pack.c\n> > +++ b/builtin/receive-pack.c\n> > @@ -112,8 +112,6 @@ static enum {\n> >  } use_keepalive;\n> >  static int keepalive_in_sec = 5;\n> >  \n> > -static struct tmp_objdir *tmp_objdir;\n> > -\n> >  static struct proc_receive_ref {\n> >  \tunsigned int want_add:1,\n> >  \t\t     want_delete:1,\n> \n> I assume the goal is that we convert all other users of the tmp-objdir\n> subsystem to also use transactions eventually, so that this becomes an\n> implementation detail fo the files transaction?\n\nYes, that is exactly my plan :)\n\n> > @@ -2106,14 +2104,13 @@ static void execute_commands(struct command *commands,\n> >  \t * Now we'll start writing out refs, which means the objects need\n> >  \t * to be in their final positions so that other processes can see them.\n> >  \t */\n> > -\tif (tmp_objdir_migrate(tmp_objdir) < 0) {\n> > +\tif (odb_transaction_commit(the_repository->objects->transaction)) {\n> >  \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n> >  \t\t\tif (!cmd->error_string)\n> >  \t\t\t\tcmd->error_string = \"unable to migrate objects to permanent storage\";\n> >  \t\t}\n> >  \t\treturn;\n> >  \t}\n> > -\ttmp_objdir = NULL;\n> \n> We don't need to unset the transaction because that's what\n> `odb_transaction_commit()` already does for us, I assume?\n\nThat is correct. The tmp_objdir global is actually removed and now\ncompetely managed by the ODB transaction. We can probably actuall remove\nthe \"tmp-objdir\" include now too. I'll do that in the next version.\n\n> > @@ -2326,7 +2323,8 @@ static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n> >  \t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n> >  }\n> >  \n> > -static const char *unpack(int err_fd, struct shallow_info *si)\n> > +static const char *unpack(int err_fd, struct shallow_info *si,\n> > +\t\t\t  struct odb_transaction *transaction)\n> >  {\n> >  \tstruct pack_header hdr;\n> >  \tconst char *hdr_err;\n> \n> It feels a bit weird that we sometimes pass the transaction as\n> parameter, whereas othertimes we access it via `the_repository`.\n\nThat's fair. I was trying to avoid the churn of wiring to all its\ncallsites, but it's probably best to be consistent. Maybe it would be\nfine to just create a transaction global like we do for the reference\ntransaction?\n\n> > @@ -2351,20 +2349,7 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n> >  \t\tstrvec_push(&child.args, alt_shallow_file);\n> >  \t}\n> >  \n> > -\ttmp_objdir = tmp_objdir_create(the_repository, \"incoming\");\n> > -\tif (!tmp_objdir) {\n> > -\t\tif (err_fd > 0)\n> > -\t\t\tclose(err_fd);\n> > -\t\treturn \"unable to create temporary object directory\";\n> > -\t}\n> > -\tstrvec_pushv(&child.env, tmp_objdir_env(tmp_objdir));\n> > -\n> > -\t/*\n> > -\t * Normally we just pass the tmp_objdir environment to the child\n> > -\t * processes that do the heavy lifting, but we may need to see these\n> > -\t * objects ourselves to set up shallow information.\n> > -\t */\n> > -\ttmp_objdir_add_as_alternate(tmp_objdir);\n> > +\tstrvec_pushv(&child.env, odb_transaction_env(transaction));\n> \n> Interesting, this here seems like a change in behaviour. Previously we\n> added the transactions as an alternate, but now we only propagate it via\n> the environment. I didn't see this mentioned in the commit message.\n\nThat's not quite correct. By using the ODB transaction interface, the\nunderlying tmp_objdir is managed transparently. When we begin the ODB\ntransaction, the temp directory that gets created is automatically\ncreated and applied as the primary ODB. This ultimately produces an\nequivalent result, its just now set up when the transaction is started\n(which happens a little bit earlier).\n\nThe only behavior change here is that the temp directory is being\napplied as the primary instead of an alternate in the main\ngit-receive-pack(1) process. Since all writes though are handled by the\nchild processes that get spawned, this shouldn't really matter though.\n\nThis is certainly rather confusing though, and should be mentioned in\nthe commit message. I'll do this in the next version.\n\n> > @@ -2707,7 +2694,10 @@ int cmd_receive_pack(int argc,\n> >  \t\tif (!si.nr_ours && !si.nr_theirs)\n> >  \t\t\tshallow_update = 0;\n> >  \t\tif (!delete_only(commands)) {\n> > -\t\t\tunpack_status = unpack_with_sideband(&si);\n> > +\t\t\tif (odb_transaction_begin(the_repository->objects, &transaction, ODB_TRANSACTION_RECEIVE))\n> > +\t\t\t\tunpack_status = \"unable to start ODB transaction\";\n> \n> s/ODB/object/\n> \n> This may be visible to the user, and \"ODB\" may mean nothing to them.\n\nWill change in the next version.\n\n> > diff --git a/object-file.c b/object-file.c\n> > index 14064d188a..e7958753ec 100644\n> > --- a/object-file.c\n> > +++ b/object-file.c\n> > @@ -1702,7 +1703,8 @@ static const char **odb_transaction_files_env(struct odb_transaction *base)\n> >  }\n> >  \n> >  int odb_transaction_files_begin(struct odb_source *source,\n> > -\t\t\t\tstruct odb_transaction **out)\n> > +\t\t\t\tstruct odb_transaction **out,\n> > +\t\t\t\tenum odb_transaction_flags flags)\n> >  {\n> >  \tstruct odb_transaction_files *transaction;\n> >  \tstruct object_database *odb = source->odb;\n> > @@ -1717,6 +1719,20 @@ int odb_transaction_files_begin(struct odb_source *source,\n> >  \ttransaction->base.commit = odb_transaction_files_commit;\n> >  \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n> >  \ttransaction->base.env = odb_transaction_files_env;\n> > +\n> > +\ttransaction->prefix = \"bulk-fsync\";\n> > +\tif (flags & ODB_TRANSACTION_RECEIVE) {\n> > +\t\t/*\n> > +\t\t * ODB transactions for git-receive-pack(1) eagerly create a\n> > +\t\t * temporary directory and use a different prefix.\n> > +\t\t */\n> > +\t\ttransaction->prefix = \"incoming\";\n> > +\t\tif (odb_transaction_files_prepare(&transaction->base)) {\n> > +\t\t\tfree(transaction);\n> > +\t\t\treturn -1;\n> > +\t\t}\n> > +\t}\n> > +\n> \n> Okay, makes sense. I really wonder whether we need to insist this much\n> on the exact name used by this, but better be safe than sorry for now I\n> guess.\n> \n> And as mentioned before, I also wonder whether it really makes sense to\n> have the lazy creation of the tmp-objdir. Maybe add a NEEDSWORK item\n> here that mentions we want to investigate whether this is even needed at\n> all?\n\nYa, I was worried that there would be some reason the temp dir prefix\nshould remain the same or there would be some performance reason to\nlazily create directories. I didn't investigate too deeply though. I'll\nadd a NEEDSWORK comment to make note of this.\n\n> > diff --git a/odb/transaction.h b/odb/transaction.h\n> > index 536458297b..78392ff13d 100644\n> > --- a/odb/transaction.h\n> > +++ b/odb/transaction.h\n> > @@ -44,6 +43,10 @@ struct odb_transaction {\n> >  \tconst char **(*env)(struct odb_transaction *transaction);\n> >  };\n> >  \n> > +enum odb_transaction_flags {\n> > +\tODB_TRANSACTION_RECEIVE = (1 << 0),\n> > +};\n> \n> It's not clear at all what this flag does based on its name, so we\n> should have documentation for it.\n\nWill update.\n\nThanks,\n-Justin\n"},{"id":"546741","messageId":"akOCM8qVi2r9YPsF@pks.im","threadId":"65861","inReplyTo":"akLLB_J-pvJ7iR7c@denethor","subject":"Re: [PATCH 6/6] builtin/receive-pack: stage incoming objects via ODB transactions","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-30T08:45:39Z","receivedAt":"2026-06-30T08:45:44Z","isPatch":true,"body":"On Mon, Jun 29, 2026 at 03:25:18PM -0500, Justin Tobler wrote:\n> On 26/06/24 01:26PM, Patrick Steinhardt wrote:\n> > On Tue, Jun 23, 2026 at 11:19:20PM -0500, Justin Tobler wrote:\n> > > Objects received by git-receive-pack(1) are quarantined in a temporary\n> > > \"incoming\" directory and migrated into the object database prior to the\n> > > reference updates. The quarantine is currently managed through\n> > > `tmp_objdir` directly. In a pluggable ODB future, how exactly an object\n> > > gets written to a transaction may vary for a given ODB source. Refactor\n> > > git-receive-pack(1) to use the ODB transaction interfaces to manage the\n> > > object staging area in a more agnostic manner accordingly.\n> > > \n> > > Note that the temporary directory created for git-receive-pack(1) is\n> > > eagerly created and uses a different prefix name. This behavior is\n> > \n> > A different prefix name compared to what?\n> \n> Currently the temporary directories created for ODB transactions all use\n> the prefix \"bulk-fsync\". The temp dir created by git-receive-pack(1) is\n> expected to have the prefix \"incoming\".\n> \n> > > special cased in the \"files\" backend by having `odb_transaction_begin()`\n> > > callers that require this behavior provide an `ODB_TRANSACTION_RECEIVE`\n> > > flag.\n> > \n> > Okay. I guess this is to retain existing behaviour where the temporary\n> > directory is created lazily everywhere else. Makes me wonder whether we\n> > should eventually change this to just unconditionally create the\n> > directory in all cases so that we can drop this new flag.\n> \n> It would be nice to not have to have a flag here, but if we want to also\n> keep the existing temp dir prefixes, we would also need to keep the\n> flags.\n\nFair. Makes me wonder whether we really need to keep the exact same\nnaming for this temporary directory. This is so deep into internals that\nI'm not sure whether we really need to treat this as part of our\ninterface. I'm rather inclined to say it's not necessary.\n\nIn any case, I think it's fine to defer that discussion and keep this\nas-is for now. But we might keep it in the back of our minds and maybe\nsimplify this in a subsequent patch series.\n\n> > > diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> > > index 19eb6a1b61..ee8e03e2ab 100644\n> > > --- a/builtin/receive-pack.c\n> > > +++ b/builtin/receive-pack.c\n> > > @@ -2326,7 +2323,8 @@ static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n> > >  \t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n> > >  }\n> > >  \n> > > -static const char *unpack(int err_fd, struct shallow_info *si)\n> > > +static const char *unpack(int err_fd, struct shallow_info *si,\n> > > +\t\t\t  struct odb_transaction *transaction)\n> > >  {\n> > >  \tstruct pack_header hdr;\n> > >  \tconst char *hdr_err;\n> > \n> > It feels a bit weird that we sometimes pass the transaction as\n> > parameter, whereas othertimes we access it via `the_repository`.\n> \n> That's fair. I was trying to avoid the churn of wiring to all its\n> callsites, but it's probably best to be consistent. Maybe it would be\n> fine to just create a transaction global like we do for the reference\n> transaction?\n\nMy first kneejerk reaction was \"no\", but then I noticed that the global\nvariable you're talking about is local to \"builtin/receive-pack.c\". So\nthat might be an okayish solution.\n\nThanks!\n\nPatrick\n"},{"id":"546742","messageId":"akOCODgg02A-2Bys@pks.im","threadId":"65861","inReplyTo":"akK1roQJknYstX0u@denethor","subject":"Re: [PATCH 2/6] object-file: propagate files transaction errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-30T08:45:44Z","receivedAt":"2026-06-30T08:45:48Z","isPatch":true,"body":"On Mon, Jun 29, 2026 at 01:58:54PM -0500, Justin Tobler wrote:\n> On 26/06/24 01:26PM, Patrick Steinhardt wrote:\n> > On Tue, Jun 23, 2026 at 11:19:16PM -0500, Justin Tobler wrote:\n> > > diff --git a/object-file.c b/object-file.c\n> > > index a3eb8d71dd..18c2df75fb 100644\n> > > --- a/object-file.c\n> > > +++ b/object-file.c\n> > > @@ -499,7 +499,7 @@ struct odb_transaction_files {\n> > >  \tstruct transaction_packfile packfile;\n> > >  };\n> > >  \n> > > -static void odb_transaction_files_prepare(struct odb_transaction *base)\n> > > +static 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> > \n> > By the way, is there any reason why those functions are still hosted in\n> > \"object-file.c\" instead of in \"odb/source-files.c\"? I should probably\n> > know, but I forgot.\n> \n> There are currently a couple spots in the \"files\" object write path in\n> \"object-file.c\" that still reach into some of these transaction function\n> that are not part of the generic ODB transaction interface. I'm hoping\n> in a future followup series to detangle this a bit and be able to get\n> all the \"files\" ODB transaction related code moved into\n> \"odb/source-files.c\".\n\nMakes sense. I also revisited that code a couple days ago, and the\nanswer is \"it's messy right now\". Hopefully this will become easier to\ndetangle as we make progress on pluggifying transactions.\n\n> > > @@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n> > >  \t * added at the time they call odb_transaction_files_begin.\n> > >  \t */\n> > >  \tif (!transaction || transaction->objdir)\n> > > -\t\treturn;\n> > > +\t\treturn 0;\n> > >  \n> > >  \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n> > > -\tif (transaction->objdir)\n> > > -\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n> > > +\tif (!transaction->objdir)\n> > > +\t\treturn -1;\n> > \n> > Huh. So previously we just didn't handle this error at all and just\n> > continued to tag along? Did that result in anything sensible or was this\n> > just YOLOing it?\n> \n> Good question. Previously if there was an error, we wouldn't end up\n> creating any tmpdir and would instead continue to use the primary ODB to\n> write objects in. This change would make it a hard error if we fail to\n> create the temp dir. This matches the behavior that git-receive-pack(1)\n> expects, but I didn't consider that the existing callers could\n> transparently handle there being no temp dir.\n> \n> I suspect we may want existing ODB transaction users to continue being\n> resilient in the same manner. In the next version, I'll maintain the\n> same behavior.\n\nHonestly I'd say that the change is a good one. I cannot think of a\nsingle reason to just blindly not create the transaction and proceed.\nBut it certainly is something that should be documented as part of the\ncommit message.\n\n> > > @@ -1670,27 +1678,34 @@ int read_loose_object(struct repository *repo,\n> > >  \treturn ret;\n> > >  }\n> > >  \n> > > -static void odb_transaction_files_commit(struct odb_transaction *base)\n> > > +static int odb_transaction_files_commit(struct odb_transaction *base)\n> > >  {\n> > >  \tstruct odb_transaction_files *transaction =\n> > >  \t\tcontainer_of(base, struct odb_transaction_files, base);\n> > >  \n> > > -\tflush_loose_object_transaction(transaction);\n> > > +\tif (flush_loose_object_transaction(transaction))\n> > > +\t\treturn -1;\n> > >  \tflush_packfile_transaction(transaction);\n> > > +\n> > > +\treturn 0;\n> > >  }\n> > >  \n> > > -struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n> > > +int odb_transaction_files_begin(struct odb_source *source,\n> > > +\t\t\t\tstruct odb_transaction **out)\n> > >  {\n> > >  \tstruct odb_transaction_files *transaction;\n> > >  \tstruct object_database *odb = source->odb;\n> > >  \n> > > -\tif (odb->transaction)\n> > > -\t\treturn NULL;\n> > > +\tif (odb->transaction) {\n> > > +\t\t*out = NULL;\n> > > +\t\treturn 0;\n> > > +\t}\n> > >  \n> > >  \ttransaction = xcalloc(1, sizeof(*transaction));\n> > >  \ttransaction->base.source = source;\n> > >  \ttransaction->base.commit = odb_transaction_files_commit;\n> > >  \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n> > > +\t*out = &transaction->base;\n> > >  \n> > > -\treturn &transaction->base;\n> > > +\treturn 0;\n> > >  }\n> > \n> > It's still somewhat fishy that we have this ODB-level transaction, but\n> > that's a preexisting issue and thus outside the scope of this patch\n> > series. Ideally though, it would be possible for there to be multiple\n> > transactions, and it would be the caller's responsibility for juggling\n> > these transactions. Just as it happens with reference transactions.\n> \n> I completely agree. One of the current problems preventing this is that\n> only a single instance of tmp_objdir is allowed and stored globally.\n> This is done to keep atexit() cleanup simple.\n> \n> My plan is to eventually convert all existing tmp_objdir callsites to\n> use ODB transactions which should hopefully allow us to remove the need\n> for a separate tmp_objdir system. At that point, we can also work to\n> change how temp dir cleanup is handled at exit.\n\nGreat.\n\n> Another problem is that there are also a couple of ODB transaction\n> callsites that require to know if there is already a pending transaction\n> for the repository and the transaction has not been wired down to these\n> callsites. My hope is that this can be addressed though as we expand ODB\n> transaction usage for object writes.\n\nYeah, agreed. Making the use of transactions explicit feels sensible to\nme.\n\nPatrick\n"},{"id":"546743","messageId":"akOCPk55yi3lerL-@pks.im","threadId":"65861","inReplyTo":"akLBFaTfBEq8vHUr@denethor","subject":"Re: [PATCH 2/6] object-file: propagate files transaction errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-30T08:45:50Z","receivedAt":"2026-06-30T08:45:55Z","isPatch":true,"body":"On Mon, Jun 29, 2026 at 02:04:08PM -0500, Justin Tobler wrote:\n> On 26/06/29 01:58PM, Justin Tobler wrote:\n> > On 26/06/24 01:26PM, Patrick Steinhardt wrote:\n> > > On Tue, Jun 23, 2026 at 11:19:16PM -0500, Justin Tobler wrote:\n> > > > @@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n> > > >  \t * added at the time they call odb_transaction_files_begin.\n> > > >  \t */\n> > > >  \tif (!transaction || transaction->objdir)\n> > > > -\t\treturn;\n> > > > +\t\treturn 0;\n> > > >  \n> > > >  \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n> > > > -\tif (transaction->objdir)\n> > > > -\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n> > > > +\tif (!transaction->objdir)\n> > > > +\t\treturn -1;\n> > > \n> > > Huh. So previously we just didn't handle this error at all and just\n> > > continued to tag along? Did that result in anything sensible or was this\n> > > just YOLOing it?\n> > \n> > Good question. Previously if there was an error, we wouldn't end up\n> > creating any tmpdir and would instead continue to use the primary ODB to\n> > write objects in. This change would make it a hard error if we fail to\n> > create the temp dir. This matches the behavior that git-receive-pack(1)\n> > expects, but I didn't consider that the existing callers could\n> > transparently handle there being no temp dir.\n> > \n> > I suspect we may want existing ODB transaction users to continue being\n> > resilient in the same manner. In the next version, I'll maintain the\n> > same behavior.\n> \n> I think I got a bit ahead of myself. The existing callers of\n> odb_transaction_files_prepare() still continue to ignore this error. So\n> the behavior already does remain the same here.\n\nOh, well, okay. I think this behaviour is plain bad -- if the caller\nwants to have a transaction, then we should bail in case we cannot\ncreate one. But this doesn't need to be fixed in this patch series.\n\nPatrick\n"},{"id":"546783","messageId":"akPOZMCq8G_DGl0h@denethor","threadId":"65861","inReplyTo":"akOCPk55yi3lerL-@pks.im","subject":"Re: [PATCH 2/6] object-file: propagate files transaction errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-06-30T14:14:47Z","receivedAt":"2026-06-30T14:14:54Z","isPatch":true,"body":"On 26/06/30 10:45AM, Patrick Steinhardt wrote:\n> On Mon, Jun 29, 2026 at 02:04:08PM -0500, Justin Tobler wrote:\n> > On 26/06/29 01:58PM, Justin Tobler wrote:\n> > > On 26/06/24 01:26PM, Patrick Steinhardt wrote:\n> > > > On Tue, Jun 23, 2026 at 11:19:16PM -0500, Justin Tobler wrote:\n> > > > > @@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n> > > > >  \t * added at the time they call odb_transaction_files_begin.\n> > > > >  \t */\n> > > > >  \tif (!transaction || transaction->objdir)\n> > > > > -\t\treturn;\n> > > > > +\t\treturn 0;\n> > > > >  \n> > > > >  \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n> > > > > -\tif (transaction->objdir)\n> > > > > -\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n> > > > > +\tif (!transaction->objdir)\n> > > > > +\t\treturn -1;\n> > > > \n> > > > Huh. So previously we just didn't handle this error at all and just\n> > > > continued to tag along? Did that result in anything sensible or was this\n> > > > just YOLOing it?\n> > > \n> > > Good question. Previously if there was an error, we wouldn't end up\n> > > creating any tmpdir and would instead continue to use the primary ODB to\n> > > write objects in. This change would make it a hard error if we fail to\n> > > create the temp dir. This matches the behavior that git-receive-pack(1)\n> > > expects, but I didn't consider that the existing callers could\n> > > transparently handle there being no temp dir.\n> > > \n> > > I suspect we may want existing ODB transaction users to continue being\n> > > resilient in the same manner. In the next version, I'll maintain the\n> > > same behavior.\n> > \n> > I think I got a bit ahead of myself. The existing callers of\n> > odb_transaction_files_prepare() still continue to ignore this error. So\n> > the behavior already does remain the same here.\n> \n> Oh, well, okay. I think this behaviour is plain bad -- if the caller\n> wants to have a transaction, then we should bail in case we cannot\n> create one. But this doesn't need to be fixed in this patch series.\n\nYa, I tend to agree. The problem here is that\nodb_transaction_files_prepare() is being invoked lazily during the\nobject write. This would be another argument against lazily creating the\ntemporary directory though. In a followup series I'll explore removing\nthis and investigate if it has any meaninful performance implications.\n\n-Justin\n"},{"id":"547445","messageId":"20260708041412.1157499-2-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v2 01/11] object-file: rename files transaction prepare function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:02Z","receivedAt":"2026-07-08T04:14:20Z","isPatch":true,"body":"The \"files\" ODB transaction backend lazily creates a temporary object\ndirectory when the first loose object is written to the transaction via\n`prepare_loose_object_transaction()`. In a subsequent commit, the\ntemporary directory is used to also write packfiles to.\n\nRename the function to `odb_transaction_files_prepare()` accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex e3d92bbda2..a3eb8d71dd 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -499,7 +499,7 @@ struct odb_transaction_files {\n \tstruct transaction_packfile packfile;\n };\n \n-static void prepare_loose_object_transaction(struct odb_transaction *base)\n+static void 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@@ -761,7 +761,7 @@ int write_loose_object(struct odb_source_loose *loose,\n \tstatic struct strbuf filename = STRBUF_INIT;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\n+\t\todb_transaction_files_prepare(loose->base.odb->transaction);\n \n \todb_loose_path(loose, &filename, oid);\n \n@@ -825,7 +825,7 @@ int odb_source_loose_write_stream(struct odb_source_loose *loose,\n \tint hdrlen;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\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-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547446","messageId":"20260708041412.1157499-1-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260624041920.2601961-1-jltobler@gmail.com","subject":"[PATCH v2 00/11] receive-pack: use ODB transactions to stage object writes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:01Z","receivedAt":"2026-07-08T04:14:20Z","isPatch":true,"body":"Greetings,\n\nThis patch series replaces direct usage of the `tmp_objdir` interfaces\nin git-receive-pack(1) to instead use the `odb_transaction` interfaces\nto create/manage a staging area to write objects to. The purpose of this\nchange is to get git-receive-pack(1) one step closer to being ODB\nbackend agnostic. For now, the object writes themselves are still\n\"files\" backend specific due to being handled by the git-index-pack(1)\nand git-unpack-objects(1) child processes. This will be tackled in a\nseparate series though.\n\nChanges since V1:\n\n  - Adapted other \"file\" ODB transaction helpers to be more consistent\n    with current naming scheme.\n  - Removed redundant NULL transaction handling from\n    `odb_transaction_files_begin()`.\n  - `odb_transaction_begin()` now returns an error if there is already\n    an inflight transaction pending instead of setting the `out` pointer\n    to NULL.\n  - Updated `odb_transaction_env()` to return an error code and append\n    environment variables to a strvec provided as an argument.\n  - Removed redundant setting of tmpdir environment variables for child\n    processes after tmpdir has been migrated.\n  - Split changes adding ODB transaction flags into a separate commit.\n  - Consistently wire the ODB transaction throughout git-receive-pack\n    code instead of reading it from `the_repository`.\n  - Updated user facing error message.\n  - Updated some comments to better document functions/flags.\n  - Clarified some commit messages.\n  - Fixed typos.\n\nThanks,\n-Justin\n\nJustin Tobler (11):\n  object-file: rename files transaction prepare function\n  object-file: rename files transaction fsync function\n  object-file: embed transaction flush logic in commit function\n  object-file: drop check for inflight transactions\n  object-file: propagate files transaction errors\n  odb/transaction: propagate begin errors\n  odb/transaction: propagate commit errors\n  odb/transaction: add transaction env interface\n  odb/transaction: introduce ODB transaction flags\n  builtin/receive-pack: drop redundant tmpdir env\n  builtin/receive-pack: stage incoming objects via ODB transactions\n\n builtin/add.c            |   2 +-\n builtin/receive-pack.c   |  69 ++++++++---------\n builtin/unpack-objects.c |   2 +-\n builtin/update-index.c   |   2 +-\n cache-tree.c             |   7 +-\n object-file.c            | 159 +++++++++++++++++++++++++--------------\n object-file.h            |   8 +-\n odb/source-files.c       |   9 +--\n odb/source-inmemory.c    |   3 +-\n odb/source-loose.c       |   3 +-\n odb/source.h             |   9 ++-\n odb/transaction.c        |  34 +++++++--\n odb/transaction.h        |  60 ++++++++++++---\n read-cache.c             |   7 +-\n 14 files changed, 244 insertions(+), 130 deletions(-)\n\nRange-diff against v1:\n 1:  9c14b219ad =  1:  9c14b219ad object-file: rename files transaction prepare function\n -:  ---------- >  2:  5703a9e93b object-file: rename files transaction fsync function\n -:  ---------- >  3:  4c37398ac8 object-file: embed transaction flush logic in commit function\n -:  ---------- >  4:  623c6b02ea object-file: drop check for inflight transactions\n 2:  201f543692 !  5:  ca59176657 object-file: propagate files transaction errors\n    @@ Commit message\n         the temporary directory used to stage objects, but silently ignores\n         these errors. Instead return errors encountered in the\n         `odb_transaction_files_{prepare,begin,commit}()` interfaces to allow\n    -    callers to handle as needed.\n    +    callers to handle them as needed.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n    @@ object-file.c: static void odb_transaction_files_prepare(struct odb_transaction\n     +\treturn 0;\n      }\n      \n    - static void fsync_loose_object_transaction(struct odb_transaction *base,\n    -@@ object-file.c: static void fsync_loose_object_transaction(struct odb_transaction *base,\n    - /*\n    -  * Cleanup after batch-mode fsync_object_files.\n    -  */\n    --static void flush_loose_object_transaction(struct odb_transaction_files *transaction)\n    -+static int flush_loose_object_transaction(struct odb_transaction_files *transaction)\n    - {\n    - \tstruct strbuf temp_path = STRBUF_INIT;\n    - \tstruct tempfile *temp;\n    - \n    - \tif (!transaction->objdir)\n    --\t\treturn;\n    -+\t\treturn 0;\n    - \n    - \t/*\n    - \t * Issue a full hardware flush against a temporary file to ensure\n    -@@ object-file.c: static void flush_loose_object_transaction(struct odb_transaction_files *transac\n    - \t * Make the object files visible in the primary ODB after their data is\n    - \t * fully durable.\n    - \t */\n    --\ttmp_objdir_migrate(transaction->objdir);\n    -+\tif (tmp_objdir_migrate(transaction->objdir))\n    -+\t\treturn -1;\n    -+\n    - \ttransaction->objdir = NULL;\n    -+\n    -+\treturn 0;\n    - }\n    - \n    - /* Finalize a file on disk, and close it. */\n    + static void odb_transaction_files_fsync(struct odb_transaction *base,\n     @@ object-file.c: int read_loose_object(struct repository *repo,\n      \treturn ret;\n      }\n    @@ object-file.c: int read_loose_object(struct repository *repo,\n      {\n      \tstruct odb_transaction_files *transaction =\n      \t\tcontainer_of(base, struct odb_transaction_files, base);\n    +@@ object-file.c: static void odb_transaction_files_commit(struct odb_transaction *base)\n    + \t\t * Make the object files visible in the primary ODB after their data is\n    + \t\t * fully durable.\n    + \t\t */\n    +-\t\ttmp_objdir_migrate(transaction->objdir);\n    ++\t\tif (tmp_objdir_migrate(transaction->objdir))\n    ++\t\t\treturn -1;\n    ++\n    + \t\ttransaction->objdir = NULL;\n    + \t}\n      \n    --\tflush_loose_object_transaction(transaction);\n    -+\tif (flush_loose_object_transaction(transaction))\n    -+\t\treturn -1;\n      \tflush_packfile_transaction(transaction);\n     +\n     +\treturn 0;\n    @@ object-file.c: int read_loose_object(struct repository *repo,\n     +\t\t\t\tstruct odb_transaction **out)\n      {\n      \tstruct odb_transaction_files *transaction;\n    - \tstruct object_database *odb = source->odb;\n    - \n    --\tif (odb->transaction)\n    --\t\treturn NULL;\n    -+\tif (odb->transaction) {\n    -+\t\t*out = NULL;\n    -+\t\treturn 0;\n    -+\t}\n      \n    - \ttransaction = xcalloc(1, sizeof(*transaction));\n    +@@ object-file.c: struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n      \ttransaction->base.source = source;\n      \ttransaction->base.commit = odb_transaction_files_commit;\n      \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n    @@ object-file.c: int read_loose_object(struct repository *repo,\n     \n      ## object-file.h ##\n     @@ object-file.h: struct odb_transaction;\n    -  * Tell the object database to optimize for adding\n       * multiple objects. odb_transaction_files_commit must be called\n    -  * to make new objects visible. If a transaction is already\n    -- * pending, NULL is returned.\n    -+ * pending, out is set to NULL.\n    +  * to make new objects visible.\n       */\n     -struct odb_transaction *odb_transaction_files_begin(struct odb_source *source);\n     +int odb_transaction_files_begin(struct odb_source *source,\n    @@ odb/source-files.c: static int odb_source_files_write_object_stream(struct odb_s\n     \n      ## odb/transaction.h ##\n     @@ odb/transaction.h: struct odb_transaction {\n    + \t/* The ODB source the transaction is opened against. */\n      \tstruct odb_source *source;\n      \n    - \t/* The ODB source specific callback invoked to commit a transaction. */\n    +-\t/* The ODB source specific callback invoked to commit a transaction. */\n     -\tvoid (*commit)(struct odb_transaction *transaction);\n    ++\t/*\n    ++\t * The ODB source specific callback invoked to commit a transaction.\n    ++\t * Returns 0 on success, a negative error code otherwise.\n    ++\t */\n     +\tint (*commit)(struct odb_transaction *transaction);\n      \n      \t/*\n 3:  68cdd88ab5 !  6:  717a1ce9a7 odb/transaction: propagate begin errors\n    @@ Commit message\n         convenience for callsites that do not need to handle errors\n         explicitly.\n     \n    +    Note that `odb_transaction_begin()` now returns an error when the ODB\n    +    already has an inflight transaction pending. ODB transaction call sites\n    +    that may encounter an inflight transaction are updated to explicitly\n    +    handle this case.\n    +\n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n      ## builtin/add.c ##\n    @@ builtin/update-index.c: int cmd_update_index(int argc,\n      \t\t\tparseopt_state = parse_options_step(&ctx, options,\n     \n      ## cache-tree.c ##\n    +@@ cache-tree.c: static int update_one(struct cache_tree *it,\n    + \n    + int cache_tree_update(struct index_state *istate, int flags)\n    + {\n    ++\tint inflight = !!the_repository->objects->transaction;\n    + \tstruct odb_transaction *transaction;\n    + \tint skip, i;\n    + \n     @@ cache-tree.c: int cache_tree_update(struct index_state *istate, int flags)\n      \n      \ttrace_performance_enter();\n      \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n     -\ttransaction = odb_transaction_begin(the_repository->objects);\n    -+\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n    ++\tif (!inflight)\n    ++\t\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n      \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n      \t\t       \"\", 0, &skip, flags);\n    - \todb_transaction_commit(transaction);\n    +-\todb_transaction_commit(transaction);\n    ++\tif (!inflight)\n    ++\t\todb_transaction_commit(transaction);\n    + \ttrace2_region_leave(\"cache_tree\", \"update\", istate->repo);\n    + \ttrace_performance_leave(\"cache_tree_update\");\n    + \tif (i < 0)\n     \n      ## object-file.c ##\n     @@ object-file.c: int index_fd(struct index_state *istate, struct object_id *oid,\n    @@ object-file.c: int index_fd(struct index_state *istate, struct object_id *oid,\n      \t\tif (flags & INDEX_WRITE_OBJECT) {\n      \t\t\tstruct object_database *odb = the_repository->objects;\n     -\t\t\tstruct odb_transaction *transaction = odb_transaction_begin(odb);\n    -+\t\t\tstruct odb_transaction *transaction;\n    ++\t\t\tstruct odb_transaction *transaction = odb->transaction;\n    ++\t\t\tint inflight = !!transaction;\n      \n    -+\t\t\todb_transaction_begin_or_die(odb, &transaction);\n    - \t\t\tret = odb_transaction_write_object_stream(odb->transaction,\n    +-\t\t\tret = odb_transaction_write_object_stream(odb->transaction,\n    ++\t\t\tif (!inflight)\n    ++\t\t\t\todb_transaction_begin_or_die(odb, &transaction);\n    ++\t\t\tret = odb_transaction_write_object_stream(transaction,\n      \t\t\t\t\t\t\t\t  &stream,\n      \t\t\t\t\t\t\t\t  xsize_t(st->st_size),\n    + \t\t\t\t\t\t\t\t  oid);\n    +-\t\t\todb_transaction_commit(transaction);\n    ++\t\t\tif (!inflight)\n    ++\t\t\t\todb_transaction_commit(transaction);\n    + \t\t} else {\n    + \t\t\tret = hash_blob_stream(&stream,\n    + \t\t\t\t\t       the_repository->hash_algo, oid,\n     \n      ## odb/transaction.c ##\n     @@\n    @@ odb/transaction.c\n     +int odb_transaction_begin(struct object_database *odb,\n     +\t\t\t  struct odb_transaction **out)\n      {\n    --\tif (odb->transaction)\n    --\t\treturn NULL;\n     +\tint ret;\n    ++\n    + \tif (odb->transaction)\n    +-\t\treturn NULL;\n    ++\t\treturn -1;\n      \n     -\todb_source_begin_transaction(odb->sources, &odb->transaction);\n    -+\tif (odb->transaction) {\n    -+\t\t*out = NULL;\n    -+\t\treturn 0;\n    -+\t}\n    - \n    --\treturn odb->transaction;\n     +\tret = odb_source_begin_transaction(odb->sources, out);\n     +\todb->transaction = *out;\n    -+\n    + \n    +-\treturn odb->transaction;\n     +\treturn ret;\n      }\n      \n    @@ odb/transaction.h: struct odb_transaction {\n     + * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n     + * written to the transaction and not committed until odb_transaction_commit()\n     + * is invoked on the transaction. Returns 0 on success and a negative value on\n    -+ * error. If the ODB already has a pending transaction, `out` is set to NULL.\n    ++ * error. Note that it is considered an error to start a new transaction if the\n    ++ * ODB already has an inflight transaction pending.\n       */\n     -struct odb_transaction *odb_transaction_begin(struct object_database *odb);\n     +int odb_transaction_begin(struct object_database *odb,\n    @@ odb/transaction.h: struct odb_transaction {\n       * Commits an ODB transaction making the written objects visible. If the\n     \n      ## read-cache.c ##\n    +@@ read-cache.c: int add_files_to_cache(struct repository *repo, const char *prefix,\n    + \t\t       const struct pathspec *pathspec, char *ps_matched,\n    + \t\t       int include_sparse, int flags, int ignored_too )\n    + {\n    ++\tint inflight = !!repo->objects->transaction;\n    + \tstruct odb_transaction *transaction;\n    + \tstruct update_callback_data data;\n    + \tstruct rev_info rev;\n     @@ read-cache.c: int add_files_to_cache(struct repository *repo, const char *prefix,\n      \t * This function is invoked from commands other than 'add', which\n      \t * may not have their own transaction active.\n      \t */\n     -\ttransaction = odb_transaction_begin(repo->objects);\n    -+\todb_transaction_begin_or_die(repo->objects, &transaction);\n    ++\tif (!inflight)\n    ++\t\todb_transaction_begin_or_die(repo->objects, &transaction);\n      \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n    - \todb_transaction_commit(transaction);\n    +-\todb_transaction_commit(transaction);\n    ++\tif (!inflight)\n    ++\t\todb_transaction_commit(transaction);\n      \n    + \trelease_revisions(&rev);\n    + \treturn !!data.add_errors;\n -:  ---------- >  7:  ff8e133965 odb/transaction: propagate commit errors\n 5:  82302db9f0 !  8:  264ba94b83 odb/transaction: add transaction env interface\n    @@ Commit message\n     \n         The ODB transaction backend is responsible for creating/managing its own\n         staging area for writing objects. Other child processes spawned by Git\n    -    may need to access to uncommitted objects or write new objects in the\n    +    may need access to uncommitted objects or write new objects in the\n         staging area though.\n     \n         Introduce `odb_transaction_env()` which is expected to provide the set\n         of environment variables needed by a child process to access the\n    -    transaction staging area.\n    +    transaction's staging area.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n      ## object-file.c ##\n    +@@\n    + #include \"path.h\"\n    + #include \"read-cache-ll.h\"\n    + #include \"setup.h\"\n    ++#include \"strvec.h\"\n    + #include \"tempfile.h\"\n    + #include \"tmp-objdir.h\"\n    + \n     @@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *base)\n      \treturn 0;\n      }\n      \n    -+static const char **odb_transaction_files_env(struct odb_transaction *base)\n    ++static int odb_transaction_files_env(struct odb_transaction *base,\n    ++\t\t\t\t     struct strvec *env)\n     +{\n     +\tstruct odb_transaction_files *transaction =\n     +\t\tcontainer_of(base, struct odb_transaction_files, base);\n     +\n     +\todb_transaction_files_prepare(&transaction->base);\n    ++\tstrvec_pushv(env, tmp_objdir_env(transaction->objdir));\n     +\n    -+\treturn tmp_objdir_env(transaction->objdir);\n    ++\treturn 0;\n     +}\n     +\n      int odb_transaction_files_begin(struct odb_source *source,\n    @@ odb/transaction.c: int odb_transaction_write_object_stream(struct odb_transactio\n      \treturn transaction->write_object_stream(transaction, stream, len, oid);\n      }\n     +\n    -+const char **odb_transaction_env(struct odb_transaction *transaction)\n    ++int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env)\n     +{\n     +\tif (!transaction)\n    -+\t\treturn NULL;\n    ++\t\treturn 0;\n     +\n    -+\treturn transaction->env(transaction);\n    ++\treturn transaction->env(transaction, env);\n     +}\n     \n      ## odb/transaction.h ##\n    @@ odb/transaction.h: struct odb_transaction {\n      \t\t\t\t   struct object_id *oid);\n     +\n     +\t/*\n    -+\t * This callback is expected to return a NULL-terminated array of\n    -+\t * environment variables that a child process should inherit so\n    -+\t * that its object writes participate in the transaction. The\n    -+\t * returned array is owned by the backend and remains valid until\n    -+\t * the transaction ends. May return NULL when the backend does not\n    -+\t * need to expose any state to child processes.\n    ++\t * This callback is expected to populate the provided strvec with the\n    ++\t * environment variables that a child process should inherit so that its\n    ++\t * object writes participate in the transaction. Returns 0 on success, a\n    ++\t * negative error code otherwise.\n     +\t */\n    -+\tconst char **(*env)(struct odb_transaction *transaction);\n    ++\tint (*env)(struct odb_transaction *transaction, struct strvec *env);\n      };\n      \n      /*\n    @@ odb/transaction.h: int odb_transaction_write_object_stream(struct odb_transactio\n      \t\t\t\t\tsize_t len, struct object_id *oid);\n      \n     +/*\n    -+ * Returns a NULL-terminated array of environment variables that a child\n    ++ * Populates the provided strvec with the environment variables that a child\n     + * process should inherit so that its object writes participate in the\n    -+ * transaction, suitable for passing via child_process.env. Returns NULL if\n    -+ * the transaction is NULL or the backend does not expose any state to child\n    -+ * processes.\n    ++ * transaction, suitable for using via child_process.env. Returns 0 on success,\n    ++ * a negative error code otherwise. Note that, if the specified transaction is\n    ++ * NULL, the function is a no-op and no error is returned.\n     + */\n    -+const char **odb_transaction_env(struct odb_transaction *transaction);\n    ++int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env);\n     +\n      #endif\n 6:  0674e8a3d3 !  9:  1e0a491ef2 builtin/receive-pack: stage incoming objects via ODB transactions\n    @@ Metadata\n     Author: Justin Tobler <jltobler@gmail.com>\n     \n      ## Commit message ##\n    -    builtin/receive-pack: stage incoming objects via ODB transactions\n    +    odb/transaction: introduce ODB transaction flags\n     \n    -    Objects received by git-receive-pack(1) are quarantined in a temporary\n    -    \"incoming\" directory and migrated into the object database prior to the\n    -    reference updates. The quarantine is currently managed through\n    -    `tmp_objdir` directly. In a pluggable ODB future, how exactly an object\n    -    gets written to a transaction may vary for a given ODB source. Refactor\n    -    git-receive-pack(1) to use the ODB transaction interfaces to manage the\n    -    object staging area in a more agnostic manner accordingly.\n    +    The temporary directory used by git-receive-pack(1) to write objects is\n    +    managed slightly differently than how it is done via ODB transactions:\n     \n    -    Note that the temporary directory created for git-receive-pack(1) is\n    -    eagerly created and uses a different prefix name. This behavior is\n    -    special cased in the \"files\" backend by having `odb_transaction_begin()`\n    -    callers that require this behavior provide an `ODB_TRANSACTION_RECEIVE`\n    -    flag.\n    +      - The temporary directory is eagerly created upfront, instead of\n    +        waiting for the first object write.\n    +\n    +      - The prefix name of the temporary directory is \"incoming\" instead of\n    +        \"bulk-fsync\".\n    +\n    +    In a subsequent commit, git-receive-pack(1) will use ODB transactions\n    +    instead of `tmp_objdir` directly. To provide a means to configure the\n    +    same transaction behavior, introduce `enum odb_transaction_flags` and\n    +    the ODB_TRANSACTION_RECEIVE flag intended as a signal for ODB\n    +    transactions using the \"files\" backend to be set up for\n    +    git-receive-pack(1). Transaction call sites are updated accordingly to\n    +    provide the required flag parameter.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n    @@ builtin/add.c: int cmd_add(int argc,\n      \tps_matched = xcalloc(pathspec.nr, 1);\n      \tif (add_renormalize)\n     \n    - ## builtin/receive-pack.c ##\n    -@@ builtin/receive-pack.c: static enum {\n    - } use_keepalive;\n    - static int keepalive_in_sec = 5;\n    - \n    --static struct tmp_objdir *tmp_objdir;\n    --\n    - static struct proc_receive_ref {\n    - \tunsigned int want_add:1,\n    - \t\t     want_delete:1,\n    -@@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n    - \t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n    - \t}\n    - \n    --\tif (tmp_objdir)\n    --\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n    -+\tif (the_repository->objects->transaction)\n    -+\t\tstrvec_pushv(&opt.env, odb_transaction_env(the_repository->objects->transaction));\n    - \n    - \tprepare_push_cert_sha1(&opt);\n    - \n    -@@ builtin/receive-pack.c: static int update_shallow_ref(struct command *cmd, struct shallow_info *si)\n    - \t\t    !delayed_reachability_test(si, i))\n    - \t\t\toid_array_append(&extra, &si->shallow->oid[i]);\n    - \n    --\topt.env = tmp_objdir_env(tmp_objdir);\n    -+\topt.env = odb_transaction_env(the_repository->objects->transaction);\n    - \tsetup_alternate_shallow(&shallow_lock, &opt.shallow_file, &extra);\n    - \tif (check_connected(command_singleton_iterator, cmd, &opt)) {\n    - \t\trollback_shallow_file(the_repository, &shallow_lock);\n    -@@ builtin/receive-pack.c: static void set_connectivity_errors(struct command *commands,\n    - \t\t\t/* to be checked in update_shallow_ref() */\n    - \t\t\tcontinue;\n    - \n    --\t\topt.env = tmp_objdir_env(tmp_objdir);\n    -+\t\topt.env = odb_transaction_env(the_repository->objects->transaction);\n    - \t\tif (!check_connected(command_singleton_iterator, &singleton,\n    - \t\t\t\t     &opt))\n    - \t\t\tcontinue;\n    -@@ builtin/receive-pack.c: static void execute_commands(struct command *commands,\n    - \t\tdata.si = si;\n    - \t\topt.err_fd = err_fd;\n    - \t\topt.progress = err_fd && !quiet;\n    --\t\topt.env = tmp_objdir_env(tmp_objdir);\n    -+\t\topt.env = odb_transaction_env(the_repository->objects->transaction);\n    - \t\topt.exclude_hidden_refs_section = \"receive\";\n    - \n    - \t\tif (check_connected(iterate_receive_command_list, &data, &opt))\n    -@@ builtin/receive-pack.c: static void execute_commands(struct command *commands,\n    - \t * Now we'll start writing out refs, which means the objects need\n    - \t * to be in their final positions so that other processes can see them.\n    - \t */\n    --\tif (tmp_objdir_migrate(tmp_objdir) < 0) {\n    -+\tif (odb_transaction_commit(the_repository->objects->transaction)) {\n    - \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n    - \t\t\tif (!cmd->error_string)\n    - \t\t\t\tcmd->error_string = \"unable to migrate objects to permanent storage\";\n    - \t\t}\n    - \t\treturn;\n    - \t}\n    --\ttmp_objdir = NULL;\n    - \n    - \tcheck_aliased_updates(commands);\n    - \n    -@@ builtin/receive-pack.c: static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n    - \t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n    - }\n    - \n    --static const char *unpack(int err_fd, struct shallow_info *si)\n    -+static const char *unpack(int err_fd, struct shallow_info *si,\n    -+\t\t\t  struct odb_transaction *transaction)\n    - {\n    - \tstruct pack_header hdr;\n    - \tconst char *hdr_err;\n    -@@ builtin/receive-pack.c: static const char *unpack(int err_fd, struct shallow_info *si)\n    - \t\tstrvec_push(&child.args, alt_shallow_file);\n    - \t}\n    - \n    --\ttmp_objdir = tmp_objdir_create(the_repository, \"incoming\");\n    --\tif (!tmp_objdir) {\n    --\t\tif (err_fd > 0)\n    --\t\t\tclose(err_fd);\n    --\t\treturn \"unable to create temporary object directory\";\n    --\t}\n    --\tstrvec_pushv(&child.env, tmp_objdir_env(tmp_objdir));\n    --\n    --\t/*\n    --\t * Normally we just pass the tmp_objdir environment to the child\n    --\t * processes that do the heavy lifting, but we may need to see these\n    --\t * objects ourselves to set up shallow information.\n    --\t */\n    --\ttmp_objdir_add_as_alternate(tmp_objdir);\n    -+\tstrvec_pushv(&child.env, odb_transaction_env(transaction));\n    - \n    - \tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n    - \t\tstrvec_push(&child.args, \"unpack-objects\");\n    -@@ builtin/receive-pack.c: static const char *unpack(int err_fd, struct shallow_info *si)\n    - \treturn NULL;\n    - }\n    - \n    --static const char *unpack_with_sideband(struct shallow_info *si)\n    -+static const char *unpack_with_sideband(struct shallow_info *si,\n    -+\t\t\t\t\tstruct odb_transaction *transaction)\n    - {\n    - \tstruct async muxer;\n    - \tconst char *ret;\n    - \n    - \tif (!use_sideband)\n    --\t\treturn unpack(0, si);\n    -+\t\treturn unpack(0, si, transaction);\n    - \n    - \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n    - \tmemset(&muxer, 0, sizeof(muxer));\n    -@@ builtin/receive-pack.c: static const char *unpack_with_sideband(struct shallow_info *si)\n    - \tif (start_async(&muxer))\n    - \t\treturn NULL;\n    - \n    --\tret = unpack(muxer.in, si);\n    -+\tret = unpack(muxer.in, si, transaction);\n    - \n    - \tfinish_async(&muxer);\n    - \treturn ret;\n    -@@ builtin/receive-pack.c: int cmd_receive_pack(int argc,\n    - \tstruct oid_array ref = OID_ARRAY_INIT;\n    - \tstruct shallow_info si;\n    - \tstruct packet_reader reader;\n    -+\tstruct odb_transaction *transaction = NULL;\n    - \n    - \tstruct option options[] = {\n    - \t\tOPT__QUIET(&quiet, N_(\"quiet\")),\n    -@@ builtin/receive-pack.c: int cmd_receive_pack(int argc,\n    - \t\tif (!si.nr_ours && !si.nr_theirs)\n    - \t\t\tshallow_update = 0;\n    - \t\tif (!delete_only(commands)) {\n    --\t\t\tunpack_status = unpack_with_sideband(&si);\n    -+\t\t\tif (odb_transaction_begin(the_repository->objects, &transaction, ODB_TRANSACTION_RECEIVE))\n    -+\t\t\t\tunpack_status = \"unable to start ODB transaction\";\n    -+\t\t\telse\n    -+\t\t\t\tunpack_status = unpack_with_sideband(&si, transaction);\n    - \t\t\tupdate_shallow_info(commands, &si, &ref);\n    - \t\t}\n    - \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n    -\n      ## builtin/unpack-objects.c ##\n     @@ builtin/unpack-objects.c: static void unpack_all(void)\n      \t\tprogress = start_progress(the_repository,\n    @@ builtin/update-index.c: int cmd_update_index(int argc,\n     \n      ## cache-tree.c ##\n     @@ cache-tree.c: int cache_tree_update(struct index_state *istate, int flags)\n    - \n      \ttrace_performance_enter();\n      \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n    --\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n    -+\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n    + \tif (!inflight)\n    +-\t\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n    ++\t\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n      \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n      \t\t       \"\", 0, &skip, flags);\n    - \todb_transaction_commit(transaction);\n    + \tif (!inflight)\n     \n      ## object-file.c ##\n     @@ object-file.c: struct odb_transaction_files {\n    @@ object-file.c: static int odb_transaction_files_prepare(struct odb_transaction *\n      \t\treturn -1;\n      \n     @@ object-file.c: int index_fd(struct index_state *istate, struct object_id *oid,\n    - \t\t\tstruct object_database *odb = the_repository->objects;\n    - \t\t\tstruct odb_transaction *transaction;\n    + \t\t\tint inflight = !!transaction;\n      \n    --\t\t\todb_transaction_begin_or_die(odb, &transaction);\n    -+\t\t\todb_transaction_begin_or_die(odb, &transaction, 0);\n    - \t\t\tret = odb_transaction_write_object_stream(odb->transaction,\n    + \t\t\tif (!inflight)\n    +-\t\t\t\todb_transaction_begin_or_die(odb, &transaction);\n    ++\t\t\t\todb_transaction_begin_or_die(odb, &transaction, 0);\n    + \t\t\tret = odb_transaction_write_object_stream(transaction,\n      \t\t\t\t\t\t\t\t  &stream,\n      \t\t\t\t\t\t\t\t  xsize_t(st->st_size),\n    -@@ object-file.c: static const char **odb_transaction_files_env(struct odb_transaction *base)\n    +@@ object-file.c: static int odb_transaction_files_env(struct odb_transaction *base,\n      }\n      \n      int odb_transaction_files_begin(struct odb_source *source,\n    @@ object-file.c: static const char **odb_transaction_files_env(struct odb_transact\n     +\t\t\t\tenum odb_transaction_flags flags)\n      {\n      \tstruct odb_transaction_files *transaction;\n    - \tstruct object_database *odb = source->odb;\n    + \n     @@ object-file.c: int odb_transaction_files_begin(struct odb_source *source,\n      \ttransaction->base.commit = odb_transaction_files_commit;\n      \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n    @@ object-file.c: int odb_transaction_files_begin(struct odb_source *source,\n     +\tif (flags & ODB_TRANSACTION_RECEIVE) {\n     +\t\t/*\n     +\t\t * ODB transactions for git-receive-pack(1) eagerly create a\n    -+\t\t * temporary directory and use a different prefix.\n    ++\t\t * temporary directory and use a different temporary directory\n    ++\t\t * prefix.\n    ++\t\t *\n    ++\t\t * NEEDSWORK: This transaction flag is only used by the \"files\"\n    ++\t\t * backend to special case temporary directory set up and\n    ++\t\t * handling. Ideally transaction users should not have to care\n    ++\t\t * though. To avoid this, we could eagerly create the temporary\n    ++\t\t * directory and use the same prefix name for all transactions.\n     +\t\t */\n     +\t\ttransaction->prefix = \"incoming\";\n     +\t\tif (odb_transaction_files_prepare(&transaction->base)) {\n    @@ object-file.h\n      /* The maximum size for an object header. */\n      #define MAX_HEADER_LEN 32\n     @@ object-file.h: struct odb_transaction;\n    -  * pending, out is set to NULL.\n    +  * to make new objects visible.\n       */\n      int odb_transaction_files_begin(struct odb_source *source,\n     -\t\t\t\tstruct odb_transaction **out);\n    @@ odb/transaction.c\n      {\n      \tint ret;\n      \n    -@@ odb/transaction.c: int odb_transaction_begin(struct object_database *odb,\n    - \t\treturn 0;\n    - \t}\n    + \tif (odb->transaction)\n    + \t\treturn -1;\n      \n     -\tret = odb_source_begin_transaction(odb->sources, out);\n     +\tret = odb_source_begin_transaction(odb->sources, out, flags);\n    @@ odb/transaction.h\n      /*\n       * A transaction may be started for an object database prior to writing new\n     @@ odb/transaction.h: struct odb_transaction {\n    - \tconst char **(*env)(struct odb_transaction *transaction);\n    + \tint (*env)(struct odb_transaction *transaction, struct strvec *env);\n      };\n      \n    ++/* Flags used to configure an ODB transaction. */\n     +enum odb_transaction_flags {\n    ++\t/* Configures the transaction for use with git-receive-pack(1). */\n     +\tODB_TRANSACTION_RECEIVE = (1 << 0),\n     +};\n     +\n    @@ odb/transaction.h: struct odb_transaction {\n       * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n       * written to the transaction and not committed until odb_transaction_commit()\n     @@ odb/transaction.h: struct odb_transaction {\n    -  * error. If the ODB already has a pending transaction, `out` is set to NULL.\n    +  * ODB already has an inflight transaction pending.\n       */\n      int odb_transaction_begin(struct object_database *odb,\n     -\t\t\t  struct odb_transaction **out);\n    @@ odb/transaction.h: struct odb_transaction {\n     \n      ## read-cache.c ##\n     @@ read-cache.c: int add_files_to_cache(struct repository *repo, const char *prefix,\n    - \t * This function is invoked from commands other than 'add', which\n      \t * may not have their own transaction active.\n      \t */\n    --\todb_transaction_begin_or_die(repo->objects, &transaction);\n    -+\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n    + \tif (!inflight)\n    +-\t\todb_transaction_begin_or_die(repo->objects, &transaction);\n    ++\t\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n      \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n    - \todb_transaction_commit(transaction);\n    - \n    + \tif (!inflight)\n    + \t\todb_transaction_commit(transaction);\n -:  ---------- > 10:  6c8d878349 builtin/receive-pack: drop redundant tmpdir env\n 4:  e9303f9b08 ! 11:  8db95fef56 odb/transaction: propagate commit errors\n    @@ Metadata\n     Author: Justin Tobler <jltobler@gmail.com>\n     \n      ## Commit message ##\n    -    odb/transaction: propagate commit errors\n    +    builtin/receive-pack: stage incoming objects via ODB transactions\n     \n    -    When `odb_transaction_commit()` is invoked, the return value of the\n    -    backend commit callback is silently discarded. A backend has no way\n    -    to signal that committing failed, such as when the \"files\" backend\n    -    cannot migrate its temporary object directory into the permanent\n    -    ODB.\n    +    Objects received by git-receive-pack(1) are quarantined in a temporary\n    +    \"incoming\" directory and migrated into the object database prior to the\n    +    reference updates. The quarantine is currently managed through\n    +    `tmp_objdir` directly. In a pluggable ODB future, how exactly an object\n    +    gets written to a transaction may vary for a given ODB source. Refactor\n    +    git-receive-pack(1) to use the ODB transaction interfaces to manage the\n    +    object staging area in a more agnostic manner accordingly.\n     \n    -    In a subsequent commit, git-receive-pack(1) starts using ODB transaction\n    -    to stage objects and consequently cares about such failures so it can\n    -    handle the error appropriately. Change the commit callback signature to\n    -    return an int error code and have `odb_transaction_commit()` forward it\n    -    accordingly.\n    +    Note that the ODB transaction is now responsible for managing the\n    +    primary and alternate ODBs for the repository. One small change as a\n    +    result is that the temporary directory is now applied as the primary ODB\n    +    in the main process instead of an alternate. This does not change\n    +    anything for git-receive-pack(1) though because it only needs access to\n    +    the newly written objects and doesn't care how exactly it is set up.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n    - ## odb/transaction.c ##\n    -@@ odb/transaction.c: int odb_transaction_begin(struct object_database *odb,\n    - \treturn ret;\n    + ## builtin/receive-pack.c ##\n    +@@\n    + #include \"sigchain.h\"\n    + #include \"string-list.h\"\n    + #include \"strvec.h\"\n    +-#include \"tmp-objdir.h\"\n    + #include \"trace.h\"\n    + #include \"trace2.h\"\n    + #include \"version.h\"\n    +@@ builtin/receive-pack.c: static enum {\n    + } use_keepalive;\n    + static int keepalive_in_sec = 5;\n    + \n    +-static struct tmp_objdir *tmp_objdir;\n    +-\n    + static struct proc_receive_ref {\n    + \tunsigned int want_add:1,\n    + \t\t     want_delete:1,\n    +@@ builtin/receive-pack.c: static void receive_hook_feed_state_free(void *data)\n    + static int run_receive_hook(struct command *commands,\n    + \t\t\t    const char *hook_name,\n    + \t\t\t    int skip_broken,\n    ++\t\t\t    struct odb_transaction *transaction,\n    + \t\t\t    const struct string_list *push_options)\n    + {\n    + \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    +@@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n    + \t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n    + \t}\n    + \n    +-\tif (tmp_objdir)\n    +-\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n    ++\tif (transaction)\n    ++\t\todb_transaction_env(transaction, &opt.env);\n    + \n    + \tprepare_push_cert_sha1(&opt);\n    + \n    +@@ builtin/receive-pack.c: static const struct object_id *command_singleton_iterator(void *cb_data)\n      }\n      \n    --void odb_transaction_commit(struct odb_transaction *transaction)\n    -+int odb_transaction_commit(struct odb_transaction *transaction)\n    + static void set_connectivity_errors(struct command *commands,\n    +-\t\t\t\t    struct shallow_info *si)\n    ++\t\t\t\t    struct shallow_info *si,\n    ++\t\t\t\t    struct odb_transaction *transaction)\n      {\n    -+\tint ret;\n    + \tstruct command *cmd;\n    + \n    + \tfor (cmd = commands; cmd; cmd = cmd->next) {\n    + \t\tstruct command *singleton = cmd;\n    + \t\tstruct check_connected_options opt = CHECK_CONNECTED_INIT;\n    ++\t\tstruct strvec env = STRVEC_INIT;\n    + \n    + \t\tif (shallow_update && si->shallow_ref[cmd->index])\n    + \t\t\t/* to be checked in update_shallow_ref() */\n    + \t\t\tcontinue;\n    + \n    +-\t\topt.env = tmp_objdir_env(tmp_objdir);\n    ++\t\todb_transaction_env(transaction, &env);\n    ++\t\topt.env = env.v;\n     +\n    - \tif (!transaction)\n    --\t\treturn;\n    -+\t\treturn 0;\n    + \t\tif (!check_connected(command_singleton_iterator, &singleton,\n    + \t\t\t\t     &opt))\n    + \t\t\tcontinue;\n      \n    - \t/*\n    - \t * Ensure the transaction ending matches the pending transaction.\n    - \t */\n    - \tASSERT(transaction == transaction->source->odb->transaction);\n    + \t\tcmd->error_string = \"missing necessary objects\";\n    ++\n    ++\t\tstrvec_clear(&env);\n    + \t}\n    + }\n    + \n    +@@ builtin/receive-pack.c: static void execute_commands_atomic(struct command *commands,\n    + static void execute_commands(struct command *commands,\n    + \t\t\t     const char *unpacker_error,\n    + \t\t\t     struct shallow_info *si,\n    ++\t\t\t     struct odb_transaction *transaction,\n    + \t\t\t     const struct string_list *push_options)\n    + {\n    + \tstruct check_connected_options opt = CHECK_CONNECTED_INIT;\n    +@@ builtin/receive-pack.c: static void execute_commands(struct command *commands,\n    + \t}\n      \n    --\ttransaction->commit(transaction);\n    -+\tret = transaction->commit(transaction);\n    -+\tif (ret)\n    -+\t\treturn ret;\n    + \tif (!skip_connectivity_check) {\n    ++\t\tstruct strvec env = STRVEC_INIT;\n     +\n    - \ttransaction->source->odb->transaction = NULL;\n    - \tfree(transaction);\n    + \t\tif (use_sideband) {\n    + \t\t\tmemset(&muxer, 0, sizeof(muxer));\n    + \t\t\tmuxer.proc = copy_to_sideband;\n    +@@ builtin/receive-pack.c: static void execute_commands(struct command *commands,\n    + \t\tdata.si = si;\n    + \t\topt.err_fd = err_fd;\n    + \t\topt.progress = err_fd && !quiet;\n    +-\t\topt.env = tmp_objdir_env(tmp_objdir);\n    ++\t\todb_transaction_env(transaction, &env);\n    ++\t\topt.env = env.v;\n    + \t\topt.exclude_hidden_refs_section = \"receive\";\n    + \n    + \t\tif (check_connected(iterate_receive_command_list, &data, &opt))\n    +-\t\t\tset_connectivity_errors(commands, si);\n    ++\t\t\tset_connectivity_errors(commands, si, transaction);\n    + \n    + \t\tif (use_sideband)\n    + \t\t\tfinish_async(&muxer);\n     +\n    -+\treturn 0;\n    ++\t\tstrvec_clear(&env);\n    + \t}\n    + \n    + \treject_updates_to_hidden(commands);\n    +@@ builtin/receive-pack.c: static void execute_commands(struct command *commands,\n    + \t\t}\n    + \t}\n    + \n    +-\tif (run_receive_hook(commands, \"pre-receive\", 0, push_options)) {\n    ++\tif (run_receive_hook(commands, \"pre-receive\", 0, transaction, push_options)) {\n    + \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n    + \t\t\tif (!cmd->error_string)\n    + \t\t\t\tcmd->error_string = \"pre-receive hook declined\";\n    +@@ builtin/receive-pack.c: static void execute_commands(struct command *commands,\n    + \t * Now we'll start writing out refs, which means the objects need\n    + \t * to be in their final positions so that other processes can see them.\n    + \t */\n    +-\tif (tmp_objdir_migrate(tmp_objdir) < 0) {\n    ++\tif (odb_transaction_commit(transaction)) {\n    + \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n    + \t\t\tif (!cmd->error_string)\n    + \t\t\t\tcmd->error_string = \"unable to migrate objects to permanent storage\";\n    + \t\t}\n    + \t\treturn;\n    + \t}\n    +-\ttmp_objdir = NULL;\n    + \n    + \tcheck_aliased_updates(commands);\n    + \n    +@@ builtin/receive-pack.c: static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n    + \t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n      }\n      \n    - int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n    -\n    - ## odb/transaction.h ##\n    -@@ odb/transaction.h: static inline void odb_transaction_begin_or_die(struct object_database *odb,\n    -  * Commits an ODB transaction making the written objects visible. If the\n    -  * specified transaction is NULL, the function is a no-op.\n    -  */\n    --void odb_transaction_commit(struct odb_transaction *transaction);\n    -+int odb_transaction_commit(struct odb_transaction *transaction);\n    - \n    - /*\n    -  * Writes the object in the provided stream into the transaction. The resulting\n    +-static const char *unpack(int err_fd, struct shallow_info *si)\n    ++static const char *unpack(int err_fd, struct shallow_info *si,\n    ++\t\t\t  struct odb_transaction *transaction)\n    + {\n    + \tstruct pack_header hdr;\n    + \tconst char *hdr_err;\n    +@@ builtin/receive-pack.c: static const char *unpack(int err_fd, struct shallow_info *si)\n    + \t\tstrvec_push(&child.args, alt_shallow_file);\n    + \t}\n    + \n    +-\ttmp_objdir = tmp_objdir_create(the_repository, \"incoming\");\n    +-\tif (!tmp_objdir) {\n    +-\t\tif (err_fd > 0)\n    +-\t\t\tclose(err_fd);\n    +-\t\treturn \"unable to create temporary object directory\";\n    +-\t}\n    +-\tstrvec_pushv(&child.env, tmp_objdir_env(tmp_objdir));\n    +-\n    +-\t/*\n    +-\t * Normally we just pass the tmp_objdir environment to the child\n    +-\t * processes that do the heavy lifting, but we may need to see these\n    +-\t * objects ourselves to set up shallow information.\n    +-\t */\n    +-\ttmp_objdir_add_as_alternate(tmp_objdir);\n    ++\todb_transaction_env(transaction, &child.env);\n    + \n    + \tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n    + \t\tstrvec_push(&child.args, \"unpack-objects\");\n    +@@ builtin/receive-pack.c: static const char *unpack(int err_fd, struct shallow_info *si)\n    + \treturn NULL;\n    + }\n    + \n    +-static const char *unpack_with_sideband(struct shallow_info *si)\n    ++static const char *unpack_with_sideband(struct shallow_info *si,\n    ++\t\t\t\t\tstruct odb_transaction *transaction)\n    + {\n    + \tstruct async muxer;\n    + \tconst char *ret;\n    + \n    + \tif (!use_sideband)\n    +-\t\treturn unpack(0, si);\n    ++\t\treturn unpack(0, si, transaction);\n    + \n    + \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n    + \tmemset(&muxer, 0, sizeof(muxer));\n    +@@ builtin/receive-pack.c: static const char *unpack_with_sideband(struct shallow_info *si)\n    + \tif (start_async(&muxer))\n    + \t\treturn NULL;\n    + \n    +-\tret = unpack(muxer.in, si);\n    ++\tret = unpack(muxer.in, si, transaction);\n    + \n    + \tfinish_async(&muxer);\n    + \treturn ret;\n    +@@ builtin/receive-pack.c: int cmd_receive_pack(int argc,\n    + \tstruct oid_array ref = OID_ARRAY_INIT;\n    + \tstruct shallow_info si;\n    + \tstruct packet_reader reader;\n    ++\tstruct odb_transaction *transaction = NULL;\n    + \n    + \tstruct option options[] = {\n    + \t\tOPT__QUIET(&quiet, N_(\"quiet\")),\n    +@@ builtin/receive-pack.c: int cmd_receive_pack(int argc,\n    + \t\tif (!si.nr_ours && !si.nr_theirs)\n    + \t\t\tshallow_update = 0;\n    + \t\tif (!delete_only(commands)) {\n    +-\t\t\tunpack_status = unpack_with_sideband(&si);\n    ++\t\t\tif (odb_transaction_begin(the_repository->objects, &transaction, ODB_TRANSACTION_RECEIVE))\n    ++\t\t\t\tunpack_status = \"unable to start object transaction\";\n    ++\t\t\telse\n    ++\t\t\t\tunpack_status = unpack_with_sideband(&si, transaction);\n    + \t\t\tupdate_shallow_info(commands, &si, &ref);\n    + \t\t}\n    + \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n    +-\t\texecute_commands(commands, unpack_status, &si,\n    ++\t\texecute_commands(commands, unpack_status, &si, transaction,\n    + \t\t\t\t &push_options);\n    + \t\tdelete_tempfile(&pack_lockfile);\n    + \t\tsigchain_push(SIGPIPE, SIG_IGN);\n    +@@ builtin/receive-pack.c: int cmd_receive_pack(int argc,\n    + \t\telse if (report_status)\n    + \t\t\treport(commands, unpack_status);\n    + \t\tsigchain_pop(SIGPIPE);\n    +-\t\trun_receive_hook(commands, \"post-receive\", 1,\n    ++\t\trun_receive_hook(commands, \"post-receive\", 1, NULL,\n    + \t\t\t\t &push_options);\n    + \t\trun_update_post_hook(commands);\n    + \t\tfree_commands(commands);\n\nbase-commit: ab776a62a78576513ee121424adb19597fbb7613\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547447","messageId":"20260708041412.1157499-3-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v2 02/11] object-file: rename files transaction fsync function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:03Z","receivedAt":"2026-07-08T04:14:21Z","isPatch":true,"body":"When writing an object to a \"files\" ODB transaction, a full hardware\nflush is not initially performed during the fsync in\n`fsync_loose_object_transaction()` and instead delayed until the\ntransaction is later committed.\n\nTo be more consistent with other \"files\" ODB transaction helpers, rename\nthe function to `odb_transaction_files_fsync()` accordingly. The\nconditional in the helper is also slightly restructured to improve\nclarity to readers.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 16 ++++++++++------\n 1 file changed, 10 insertions(+), 6 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex a3eb8d71dd..d68824bb44 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -518,12 +518,17 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n \t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n }\n \n-static void fsync_loose_object_transaction(struct odb_transaction *base,\n-\t\t\t\t\t   int fd, const char *filename)\n+static void odb_transaction_files_fsync(struct odb_transaction *base,\n+\t\t\t\t\tint fd, const char *filename)\n {\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of_or_null(base, struct odb_transaction_files, base);\n \n+\tif (!transaction || !transaction->objdir) {\n+\t\tfsync_or_die(fd, filename);\n+\t\treturn;\n+\t}\n+\n \t/*\n \t * If we have an active ODB transaction, we issue a call that\n \t * cleans the filesystem page cache but avoids a hardware flush\n@@ -531,8 +536,7 @@ static void fsync_loose_object_transaction(struct odb_transaction *base,\n \t * before renaming the objects to their final names as part of\n \t * flush_batch_fsync.\n \t */\n-\tif (!transaction || !transaction->objdir ||\n-\t    git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) {\n+\tif (git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) {\n \t\tif (errno == ENOSYS)\n \t\t\twarning(_(\"core.fsyncMethod = batch is unsupported on this platform\"));\n \t\tfsync_or_die(fd, filename);\n@@ -553,7 +557,7 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac\n \t/*\n \t * Issue a full hardware flush against a temporary file to ensure\n \t * that all objects are durable before any renames occur. The code in\n-\t * fsync_loose_object_transaction has already issued a writeout\n+\t * odb_transaction_files_fsync has already issued a writeout\n \t * request, but it has not flushed any writeback cache in the storage\n \t * hardware or any filesystem logs. This fsync call acts as a barrier\n \t * to ensure that the data in each new object file is durable before\n@@ -582,7 +586,7 @@ static void close_loose_object(struct odb_source_loose *loose,\n \t\tgoto out;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tfsync_loose_object_transaction(loose->base.odb->transaction, fd, filename);\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-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547448","messageId":"20260708041412.1157499-4-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v2 03/11] object-file: embed transaction flush logic in commit function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:04Z","receivedAt":"2026-07-08T04:14:22Z","isPatch":true,"body":"When a \"files\" transaction is committed,\n`flush_loose_object_transaction()` is invoked to handle performing a\nhardware flush along with migrating the temporary object directory into\nthe primary. In a subsequent commit, the temporary directory is also\nused to write packfiles.\n\nInstead of maintaining a separate helper function, embed the logic to\nflush and migrate the temporary directory directly into\n`odb_transaction_files_commit()`.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 64 ++++++++++++++++++++++-----------------------------\n 1 file changed, 28 insertions(+), 36 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex d68824bb44..33bd6c6810 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -543,41 +543,6 @@ static void odb_transaction_files_fsync(struct odb_transaction *base,\n \t}\n }\n \n-/*\n- * Cleanup after batch-mode fsync_object_files.\n- */\n-static void flush_loose_object_transaction(struct odb_transaction_files *transaction)\n-{\n-\tstruct strbuf temp_path = STRBUF_INIT;\n-\tstruct tempfile *temp;\n-\n-\tif (!transaction->objdir)\n-\t\treturn;\n-\n-\t/*\n-\t * Issue a full hardware flush against a temporary file to ensure\n-\t * that all objects are durable before any renames occur. The code in\n-\t * odb_transaction_files_fsync has already issued a writeout\n-\t * request, but it has not flushed any writeback cache in the storage\n-\t * hardware or any filesystem logs. This fsync call acts as a barrier\n-\t * to ensure that the data in each new object file is durable before\n-\t * the final name is visible.\n-\t */\n-\tstrbuf_addf(&temp_path, \"%s/bulk_fsync_XXXXXX\",\n-\t\t    repo_get_object_directory(transaction->base.source->odb->repo));\n-\ttemp = xmks_tempfile(temp_path.buf);\n-\tfsync_or_die(get_tempfile_fd(temp), get_tempfile_path(temp));\n-\tdelete_tempfile(&temp);\n-\tstrbuf_release(&temp_path);\n-\n-\t/*\n-\t * Make the object files visible in the primary ODB after their data is\n-\t * fully durable.\n-\t */\n-\ttmp_objdir_migrate(transaction->objdir);\n-\ttransaction->objdir = NULL;\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@@ -1679,7 +1644,34 @@ static void odb_transaction_files_commit(struct odb_transaction *base)\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of(base, struct odb_transaction_files, base);\n \n-\tflush_loose_object_transaction(transaction);\n+\tif (transaction->objdir) {\n+\t\tstruct strbuf temp_path = STRBUF_INIT;\n+\t\tstruct tempfile *temp;\n+\n+\t\t/*\n+\t\t * Issue a full hardware flush against a temporary file to ensure\n+\t\t * that all objects are durable before any renames occur. The code in\n+\t\t * odb_transaction_files_fsync has already issued a writeout\n+\t\t * request, but it has not flushed any writeback cache in the storage\n+\t\t * hardware or any filesystem logs. This fsync call acts as a barrier\n+\t\t * to ensure that the data in each new object file is durable before\n+\t\t * the final name is visible.\n+\t\t */\n+\t\tstrbuf_addf(&temp_path, \"%s/bulk_fsync_XXXXXX\",\n+\t\t\t    repo_get_object_directory(transaction->base.source->odb->repo));\n+\t\ttemp = xmks_tempfile(temp_path.buf);\n+\t\tfsync_or_die(get_tempfile_fd(temp), get_tempfile_path(temp));\n+\t\tdelete_tempfile(&temp);\n+\t\tstrbuf_release(&temp_path);\n+\n+\t\t/*\n+\t\t * Make the object files visible in the primary ODB after their data is\n+\t\t * fully durable.\n+\t\t */\n+\t\ttmp_objdir_migrate(transaction->objdir);\n+\t\ttransaction->objdir = NULL;\n+\t}\n+\n \tflush_packfile_transaction(transaction);\n }\n \n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547449","messageId":"20260708041412.1157499-5-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v2 04/11] object-file: drop check for inflight transactions","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:05Z","receivedAt":"2026-07-08T04:14:23Z","isPatch":true,"body":"ODB transactions are started via `odb_transaction_begin()` and contain\nvalidation to avoid starting multiple transactions at the same time. The\n\"files\" backend also has the same logic, but is redundant due to the\ngeneric layer already handling it. Drop this validation from the \"files\"\nbackend accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 4 ----\n object-file.h | 3 +--\n 2 files changed, 1 insertion(+), 6 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 33bd6c6810..e51389833a 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1678,10 +1678,6 @@ static void odb_transaction_files_commit(struct odb_transaction *base)\n struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n {\n \tstruct odb_transaction_files *transaction;\n-\tstruct object_database *odb = source->odb;\n-\n-\tif (odb->transaction)\n-\t\treturn NULL;\n \n \ttransaction = xcalloc(1, sizeof(*transaction));\n \ttransaction->base.source = source;\ndiff --git a/object-file.h b/object-file.h\nindex 528c4e6e69..ea43d818f0 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -194,8 +194,7 @@ struct odb_transaction;\n /*\n  * Tell the object database to optimize for adding\n  * multiple objects. odb_transaction_files_commit must be called\n- * to make new objects visible. If a transaction is already\n- * pending, NULL is returned.\n+ * to make new objects visible.\n  */\n struct odb_transaction *odb_transaction_files_begin(struct odb_source *source);\n \n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547450","messageId":"20260708041412.1157499-6-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v2 05/11] object-file: propagate files transaction errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:06Z","receivedAt":"2026-07-08T04:14:25Z","isPatch":true,"body":"The \"files\" transaction backend may encounter errors related to managing\nthe temporary directory used to stage objects, but silently ignores\nthese errors. Instead return errors encountered in the\n`odb_transaction_files_{prepare,begin,commit}()` interfaces to allow\ncallers to handle them as needed.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c      | 26 ++++++++++++++++++--------\n object-file.h      |  3 ++-\n odb/source-files.c |  6 +-----\n odb/transaction.h  |  7 +++++--\n 4 files changed, 26 insertions(+), 16 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex e51389833a..64cb874fe7 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -499,7 +499,7 @@ struct odb_transaction_files {\n \tstruct transaction_packfile packfile;\n };\n \n-static void odb_transaction_files_prepare(struct odb_transaction *base)\n+static 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@@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n \t * added at the time they call odb_transaction_files_begin.\n \t */\n \tif (!transaction || transaction->objdir)\n-\t\treturn;\n+\t\treturn 0;\n \n \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n-\tif (transaction->objdir)\n-\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n+\tif (!transaction->objdir)\n+\t\treturn -1;\n+\n+\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n+\n+\treturn 0;\n }\n \n static void odb_transaction_files_fsync(struct odb_transaction *base,\n@@ -1639,7 +1643,7 @@ int read_loose_object(struct repository *repo,\n \treturn ret;\n }\n \n-static void odb_transaction_files_commit(struct odb_transaction *base)\n+static int odb_transaction_files_commit(struct odb_transaction *base)\n {\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of(base, struct odb_transaction_files, base);\n@@ -1668,14 +1672,19 @@ static void odb_transaction_files_commit(struct odb_transaction *base)\n \t\t * Make the object files visible in the primary ODB after their data is\n \t\t * fully durable.\n \t\t */\n-\t\ttmp_objdir_migrate(transaction->objdir);\n+\t\tif (tmp_objdir_migrate(transaction->objdir))\n+\t\t\treturn -1;\n+\n \t\ttransaction->objdir = NULL;\n \t}\n \n \tflush_packfile_transaction(transaction);\n+\n+\treturn 0;\n }\n \n-struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n+int odb_transaction_files_begin(struct odb_source *source,\n+\t\t\t\tstruct odb_transaction **out)\n {\n \tstruct odb_transaction_files *transaction;\n \n@@ -1683,6 +1692,7 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\t*out = &transaction->base;\n \n-\treturn &transaction->base;\n+\treturn 0;\n }\ndiff --git a/object-file.h b/object-file.h\nindex ea43d818f0..1a023226ac 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -196,6 +196,7 @@ struct odb_transaction;\n  * multiple objects. odb_transaction_files_commit must be called\n  * to make new objects visible.\n  */\n-struct odb_transaction *odb_transaction_files_begin(struct odb_source *source);\n+int odb_transaction_files_begin(struct odb_source *source,\n+\t\t\t\tstruct odb_transaction **out);\n \n #endif /* OBJECT_FILE_H */\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 5bdd042922..2545bd81d4 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -182,11 +182,7 @@ static int odb_source_files_write_object_stream(struct odb_source *source,\n static int odb_source_files_begin_transaction(struct odb_source *source,\n \t\t\t\t\t      struct odb_transaction **out)\n {\n-\tstruct odb_transaction *tx = odb_transaction_files_begin(source);\n-\tif (!tx)\n-\t\treturn -1;\n-\t*out = tx;\n-\treturn 0;\n+\treturn odb_transaction_files_begin(source, out);\n }\n \n static int odb_source_files_read_alternates(struct odb_source *source,\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 854fda06f5..d52f0533ce 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -16,8 +16,11 @@ struct odb_transaction {\n \t/* The ODB source the transaction is opened against. */\n \tstruct odb_source *source;\n \n-\t/* The ODB source specific callback invoked to commit a transaction. */\n-\tvoid (*commit)(struct odb_transaction *transaction);\n+\t/*\n+\t * The ODB source specific callback invoked to commit a transaction.\n+\t * Returns 0 on success, a negative error code otherwise.\n+\t */\n+\tint (*commit)(struct odb_transaction *transaction);\n \n \t/*\n \t * This callback is expected to write the given object stream into\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547451","messageId":"20260708041412.1157499-7-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v2 06/11] odb/transaction: propagate begin errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:07Z","receivedAt":"2026-07-08T04:14:26Z","isPatch":true,"body":"When `odb_transaction_begin()` is invoked, the function returns the\ntransaction pointer directly. There is no way for the backend to\nsignal that it failed to set up its state, such as when creating the\ntemporary object directory backing the transaction.\n\nIn a subsequent commit, git-receive-pack(1) starts using ODB\ntransactions and needs to be able to report such failures rather\nthan silently ignore them. Refactor `odb_transaction_begin()` to\nreturn an int error code and write the resulting transaction into an\nout parameter. Also introduce `odb_transaction_begin_or_die()` as a\nconvenience for callsites that do not need to handle errors\nexplicitly.\n\nNote that `odb_transaction_begin()` now returns an error when the ODB\nalready has an inflight transaction pending. ODB transaction call sites\nthat may encounter an inflight transaction are updated to explicitly\nhandle this case.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  2 +-\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  2 +-\n cache-tree.c             |  7 +++++--\n object-file.c            | 10 +++++++---\n odb/transaction.c        | 12 ++++++++----\n odb/transaction.h        | 20 ++++++++++++++++----\n read-cache.c             |  7 +++++--\n 8 files changed, 44 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex c859f66519..3d5d9cfdb9 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -581,7 +581,7 @@ int cmd_add(int argc,\n \t\tstring_list_clear(&only_match_skip_worktree, 0);\n \t}\n \n-\ttransaction = odb_transaction_begin(repo->objects);\n+\todb_transaction_begin_or_die(repo->objects, &transaction);\n \n \tps_matched = xcalloc(pathspec.nr, 1);\n \tif (add_renormalize)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex f3849bb654..d0136cdd99 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -598,7 +598,7 @@ static void unpack_all(void)\n \t\tprogress = start_progress(the_repository,\n \t\t\t\t\t  _(\"Unpacking objects\"), nr_objects);\n \tCALLOC_ARRAY(obj_list, nr_objects);\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \tfor (i = 0; i < nr_objects; i++) {\n \t\tunpack_one(i);\n \t\tdisplay_progress(progress, i + 1);\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 3d6646c318..17f3ea284c 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1124,7 +1124,7 @@ int cmd_update_index(int argc,\n \t * Allow the object layer to optimize adding multiple objects in\n \t * a batch.\n \t */\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \twhile (ctx.argc) {\n \t\tif (parseopt_state != PARSE_OPT_DONE)\n \t\t\tparseopt_state = parse_options_step(&ctx, options,\ndiff --git a/cache-tree.c b/cache-tree.c\nindex 184f7e2635..8eec1d4d52 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -474,6 +474,7 @@ static int update_one(struct cache_tree *it,\n \n int cache_tree_update(struct index_state *istate, int flags)\n {\n+\tint inflight = !!the_repository->objects->transaction;\n \tstruct odb_transaction *transaction;\n \tint skip, i;\n \n@@ -490,10 +491,12 @@ int cache_tree_update(struct index_state *istate, int flags)\n \n \ttrace_performance_enter();\n \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\tif (!inflight)\n+\t\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n \t\t       \"\", 0, &skip, flags);\n-\todb_transaction_commit(transaction);\n+\tif (!inflight)\n+\t\todb_transaction_commit(transaction);\n \ttrace2_region_leave(\"cache_tree\", \"update\", istate->repo);\n \ttrace_performance_leave(\"cache_tree_update\");\n \tif (i < 0)\ndiff --git a/object-file.c b/object-file.c\nindex 64cb874fe7..cd1aa36462 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1354,13 +1354,17 @@ int index_fd(struct index_state *istate, struct object_id *oid,\n \n \t\tif (flags & INDEX_WRITE_OBJECT) {\n \t\t\tstruct object_database *odb = the_repository->objects;\n-\t\t\tstruct odb_transaction *transaction = odb_transaction_begin(odb);\n+\t\t\tstruct odb_transaction *transaction = odb->transaction;\n+\t\t\tint inflight = !!transaction;\n \n-\t\t\tret = odb_transaction_write_object_stream(odb->transaction,\n+\t\t\tif (!inflight)\n+\t\t\t\todb_transaction_begin_or_die(odb, &transaction);\n+\t\t\tret = odb_transaction_write_object_stream(transaction,\n \t\t\t\t\t\t\t\t  &stream,\n \t\t\t\t\t\t\t\t  xsize_t(st->st_size),\n \t\t\t\t\t\t\t\t  oid);\n-\t\t\todb_transaction_commit(transaction);\n+\t\t\tif (!inflight)\n+\t\t\t\todb_transaction_commit(transaction);\n \t\t} else {\n \t\t\tret = hash_blob_stream(&stream,\n \t\t\t\t\t       the_repository->hash_algo, oid,\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex b16e07aebf..df4275151b 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -2,14 +2,18 @@\n #include \"odb/source.h\"\n #include \"odb/transaction.h\"\n \n-struct odb_transaction *odb_transaction_begin(struct object_database *odb)\n+int odb_transaction_begin(struct object_database *odb,\n+\t\t\t  struct odb_transaction **out)\n {\n+\tint ret;\n+\n \tif (odb->transaction)\n-\t\treturn NULL;\n+\t\treturn -1;\n \n-\todb_source_begin_transaction(odb->sources, &odb->transaction);\n+\tret = odb_source_begin_transaction(odb->sources, out);\n+\todb->transaction = *out;\n \n-\treturn odb->transaction;\n+\treturn ret;\n }\n \n void odb_transaction_commit(struct odb_transaction *transaction)\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex d52f0533ce..36032a5365 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -1,6 +1,8 @@\n #ifndef ODB_TRANSACTION_H\n #define ODB_TRANSACTION_H\n \n+#include \"git-compat-util.h\"\n+#include \"gettext.h\"\n #include \"odb.h\"\n #include \"odb/source.h\"\n \n@@ -36,11 +38,21 @@ struct odb_transaction {\n };\n \n /*\n- * Starts an ODB transaction. Subsequent objects are written to the transaction\n- * and not committed until odb_transaction_commit() is invoked on the\n- * transaction. If the ODB already has a pending transaction, NULL is returned.\n+ * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n+ * written to the transaction and not committed until odb_transaction_commit()\n+ * is invoked on the transaction. Returns 0 on success and a negative value on\n+ * error. Note that it is considered an error to start a new transaction if the\n+ * ODB already has an inflight transaction pending.\n  */\n-struct odb_transaction *odb_transaction_begin(struct object_database *odb);\n+int odb_transaction_begin(struct object_database *odb,\n+\t\t\t  struct odb_transaction **out);\n+\n+static inline void odb_transaction_begin_or_die(struct object_database *odb,\n+\t\t\t\t\t\tstruct odb_transaction **out)\n+{\n+\tif (odb_transaction_begin(odb, out))\n+\t\tdie(_(\"failed to start ODB transaction\"));\n+}\n \n /*\n  * Commits an ODB transaction making the written objects visible. If the\ndiff --git a/read-cache.c b/read-cache.c\nindex 21ca58beea..d511d25834 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4012,6 +4012,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \t\t       const struct pathspec *pathspec, char *ps_matched,\n \t\t       int include_sparse, int flags, int ignored_too )\n {\n+\tint inflight = !!repo->objects->transaction;\n \tstruct odb_transaction *transaction;\n \tstruct update_callback_data data;\n \tstruct rev_info rev;\n@@ -4042,9 +4043,11 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \t * This function is invoked from commands other than 'add', which\n \t * may not have their own transaction active.\n \t */\n-\ttransaction = odb_transaction_begin(repo->objects);\n+\tif (!inflight)\n+\t\todb_transaction_begin_or_die(repo->objects, &transaction);\n \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n-\todb_transaction_commit(transaction);\n+\tif (!inflight)\n+\t\todb_transaction_commit(transaction);\n \n \trelease_revisions(&rev);\n \treturn !!data.add_errors;\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547452","messageId":"20260708041412.1157499-8-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v2 07/11] odb/transaction: propagate commit errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:08Z","receivedAt":"2026-07-08T04:14:27Z","isPatch":true,"body":"When `odb_transaction_commit()` is invoked, the return value of the\nbackend commit callback is silently discarded. A backend has no way\nto signal that committing failed, such as when the \"files\" backend\ncannot migrate its temporary object directory into the permanent\nODB.\n\nIn a subsequent commit, git-receive-pack(1) starts using ODB transaction\nto stage objects and consequently cares about such failures so it can\nhandle the error appropriately. Change the commit callback signature to\nreturn an int error code and have `odb_transaction_commit()` forward it\naccordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n odb/transaction.c | 13 ++++++++++---\n odb/transaction.h |  7 ++++---\n 2 files changed, 14 insertions(+), 6 deletions(-)\n\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex df4275151b..51af2c9a61 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -16,19 +16,26 @@ int odb_transaction_begin(struct object_database *odb,\n \treturn ret;\n }\n \n-void odb_transaction_commit(struct odb_transaction *transaction)\n+int odb_transaction_commit(struct odb_transaction *transaction)\n {\n+\tint ret;\n+\n \tif (!transaction)\n-\t\treturn;\n+\t\treturn 0;\n \n \t/*\n \t * Ensure the transaction ending matches the pending transaction.\n \t */\n \tASSERT(transaction == transaction->source->odb->transaction);\n \n-\ttransaction->commit(transaction);\n+\tret = transaction->commit(transaction);\n+\tif (ret)\n+\t\treturn ret;\n+\n \ttransaction->source->odb->transaction = NULL;\n \tfree(transaction);\n+\n+\treturn 0;\n }\n \n int odb_transaction_write_object_stream(struct odb_transaction *transaction,\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 36032a5365..9557ee0fd2 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -55,10 +55,11 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n }\n \n /*\n- * Commits an ODB transaction making the written objects visible. If the\n- * specified transaction is NULL, the function is a no-op.\n+ * Commits an ODB transaction making the written objects visible. Returns 0 on\n+ * success, a negative error code otherwise. Note that, if the specified\n+ * transaction is NULL, the function is a no-op and no error is returned.\n  */\n-void odb_transaction_commit(struct odb_transaction *transaction);\n+int odb_transaction_commit(struct odb_transaction *transaction);\n \n /*\n  * Writes the object in the provided stream into the transaction. The resulting\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547453","messageId":"20260708041412.1157499-9-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v2 08/11] odb/transaction: add transaction env interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:09Z","receivedAt":"2026-07-08T04:14:28Z","isPatch":true,"body":"The ODB transaction backend is responsible for creating/managing its own\nstaging area for writing objects. Other child processes spawned by Git\nmay need access to uncommitted objects or write new objects in the\nstaging area though.\n\nIntroduce `odb_transaction_env()` which is expected to provide the set\nof environment variables needed by a child process to access the\ntransaction's staging area.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c     | 14 ++++++++++++++\n odb/transaction.c |  8 ++++++++\n odb/transaction.h | 17 +++++++++++++++++\n 3 files changed, 39 insertions(+)\n\ndiff --git a/object-file.c b/object-file.c\nindex cd1aa36462..9b8ee6f36c 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -27,6 +27,7 @@\n #include \"path.h\"\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n+#include \"strvec.h\"\n #include \"tempfile.h\"\n #include \"tmp-objdir.h\"\n \n@@ -1687,6 +1688,18 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n \treturn 0;\n }\n \n+static int odb_transaction_files_env(struct odb_transaction *base,\n+\t\t\t\t     struct strvec *env)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\n+\todb_transaction_files_prepare(&transaction->base);\n+\tstrvec_pushv(env, tmp_objdir_env(transaction->objdir));\n+\n+\treturn 0;\n+}\n+\n int odb_transaction_files_begin(struct odb_source *source,\n \t\t\t\tstruct odb_transaction **out)\n {\n@@ -1696,6 +1709,7 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\ttransaction->base.env = odb_transaction_files_env;\n \t*out = &transaction->base;\n \n \treturn 0;\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex 51af2c9a61..acb1c967e7 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -44,3 +44,11 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n {\n \treturn transaction->write_object_stream(transaction, stream, len, oid);\n }\n+\n+int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env)\n+{\n+\tif (!transaction)\n+\t\treturn 0;\n+\n+\treturn transaction->env(transaction, env);\n+}\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 9557ee0fd2..1c6c97a53e 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -35,6 +35,14 @@ struct odb_transaction {\n \tint (*write_object_stream)(struct odb_transaction *transaction,\n \t\t\t\t   struct odb_write_stream *stream, size_t len,\n \t\t\t\t   struct object_id *oid);\n+\n+\t/*\n+\t * This callback is expected to populate the provided strvec with the\n+\t * environment variables that a child process should inherit so that its\n+\t * object writes participate in the transaction. Returns 0 on success, a\n+\t * negative error code otherwise.\n+\t */\n+\tint (*env)(struct odb_transaction *transaction, struct strvec *env);\n };\n \n /*\n@@ -70,4 +78,13 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n \t\t\t\t\tstruct odb_write_stream *stream,\n \t\t\t\t\tsize_t len, struct object_id *oid);\n \n+/*\n+ * Populates the provided strvec with the environment variables that a child\n+ * process should inherit so that its object writes participate in the\n+ * transaction, suitable for using via child_process.env. Returns 0 on success,\n+ * a negative error code otherwise. Note that, if the specified transaction is\n+ * NULL, the function is a no-op and no error is returned.\n+ */\n+int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env);\n+\n #endif\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547454","messageId":"20260708041412.1157499-10-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v2 09/11] odb/transaction: introduce ODB transaction flags","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:10Z","receivedAt":"2026-07-08T04:14:30Z","isPatch":true,"body":"The temporary directory used by git-receive-pack(1) to write objects is\nmanaged slightly differently than how it is done via ODB transactions:\n\n  - The temporary directory is eagerly created upfront, instead of\n    waiting for the first object write.\n\n  - The prefix name of the temporary directory is \"incoming\" instead of\n    \"bulk-fsync\".\n\nIn a subsequent commit, git-receive-pack(1) will use ODB transactions\ninstead of `tmp_objdir` directly. To provide a means to configure the\nsame transaction behavior, introduce `enum odb_transaction_flags` and\nthe ODB_TRANSACTION_RECEIVE flag intended as a signal for ODB\ntransactions using the \"files\" backend to be set up for\ngit-receive-pack(1). Transaction call sites are updated accordingly to\nprovide the required flag parameter.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  2 +-\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  2 +-\n cache-tree.c             |  2 +-\n object-file.c            | 29 ++++++++++++++++++++++++++---\n object-file.h            |  4 +++-\n odb/source-files.c       |  5 +++--\n odb/source-inmemory.c    |  3 ++-\n odb/source-loose.c       |  3 ++-\n odb/source.h             |  9 ++++++---\n odb/transaction.c        |  5 +++--\n odb/transaction.h        | 15 +++++++++++----\n read-cache.c             |  2 +-\n 13 files changed, 61 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 3d5d9cfdb9..60ffbede2b 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -581,7 +581,7 @@ int cmd_add(int argc,\n \t\tstring_list_clear(&only_match_skip_worktree, 0);\n \t}\n \n-\todb_transaction_begin_or_die(repo->objects, &transaction);\n+\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \n \tps_matched = xcalloc(pathspec.nr, 1);\n \tif (add_renormalize)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex d0136cdd99..c3d0fc7507 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -598,7 +598,7 @@ static void unpack_all(void)\n \t\tprogress = start_progress(the_repository,\n \t\t\t\t\t  _(\"Unpacking objects\"), nr_objects);\n \tCALLOC_ARRAY(obj_list, nr_objects);\n-\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \tfor (i = 0; i < nr_objects; i++) {\n \t\tunpack_one(i);\n \t\tdisplay_progress(progress, i + 1);\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 17f3ea284c..bf6ea60ef4 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1124,7 +1124,7 @@ int cmd_update_index(int argc,\n \t * Allow the object layer to optimize adding multiple objects in\n \t * a batch.\n \t */\n-\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \twhile (ctx.argc) {\n \t\tif (parseopt_state != PARSE_OPT_DONE)\n \t\t\tparseopt_state = parse_options_step(&ctx, options,\ndiff --git a/cache-tree.c b/cache-tree.c\nindex 8eec1d4d52..99c6a0a7d0 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -492,7 +492,7 @@ int cache_tree_update(struct index_state *istate, int flags)\n \ttrace_performance_enter();\n \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n \tif (!inflight)\n-\t\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\t\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n \t\t       \"\", 0, &skip, flags);\n \tif (!inflight)\ndiff --git a/object-file.c b/object-file.c\nindex 9b8ee6f36c..d95bdabba5 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -498,6 +498,7 @@ struct odb_transaction_files {\n \n \tstruct tmp_objdir *objdir;\n \tstruct transaction_packfile packfile;\n+\tconst char *prefix;\n };\n \n static int odb_transaction_files_prepare(struct odb_transaction *base)\n@@ -514,7 +515,7 @@ static int odb_transaction_files_prepare(struct odb_transaction *base)\n \tif (!transaction || transaction->objdir)\n \t\treturn 0;\n \n-\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n+\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, transaction->prefix);\n \tif (!transaction->objdir)\n \t\treturn -1;\n \n@@ -1359,7 +1360,7 @@ int index_fd(struct index_state *istate, struct object_id *oid,\n \t\t\tint inflight = !!transaction;\n \n \t\t\tif (!inflight)\n-\t\t\t\todb_transaction_begin_or_die(odb, &transaction);\n+\t\t\t\todb_transaction_begin_or_die(odb, &transaction, 0);\n \t\t\tret = odb_transaction_write_object_stream(transaction,\n \t\t\t\t\t\t\t\t  &stream,\n \t\t\t\t\t\t\t\t  xsize_t(st->st_size),\n@@ -1701,7 +1702,8 @@ static int odb_transaction_files_env(struct odb_transaction *base,\n }\n \n int odb_transaction_files_begin(struct odb_source *source,\n-\t\t\t\tstruct odb_transaction **out)\n+\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\tenum odb_transaction_flags flags)\n {\n \tstruct odb_transaction_files *transaction;\n \n@@ -1710,6 +1712,27 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n \ttransaction->base.env = odb_transaction_files_env;\n+\n+\ttransaction->prefix = \"bulk-fsync\";\n+\tif (flags & ODB_TRANSACTION_RECEIVE) {\n+\t\t/*\n+\t\t * ODB transactions for git-receive-pack(1) eagerly create a\n+\t\t * temporary directory and use a different temporary directory\n+\t\t * prefix.\n+\t\t *\n+\t\t * NEEDSWORK: This transaction flag is only used by the \"files\"\n+\t\t * backend to special case temporary directory set up and\n+\t\t * handling. Ideally transaction users should not have to care\n+\t\t * though. To avoid this, we could eagerly create the temporary\n+\t\t * directory and use the same prefix name for all transactions.\n+\t\t */\n+\t\ttransaction->prefix = \"incoming\";\n+\t\tif (odb_transaction_files_prepare(&transaction->base)) {\n+\t\t\tfree(transaction);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\n \t*out = &transaction->base;\n \n \treturn 0;\ndiff --git a/object-file.h b/object-file.h\nindex 1a023226ac..bdd2d67a2e 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -5,6 +5,7 @@\n #include \"object.h\"\n #include \"odb.h\"\n #include \"odb/source-loose.h\"\n+#include \"odb/transaction.h\"\n \n /* The maximum size for an object header. */\n #define MAX_HEADER_LEN 32\n@@ -197,6 +198,7 @@ struct odb_transaction;\n  * to make new objects visible.\n  */\n int odb_transaction_files_begin(struct odb_source *source,\n-\t\t\t\tstruct odb_transaction **out);\n+\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\tenum odb_transaction_flags flags);\n \n #endif /* OBJECT_FILE_H */\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 2545bd81d4..534f48aad9 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -180,9 +180,10 @@ static int odb_source_files_write_object_stream(struct odb_source *source,\n }\n \n static int odb_source_files_begin_transaction(struct odb_source *source,\n-\t\t\t\t\t      struct odb_transaction **out)\n+\t\t\t\t\t      struct odb_transaction **out,\n+\t\t\t\t\t      enum odb_transaction_flags flags)\n {\n-\treturn odb_transaction_files_begin(source, out);\n+\treturn odb_transaction_files_begin(source, out, flags);\n }\n \n static int odb_source_files_read_alternates(struct odb_source *source,\ndiff --git a/odb/source-inmemory.c b/odb/source-inmemory.c\nindex e004566d76..9644d9d474 100644\n--- a/odb/source-inmemory.c\n+++ b/odb/source-inmemory.c\n@@ -304,7 +304,8 @@ static int odb_source_inmemory_freshen_object(struct odb_source *source,\n }\n \n static int odb_source_inmemory_begin_transaction(struct odb_source *source UNUSED,\n-\t\t\t\t\t\t struct odb_transaction **out UNUSED)\n+\t\t\t\t\t\t struct odb_transaction **out UNUSED,\n+\t\t\t\t\t\t enum odb_transaction_flags flags UNUSED)\n {\n \treturn error(\"in-memory source does not support transactions\");\n }\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 66e6bb8d3f..57c91986b4 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -638,7 +638,8 @@ static int odb_source_loose_write_object_stream(struct odb_source *source,\n }\n \n static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED,\n-\t\t\t\t\t      struct odb_transaction **out UNUSED)\n+\t\t\t\t\t      struct odb_transaction **out UNUSED,\n+\t\t\t\t\t      enum odb_transaction_flags flags UNUSED)\n {\n \t/* TODO: this is a known omission that we'll want to address eventually. */\n \treturn error(\"loose source does not support transactions\");\ndiff --git a/odb/source.h b/odb/source.h\nindex 2192a101b8..3790d03ff2 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -3,6 +3,7 @@\n \n #include \"object.h\"\n #include \"odb.h\"\n+#include \"odb/transaction.h\"\n \n enum odb_source_type {\n \t/*\n@@ -228,7 +229,8 @@ struct odb_source {\n \t * negative error code otherwise.\n \t */\n \tint (*begin_transaction)(struct odb_source *source,\n-\t\t\t\t struct odb_transaction **out);\n+\t\t\t\t struct odb_transaction **out,\n+\t\t\t\t enum odb_transaction_flags flags);\n \n \t/*\n \t * This callback is expected to read the list of alternate object\n@@ -467,9 +469,10 @@ static inline int odb_source_write_alternate(struct odb_source *source,\n  * Returns 0 on success, a negative error code otherwise.\n  */\n static inline int odb_source_begin_transaction(struct odb_source *source,\n-\t\t\t\t\t       struct odb_transaction **out)\n+\t\t\t\t\t       struct odb_transaction **out,\n+\t\t\t\t\t       enum odb_transaction_flags flags)\n {\n-\treturn source->begin_transaction(source, out);\n+\treturn source->begin_transaction(source, out, flags);\n }\n \n #endif\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex acb1c967e7..007ab73c0c 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -3,14 +3,15 @@\n #include \"odb/transaction.h\"\n \n int odb_transaction_begin(struct object_database *odb,\n-\t\t\t  struct odb_transaction **out)\n+\t\t\t  struct odb_transaction **out,\n+\t\t\t  enum odb_transaction_flags flags)\n {\n \tint ret;\n \n \tif (odb->transaction)\n \t\treturn -1;\n \n-\tret = odb_source_begin_transaction(odb->sources, out);\n+\tret = odb_source_begin_transaction(odb->sources, out, flags);\n \todb->transaction = *out;\n \n \treturn ret;\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 1c6c97a53e..b19f180aee 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -4,7 +4,6 @@\n #include \"git-compat-util.h\"\n #include \"gettext.h\"\n #include \"odb.h\"\n-#include \"odb/source.h\"\n \n /*\n  * A transaction may be started for an object database prior to writing new\n@@ -45,6 +44,12 @@ struct odb_transaction {\n \tint (*env)(struct odb_transaction *transaction, struct strvec *env);\n };\n \n+/* Flags used to configure an ODB transaction. */\n+enum odb_transaction_flags {\n+\t/* Configures the transaction for use with git-receive-pack(1). */\n+\tODB_TRANSACTION_RECEIVE = (1 << 0),\n+};\n+\n /*\n  * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n  * written to the transaction and not committed until odb_transaction_commit()\n@@ -53,12 +58,14 @@ struct odb_transaction {\n  * ODB already has an inflight transaction pending.\n  */\n int odb_transaction_begin(struct object_database *odb,\n-\t\t\t  struct odb_transaction **out);\n+\t\t\t  struct odb_transaction **out,\n+\t\t\t  enum odb_transaction_flags flags);\n \n static inline void odb_transaction_begin_or_die(struct object_database *odb,\n-\t\t\t\t\t\tstruct odb_transaction **out)\n+\t\t\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\t\t\tenum odb_transaction_flags flags)\n {\n-\tif (odb_transaction_begin(odb, out))\n+\tif (odb_transaction_begin(odb, out, flags))\n \t\tdie(_(\"failed to start ODB transaction\"));\n }\n \ndiff --git a/read-cache.c b/read-cache.c\nindex d511d25834..50e2320c8d 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4044,7 +4044,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \t * may not have their own transaction active.\n \t */\n \tif (!inflight)\n-\t\todb_transaction_begin_or_die(repo->objects, &transaction);\n+\t\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n \tif (!inflight)\n \t\todb_transaction_commit(transaction);\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547455","messageId":"20260708041412.1157499-11-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v2 10/11] builtin/receive-pack: drop redundant tmpdir env","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:11Z","receivedAt":"2026-07-08T04:14:31Z","isPatch":true,"body":"When performing the connectivity checks for a shallow ref in\n`update_shallow_ref()`, the child process environment variables are\npopulated via `tmp_objdir_env()`. This is unnecessary though as\n`update_shallow_ref()` is only reached after `tmp_objdir_migrate()` has\nbeen performed which means there is no longer a temporary directory that\nneeds to be shared with child processes.\n\nDrop the call to `tmp_objdir_env()` accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 1 -\n 1 file changed, 1 deletion(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 19eb6a1b61..50bc05c70c 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1363,7 +1363,6 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)\n \t\t    !delayed_reachability_test(si, i))\n \t\t\toid_array_append(&extra, &si->shallow->oid[i]);\n \n-\topt.env = tmp_objdir_env(tmp_objdir);\n \tsetup_alternate_shallow(&shallow_lock, &opt.shallow_file, &extra);\n \tif (check_connected(command_singleton_iterator, cmd, &opt)) {\n \t\trollback_shallow_file(the_repository, &shallow_lock);\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547456","messageId":"20260708041412.1157499-12-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v2 11/11] builtin/receive-pack: stage incoming objects via ODB transactions","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T04:14:12Z","receivedAt":"2026-07-08T04:14:32Z","isPatch":true,"body":"Objects received by git-receive-pack(1) are quarantined in a temporary\n\"incoming\" directory and migrated into the object database prior to the\nreference updates. The quarantine is currently managed through\n`tmp_objdir` directly. In a pluggable ODB future, how exactly an object\ngets written to a transaction may vary for a given ODB source. Refactor\ngit-receive-pack(1) to use the ODB transaction interfaces to manage the\nobject staging area in a more agnostic manner accordingly.\n\nNote that the ODB transaction is now responsible for managing the\nprimary and alternate ODBs for the repository. One small change as a\nresult is that the temporary directory is now applied as the primary ODB\nin the main process instead of an alternate. This does not change\nanything for git-receive-pack(1) though because it only needs access to\nthe newly written objects and doesn't care how exactly it is set up.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 68 ++++++++++++++++++++++--------------------\n 1 file changed, 35 insertions(+), 33 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 50bc05c70c..8b8c20dc1a 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -37,7 +37,6 @@\n #include \"sigchain.h\"\n #include \"string-list.h\"\n #include \"strvec.h\"\n-#include \"tmp-objdir.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n #include \"version.h\"\n@@ -112,8 +111,6 @@ static enum {\n } use_keepalive;\n static int keepalive_in_sec = 5;\n \n-static struct tmp_objdir *tmp_objdir;\n-\n static struct proc_receive_ref {\n \tunsigned int want_add:1,\n \t\t     want_delete:1,\n@@ -926,6 +923,7 @@ static void receive_hook_feed_state_free(void *data)\n static int run_receive_hook(struct command *commands,\n \t\t\t    const char *hook_name,\n \t\t\t    int skip_broken,\n+\t\t\t    struct odb_transaction *transaction,\n \t\t\t    const struct string_list *push_options)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n@@ -959,8 +957,8 @@ static int run_receive_hook(struct command *commands,\n \t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n \t}\n \n-\tif (tmp_objdir)\n-\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n+\tif (transaction)\n+\t\todb_transaction_env(transaction, &opt.env);\n \n \tprepare_push_cert_sha1(&opt);\n \n@@ -1789,24 +1787,30 @@ static const struct object_id *command_singleton_iterator(void *cb_data)\n }\n \n static void set_connectivity_errors(struct command *commands,\n-\t\t\t\t    struct shallow_info *si)\n+\t\t\t\t    struct shallow_info *si,\n+\t\t\t\t    struct odb_transaction *transaction)\n {\n \tstruct command *cmd;\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tstruct command *singleton = cmd;\n \t\tstruct check_connected_options opt = CHECK_CONNECTED_INIT;\n+\t\tstruct strvec env = STRVEC_INIT;\n \n \t\tif (shallow_update && si->shallow_ref[cmd->index])\n \t\t\t/* to be checked in update_shallow_ref() */\n \t\t\tcontinue;\n \n-\t\topt.env = tmp_objdir_env(tmp_objdir);\n+\t\todb_transaction_env(transaction, &env);\n+\t\topt.env = env.v;\n+\n \t\tif (!check_connected(command_singleton_iterator, &singleton,\n \t\t\t\t     &opt))\n \t\t\tcontinue;\n \n \t\tcmd->error_string = \"missing necessary objects\";\n+\n+\t\tstrvec_clear(&env);\n \t}\n }\n \n@@ -2027,6 +2031,7 @@ static void execute_commands_atomic(struct command *commands,\n static void execute_commands(struct command *commands,\n \t\t\t     const char *unpacker_error,\n \t\t\t     struct shallow_info *si,\n+\t\t\t     struct odb_transaction *transaction,\n \t\t\t     const struct string_list *push_options)\n {\n \tstruct check_connected_options opt = CHECK_CONNECTED_INIT;\n@@ -2043,6 +2048,8 @@ static void execute_commands(struct command *commands,\n \t}\n \n \tif (!skip_connectivity_check) {\n+\t\tstruct strvec env = STRVEC_INIT;\n+\n \t\tif (use_sideband) {\n \t\t\tmemset(&muxer, 0, sizeof(muxer));\n \t\t\tmuxer.proc = copy_to_sideband;\n@@ -2056,14 +2063,17 @@ static void execute_commands(struct command *commands,\n \t\tdata.si = si;\n \t\topt.err_fd = err_fd;\n \t\topt.progress = err_fd && !quiet;\n-\t\topt.env = tmp_objdir_env(tmp_objdir);\n+\t\todb_transaction_env(transaction, &env);\n+\t\topt.env = env.v;\n \t\topt.exclude_hidden_refs_section = \"receive\";\n \n \t\tif (check_connected(iterate_receive_command_list, &data, &opt))\n-\t\t\tset_connectivity_errors(commands, si);\n+\t\t\tset_connectivity_errors(commands, si, transaction);\n \n \t\tif (use_sideband)\n \t\t\tfinish_async(&muxer);\n+\n+\t\tstrvec_clear(&env);\n \t}\n \n \treject_updates_to_hidden(commands);\n@@ -2084,7 +2094,7 @@ static void execute_commands(struct command *commands,\n \t\t}\n \t}\n \n-\tif (run_receive_hook(commands, \"pre-receive\", 0, push_options)) {\n+\tif (run_receive_hook(commands, \"pre-receive\", 0, transaction, push_options)) {\n \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\t\tif (!cmd->error_string)\n \t\t\t\tcmd->error_string = \"pre-receive hook declined\";\n@@ -2105,14 +2115,13 @@ static void execute_commands(struct command *commands,\n \t * Now we'll start writing out refs, which means the objects need\n \t * to be in their final positions so that other processes can see them.\n \t */\n-\tif (tmp_objdir_migrate(tmp_objdir) < 0) {\n+\tif (odb_transaction_commit(transaction)) {\n \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\t\tif (!cmd->error_string)\n \t\t\t\tcmd->error_string = \"unable to migrate objects to permanent storage\";\n \t\t}\n \t\treturn;\n \t}\n-\ttmp_objdir = NULL;\n \n \tcheck_aliased_updates(commands);\n \n@@ -2325,7 +2334,8 @@ static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n \t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n }\n \n-static const char *unpack(int err_fd, struct shallow_info *si)\n+static const char *unpack(int err_fd, struct shallow_info *si,\n+\t\t\t  struct odb_transaction *transaction)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2350,20 +2360,7 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n \t\tstrvec_push(&child.args, alt_shallow_file);\n \t}\n \n-\ttmp_objdir = tmp_objdir_create(the_repository, \"incoming\");\n-\tif (!tmp_objdir) {\n-\t\tif (err_fd > 0)\n-\t\t\tclose(err_fd);\n-\t\treturn \"unable to create temporary object directory\";\n-\t}\n-\tstrvec_pushv(&child.env, tmp_objdir_env(tmp_objdir));\n-\n-\t/*\n-\t * Normally we just pass the tmp_objdir environment to the child\n-\t * processes that do the heavy lifting, but we may need to see these\n-\t * objects ourselves to set up shallow information.\n-\t */\n-\ttmp_objdir_add_as_alternate(tmp_objdir);\n+\todb_transaction_env(transaction, &child.env);\n \n \tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n@@ -2430,13 +2427,14 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n \treturn NULL;\n }\n \n-static const char *unpack_with_sideband(struct shallow_info *si)\n+static const char *unpack_with_sideband(struct shallow_info *si,\n+\t\t\t\t\tstruct odb_transaction *transaction)\n {\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(0, si);\n+\t\treturn unpack(0, si, transaction);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2445,7 +2443,7 @@ static const char *unpack_with_sideband(struct shallow_info *si)\n \tif (start_async(&muxer))\n \t\treturn NULL;\n \n-\tret = unpack(muxer.in, si);\n+\tret = unpack(muxer.in, si, transaction);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2622,6 +2620,7 @@ int cmd_receive_pack(int argc,\n \tstruct oid_array ref = OID_ARRAY_INIT;\n \tstruct shallow_info si;\n \tstruct packet_reader reader;\n+\tstruct odb_transaction *transaction = NULL;\n \n \tstruct option options[] = {\n \t\tOPT__QUIET(&quiet, N_(\"quiet\")),\n@@ -2706,11 +2705,14 @@ int cmd_receive_pack(int argc,\n \t\tif (!si.nr_ours && !si.nr_theirs)\n \t\t\tshallow_update = 0;\n \t\tif (!delete_only(commands)) {\n-\t\t\tunpack_status = unpack_with_sideband(&si);\n+\t\t\tif (odb_transaction_begin(the_repository->objects, &transaction, ODB_TRANSACTION_RECEIVE))\n+\t\t\t\tunpack_status = \"unable to start object transaction\";\n+\t\t\telse\n+\t\t\t\tunpack_status = unpack_with_sideband(&si, transaction);\n \t\t\tupdate_shallow_info(commands, &si, &ref);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n-\t\texecute_commands(commands, unpack_status, &si,\n+\t\texecute_commands(commands, unpack_status, &si, transaction,\n \t\t\t\t &push_options);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n@@ -2719,7 +2721,7 @@ int cmd_receive_pack(int argc,\n \t\telse if (report_status)\n \t\t\treport(commands, unpack_status);\n \t\tsigchain_pop(SIGPIPE);\n-\t\trun_receive_hook(commands, \"post-receive\", 1,\n+\t\trun_receive_hook(commands, \"post-receive\", 1, NULL,\n \t\t\t\t &push_options);\n \t\trun_update_post_hook(commands);\n \t\tfree_commands(commands);\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547463","messageId":"ak3xB-18mCVWMUVn@pks.im","threadId":"65861","inReplyTo":"20260708041412.1157499-3-jltobler@gmail.com","subject":"Re: [PATCH v2 02/11] object-file: rename files transaction fsync function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-08T06:41:11Z","receivedAt":"2026-07-08T06:41:22Z","isPatch":true,"body":"On Tue, Jul 07, 2026 at 11:14:03PM -0500, Justin Tobler wrote:\n> diff --git a/object-file.c b/object-file.c\n> index a3eb8d71dd..d68824bb44 100644\n> --- a/object-file.c\n> +++ b/object-file.c\n> @@ -518,12 +518,17 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n>  \t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n>  }\n>  \n> -static void fsync_loose_object_transaction(struct odb_transaction *base,\n> -\t\t\t\t\t   int fd, const char *filename)\n> +static void odb_transaction_files_fsync(struct odb_transaction *base,\n> +\t\t\t\t\tint fd, const char *filename)\n>  {\n>  \tstruct odb_transaction_files *transaction =\n>  \t\tcontainer_of_or_null(base, struct odb_transaction_files, base);\n>  \n> +\tif (!transaction || !transaction->objdir) {\n> +\t\tfsync_or_die(fd, filename);\n> +\t\treturn;\n> +\t}\n\nThe change results in a tiny bit of duplication, but I agree that it's\neasier to reason about.\n\n> @@ -531,8 +536,7 @@ static void fsync_loose_object_transaction(struct odb_transaction *base,\n\nSomewhat funny that the diff renderer decided to put the hunk header\nhere instead of showing the single line that's now missing from the\ndiff.\n\n>  \t * before renaming the objects to their final names as part of\n>  \t * flush_batch_fsync.\n>  \t */\n> -\tif (!transaction || !transaction->objdir ||\n> -\t    git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) {\n> +\tif (git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) {\n>  \t\tif (errno == ENOSYS)\n>  \t\t\twarning(_(\"core.fsyncMethod = batch is unsupported on this platform\"));\n>  \t\tfsync_or_die(fd, filename);\n\nThanks!\n\nPatrick\n"},{"id":"547464","messageId":"ak3xFCzvgj1-Ev_3@pks.im","threadId":"65861","inReplyTo":"20260708041412.1157499-4-jltobler@gmail.com","subject":"Re: [PATCH v2 03/11] object-file: embed transaction flush logic in commit function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-08T06:41:24Z","receivedAt":"2026-07-08T06:41:31Z","isPatch":true,"body":"On Tue, Jul 07, 2026 at 11:14:04PM -0500, Justin Tobler wrote:\n> When a \"files\" transaction is committed,\n> `flush_loose_object_transaction()` is invoked to handle performing a\n> hardware flush along with migrating the temporary object directory into\n> the primary. In a subsequent commit, the temporary directory is also\n> used to write packfiles.\n> \n> Instead of maintaining a separate helper function, embed the logic to\n> flush and migrate the temporary directory directly into\n> `odb_transaction_files_commit()`.\n\nThe change itself looks simple enough, but this makes me wonder why we\nwant to change this. Like, what subsequent step does this enable that\nwould otherwise be harder to do?\n\nMaybe this will be answered by a subsequent commit.\n\nPatrick\n"},{"id":"547465","messageId":"ak3xGYaGC2njU-Mn@pks.im","threadId":"65861","inReplyTo":"20260708041412.1157499-5-jltobler@gmail.com","subject":"Re: [PATCH v2 04/11] object-file: drop check for inflight transactions","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-08T06:41:29Z","receivedAt":"2026-07-08T06:41:37Z","isPatch":true,"body":"On Tue, Jul 07, 2026 at 11:14:05PM -0500, Justin Tobler wrote:\n> ODB transactions are started via `odb_transaction_begin()` and contain\n> validation to avoid starting multiple transactions at the same time. The\n> \"files\" backend also has the same logic, but is redundant due to the\n> generic layer already handling it. Drop this validation from the \"files\"\n> backend accordingly.\n\nMakes sense, and it fixes a layering violation: in the best case, a\nsource only has to care about itself and not about the owning object\ndatabase. Managing object-database-level state should be done by the\nobject database itself.\n\nPatrick\n"},{"id":"547467","messageId":"ak3xHiOQfNxqFR58@pks.im","threadId":"65861","inReplyTo":"20260708041412.1157499-6-jltobler@gmail.com","subject":"Re: [PATCH v2 05/11] object-file: propagate files transaction errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-08T06:41:34Z","receivedAt":"2026-07-08T06:41:44Z","isPatch":true,"body":"On Tue, Jul 07, 2026 at 11:14:06PM -0500, Justin Tobler wrote:\n> diff --git a/object-file.c b/object-file.c\n> index e51389833a..64cb874fe7 100644\n> --- a/object-file.c\n> +++ b/object-file.c\n> @@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n>  \t * added at the time they call odb_transaction_files_begin.\n>  \t */\n>  \tif (!transaction || transaction->objdir)\n> -\t\treturn;\n> +\t\treturn 0;\n>  \n>  \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n> -\tif (transaction->objdir)\n> -\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n> +\tif (!transaction->objdir)\n> +\t\treturn -1;\n\nAs far as I can see we don't report any errors as part of\n`tmp_objdir_create()`, so we should probably print an error here.\n\n> @@ -1668,14 +1672,19 @@ static void odb_transaction_files_commit(struct odb_transaction *base)\n>  \t\t * Make the object files visible in the primary ODB after their data is\n>  \t\t * fully durable.\n>  \t\t */\n> -\t\ttmp_objdir_migrate(transaction->objdir);\n> +\t\tif (tmp_objdir_migrate(transaction->objdir))\n> +\t\t\treturn -1;\n> +\n\nLikewise.\n\nPatrick\n"},{"id":"547466","messageId":"ak3xJFDqTSN1Naoy@pks.im","threadId":"65861","inReplyTo":"20260708041412.1157499-7-jltobler@gmail.com","subject":"Re: [PATCH v2 06/11] odb/transaction: propagate begin errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-08T06:41:40Z","receivedAt":"2026-07-08T06:41:48Z","isPatch":true,"body":"On Tue, Jul 07, 2026 at 11:14:07PM -0500, Justin Tobler wrote:\n> When `odb_transaction_begin()` is invoked, the function returns the\n> transaction pointer directly. There is no way for the backend to\n> signal that it failed to set up its state, such as when creating the\n> temporary object directory backing the transaction.\n> \n> In a subsequent commit, git-receive-pack(1) starts using ODB\n> transactions and needs to be able to report such failures rather\n> than silently ignore them. Refactor `odb_transaction_begin()` to\n> return an int error code and write the resulting transaction into an\n> out parameter. Also introduce `odb_transaction_begin_or_die()` as a\n> convenience for callsites that do not need to handle errors\n> explicitly.\n> \n> Note that `odb_transaction_begin()` now returns an error when the ODB\n> already has an inflight transaction pending. ODB transaction call sites\n> that may encounter an inflight transaction are updated to explicitly\n> handle this case.\n\nYeah, this change is very much welcome and results in much saner\nbehaviour with less surprises. Thanks for making the change.\n\n> diff --git a/cache-tree.c b/cache-tree.c\n> index 184f7e2635..8eec1d4d52 100644\n> --- a/cache-tree.c\n> +++ b/cache-tree.c\n> @@ -474,6 +474,7 @@ static int update_one(struct cache_tree *it,\n>  \n>  int cache_tree_update(struct index_state *istate, int flags)\n>  {\n> +\tint inflight = !!the_repository->objects->transaction;\n>  \tstruct odb_transaction *transaction;\n>  \tint skip, i;\n>  \n> @@ -490,10 +491,12 @@ int cache_tree_update(struct index_state *istate, int flags)\n>  \n>  \ttrace_performance_enter();\n>  \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n> -\ttransaction = odb_transaction_begin(the_repository->objects);\n> +\tif (!inflight)\n> +\t\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n>  \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n>  \t\t       \"\", 0, &skip, flags);\n> -\todb_transaction_commit(transaction);\n> +\tif (!inflight)\n> +\t\todb_transaction_commit(transaction);\n>  \ttrace2_region_leave(\"cache_tree\", \"update\", istate->repo);\n>  \ttrace_performance_leave(\"cache_tree_update\");\n>  \tif (i < 0)\n\nCallsites like this really make me wonder why we even care to create\na transaction in the first place if we basically just commit it\nimmediately anyway. And while it's a bit sad that we have so many sites\nwhere we don't really know whether we even have a transaction, I think\nit's a good change that we have now annotated them clearly. A subsequent\npatch series may then eventually refactor those sites so that we stop\ndepending on `odb->transaction` and inject the transaction via a\nparameter.\n\n> diff --git a/odb/transaction.h b/odb/transaction.h\n> index d52f0533ce..36032a5365 100644\n> --- a/odb/transaction.h\n> +++ b/odb/transaction.h\n> @@ -1,6 +1,8 @@\n>  #ifndef ODB_TRANSACTION_H\n>  #define ODB_TRANSACTION_H\n>  \n> +#include \"git-compat-util.h\"\n\nWe typically don't include \"git-compat-util.h\" in header files.\n\n> @@ -36,11 +38,21 @@ struct odb_transaction {\n>  };\n>  \n>  /*\n> - * Starts an ODB transaction. Subsequent objects are written to the transaction\n> - * and not committed until odb_transaction_commit() is invoked on the\n> - * transaction. If the ODB already has a pending transaction, NULL is returned.\n> + * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n> + * written to the transaction and not committed until odb_transaction_commit()\n> + * is invoked on the transaction. Returns 0 on success and a negative value on\n> + * error. Note that it is considered an error to start a new transaction if the\n> + * ODB already has an inflight transaction pending.\n>   */\n> -struct odb_transaction *odb_transaction_begin(struct object_database *odb);\n> +int odb_transaction_begin(struct object_database *odb,\n> +\t\t\t  struct odb_transaction **out);\n> +\n> +static inline void odb_transaction_begin_or_die(struct object_database *odb,\n> +\t\t\t\t\t\tstruct odb_transaction **out)\n> +{\n> +\tif (odb_transaction_begin(odb, out))\n> +\t\tdie(_(\"failed to start ODB transaction\"));\n> +}\n\nWe could make it a bit simpler to use this function by continuing to\nreturn the transaction directly. But on the other hand this results in a\nmore consistent interface.\n\nPatrick\n"},{"id":"547468","messageId":"ak3xKlQTPP5OXYRn@pks.im","threadId":"65861","inReplyTo":"20260708041412.1157499-8-jltobler@gmail.com","subject":"Re: [PATCH v2 07/11] odb/transaction: propagate commit errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-08T06:41:46Z","receivedAt":"2026-07-08T06:41:53Z","isPatch":true,"body":"On Tue, Jul 07, 2026 at 11:14:08PM -0500, Justin Tobler wrote:\n> diff --git a/odb/transaction.c b/odb/transaction.c\n> index df4275151b..51af2c9a61 100644\n> --- a/odb/transaction.c\n> +++ b/odb/transaction.c\n> @@ -16,19 +16,26 @@ int odb_transaction_begin(struct object_database *odb,\n>  \treturn ret;\n>  }\n>  \n> -void odb_transaction_commit(struct odb_transaction *transaction)\n> +int odb_transaction_commit(struct odb_transaction *transaction)\n>  {\n> +\tint ret;\n> +\n>  \tif (!transaction)\n> -\t\treturn;\n> +\t\treturn 0;\n>  \n>  \t/*\n>  \t * Ensure the transaction ending matches the pending transaction.\n>  \t */\n>  \tASSERT(transaction == transaction->source->odb->transaction);\n>  \n> -\ttransaction->commit(transaction);\n> +\tret = transaction->commit(transaction);\n> +\tif (ret)\n> +\t\treturn ret;\n> +\n>  \ttransaction->source->odb->transaction = NULL;\n>  \tfree(transaction);\n> +\n> +\treturn 0;\n>  }\n\nDoesn't this cause a leak now?\n\nI think this interface here is doing the same mistake that our reference\ntransactions did, where we automatically released the transaction on\ncommit. That caused multiple lifetime issues with references all over\nthe place.\n\nThis isn't an issue introduced by this patch series though, so it's fine\nto ignore this for now.\n\nPatrick\n"},{"id":"547469","messageId":"ak3xMNbhQnGCMe2c@pks.im","threadId":"65861","inReplyTo":"20260708041412.1157499-10-jltobler@gmail.com","subject":"Re: [PATCH v2 09/11] odb/transaction: introduce ODB transaction flags","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-08T06:41:52Z","receivedAt":"2026-07-08T06:41:58Z","isPatch":true,"body":"On Tue, Jul 07, 2026 at 11:14:10PM -0500, Justin Tobler wrote:\n> diff --git a/object-file.c b/object-file.c\n> index 9b8ee6f36c..d95bdabba5 100644\n> --- a/object-file.c\n> +++ b/object-file.c\n> @@ -1710,6 +1712,27 @@ int odb_transaction_files_begin(struct odb_source *source,\n>  \ttransaction->base.commit = odb_transaction_files_commit;\n>  \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n>  \ttransaction->base.env = odb_transaction_files_env;\n> +\n> +\ttransaction->prefix = \"bulk-fsync\";\n> +\tif (flags & ODB_TRANSACTION_RECEIVE) {\n> +\t\t/*\n> +\t\t * ODB transactions for git-receive-pack(1) eagerly create a\n> +\t\t * temporary directory and use a different temporary directory\n> +\t\t * prefix.\n> +\t\t *\n> +\t\t * NEEDSWORK: This transaction flag is only used by the \"files\"\n> +\t\t * backend to special case temporary directory set up and\n> +\t\t * handling. Ideally transaction users should not have to care\n> +\t\t * though. To avoid this, we could eagerly create the temporary\n> +\t\t * directory and use the same prefix name for all transactions.\n> +\t\t */\n\nYup, agreed, thanks for noting this here.\n\n> diff --git a/odb/transaction.h b/odb/transaction.h\n> index 1c6c97a53e..b19f180aee 100644\n> --- a/odb/transaction.h\n> +++ b/odb/transaction.h\n> @@ -4,7 +4,6 @@\n>  #include \"git-compat-util.h\"\n>  #include \"gettext.h\"\n>  #include \"odb.h\"\n> -#include \"odb/source.h\"\n\nThis is curious, and likely a result of you adding \"odb/transaction.h\"\nto \"odb/source.h\".\n\n> @@ -45,6 +44,12 @@ struct odb_transaction {\n>  \tint (*env)(struct odb_transaction *transaction, struct strvec *env);\n>  };\n>  \n> +/* Flags used to configure an ODB transaction. */\n> +enum odb_transaction_flags {\n> +\t/* Configures the transaction for use with git-receive-pack(1). */\n> +\tODB_TRANSACTION_RECEIVE = (1 << 0),\n> +};\n> +\n>  /*\n>   * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n>   * written to the transaction and not committed until odb_transaction_commit()\n\nAnd this is the reason you have to add the include, so that the flags\nare visible in both \"odb/source.h\" and in \"odb/transaction.h\".\n\nThis makes me wonder whether there's really much value in having this\nheader here be split out of \"odb/source.h\".\n\nPatrick\n"},{"id":"547470","messageId":"ak3xNaHeMli-H1wW@pks.im","threadId":"65861","inReplyTo":"20260708041412.1157499-11-jltobler@gmail.com","subject":"Re: [PATCH v2 10/11] builtin/receive-pack: drop redundant tmpdir env","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-08T06:41:57Z","receivedAt":"2026-07-08T06:42:05Z","isPatch":true,"body":"On Tue, Jul 07, 2026 at 11:14:11PM -0500, Justin Tobler wrote:\n> When performing the connectivity checks for a shallow ref in\n> `update_shallow_ref()`, the child process environment variables are\n> populated via `tmp_objdir_env()`. This is unnecessary though as\n> `update_shallow_ref()` is only reached after `tmp_objdir_migrate()` has\n> been performed which means there is no longer a temporary directory that\n> needs to be shared with child processes.\n\nRight. We call it transitively via either `execute_commands_atomic()` or\n`execute_commands_not_atomic()`, both of which are called after\n`tmp_objdir_migrate()`.\n\nPatrick\n"},{"id":"547471","messageId":"ak3xYym22Z7PFZ5y@pks.im","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"Re: [PATCH v2 00/11] receive-pack: use ODB transactions to stage object writes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-08T06:42:43Z","receivedAt":"2026-07-08T06:42:49Z","isPatch":true,"body":"On Tue, Jul 07, 2026 at 11:14:01PM -0500, Justin Tobler wrote:\n> Changes since V1:\n> \n>   - Adapted other \"file\" ODB transaction helpers to be more consistent\n>     with current naming scheme.\n>   - Removed redundant NULL transaction handling from\n>     `odb_transaction_files_begin()`.\n>   - `odb_transaction_begin()` now returns an error if there is already\n>     an inflight transaction pending instead of setting the `out` pointer\n>     to NULL.\n>   - Updated `odb_transaction_env()` to return an error code and append\n>     environment variables to a strvec provided as an argument.\n>   - Removed redundant setting of tmpdir environment variables for child\n>     processes after tmpdir has been migrated.\n>   - Split changes adding ODB transaction flags into a separate commit.\n>   - Consistently wire the ODB transaction throughout git-receive-pack\n>     code instead of reading it from `the_repository`.\n>   - Updated user facing error message.\n>   - Updated some comments to better document functions/flags.\n>   - Clarified some commit messages.\n>   - Fixed typos.\n\nI've got a couple smaller nits, but overall I'm quite happy with the\nshape of this series now. Thanks!\n\nPatrick\n"},{"id":"547506","messageId":"ak5rFZOYdEQ3zRni@denethor","threadId":"65861","inReplyTo":"ak3xFCzvgj1-Ev_3@pks.im","subject":"Re: [PATCH v2 03/11] object-file: embed transaction flush logic in commit function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T16:08:10Z","receivedAt":"2026-07-08T16:08:15Z","isPatch":true,"body":"On 26/07/08 08:41AM, Patrick Steinhardt wrote:\n> On Tue, Jul 07, 2026 at 11:14:04PM -0500, Justin Tobler wrote:\n> > When a \"files\" transaction is committed,\n> > `flush_loose_object_transaction()` is invoked to handle performing a\n> > hardware flush along with migrating the temporary object directory into\n> > the primary. In a subsequent commit, the temporary directory is also\n> > used to write packfiles.\n> > \n> > Instead of maintaining a separate helper function, embed the logic to\n> > flush and migrate the temporary directory directly into\n> > `odb_transaction_files_commit()`.\n> \n> The change itself looks simple enough, but this makes me wonder why we\n> want to change this. Like, what subsequent step does this enable that\n> would otherwise be harder to do?\n\nOriginally, I was planning to rename both\n`{fsync,flush}_loose_object_transaction()` to\n`odb_transaction_files_{fsync,flush}()` respectively. For the flush\nhelper though, it's doing more than just \"flushing\" the temporary\ndirectory files; it's also migrating the objects to the primary ODB and\nreconfiguring the repository ODB sources accordingly.\n\nThis is really what you think of happening during the commit phase.\nConsequently, it made more sense to me organizationally to just embed\nthe logic `odb_transaction_files_commit()`.\n\n> Maybe this will be answered by a subsequent commit.\n\nWill update the commit message to properly explain this.\n\n-Justin\n"},{"id":"547508","messageId":"ak54WpyT2QRMtfa1@denethor","threadId":"65861","inReplyTo":"ak3xHiOQfNxqFR58@pks.im","subject":"Re: [PATCH v2 05/11] object-file: propagate files transaction errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T16:21:48Z","receivedAt":"2026-07-08T16:21:52Z","isPatch":true,"body":"On 26/07/08 08:41AM, Patrick Steinhardt wrote:\n> On Tue, Jul 07, 2026 at 11:14:06PM -0500, Justin Tobler wrote:\n> > diff --git a/object-file.c b/object-file.c\n> > index e51389833a..64cb874fe7 100644\n> > --- a/object-file.c\n> > +++ b/object-file.c\n> > @@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n> >  \t * added at the time they call odb_transaction_files_begin.\n> >  \t */\n> >  \tif (!transaction || transaction->objdir)\n> > -\t\treturn;\n> > +\t\treturn 0;\n> >  \n> >  \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n> > -\tif (transaction->objdir)\n> > -\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n> > +\tif (!transaction->objdir)\n> > +\t\treturn -1;\n> \n> As far as I can see we don't report any errors as part of\n> `tmp_objdir_create()`, so we should probably print an error here.\n> \n> > @@ -1668,14 +1672,19 @@ static void odb_transaction_files_commit(struct odb_transaction *base)\n> >  \t\t * Make the object files visible in the primary ODB after their data is\n> >  \t\t * fully durable.\n> >  \t\t */\n> > -\t\ttmp_objdir_migrate(transaction->objdir);\n> > +\t\tif (tmp_objdir_migrate(transaction->objdir))\n> > +\t\t\treturn -1;\n> > +\n> \n> Likewise.\n\nYa, printing some error messages seems like a good idea here. Will do so\nin the next version.\n\n-Justin\n"},{"id":"547510","messageId":"ak57VEF56HkRKygQ@denethor","threadId":"65861","inReplyTo":"ak3xJFDqTSN1Naoy@pks.im","subject":"Re: [PATCH v2 06/11] odb/transaction: propagate begin errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T16:56:43Z","receivedAt":"2026-07-08T16:56:47Z","isPatch":true,"body":"On 26/07/08 08:41AM, Patrick Steinhardt wrote:\n> On Tue, Jul 07, 2026 at 11:14:07PM -0500, Justin Tobler wrote:\n> > @@ -490,10 +491,12 @@ int cache_tree_update(struct index_state *istate, int flags)\n> >  \n> >  \ttrace_performance_enter();\n> >  \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n> > -\ttransaction = odb_transaction_begin(the_repository->objects);\n> > +\tif (!inflight)\n> > +\t\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n> >  \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n> >  \t\t       \"\", 0, &skip, flags);\n> > -\todb_transaction_commit(transaction);\n> > +\tif (!inflight)\n> > +\t\todb_transaction_commit(transaction);\n> >  \ttrace2_region_leave(\"cache_tree\", \"update\", istate->repo);\n> >  \ttrace_performance_leave(\"cache_tree_update\");\n> >  \tif (i < 0)\n> \n> Callsites like this really make me wonder why we even care to create\n> a transaction in the first place if we basically just commit it\n> immediately anyway. And while it's a bit sad that we have so many sites\n> where we don't really know whether we even have a transaction, I think\n> it's a good change that we have now annotated them clearly. A subsequent\n> patch series may then eventually refactor those sites so that we stop\n> depending on `odb->transaction` and inject the transaction via a\n> parameter.\n\nCall sites like the one mentioned above are using ODB transactions as an\noptimization to batch the full fsyncs in bulk. In cases where the is not\nalready a transaction, they start one to take advantage of it.\n\nI fully agree though that an ODB transaction should ideally be started\nat a higher layer and wired down to these call sites. I have a couple of\npatches in my tree that start to tackle this which I plan to send in\nanother series. :)\n\n> > diff --git a/odb/transaction.h b/odb/transaction.h\n> > index d52f0533ce..36032a5365 100644\n> > --- a/odb/transaction.h\n> > +++ b/odb/transaction.h\n> > @@ -1,6 +1,8 @@\n> >  #ifndef ODB_TRANSACTION_H\n> >  #define ODB_TRANSACTION_H\n> >  \n> > +#include \"git-compat-util.h\"\n> \n> We typically don't include \"git-compat-util.h\" in header files.\n\nWill remove. Thanks\n\n> > @@ -36,11 +38,21 @@ struct odb_transaction {\n> >  };\n> >  \n> >  /*\n> > - * Starts an ODB transaction. Subsequent objects are written to the transaction\n> > - * and not committed until odb_transaction_commit() is invoked on the\n> > - * transaction. If the ODB already has a pending transaction, NULL is returned.\n> > + * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n> > + * written to the transaction and not committed until odb_transaction_commit()\n> > + * is invoked on the transaction. Returns 0 on success and a negative value on\n> > + * error. Note that it is considered an error to start a new transaction if the\n> > + * ODB already has an inflight transaction pending.\n> >   */\n> > -struct odb_transaction *odb_transaction_begin(struct object_database *odb);\n> > +int odb_transaction_begin(struct object_database *odb,\n> > +\t\t\t  struct odb_transaction **out);\n> > +\n> > +static inline void odb_transaction_begin_or_die(struct object_database *odb,\n> > +\t\t\t\t\t\tstruct odb_transaction **out)\n> > +{\n> > +\tif (odb_transaction_begin(odb, out))\n> > +\t\tdie(_(\"failed to start ODB transaction\"));\n> > +}\n> \n> We could make it a bit simpler to use this function by continuing to\n> return the transaction directly. But on the other hand this results in a\n> more consistent interface.\n\nYa, I was a bit back and forth about this myself. I ultimately landed on\nkeeping a more consistent interface though. Happy to change if others\nfeel differently though.\n\n-Justin\n"},{"id":"547512","messageId":"ak6FHKdfPyi4_DLX@denethor","threadId":"65861","inReplyTo":"ak3xKlQTPP5OXYRn@pks.im","subject":"Re: [PATCH v2 07/11] odb/transaction: propagate commit errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T17:24:04Z","receivedAt":"2026-07-08T17:24:11Z","isPatch":true,"body":"On 26/07/08 08:41AM, Patrick Steinhardt wrote:\n> On Tue, Jul 07, 2026 at 11:14:08PM -0500, Justin Tobler wrote:\n> > diff --git a/odb/transaction.c b/odb/transaction.c\n> > index df4275151b..51af2c9a61 100644\n> > --- a/odb/transaction.c\n> > +++ b/odb/transaction.c\n> > @@ -16,19 +16,26 @@ int odb_transaction_begin(struct object_database *odb,\n> >  \treturn ret;\n> >  }\n> >  \n> > -void odb_transaction_commit(struct odb_transaction *transaction)\n> > +int odb_transaction_commit(struct odb_transaction *transaction)\n> >  {\n> > +\tint ret;\n> > +\n> >  \tif (!transaction)\n> > -\t\treturn;\n> > +\t\treturn 0;\n> >  \n> >  \t/*\n> >  \t * Ensure the transaction ending matches the pending transaction.\n> >  \t */\n> >  \tASSERT(transaction == transaction->source->odb->transaction);\n> >  \n> > -\ttransaction->commit(transaction);\n> > +\tret = transaction->commit(transaction);\n> > +\tif (ret)\n> > +\t\treturn ret;\n> > +\n> >  \ttransaction->source->odb->transaction = NULL;\n> >  \tfree(transaction);\n> > +\n> > +\treturn 0;\n> >  }\n> \n> Doesn't this cause a leak now?\n\nGood call. Ya, if odb_transaction_commit() fails, we don't free the\ntransaction. In the next version I'll go ahead and clear the transaction\nif we fail.\n\n> I think this interface here is doing the same mistake that our reference\n> transactions did, where we automatically released the transaction on\n> commit. That caused multiple lifetime issues with references all over\n> the place.\n\nYa, it probaby makes sense to introduce a separate\n`odb_transaction_release()` function to make this explicit and update\ncallers accordingly. I have another series I working on that introduces\n`odb_transaction_abort()`. This might be a good place to add it in too.\n\n-Justin\n"},{"id":"547514","messageId":"ak6IGmwhoJKLrrlr@denethor","threadId":"65861","inReplyTo":"ak3xMNbhQnGCMe2c@pks.im","subject":"Re: [PATCH v2 09/11] odb/transaction: introduce ODB transaction flags","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T17:34:27Z","receivedAt":"2026-07-08T17:34:31Z","isPatch":true,"body":"On 26/07/08 08:41AM, Patrick Steinhardt wrote:\n> On Tue, Jul 07, 2026 at 11:14:10PM -0500, Justin Tobler wrote:\n> > +/* Flags used to configure an ODB transaction. */\n> > +enum odb_transaction_flags {\n> > +\t/* Configures the transaction for use with git-receive-pack(1). */\n> > +\tODB_TRANSACTION_RECEIVE = (1 << 0),\n> > +};\n> > +\n> >  /*\n> >   * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n> >   * written to the transaction and not committed until odb_transaction_commit()\n> \n> And this is the reason you have to add the include, so that the flags\n> are visible in both \"odb/source.h\" and in \"odb/transaction.h\".\n> \n> This makes me wonder whether there's really much value in having this\n> header here be split out of \"odb/source.h\".\n\nYa, I've started wondering the same thing. A transaction implementation\nis always going to be tightly coupled to the ODB source it pertains too.\nIt probably makes sense to merge \"odb/transaction.{c,h}\" with\n\"odb/source.{c,h}\". I'll leave it as-is for now and likely explore this\nis a future series though.\n\n-Justin\n"},{"id":"547515","messageId":"ak6KkcSiMR_XpxfZ@denethor","threadId":"65861","inReplyTo":"ak3xYym22Z7PFZ5y@pks.im","subject":"Re: [PATCH v2 00/11] receive-pack: use ODB transactions to stage object writes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T17:36:48Z","receivedAt":"2026-07-08T17:36:50Z","isPatch":true,"body":"On 26/07/08 08:42AM, Patrick Steinhardt wrote:\n> On Tue, Jul 07, 2026 at 11:14:01PM -0500, Justin Tobler wrote:\n> > Changes since V1:\n> > \n> >   - Adapted other \"file\" ODB transaction helpers to be more consistent\n> >     with current naming scheme.\n> >   - Removed redundant NULL transaction handling from\n> >     `odb_transaction_files_begin()`.\n> >   - `odb_transaction_begin()` now returns an error if there is already\n> >     an inflight transaction pending instead of setting the `out` pointer\n> >     to NULL.\n> >   - Updated `odb_transaction_env()` to return an error code and append\n> >     environment variables to a strvec provided as an argument.\n> >   - Removed redundant setting of tmpdir environment variables for child\n> >     processes after tmpdir has been migrated.\n> >   - Split changes adding ODB transaction flags into a separate commit.\n> >   - Consistently wire the ODB transaction throughout git-receive-pack\n> >     code instead of reading it from `the_repository`.\n> >   - Updated user facing error message.\n> >   - Updated some comments to better document functions/flags.\n> >   - Clarified some commit messages.\n> >   - Fixed typos.\n> \n> I've got a couple smaller nits, but overall I'm quite happy with the\n> shape of this series now. Thanks!\n\nThanks for the review! I'll send another version later today. :)\n\n-Justin\n"},{"id":"547539","messageId":"20260708235925.3992097-1-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708041412.1157499-1-jltobler@gmail.com","subject":"[PATCH v3 00/11] receive-pack: use ODB transactions to stage object writes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:14Z","receivedAt":"2026-07-08T23:59:31Z","isPatch":true,"body":"Greetings,\n\nThis patch series replaces direct usage of the `tmp_objdir` interfaces\nin git-receive-pack(1) to instead use the `odb_transaction` interfaces\nto create/manage a staging area to write objects to. The purpose of this\nchange is to get git-receive-pack(1) one step closer to being ODB\nbackend agnostic. For now, the object writes themselves are still\n\"files\" backend specific due to being handled by the git-index-pack(1)\nand git-unpack-objects(1) child processes. This will be tackled in a\nseparate series though.\n\nChanges since V2:\n  - Clarified commit log reasoning for embedding\n    `flush_loose_object_transaction()` logic in commit function.\n  - Started printed some error messages on transaction errors.\n  - Removed include statement.\n  - Fixed transaction leak on `odb_transaction_commit()` error.\n\nChanges since V1:\n  - Adapted other \"file\" ODB transaction helpers to be more consistent\n    with current naming scheme.\n  - Removed redundant NULL transaction handling from\n    `odb_transaction_files_begin()`.\n  - `odb_transaction_begin()` now returns an error if there is already\n    an inflight transaction pending instead of setting the `out` pointer\n    to NULL.\n  - Updated `odb_transaction_env()` to return an error code and append\n    environment variables to a strvec provided as an argument.\n  - Removed redundant setting of tmpdir environment variables for child\n    processes after tmpdir has been migrated.\n  - Split changes adding ODB transaction flags into a separate commit.\n  - Consistently wire the ODB transaction throughout git-receive-pack\n    code instead of reading it from `the_repository`.\n  - Updated user facing error message.\n  - Updated some comments to better document functions/flags.\n  - Clarified some commit messages.\n  - Fixed typos.\n\nThanks,\n-Justin\n\nJustin Tobler (11):\n  object-file: rename files transaction prepare function\n  object-file: rename files transaction fsync function\n  object-file: embed transaction flush logic in commit function\n  object-file: drop check for inflight transactions\n  object-file: propagate files transaction errors\n  odb/transaction: propagate begin errors\n  odb/transaction: propagate commit errors\n  odb/transaction: add transaction env interface\n  odb/transaction: introduce ODB transaction flags\n  builtin/receive-pack: drop redundant tmpdir env\n  builtin/receive-pack: stage incoming objects via ODB transactions\n\n builtin/add.c            |   2 +-\n builtin/receive-pack.c   |  69 ++++++++---------\n builtin/unpack-objects.c |   2 +-\n builtin/update-index.c   |   2 +-\n cache-tree.c             |   7 +-\n object-file.c            | 159 +++++++++++++++++++++++++--------------\n object-file.h            |   8 +-\n odb/source-files.c       |   9 +--\n odb/source-inmemory.c    |   3 +-\n odb/source-loose.c       |   3 +-\n odb/source.h             |   9 ++-\n odb/transaction.c        |  32 ++++++--\n odb/transaction.h        |  59 ++++++++++++---\n read-cache.c             |   7 +-\n 14 files changed, 241 insertions(+), 130 deletions(-)\n\nRange-diff against v2:\n 1:  9c14b219ad =  1:  9c14b219ad object-file: rename files transaction prepare function\n 2:  5703a9e93b =  2:  5703a9e93b object-file: rename files transaction fsync function\n 3:  4c37398ac8 !  3:  76204847f2 object-file: embed transaction flush logic in commit function\n    @@ Commit message\n         When a \"files\" transaction is committed,\n         `flush_loose_object_transaction()` is invoked to handle performing a\n         hardware flush along with migrating the temporary object directory into\n    -    the primary. In a subsequent commit, the temporary directory is also\n    -    used to write packfiles.\n    +    the primary and configuring the repository ODB source accordingly. The\n    +    function name here is a bit misleading because the helper is doing a bit\n    +    more than just \"flushing\" the transaction contents. Also, in a\n    +    subsequent commit, the transaction temporary directory is used to stage\n    +    packfiles and not just loose objects anymore.\n     \n    -    Instead of maintaining a separate helper function, embed the logic to\n    -    flush and migrate the temporary directory directly into\n    -    `odb_transaction_files_commit()`.\n    +    Lift the helper function logic directly into\n    +    `odb_transaction_files_commit()` to more accurately signal to readers\n    +    the operation being performed.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n 4:  623c6b02ea =  4:  c97eb7763f object-file: drop check for inflight transactions\n 5:  ca59176657 !  5:  1f3a1f7714 object-file: propagate files transaction errors\n    @@ object-file.c: static void odb_transaction_files_prepare(struct odb_transaction\n     -\tif (transaction->objdir)\n     -\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n     +\tif (!transaction->objdir)\n    -+\t\treturn -1;\n    ++\t\treturn error(_(\"unable to create temporary object directory\"));\n     +\n     +\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n     +\n    @@ object-file.c: static void odb_transaction_files_commit(struct odb_transaction *\n      \t\t */\n     -\t\ttmp_objdir_migrate(transaction->objdir);\n     +\t\tif (tmp_objdir_migrate(transaction->objdir))\n    -+\t\t\treturn -1;\n    ++\t\t\treturn error(_(\"unable to migrate temporary objects\"));\n     +\n      \t\ttransaction->objdir = NULL;\n      \t}\n 6:  717a1ce9a7 !  6:  09d13272d5 odb/transaction: propagate begin errors\n    @@ object-file.c: int index_fd(struct index_state *istate, struct object_id *oid,\n     \n      ## odb/transaction.c ##\n     @@\n    + #include \"git-compat-util.h\"\n    ++#include \"gettext.h\"\n      #include \"odb/source.h\"\n      #include \"odb/transaction.h\"\n      \n    @@ odb/transaction.c\n     +\n      \tif (odb->transaction)\n     -\t\treturn NULL;\n    -+\t\treturn -1;\n    ++\t\treturn error(_(\"object database transaction already pending\"));\n      \n     -\todb_source_begin_transaction(odb->sources, &odb->transaction);\n     +\tret = odb_source_begin_transaction(odb->sources, out);\n    @@ odb/transaction.h\n      #ifndef ODB_TRANSACTION_H\n      #define ODB_TRANSACTION_H\n      \n    -+#include \"git-compat-util.h\"\n     +#include \"gettext.h\"\n      #include \"odb.h\"\n      #include \"odb/source.h\"\n 7:  ff8e133965 !  7:  12833d6773 odb/transaction: propagate commit errors\n    @@ odb/transaction.c: int odb_transaction_begin(struct object_database *odb,\n      \n     -\ttransaction->commit(transaction);\n     +\tret = transaction->commit(transaction);\n    -+\tif (ret)\n    -+\t\treturn ret;\n    -+\n      \ttransaction->source->odb->transaction = NULL;\n      \tfree(transaction);\n     +\n    -+\treturn 0;\n    ++\treturn ret;\n      }\n      \n      int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n 8:  264ba94b83 =  8:  f2586f2f34 odb/transaction: add transaction env interface\n 9:  1e0a491ef2 !  9:  9d082b5e47 odb/transaction: introduce ODB transaction flags\n    @@ object-file.c: static int odb_transaction_files_prepare(struct odb_transaction *\n     -\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n     +\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, transaction->prefix);\n      \tif (!transaction->objdir)\n    - \t\treturn -1;\n    + \t\treturn error(_(\"unable to create temporary object directory\"));\n      \n     @@ object-file.c: int index_fd(struct index_state *istate, struct object_id *oid,\n      \t\t\tint inflight = !!transaction;\n    @@ odb/transaction.c\n      \tint ret;\n      \n      \tif (odb->transaction)\n    - \t\treturn -1;\n    + \t\treturn error(_(\"object database transaction already pending\"));\n      \n     -\tret = odb_source_begin_transaction(odb->sources, out);\n     +\tret = odb_source_begin_transaction(odb->sources, out, flags);\n    @@ odb/transaction.c\n     \n      ## odb/transaction.h ##\n     @@\n    - #include \"git-compat-util.h\"\n    + \n      #include \"gettext.h\"\n      #include \"odb.h\"\n     -#include \"odb/source.h\"\n10:  6c8d878349 = 10:  e11d8a6676 builtin/receive-pack: drop redundant tmpdir env\n11:  8db95fef56 = 11:  fee57c2817 builtin/receive-pack: stage incoming objects via ODB transactions\n\nbase-commit: ab776a62a78576513ee121424adb19597fbb7613\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547540","messageId":"20260708235925.3992097-2-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v3 01/11] object-file: rename files transaction prepare function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:15Z","receivedAt":"2026-07-08T23:59:32Z","isPatch":true,"body":"The \"files\" ODB transaction backend lazily creates a temporary object\ndirectory when the first loose object is written to the transaction via\n`prepare_loose_object_transaction()`. In a subsequent commit, the\ntemporary directory is used to also write packfiles to.\n\nRename the function to `odb_transaction_files_prepare()` accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex e3d92bbda2..a3eb8d71dd 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -499,7 +499,7 @@ struct odb_transaction_files {\n \tstruct transaction_packfile packfile;\n };\n \n-static void prepare_loose_object_transaction(struct odb_transaction *base)\n+static void 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@@ -761,7 +761,7 @@ int write_loose_object(struct odb_source_loose *loose,\n \tstatic struct strbuf filename = STRBUF_INIT;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\n+\t\todb_transaction_files_prepare(loose->base.odb->transaction);\n \n \todb_loose_path(loose, &filename, oid);\n \n@@ -825,7 +825,7 @@ int odb_source_loose_write_stream(struct odb_source_loose *loose,\n \tint hdrlen;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\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-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547541","messageId":"20260708235925.3992097-3-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v3 02/11] object-file: rename files transaction fsync function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:16Z","receivedAt":"2026-07-08T23:59:33Z","isPatch":true,"body":"When writing an object to a \"files\" ODB transaction, a full hardware\nflush is not initially performed during the fsync in\n`fsync_loose_object_transaction()` and instead delayed until the\ntransaction is later committed.\n\nTo be more consistent with other \"files\" ODB transaction helpers, rename\nthe function to `odb_transaction_files_fsync()` accordingly. The\nconditional in the helper is also slightly restructured to improve\nclarity to readers.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 16 ++++++++++------\n 1 file changed, 10 insertions(+), 6 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex a3eb8d71dd..d68824bb44 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -518,12 +518,17 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n \t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n }\n \n-static void fsync_loose_object_transaction(struct odb_transaction *base,\n-\t\t\t\t\t   int fd, const char *filename)\n+static void odb_transaction_files_fsync(struct odb_transaction *base,\n+\t\t\t\t\tint fd, const char *filename)\n {\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of_or_null(base, struct odb_transaction_files, base);\n \n+\tif (!transaction || !transaction->objdir) {\n+\t\tfsync_or_die(fd, filename);\n+\t\treturn;\n+\t}\n+\n \t/*\n \t * If we have an active ODB transaction, we issue a call that\n \t * cleans the filesystem page cache but avoids a hardware flush\n@@ -531,8 +536,7 @@ static void fsync_loose_object_transaction(struct odb_transaction *base,\n \t * before renaming the objects to their final names as part of\n \t * flush_batch_fsync.\n \t */\n-\tif (!transaction || !transaction->objdir ||\n-\t    git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) {\n+\tif (git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) {\n \t\tif (errno == ENOSYS)\n \t\t\twarning(_(\"core.fsyncMethod = batch is unsupported on this platform\"));\n \t\tfsync_or_die(fd, filename);\n@@ -553,7 +557,7 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac\n \t/*\n \t * Issue a full hardware flush against a temporary file to ensure\n \t * that all objects are durable before any renames occur. The code in\n-\t * fsync_loose_object_transaction has already issued a writeout\n+\t * odb_transaction_files_fsync has already issued a writeout\n \t * request, but it has not flushed any writeback cache in the storage\n \t * hardware or any filesystem logs. This fsync call acts as a barrier\n \t * to ensure that the data in each new object file is durable before\n@@ -582,7 +586,7 @@ static void close_loose_object(struct odb_source_loose *loose,\n \t\tgoto out;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tfsync_loose_object_transaction(loose->base.odb->transaction, fd, filename);\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-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547542","messageId":"20260708235925.3992097-4-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v3 03/11] object-file: embed transaction flush logic in commit function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:17Z","receivedAt":"2026-07-08T23:59:34Z","isPatch":true,"body":"When a \"files\" transaction is committed,\n`flush_loose_object_transaction()` is invoked to handle performing a\nhardware flush along with migrating the temporary object directory into\nthe primary and configuring the repository ODB source accordingly. The\nfunction name here is a bit misleading because the helper is doing a bit\nmore than just \"flushing\" the transaction contents. Also, in a\nsubsequent commit, the transaction temporary directory is used to stage\npackfiles and not just loose objects anymore.\n\nLift the helper function logic directly into\n`odb_transaction_files_commit()` to more accurately signal to readers\nthe operation being performed.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 64 ++++++++++++++++++++++-----------------------------\n 1 file changed, 28 insertions(+), 36 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex d68824bb44..33bd6c6810 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -543,41 +543,6 @@ static void odb_transaction_files_fsync(struct odb_transaction *base,\n \t}\n }\n \n-/*\n- * Cleanup after batch-mode fsync_object_files.\n- */\n-static void flush_loose_object_transaction(struct odb_transaction_files *transaction)\n-{\n-\tstruct strbuf temp_path = STRBUF_INIT;\n-\tstruct tempfile *temp;\n-\n-\tif (!transaction->objdir)\n-\t\treturn;\n-\n-\t/*\n-\t * Issue a full hardware flush against a temporary file to ensure\n-\t * that all objects are durable before any renames occur. The code in\n-\t * odb_transaction_files_fsync has already issued a writeout\n-\t * request, but it has not flushed any writeback cache in the storage\n-\t * hardware or any filesystem logs. This fsync call acts as a barrier\n-\t * to ensure that the data in each new object file is durable before\n-\t * the final name is visible.\n-\t */\n-\tstrbuf_addf(&temp_path, \"%s/bulk_fsync_XXXXXX\",\n-\t\t    repo_get_object_directory(transaction->base.source->odb->repo));\n-\ttemp = xmks_tempfile(temp_path.buf);\n-\tfsync_or_die(get_tempfile_fd(temp), get_tempfile_path(temp));\n-\tdelete_tempfile(&temp);\n-\tstrbuf_release(&temp_path);\n-\n-\t/*\n-\t * Make the object files visible in the primary ODB after their data is\n-\t * fully durable.\n-\t */\n-\ttmp_objdir_migrate(transaction->objdir);\n-\ttransaction->objdir = NULL;\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@@ -1679,7 +1644,34 @@ static void odb_transaction_files_commit(struct odb_transaction *base)\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of(base, struct odb_transaction_files, base);\n \n-\tflush_loose_object_transaction(transaction);\n+\tif (transaction->objdir) {\n+\t\tstruct strbuf temp_path = STRBUF_INIT;\n+\t\tstruct tempfile *temp;\n+\n+\t\t/*\n+\t\t * Issue a full hardware flush against a temporary file to ensure\n+\t\t * that all objects are durable before any renames occur. The code in\n+\t\t * odb_transaction_files_fsync has already issued a writeout\n+\t\t * request, but it has not flushed any writeback cache in the storage\n+\t\t * hardware or any filesystem logs. This fsync call acts as a barrier\n+\t\t * to ensure that the data in each new object file is durable before\n+\t\t * the final name is visible.\n+\t\t */\n+\t\tstrbuf_addf(&temp_path, \"%s/bulk_fsync_XXXXXX\",\n+\t\t\t    repo_get_object_directory(transaction->base.source->odb->repo));\n+\t\ttemp = xmks_tempfile(temp_path.buf);\n+\t\tfsync_or_die(get_tempfile_fd(temp), get_tempfile_path(temp));\n+\t\tdelete_tempfile(&temp);\n+\t\tstrbuf_release(&temp_path);\n+\n+\t\t/*\n+\t\t * Make the object files visible in the primary ODB after their data is\n+\t\t * fully durable.\n+\t\t */\n+\t\ttmp_objdir_migrate(transaction->objdir);\n+\t\ttransaction->objdir = NULL;\n+\t}\n+\n \tflush_packfile_transaction(transaction);\n }\n \n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547543","messageId":"20260708235925.3992097-5-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v3 04/11] object-file: drop check for inflight transactions","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:18Z","receivedAt":"2026-07-08T23:59:35Z","isPatch":true,"body":"ODB transactions are started via `odb_transaction_begin()` and contain\nvalidation to avoid starting multiple transactions at the same time. The\n\"files\" backend also has the same logic, but is redundant due to the\ngeneric layer already handling it. Drop this validation from the \"files\"\nbackend accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 4 ----\n object-file.h | 3 +--\n 2 files changed, 1 insertion(+), 6 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 33bd6c6810..e51389833a 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1678,10 +1678,6 @@ static void odb_transaction_files_commit(struct odb_transaction *base)\n struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n {\n \tstruct odb_transaction_files *transaction;\n-\tstruct object_database *odb = source->odb;\n-\n-\tif (odb->transaction)\n-\t\treturn NULL;\n \n \ttransaction = xcalloc(1, sizeof(*transaction));\n \ttransaction->base.source = source;\ndiff --git a/object-file.h b/object-file.h\nindex 528c4e6e69..ea43d818f0 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -194,8 +194,7 @@ struct odb_transaction;\n /*\n  * Tell the object database to optimize for adding\n  * multiple objects. odb_transaction_files_commit must be called\n- * to make new objects visible. If a transaction is already\n- * pending, NULL is returned.\n+ * to make new objects visible.\n  */\n struct odb_transaction *odb_transaction_files_begin(struct odb_source *source);\n \n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547544","messageId":"20260708235925.3992097-6-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v3 05/11] object-file: propagate files transaction errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:19Z","receivedAt":"2026-07-08T23:59:36Z","isPatch":true,"body":"The \"files\" transaction backend may encounter errors related to managing\nthe temporary directory used to stage objects, but silently ignores\nthese errors. Instead return errors encountered in the\n`odb_transaction_files_{prepare,begin,commit}()` interfaces to allow\ncallers to handle them as needed.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c      | 26 ++++++++++++++++++--------\n object-file.h      |  3 ++-\n odb/source-files.c |  6 +-----\n odb/transaction.h  |  7 +++++--\n 4 files changed, 26 insertions(+), 16 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex e51389833a..3651605ea2 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -499,7 +499,7 @@ struct odb_transaction_files {\n \tstruct transaction_packfile packfile;\n };\n \n-static void odb_transaction_files_prepare(struct odb_transaction *base)\n+static 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@@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n \t * added at the time they call odb_transaction_files_begin.\n \t */\n \tif (!transaction || transaction->objdir)\n-\t\treturn;\n+\t\treturn 0;\n \n \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n-\tif (transaction->objdir)\n-\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n+\tif (!transaction->objdir)\n+\t\treturn error(_(\"unable to create temporary object directory\"));\n+\n+\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n+\n+\treturn 0;\n }\n \n static void odb_transaction_files_fsync(struct odb_transaction *base,\n@@ -1639,7 +1643,7 @@ int read_loose_object(struct repository *repo,\n \treturn ret;\n }\n \n-static void odb_transaction_files_commit(struct odb_transaction *base)\n+static int odb_transaction_files_commit(struct odb_transaction *base)\n {\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of(base, struct odb_transaction_files, base);\n@@ -1668,14 +1672,19 @@ static void odb_transaction_files_commit(struct odb_transaction *base)\n \t\t * Make the object files visible in the primary ODB after their data is\n \t\t * fully durable.\n \t\t */\n-\t\ttmp_objdir_migrate(transaction->objdir);\n+\t\tif (tmp_objdir_migrate(transaction->objdir))\n+\t\t\treturn error(_(\"unable to migrate temporary objects\"));\n+\n \t\ttransaction->objdir = NULL;\n \t}\n \n \tflush_packfile_transaction(transaction);\n+\n+\treturn 0;\n }\n \n-struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n+int odb_transaction_files_begin(struct odb_source *source,\n+\t\t\t\tstruct odb_transaction **out)\n {\n \tstruct odb_transaction_files *transaction;\n \n@@ -1683,6 +1692,7 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\t*out = &transaction->base;\n \n-\treturn &transaction->base;\n+\treturn 0;\n }\ndiff --git a/object-file.h b/object-file.h\nindex ea43d818f0..1a023226ac 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -196,6 +196,7 @@ struct odb_transaction;\n  * multiple objects. odb_transaction_files_commit must be called\n  * to make new objects visible.\n  */\n-struct odb_transaction *odb_transaction_files_begin(struct odb_source *source);\n+int odb_transaction_files_begin(struct odb_source *source,\n+\t\t\t\tstruct odb_transaction **out);\n \n #endif /* OBJECT_FILE_H */\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 5bdd042922..2545bd81d4 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -182,11 +182,7 @@ static int odb_source_files_write_object_stream(struct odb_source *source,\n static int odb_source_files_begin_transaction(struct odb_source *source,\n \t\t\t\t\t      struct odb_transaction **out)\n {\n-\tstruct odb_transaction *tx = odb_transaction_files_begin(source);\n-\tif (!tx)\n-\t\treturn -1;\n-\t*out = tx;\n-\treturn 0;\n+\treturn odb_transaction_files_begin(source, out);\n }\n \n static int odb_source_files_read_alternates(struct odb_source *source,\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 854fda06f5..d52f0533ce 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -16,8 +16,11 @@ struct odb_transaction {\n \t/* The ODB source the transaction is opened against. */\n \tstruct odb_source *source;\n \n-\t/* The ODB source specific callback invoked to commit a transaction. */\n-\tvoid (*commit)(struct odb_transaction *transaction);\n+\t/*\n+\t * The ODB source specific callback invoked to commit a transaction.\n+\t * Returns 0 on success, a negative error code otherwise.\n+\t */\n+\tint (*commit)(struct odb_transaction *transaction);\n \n \t/*\n \t * This callback is expected to write the given object stream into\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547545","messageId":"20260708235925.3992097-7-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v3 06/11] odb/transaction: propagate begin errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:20Z","receivedAt":"2026-07-08T23:59:37Z","isPatch":true,"body":"When `odb_transaction_begin()` is invoked, the function returns the\ntransaction pointer directly. There is no way for the backend to\nsignal that it failed to set up its state, such as when creating the\ntemporary object directory backing the transaction.\n\nIn a subsequent commit, git-receive-pack(1) starts using ODB\ntransactions and needs to be able to report such failures rather\nthan silently ignore them. Refactor `odb_transaction_begin()` to\nreturn an int error code and write the resulting transaction into an\nout parameter. Also introduce `odb_transaction_begin_or_die()` as a\nconvenience for callsites that do not need to handle errors\nexplicitly.\n\nNote that `odb_transaction_begin()` now returns an error when the ODB\nalready has an inflight transaction pending. ODB transaction call sites\nthat may encounter an inflight transaction are updated to explicitly\nhandle this case.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  2 +-\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  2 +-\n cache-tree.c             |  7 +++++--\n object-file.c            | 10 +++++++---\n odb/transaction.c        | 13 +++++++++----\n odb/transaction.h        | 19 +++++++++++++++----\n read-cache.c             |  7 +++++--\n 8 files changed, 44 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex c859f66519..3d5d9cfdb9 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -581,7 +581,7 @@ int cmd_add(int argc,\n \t\tstring_list_clear(&only_match_skip_worktree, 0);\n \t}\n \n-\ttransaction = odb_transaction_begin(repo->objects);\n+\todb_transaction_begin_or_die(repo->objects, &transaction);\n \n \tps_matched = xcalloc(pathspec.nr, 1);\n \tif (add_renormalize)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex f3849bb654..d0136cdd99 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -598,7 +598,7 @@ static void unpack_all(void)\n \t\tprogress = start_progress(the_repository,\n \t\t\t\t\t  _(\"Unpacking objects\"), nr_objects);\n \tCALLOC_ARRAY(obj_list, nr_objects);\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \tfor (i = 0; i < nr_objects; i++) {\n \t\tunpack_one(i);\n \t\tdisplay_progress(progress, i + 1);\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 3d6646c318..17f3ea284c 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1124,7 +1124,7 @@ int cmd_update_index(int argc,\n \t * Allow the object layer to optimize adding multiple objects in\n \t * a batch.\n \t */\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \twhile (ctx.argc) {\n \t\tif (parseopt_state != PARSE_OPT_DONE)\n \t\t\tparseopt_state = parse_options_step(&ctx, options,\ndiff --git a/cache-tree.c b/cache-tree.c\nindex 184f7e2635..8eec1d4d52 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -474,6 +474,7 @@ static int update_one(struct cache_tree *it,\n \n int cache_tree_update(struct index_state *istate, int flags)\n {\n+\tint inflight = !!the_repository->objects->transaction;\n \tstruct odb_transaction *transaction;\n \tint skip, i;\n \n@@ -490,10 +491,12 @@ int cache_tree_update(struct index_state *istate, int flags)\n \n \ttrace_performance_enter();\n \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\tif (!inflight)\n+\t\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n \t\t       \"\", 0, &skip, flags);\n-\todb_transaction_commit(transaction);\n+\tif (!inflight)\n+\t\todb_transaction_commit(transaction);\n \ttrace2_region_leave(\"cache_tree\", \"update\", istate->repo);\n \ttrace_performance_leave(\"cache_tree_update\");\n \tif (i < 0)\ndiff --git a/object-file.c b/object-file.c\nindex 3651605ea2..358684beae 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1354,13 +1354,17 @@ int index_fd(struct index_state *istate, struct object_id *oid,\n \n \t\tif (flags & INDEX_WRITE_OBJECT) {\n \t\t\tstruct object_database *odb = the_repository->objects;\n-\t\t\tstruct odb_transaction *transaction = odb_transaction_begin(odb);\n+\t\t\tstruct odb_transaction *transaction = odb->transaction;\n+\t\t\tint inflight = !!transaction;\n \n-\t\t\tret = odb_transaction_write_object_stream(odb->transaction,\n+\t\t\tif (!inflight)\n+\t\t\t\todb_transaction_begin_or_die(odb, &transaction);\n+\t\t\tret = odb_transaction_write_object_stream(transaction,\n \t\t\t\t\t\t\t\t  &stream,\n \t\t\t\t\t\t\t\t  xsize_t(st->st_size),\n \t\t\t\t\t\t\t\t  oid);\n-\t\t\todb_transaction_commit(transaction);\n+\t\t\tif (!inflight)\n+\t\t\t\todb_transaction_commit(transaction);\n \t\t} else {\n \t\t\tret = hash_blob_stream(&stream,\n \t\t\t\t\t       the_repository->hash_algo, oid,\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex b16e07aebf..a5fba7f908 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -1,15 +1,20 @@\n #include \"git-compat-util.h\"\n+#include \"gettext.h\"\n #include \"odb/source.h\"\n #include \"odb/transaction.h\"\n \n-struct odb_transaction *odb_transaction_begin(struct object_database *odb)\n+int odb_transaction_begin(struct object_database *odb,\n+\t\t\t  struct odb_transaction **out)\n {\n+\tint ret;\n+\n \tif (odb->transaction)\n-\t\treturn NULL;\n+\t\treturn error(_(\"object database transaction already pending\"));\n \n-\todb_source_begin_transaction(odb->sources, &odb->transaction);\n+\tret = odb_source_begin_transaction(odb->sources, out);\n+\todb->transaction = *out;\n \n-\treturn odb->transaction;\n+\treturn ret;\n }\n \n void odb_transaction_commit(struct odb_transaction *transaction)\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex d52f0533ce..f5c43187c9 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -1,6 +1,7 @@\n #ifndef ODB_TRANSACTION_H\n #define ODB_TRANSACTION_H\n \n+#include \"gettext.h\"\n #include \"odb.h\"\n #include \"odb/source.h\"\n \n@@ -36,11 +37,21 @@ struct odb_transaction {\n };\n \n /*\n- * Starts an ODB transaction. Subsequent objects are written to the transaction\n- * and not committed until odb_transaction_commit() is invoked on the\n- * transaction. If the ODB already has a pending transaction, NULL is returned.\n+ * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n+ * written to the transaction and not committed until odb_transaction_commit()\n+ * is invoked on the transaction. Returns 0 on success and a negative value on\n+ * error. Note that it is considered an error to start a new transaction if the\n+ * ODB already has an inflight transaction pending.\n  */\n-struct odb_transaction *odb_transaction_begin(struct object_database *odb);\n+int odb_transaction_begin(struct object_database *odb,\n+\t\t\t  struct odb_transaction **out);\n+\n+static inline void odb_transaction_begin_or_die(struct object_database *odb,\n+\t\t\t\t\t\tstruct odb_transaction **out)\n+{\n+\tif (odb_transaction_begin(odb, out))\n+\t\tdie(_(\"failed to start ODB transaction\"));\n+}\n \n /*\n  * Commits an ODB transaction making the written objects visible. If the\ndiff --git a/read-cache.c b/read-cache.c\nindex 21ca58beea..d511d25834 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4012,6 +4012,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \t\t       const struct pathspec *pathspec, char *ps_matched,\n \t\t       int include_sparse, int flags, int ignored_too )\n {\n+\tint inflight = !!repo->objects->transaction;\n \tstruct odb_transaction *transaction;\n \tstruct update_callback_data data;\n \tstruct rev_info rev;\n@@ -4042,9 +4043,11 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \t * This function is invoked from commands other than 'add', which\n \t * may not have their own transaction active.\n \t */\n-\ttransaction = odb_transaction_begin(repo->objects);\n+\tif (!inflight)\n+\t\todb_transaction_begin_or_die(repo->objects, &transaction);\n \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n-\todb_transaction_commit(transaction);\n+\tif (!inflight)\n+\t\todb_transaction_commit(transaction);\n \n \trelease_revisions(&rev);\n \treturn !!data.add_errors;\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547546","messageId":"20260708235925.3992097-8-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v3 07/11] odb/transaction: propagate commit errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:21Z","receivedAt":"2026-07-08T23:59:38Z","isPatch":true,"body":"When `odb_transaction_commit()` is invoked, the return value of the\nbackend commit callback is silently discarded. A backend has no way\nto signal that committing failed, such as when the \"files\" backend\ncannot migrate its temporary object directory into the permanent\nODB.\n\nIn a subsequent commit, git-receive-pack(1) starts using ODB transaction\nto stage objects and consequently cares about such failures so it can\nhandle the error appropriately. Change the commit callback signature to\nreturn an int error code and have `odb_transaction_commit()` forward it\naccordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n odb/transaction.c | 10 +++++++---\n odb/transaction.h |  7 ++++---\n 2 files changed, 11 insertions(+), 6 deletions(-)\n\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex a5fba7f908..0a924e73f7 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -17,19 +17,23 @@ int odb_transaction_begin(struct object_database *odb,\n \treturn ret;\n }\n \n-void odb_transaction_commit(struct odb_transaction *transaction)\n+int odb_transaction_commit(struct odb_transaction *transaction)\n {\n+\tint ret;\n+\n \tif (!transaction)\n-\t\treturn;\n+\t\treturn 0;\n \n \t/*\n \t * Ensure the transaction ending matches the pending transaction.\n \t */\n \tASSERT(transaction == transaction->source->odb->transaction);\n \n-\ttransaction->commit(transaction);\n+\tret = transaction->commit(transaction);\n \ttransaction->source->odb->transaction = NULL;\n \tfree(transaction);\n+\n+\treturn ret;\n }\n \n int odb_transaction_write_object_stream(struct odb_transaction *transaction,\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex f5c43187c9..3b0a5a78e5 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -54,10 +54,11 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n }\n \n /*\n- * Commits an ODB transaction making the written objects visible. If the\n- * specified transaction is NULL, the function is a no-op.\n+ * Commits an ODB transaction making the written objects visible. Returns 0 on\n+ * success, a negative error code otherwise. Note that, if the specified\n+ * transaction is NULL, the function is a no-op and no error is returned.\n  */\n-void odb_transaction_commit(struct odb_transaction *transaction);\n+int odb_transaction_commit(struct odb_transaction *transaction);\n \n /*\n  * Writes the object in the provided stream into the transaction. The resulting\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547547","messageId":"20260708235925.3992097-10-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v3 09/11] odb/transaction: introduce ODB transaction flags","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:23Z","receivedAt":"2026-07-08T23:59:40Z","isPatch":true,"body":"The temporary directory used by git-receive-pack(1) to write objects is\nmanaged slightly differently than how it is done via ODB transactions:\n\n  - The temporary directory is eagerly created upfront, instead of\n    waiting for the first object write.\n\n  - The prefix name of the temporary directory is \"incoming\" instead of\n    \"bulk-fsync\".\n\nIn a subsequent commit, git-receive-pack(1) will use ODB transactions\ninstead of `tmp_objdir` directly. To provide a means to configure the\nsame transaction behavior, introduce `enum odb_transaction_flags` and\nthe ODB_TRANSACTION_RECEIVE flag intended as a signal for ODB\ntransactions using the \"files\" backend to be set up for\ngit-receive-pack(1). Transaction call sites are updated accordingly to\nprovide the required flag parameter.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  2 +-\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  2 +-\n cache-tree.c             |  2 +-\n object-file.c            | 29 ++++++++++++++++++++++++++---\n object-file.h            |  4 +++-\n odb/source-files.c       |  5 +++--\n odb/source-inmemory.c    |  3 ++-\n odb/source-loose.c       |  3 ++-\n odb/source.h             |  9 ++++++---\n odb/transaction.c        |  5 +++--\n odb/transaction.h        | 15 +++++++++++----\n read-cache.c             |  2 +-\n 13 files changed, 61 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 3d5d9cfdb9..60ffbede2b 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -581,7 +581,7 @@ int cmd_add(int argc,\n \t\tstring_list_clear(&only_match_skip_worktree, 0);\n \t}\n \n-\todb_transaction_begin_or_die(repo->objects, &transaction);\n+\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \n \tps_matched = xcalloc(pathspec.nr, 1);\n \tif (add_renormalize)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex d0136cdd99..c3d0fc7507 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -598,7 +598,7 @@ static void unpack_all(void)\n \t\tprogress = start_progress(the_repository,\n \t\t\t\t\t  _(\"Unpacking objects\"), nr_objects);\n \tCALLOC_ARRAY(obj_list, nr_objects);\n-\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \tfor (i = 0; i < nr_objects; i++) {\n \t\tunpack_one(i);\n \t\tdisplay_progress(progress, i + 1);\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 17f3ea284c..bf6ea60ef4 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1124,7 +1124,7 @@ int cmd_update_index(int argc,\n \t * Allow the object layer to optimize adding multiple objects in\n \t * a batch.\n \t */\n-\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \twhile (ctx.argc) {\n \t\tif (parseopt_state != PARSE_OPT_DONE)\n \t\t\tparseopt_state = parse_options_step(&ctx, options,\ndiff --git a/cache-tree.c b/cache-tree.c\nindex 8eec1d4d52..99c6a0a7d0 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -492,7 +492,7 @@ int cache_tree_update(struct index_state *istate, int flags)\n \ttrace_performance_enter();\n \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n \tif (!inflight)\n-\t\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\t\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n \t\t       \"\", 0, &skip, flags);\n \tif (!inflight)\ndiff --git a/object-file.c b/object-file.c\nindex f0b066798a..d2508d148f 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -498,6 +498,7 @@ struct odb_transaction_files {\n \n \tstruct tmp_objdir *objdir;\n \tstruct transaction_packfile packfile;\n+\tconst char *prefix;\n };\n \n static int odb_transaction_files_prepare(struct odb_transaction *base)\n@@ -514,7 +515,7 @@ static int odb_transaction_files_prepare(struct odb_transaction *base)\n \tif (!transaction || transaction->objdir)\n \t\treturn 0;\n \n-\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n+\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, transaction->prefix);\n \tif (!transaction->objdir)\n \t\treturn error(_(\"unable to create temporary object directory\"));\n \n@@ -1359,7 +1360,7 @@ int index_fd(struct index_state *istate, struct object_id *oid,\n \t\t\tint inflight = !!transaction;\n \n \t\t\tif (!inflight)\n-\t\t\t\todb_transaction_begin_or_die(odb, &transaction);\n+\t\t\t\todb_transaction_begin_or_die(odb, &transaction, 0);\n \t\t\tret = odb_transaction_write_object_stream(transaction,\n \t\t\t\t\t\t\t\t  &stream,\n \t\t\t\t\t\t\t\t  xsize_t(st->st_size),\n@@ -1701,7 +1702,8 @@ static int odb_transaction_files_env(struct odb_transaction *base,\n }\n \n int odb_transaction_files_begin(struct odb_source *source,\n-\t\t\t\tstruct odb_transaction **out)\n+\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\tenum odb_transaction_flags flags)\n {\n \tstruct odb_transaction_files *transaction;\n \n@@ -1710,6 +1712,27 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n \ttransaction->base.env = odb_transaction_files_env;\n+\n+\ttransaction->prefix = \"bulk-fsync\";\n+\tif (flags & ODB_TRANSACTION_RECEIVE) {\n+\t\t/*\n+\t\t * ODB transactions for git-receive-pack(1) eagerly create a\n+\t\t * temporary directory and use a different temporary directory\n+\t\t * prefix.\n+\t\t *\n+\t\t * NEEDSWORK: This transaction flag is only used by the \"files\"\n+\t\t * backend to special case temporary directory set up and\n+\t\t * handling. Ideally transaction users should not have to care\n+\t\t * though. To avoid this, we could eagerly create the temporary\n+\t\t * directory and use the same prefix name for all transactions.\n+\t\t */\n+\t\ttransaction->prefix = \"incoming\";\n+\t\tif (odb_transaction_files_prepare(&transaction->base)) {\n+\t\t\tfree(transaction);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\n \t*out = &transaction->base;\n \n \treturn 0;\ndiff --git a/object-file.h b/object-file.h\nindex 1a023226ac..bdd2d67a2e 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -5,6 +5,7 @@\n #include \"object.h\"\n #include \"odb.h\"\n #include \"odb/source-loose.h\"\n+#include \"odb/transaction.h\"\n \n /* The maximum size for an object header. */\n #define MAX_HEADER_LEN 32\n@@ -197,6 +198,7 @@ struct odb_transaction;\n  * to make new objects visible.\n  */\n int odb_transaction_files_begin(struct odb_source *source,\n-\t\t\t\tstruct odb_transaction **out);\n+\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\tenum odb_transaction_flags flags);\n \n #endif /* OBJECT_FILE_H */\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 2545bd81d4..534f48aad9 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -180,9 +180,10 @@ static int odb_source_files_write_object_stream(struct odb_source *source,\n }\n \n static int odb_source_files_begin_transaction(struct odb_source *source,\n-\t\t\t\t\t      struct odb_transaction **out)\n+\t\t\t\t\t      struct odb_transaction **out,\n+\t\t\t\t\t      enum odb_transaction_flags flags)\n {\n-\treturn odb_transaction_files_begin(source, out);\n+\treturn odb_transaction_files_begin(source, out, flags);\n }\n \n static int odb_source_files_read_alternates(struct odb_source *source,\ndiff --git a/odb/source-inmemory.c b/odb/source-inmemory.c\nindex e004566d76..9644d9d474 100644\n--- a/odb/source-inmemory.c\n+++ b/odb/source-inmemory.c\n@@ -304,7 +304,8 @@ static int odb_source_inmemory_freshen_object(struct odb_source *source,\n }\n \n static int odb_source_inmemory_begin_transaction(struct odb_source *source UNUSED,\n-\t\t\t\t\t\t struct odb_transaction **out UNUSED)\n+\t\t\t\t\t\t struct odb_transaction **out UNUSED,\n+\t\t\t\t\t\t enum odb_transaction_flags flags UNUSED)\n {\n \treturn error(\"in-memory source does not support transactions\");\n }\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 66e6bb8d3f..57c91986b4 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -638,7 +638,8 @@ static int odb_source_loose_write_object_stream(struct odb_source *source,\n }\n \n static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED,\n-\t\t\t\t\t      struct odb_transaction **out UNUSED)\n+\t\t\t\t\t      struct odb_transaction **out UNUSED,\n+\t\t\t\t\t      enum odb_transaction_flags flags UNUSED)\n {\n \t/* TODO: this is a known omission that we'll want to address eventually. */\n \treturn error(\"loose source does not support transactions\");\ndiff --git a/odb/source.h b/odb/source.h\nindex 2192a101b8..3790d03ff2 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -3,6 +3,7 @@\n \n #include \"object.h\"\n #include \"odb.h\"\n+#include \"odb/transaction.h\"\n \n enum odb_source_type {\n \t/*\n@@ -228,7 +229,8 @@ struct odb_source {\n \t * negative error code otherwise.\n \t */\n \tint (*begin_transaction)(struct odb_source *source,\n-\t\t\t\t struct odb_transaction **out);\n+\t\t\t\t struct odb_transaction **out,\n+\t\t\t\t enum odb_transaction_flags flags);\n \n \t/*\n \t * This callback is expected to read the list of alternate object\n@@ -467,9 +469,10 @@ static inline int odb_source_write_alternate(struct odb_source *source,\n  * Returns 0 on success, a negative error code otherwise.\n  */\n static inline int odb_source_begin_transaction(struct odb_source *source,\n-\t\t\t\t\t       struct odb_transaction **out)\n+\t\t\t\t\t       struct odb_transaction **out,\n+\t\t\t\t\t       enum odb_transaction_flags flags)\n {\n-\treturn source->begin_transaction(source, out);\n+\treturn source->begin_transaction(source, out, flags);\n }\n \n #endif\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex 7f1b30945d..edf5488c81 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -4,14 +4,15 @@\n #include \"odb/transaction.h\"\n \n int odb_transaction_begin(struct object_database *odb,\n-\t\t\t  struct odb_transaction **out)\n+\t\t\t  struct odb_transaction **out,\n+\t\t\t  enum odb_transaction_flags flags)\n {\n \tint ret;\n \n \tif (odb->transaction)\n \t\treturn error(_(\"object database transaction already pending\"));\n \n-\tret = odb_source_begin_transaction(odb->sources, out);\n+\tret = odb_source_begin_transaction(odb->sources, out, flags);\n \todb->transaction = *out;\n \n \treturn ret;\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 5e51ce5ca4..4cb2eafcbf 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -3,7 +3,6 @@\n \n #include \"gettext.h\"\n #include \"odb.h\"\n-#include \"odb/source.h\"\n \n /*\n  * A transaction may be started for an object database prior to writing new\n@@ -44,6 +43,12 @@ struct odb_transaction {\n \tint (*env)(struct odb_transaction *transaction, struct strvec *env);\n };\n \n+/* Flags used to configure an ODB transaction. */\n+enum odb_transaction_flags {\n+\t/* Configures the transaction for use with git-receive-pack(1). */\n+\tODB_TRANSACTION_RECEIVE = (1 << 0),\n+};\n+\n /*\n  * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n  * written to the transaction and not committed until odb_transaction_commit()\n@@ -52,12 +57,14 @@ struct odb_transaction {\n  * ODB already has an inflight transaction pending.\n  */\n int odb_transaction_begin(struct object_database *odb,\n-\t\t\t  struct odb_transaction **out);\n+\t\t\t  struct odb_transaction **out,\n+\t\t\t  enum odb_transaction_flags flags);\n \n static inline void odb_transaction_begin_or_die(struct object_database *odb,\n-\t\t\t\t\t\tstruct odb_transaction **out)\n+\t\t\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\t\t\tenum odb_transaction_flags flags)\n {\n-\tif (odb_transaction_begin(odb, out))\n+\tif (odb_transaction_begin(odb, out, flags))\n \t\tdie(_(\"failed to start ODB transaction\"));\n }\n \ndiff --git a/read-cache.c b/read-cache.c\nindex d511d25834..50e2320c8d 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4044,7 +4044,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \t * may not have their own transaction active.\n \t */\n \tif (!inflight)\n-\t\todb_transaction_begin_or_die(repo->objects, &transaction);\n+\t\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n \tif (!inflight)\n \t\todb_transaction_commit(transaction);\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547550","messageId":"20260708235925.3992097-11-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v3 10/11] builtin/receive-pack: drop redundant tmpdir env","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:24Z","receivedAt":"2026-07-08T23:59:40Z","isPatch":true,"body":"When performing the connectivity checks for a shallow ref in\n`update_shallow_ref()`, the child process environment variables are\npopulated via `tmp_objdir_env()`. This is unnecessary though as\n`update_shallow_ref()` is only reached after `tmp_objdir_migrate()` has\nbeen performed which means there is no longer a temporary directory that\nneeds to be shared with child processes.\n\nDrop the call to `tmp_objdir_env()` accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 1 -\n 1 file changed, 1 deletion(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 19eb6a1b61..50bc05c70c 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1363,7 +1363,6 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)\n \t\t    !delayed_reachability_test(si, i))\n \t\t\toid_array_append(&extra, &si->shallow->oid[i]);\n \n-\topt.env = tmp_objdir_env(tmp_objdir);\n \tsetup_alternate_shallow(&shallow_lock, &opt.shallow_file, &extra);\n \tif (check_connected(command_singleton_iterator, cmd, &opt)) {\n \t\trollback_shallow_file(the_repository, &shallow_lock);\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547548","messageId":"20260708235925.3992097-9-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v3 08/11] odb/transaction: add transaction env interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:22Z","receivedAt":"2026-07-08T23:59:41Z","isPatch":true,"body":"The ODB transaction backend is responsible for creating/managing its own\nstaging area for writing objects. Other child processes spawned by Git\nmay need access to uncommitted objects or write new objects in the\nstaging area though.\n\nIntroduce `odb_transaction_env()` which is expected to provide the set\nof environment variables needed by a child process to access the\ntransaction's staging area.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c     | 14 ++++++++++++++\n odb/transaction.c |  8 ++++++++\n odb/transaction.h | 17 +++++++++++++++++\n 3 files changed, 39 insertions(+)\n\ndiff --git a/object-file.c b/object-file.c\nindex 358684beae..f0b066798a 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -27,6 +27,7 @@\n #include \"path.h\"\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n+#include \"strvec.h\"\n #include \"tempfile.h\"\n #include \"tmp-objdir.h\"\n \n@@ -1687,6 +1688,18 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n \treturn 0;\n }\n \n+static int odb_transaction_files_env(struct odb_transaction *base,\n+\t\t\t\t     struct strvec *env)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\n+\todb_transaction_files_prepare(&transaction->base);\n+\tstrvec_pushv(env, tmp_objdir_env(transaction->objdir));\n+\n+\treturn 0;\n+}\n+\n int odb_transaction_files_begin(struct odb_source *source,\n \t\t\t\tstruct odb_transaction **out)\n {\n@@ -1696,6 +1709,7 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\ttransaction->base.env = odb_transaction_files_env;\n \t*out = &transaction->base;\n \n \treturn 0;\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex 0a924e73f7..7f1b30945d 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -42,3 +42,11 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n {\n \treturn transaction->write_object_stream(transaction, stream, len, oid);\n }\n+\n+int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env)\n+{\n+\tif (!transaction)\n+\t\treturn 0;\n+\n+\treturn transaction->env(transaction, env);\n+}\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 3b0a5a78e5..5e51ce5ca4 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -34,6 +34,14 @@ struct odb_transaction {\n \tint (*write_object_stream)(struct odb_transaction *transaction,\n \t\t\t\t   struct odb_write_stream *stream, size_t len,\n \t\t\t\t   struct object_id *oid);\n+\n+\t/*\n+\t * This callback is expected to populate the provided strvec with the\n+\t * environment variables that a child process should inherit so that its\n+\t * object writes participate in the transaction. Returns 0 on success, a\n+\t * negative error code otherwise.\n+\t */\n+\tint (*env)(struct odb_transaction *transaction, struct strvec *env);\n };\n \n /*\n@@ -69,4 +77,13 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n \t\t\t\t\tstruct odb_write_stream *stream,\n \t\t\t\t\tsize_t len, struct object_id *oid);\n \n+/*\n+ * Populates the provided strvec with the environment variables that a child\n+ * process should inherit so that its object writes participate in the\n+ * transaction, suitable for using via child_process.env. Returns 0 on success,\n+ * a negative error code otherwise. Note that, if the specified transaction is\n+ * NULL, the function is a no-op and no error is returned.\n+ */\n+int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env);\n+\n #endif\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547549","messageId":"20260708235925.3992097-12-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v3 11/11] builtin/receive-pack: stage incoming objects via ODB transactions","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-08T23:59:25Z","receivedAt":"2026-07-08T23:59:41Z","isPatch":true,"body":"Objects received by git-receive-pack(1) are quarantined in a temporary\n\"incoming\" directory and migrated into the object database prior to the\nreference updates. The quarantine is currently managed through\n`tmp_objdir` directly. In a pluggable ODB future, how exactly an object\ngets written to a transaction may vary for a given ODB source. Refactor\ngit-receive-pack(1) to use the ODB transaction interfaces to manage the\nobject staging area in a more agnostic manner accordingly.\n\nNote that the ODB transaction is now responsible for managing the\nprimary and alternate ODBs for the repository. One small change as a\nresult is that the temporary directory is now applied as the primary ODB\nin the main process instead of an alternate. This does not change\nanything for git-receive-pack(1) though because it only needs access to\nthe newly written objects and doesn't care how exactly it is set up.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 68 ++++++++++++++++++++++--------------------\n 1 file changed, 35 insertions(+), 33 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 50bc05c70c..8b8c20dc1a 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -37,7 +37,6 @@\n #include \"sigchain.h\"\n #include \"string-list.h\"\n #include \"strvec.h\"\n-#include \"tmp-objdir.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n #include \"version.h\"\n@@ -112,8 +111,6 @@ static enum {\n } use_keepalive;\n static int keepalive_in_sec = 5;\n \n-static struct tmp_objdir *tmp_objdir;\n-\n static struct proc_receive_ref {\n \tunsigned int want_add:1,\n \t\t     want_delete:1,\n@@ -926,6 +923,7 @@ static void receive_hook_feed_state_free(void *data)\n static int run_receive_hook(struct command *commands,\n \t\t\t    const char *hook_name,\n \t\t\t    int skip_broken,\n+\t\t\t    struct odb_transaction *transaction,\n \t\t\t    const struct string_list *push_options)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n@@ -959,8 +957,8 @@ static int run_receive_hook(struct command *commands,\n \t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n \t}\n \n-\tif (tmp_objdir)\n-\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n+\tif (transaction)\n+\t\todb_transaction_env(transaction, &opt.env);\n \n \tprepare_push_cert_sha1(&opt);\n \n@@ -1789,24 +1787,30 @@ static const struct object_id *command_singleton_iterator(void *cb_data)\n }\n \n static void set_connectivity_errors(struct command *commands,\n-\t\t\t\t    struct shallow_info *si)\n+\t\t\t\t    struct shallow_info *si,\n+\t\t\t\t    struct odb_transaction *transaction)\n {\n \tstruct command *cmd;\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tstruct command *singleton = cmd;\n \t\tstruct check_connected_options opt = CHECK_CONNECTED_INIT;\n+\t\tstruct strvec env = STRVEC_INIT;\n \n \t\tif (shallow_update && si->shallow_ref[cmd->index])\n \t\t\t/* to be checked in update_shallow_ref() */\n \t\t\tcontinue;\n \n-\t\topt.env = tmp_objdir_env(tmp_objdir);\n+\t\todb_transaction_env(transaction, &env);\n+\t\topt.env = env.v;\n+\n \t\tif (!check_connected(command_singleton_iterator, &singleton,\n \t\t\t\t     &opt))\n \t\t\tcontinue;\n \n \t\tcmd->error_string = \"missing necessary objects\";\n+\n+\t\tstrvec_clear(&env);\n \t}\n }\n \n@@ -2027,6 +2031,7 @@ static void execute_commands_atomic(struct command *commands,\n static void execute_commands(struct command *commands,\n \t\t\t     const char *unpacker_error,\n \t\t\t     struct shallow_info *si,\n+\t\t\t     struct odb_transaction *transaction,\n \t\t\t     const struct string_list *push_options)\n {\n \tstruct check_connected_options opt = CHECK_CONNECTED_INIT;\n@@ -2043,6 +2048,8 @@ static void execute_commands(struct command *commands,\n \t}\n \n \tif (!skip_connectivity_check) {\n+\t\tstruct strvec env = STRVEC_INIT;\n+\n \t\tif (use_sideband) {\n \t\t\tmemset(&muxer, 0, sizeof(muxer));\n \t\t\tmuxer.proc = copy_to_sideband;\n@@ -2056,14 +2063,17 @@ static void execute_commands(struct command *commands,\n \t\tdata.si = si;\n \t\topt.err_fd = err_fd;\n \t\topt.progress = err_fd && !quiet;\n-\t\topt.env = tmp_objdir_env(tmp_objdir);\n+\t\todb_transaction_env(transaction, &env);\n+\t\topt.env = env.v;\n \t\topt.exclude_hidden_refs_section = \"receive\";\n \n \t\tif (check_connected(iterate_receive_command_list, &data, &opt))\n-\t\t\tset_connectivity_errors(commands, si);\n+\t\t\tset_connectivity_errors(commands, si, transaction);\n \n \t\tif (use_sideband)\n \t\t\tfinish_async(&muxer);\n+\n+\t\tstrvec_clear(&env);\n \t}\n \n \treject_updates_to_hidden(commands);\n@@ -2084,7 +2094,7 @@ static void execute_commands(struct command *commands,\n \t\t}\n \t}\n \n-\tif (run_receive_hook(commands, \"pre-receive\", 0, push_options)) {\n+\tif (run_receive_hook(commands, \"pre-receive\", 0, transaction, push_options)) {\n \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\t\tif (!cmd->error_string)\n \t\t\t\tcmd->error_string = \"pre-receive hook declined\";\n@@ -2105,14 +2115,13 @@ static void execute_commands(struct command *commands,\n \t * Now we'll start writing out refs, which means the objects need\n \t * to be in their final positions so that other processes can see them.\n \t */\n-\tif (tmp_objdir_migrate(tmp_objdir) < 0) {\n+\tif (odb_transaction_commit(transaction)) {\n \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\t\tif (!cmd->error_string)\n \t\t\t\tcmd->error_string = \"unable to migrate objects to permanent storage\";\n \t\t}\n \t\treturn;\n \t}\n-\ttmp_objdir = NULL;\n \n \tcheck_aliased_updates(commands);\n \n@@ -2325,7 +2334,8 @@ static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n \t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n }\n \n-static const char *unpack(int err_fd, struct shallow_info *si)\n+static const char *unpack(int err_fd, struct shallow_info *si,\n+\t\t\t  struct odb_transaction *transaction)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2350,20 +2360,7 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n \t\tstrvec_push(&child.args, alt_shallow_file);\n \t}\n \n-\ttmp_objdir = tmp_objdir_create(the_repository, \"incoming\");\n-\tif (!tmp_objdir) {\n-\t\tif (err_fd > 0)\n-\t\t\tclose(err_fd);\n-\t\treturn \"unable to create temporary object directory\";\n-\t}\n-\tstrvec_pushv(&child.env, tmp_objdir_env(tmp_objdir));\n-\n-\t/*\n-\t * Normally we just pass the tmp_objdir environment to the child\n-\t * processes that do the heavy lifting, but we may need to see these\n-\t * objects ourselves to set up shallow information.\n-\t */\n-\ttmp_objdir_add_as_alternate(tmp_objdir);\n+\todb_transaction_env(transaction, &child.env);\n \n \tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n@@ -2430,13 +2427,14 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n \treturn NULL;\n }\n \n-static const char *unpack_with_sideband(struct shallow_info *si)\n+static const char *unpack_with_sideband(struct shallow_info *si,\n+\t\t\t\t\tstruct odb_transaction *transaction)\n {\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(0, si);\n+\t\treturn unpack(0, si, transaction);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2445,7 +2443,7 @@ static const char *unpack_with_sideband(struct shallow_info *si)\n \tif (start_async(&muxer))\n \t\treturn NULL;\n \n-\tret = unpack(muxer.in, si);\n+\tret = unpack(muxer.in, si, transaction);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2622,6 +2620,7 @@ int cmd_receive_pack(int argc,\n \tstruct oid_array ref = OID_ARRAY_INIT;\n \tstruct shallow_info si;\n \tstruct packet_reader reader;\n+\tstruct odb_transaction *transaction = NULL;\n \n \tstruct option options[] = {\n \t\tOPT__QUIET(&quiet, N_(\"quiet\")),\n@@ -2706,11 +2705,14 @@ int cmd_receive_pack(int argc,\n \t\tif (!si.nr_ours && !si.nr_theirs)\n \t\t\tshallow_update = 0;\n \t\tif (!delete_only(commands)) {\n-\t\t\tunpack_status = unpack_with_sideband(&si);\n+\t\t\tif (odb_transaction_begin(the_repository->objects, &transaction, ODB_TRANSACTION_RECEIVE))\n+\t\t\t\tunpack_status = \"unable to start object transaction\";\n+\t\t\telse\n+\t\t\t\tunpack_status = unpack_with_sideband(&si, transaction);\n \t\t\tupdate_shallow_info(commands, &si, &ref);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n-\t\texecute_commands(commands, unpack_status, &si,\n+\t\texecute_commands(commands, unpack_status, &si, transaction,\n \t\t\t\t &push_options);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n@@ -2719,7 +2721,7 @@ int cmd_receive_pack(int argc,\n \t\telse if (report_status)\n \t\t\treport(commands, unpack_status);\n \t\tsigchain_pop(SIGPIPE);\n-\t\trun_receive_hook(commands, \"post-receive\", 1,\n+\t\trun_receive_hook(commands, \"post-receive\", 1, NULL,\n \t\t\t\t &push_options);\n \t\trun_update_post_hook(commands);\n \t\tfree_commands(commands);\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547558","messageId":"xmqqjyr4rg78.fsf@gitster.g","threadId":"65861","inReplyTo":"20260708235925.3992097-7-jltobler@gmail.com","subject":"Re: [PATCH v3 06/11] odb/transaction: propagate begin errors","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-09T03:32:11Z","receivedAt":"2026-07-09T03:32:14Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> When `odb_transaction_begin()` is invoked, the function returns the\n> transaction pointer directly. There is no way for the backend to\n> signal that it failed to set up its state, such as when creating the\n> temporary object directory backing the transaction.\n>\n> In a subsequent commit, git-receive-pack(1) starts using ODB\n> transactions and needs to be able to report such failures rather\n> than silently ignore them. Refactor `odb_transaction_begin()` to\n> return an int error code and write the resulting transaction into an\n> out parameter. Also introduce `odb_transaction_begin_or_die()` as a\n> convenience for callsites that do not need to handle errors\n> explicitly.\n>\n> Note that `odb_transaction_begin()` now returns an error when the ODB\n> already has an inflight transaction pending. ODB transaction call sites\n> that may encounter an inflight transaction are updated to explicitly\n> handle this case.\n>\n> Signed-off-by: Justin Tobler <jltobler@gmail.com>\n> ---\n> ...\n> diff --git a/odb/transaction.c b/odb/transaction.c\n> index b16e07aebf..a5fba7f908 100644\n> --- a/odb/transaction.c\n> +++ b/odb/transaction.c\n> @@ -1,15 +1,20 @@\n>  #include \"git-compat-util.h\"\n> +#include \"gettext.h\"\n>  #include \"odb/source.h\"\n>  #include \"odb/transaction.h\"\n>  \n> -struct odb_transaction *odb_transaction_begin(struct object_database *odb)\n> +int odb_transaction_begin(struct object_database *odb,\n> +\t\t\t  struct odb_transaction **out)\n>  {\n> +\tint ret;\n> +\n>  \tif (odb->transaction)\n> -\t\treturn NULL;\n> +\t\treturn error(_(\"object database transaction already pending\"));\n>  \n> -\todb_source_begin_transaction(odb->sources, &odb->transaction);\n> +\tret = odb_source_begin_transaction(odb->sources, out);\n> +\todb->transaction = *out;\n\nCan odb_source_begin_transaction() ever fail?  If so, and when it\nfails, would *out be left untouched?  I am wondering if we want\n\n\tif (!(ret = odb_source_begin_transaction(odb->sources, out)))\n        \todb->transaction = *out;\n\nor something like that.\n"},{"id":"547559","messageId":"xmqqbjcgrg0e.fsf@gitster.g","threadId":"65861","inReplyTo":"20260708235925.3992097-9-jltobler@gmail.com","subject":"Re: [PATCH v3 08/11] odb/transaction: add transaction env interface","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-09T03:36:17Z","receivedAt":"2026-07-09T03:36:20Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> +static int odb_transaction_files_env(struct odb_transaction *base,\n> +\t\t\t\t     struct strvec *env)\n> +{\n> +\tstruct odb_transaction_files *transaction =\n> +\t\tcontainer_of(base, struct odb_transaction_files, base);\n> +\n> +\todb_transaction_files_prepare(&transaction->base);\n\nCan this fail?  The caller of us would not notice that something\nwent wrong, and ...\n\n> +\tstrvec_pushv(env, tmp_objdir_env(transaction->objdir));\n\n... happily ends up using transaction->objdir that may not be\nappropriate for it to use if it fails, no?\n\n> +\treturn 0;\n> +}\n"},{"id":"547560","messageId":"xmqq33xsrfeu.fsf@gitster.g","threadId":"65861","inReplyTo":"20260708235925.3992097-12-jltobler@gmail.com","subject":"Re: [PATCH v3 11/11] builtin/receive-pack: stage incoming objects via ODB transactions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-09T03:49:13Z","receivedAt":"2026-07-09T03:49:16Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> @@ -2027,6 +2031,7 @@ static void execute_commands_atomic(struct command *commands,\n>  static void execute_commands(struct command *commands,\n>  \t\t\t     const char *unpacker_error,\n>  \t\t\t     struct shallow_info *si,\n> +\t\t\t     struct odb_transaction *transaction,\n>  \t\t\t     const struct string_list *push_options)\n>  {\n>  \tstruct check_connected_options opt = CHECK_CONNECTED_INIT;\n> ...\n\nHidden in the context early in this function is an error return.\nWhen unpacker_error string is non NULL, we mark all the commands in\nthe linked commands list as failed, and return early from this\nfunction.\n\n> @@ -2105,14 +2115,13 @@ static void execute_commands(struct command *commands,\n>  \t * Now we'll start writing out refs, which means the objects need\n>  \t * to be in their final positions so that other processes can see them.\n>  \t */\n> -\tif (tmp_objdir_migrate(tmp_objdir) < 0) {\n> +\tif (odb_transaction_commit(transaction)) {\n>  \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n>  \t\t\tif (!cmd->error_string)\n>  \t\t\t\tcmd->error_string = \"unable to migrate objects to permanent storage\";\n>  \t\t}\n>  \t\treturn;\n>  \t}\n> -\ttmp_objdir = NULL;\n>  \n>  \tcheck_aliased_updates(commands);\n\nIn the \"happy case\", execute_commands() would commit the transaction\nbefore going on to do the execute_commands_{atomic,nonatomic}() that\nappears later in it.\n\n> @@ -2706,11 +2705,14 @@ int cmd_receive_pack(int argc,\n>  \t\tif (!si.nr_ours && !si.nr_theirs)\n>  \t\t\tshallow_update = 0;\n>  \t\tif (!delete_only(commands)) {\n> -\t\t\tunpack_status = unpack_with_sideband(&si);\n> +\t\t\tif (odb_transaction_begin(the_repository->objects, &transaction, ODB_TRANSACTION_RECEIVE))\n\nIn the \"main\" program, we start a transaction here, and\n\n> +\t\t\t\tunpack_status = \"unable to start object transaction\";\n> +\t\t\telse\n> +\t\t\t\tunpack_status = unpack_with_sideband(&si, transaction);\n\nthen call unpack_with_sideband().  It may fail.\n\n>  \t\t\tupdate_shallow_info(commands, &si, &ref);\n>  \t\t}\n>  \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n> -\t\texecute_commands(commands, unpack_status, &si,\n> +\t\texecute_commands(commands, unpack_status, &si, transaction,\n>  \t\t\t\t &push_options);\n\nAnd in such a case, execute_commands() returns without committing\nthe transaction.  Is there a need to add and make an\nodb_transaction_abort() call or something in such a case?\nEverything should be cleaned up upon process exit, and on file based\nbackends, we probably let the tempfile/lockfile API do their thing\nto clean up, but are there other things we may want to clean up?\n\n>  \t\tdelete_tempfile(&pack_lockfile);\n>  \t\tsigchain_push(SIGPIPE, SIG_IGN);\n> @@ -2719,7 +2721,7 @@ int cmd_receive_pack(int argc,\n>  \t\telse if (report_status)\n>  \t\t\treport(commands, unpack_status);\n>  \t\tsigchain_pop(SIGPIPE);\n> -\t\trun_receive_hook(commands, \"post-receive\", 1,\n> +\t\trun_receive_hook(commands, \"post-receive\", 1, NULL,\n>  \t\t\t\t &push_options);\n>  \t\trun_update_post_hook(commands);\n>  \t\tfree_commands(commands);\n"},{"id":"547581","messageId":"ak9sKf2miKK_PAIf@pks.im","threadId":"65861","inReplyTo":"20260708235925.3992097-4-jltobler@gmail.com","subject":"Re: [PATCH v3 03/11] object-file: embed transaction flush logic in commit function","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-09T09:38:49Z","receivedAt":"2026-07-09T09:39:05Z","isPatch":true,"body":"On Wed, Jul 08, 2026 at 06:59:17PM -0500, Justin Tobler wrote:\n> When a \"files\" transaction is committed,\n> `flush_loose_object_transaction()` is invoked to handle performing a\n> hardware flush along with migrating the temporary object directory into\n> the primary and configuring the repository ODB source accordingly. The\n> function name here is a bit misleading because the helper is doing a bit\n> more than just \"flushing\" the transaction contents. Also, in a\n> subsequent commit, the transaction temporary directory is used to stage\n> packfiles and not just loose objects anymore.\n> \n> Lift the helper function logic directly into\n> `odb_transaction_files_commit()` to more accurately signal to readers\n> the operation being performed.\n\nThis line break makes my eyes bleed.\n\nPatrick\n"},{"id":"547582","messageId":"ak9sOqq-WvZ7U0Hq@pks.im","threadId":"65861","inReplyTo":"ak57VEF56HkRKygQ@denethor","subject":"Re: [PATCH v2 06/11] odb/transaction: propagate begin errors","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-09T09:39:06Z","receivedAt":"2026-07-09T09:39:12Z","isPatch":true,"body":"On Wed, Jul 08, 2026 at 11:56:43AM -0500, Justin Tobler wrote:\n> On 26/07/08 08:41AM, Patrick Steinhardt wrote:\n> > On Tue, Jul 07, 2026 at 11:14:07PM -0500, Justin Tobler wrote:\n> > > @@ -490,10 +491,12 @@ int cache_tree_update(struct index_state *istate, int flags)\n> > >  \n> > >  \ttrace_performance_enter();\n> > >  \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n> > > -\ttransaction = odb_transaction_begin(the_repository->objects);\n> > > +\tif (!inflight)\n> > > +\t\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n> > >  \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n> > >  \t\t       \"\", 0, &skip, flags);\n> > > -\todb_transaction_commit(transaction);\n> > > +\tif (!inflight)\n> > > +\t\todb_transaction_commit(transaction);\n> > >  \ttrace2_region_leave(\"cache_tree\", \"update\", istate->repo);\n> > >  \ttrace_performance_leave(\"cache_tree_update\");\n> > >  \tif (i < 0)\n> > \n> > Callsites like this really make me wonder why we even care to create\n> > a transaction in the first place if we basically just commit it\n> > immediately anyway. And while it's a bit sad that we have so many sites\n> > where we don't really know whether we even have a transaction, I think\n> > it's a good change that we have now annotated them clearly. A subsequent\n> > patch series may then eventually refactor those sites so that we stop\n> > depending on `odb->transaction` and inject the transaction via a\n> > parameter.\n> \n> Call sites like the one mentioned above are using ODB transactions as an\n> optimization to batch the full fsyncs in bulk. In cases where the is not\n> already a transaction, they start one to take advantage of it.\n\nI know. But in the case where we don't want to batch we create the\ntransaction anyway as far as I can see, and then we commit it\nimmediately. So arguably, we could've just `odb_write_object()` and call\nit a day.\n\n> I fully agree though that an ODB transaction should ideally be started\n> at a higher layer and wired down to these call sites. I have a couple of\n> patches in my tree that start to tackle this which I plan to send in\n> another series. :)\n\nYeah, let's not worry about that too much for now then. One step at a\ntime :)\n\n> > > @@ -36,11 +38,21 @@ struct odb_transaction {\n> > >  };\n> > >  \n> > >  /*\n> > > - * Starts an ODB transaction. Subsequent objects are written to the transaction\n> > > - * and not committed until odb_transaction_commit() is invoked on the\n> > > - * transaction. If the ODB already has a pending transaction, NULL is returned.\n> > > + * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n> > > + * written to the transaction and not committed until odb_transaction_commit()\n> > > + * is invoked on the transaction. Returns 0 on success and a negative value on\n> > > + * error. Note that it is considered an error to start a new transaction if the\n> > > + * ODB already has an inflight transaction pending.\n> > >   */\n> > > -struct odb_transaction *odb_transaction_begin(struct object_database *odb);\n> > > +int odb_transaction_begin(struct object_database *odb,\n> > > +\t\t\t  struct odb_transaction **out);\n> > > +\n> > > +static inline void odb_transaction_begin_or_die(struct object_database *odb,\n> > > +\t\t\t\t\t\tstruct odb_transaction **out)\n> > > +{\n> > > +\tif (odb_transaction_begin(odb, out))\n> > > +\t\tdie(_(\"failed to start ODB transaction\"));\n> > > +}\n> > \n> > We could make it a bit simpler to use this function by continuing to\n> > return the transaction directly. But on the other hand this results in a\n> > more consistent interface.\n> \n> Ya, I was a bit back and forth about this myself. I ultimately landed on\n> keeping a more consistent interface though. Happy to change if others\n> feel differently though.\n\nAs said, I can see both arguments. And ultimately, I don't care too\nmuch, so it's fine if this is just kept as-is.\n\nPatrick\n"},{"id":"547583","messageId":"ak9sUt-mwXyoTDLj@pks.im","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"Re: [PATCH v3 00/11] receive-pack: use ODB transactions to stage object writes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-09T09:39:30Z","receivedAt":"2026-07-09T09:39:37Z","isPatch":true,"body":"On Wed, Jul 08, 2026 at 06:59:14PM -0500, Justin Tobler wrote:\n> Changes since V2:\n>   - Clarified commit log reasoning for embedding\n>     `flush_loose_object_transaction()` logic in commit function.\n>   - Started printed some error messages on transaction errors.\n>   - Removed include statement.\n>   - Fixed transaction leak on `odb_transaction_commit()` error.\n\nThanks, the changes all look good to me and I don't have anything else\nto add.\n\nPatrick\n"},{"id":"547608","messageId":"ak-ntVKQ8XqMr6zv@denethor","threadId":"65861","inReplyTo":"xmqqjyr4rg78.fsf@gitster.g","subject":"Re: [PATCH v3 06/11] odb/transaction: propagate begin errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-09T14:03:59Z","receivedAt":"2026-07-09T14:04:04Z","isPatch":true,"body":"On 26/07/08 08:32PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > When `odb_transaction_begin()` is invoked, the function returns the\n> > transaction pointer directly. There is no way for the backend to\n> > signal that it failed to set up its state, such as when creating the\n> > temporary object directory backing the transaction.\n> >\n> > In a subsequent commit, git-receive-pack(1) starts using ODB\n> > transactions and needs to be able to report such failures rather\n> > than silently ignore them. Refactor `odb_transaction_begin()` to\n> > return an int error code and write the resulting transaction into an\n> > out parameter. Also introduce `odb_transaction_begin_or_die()` as a\n> > convenience for callsites that do not need to handle errors\n> > explicitly.\n> >\n> > Note that `odb_transaction_begin()` now returns an error when the ODB\n> > already has an inflight transaction pending. ODB transaction call sites\n> > that may encounter an inflight transaction are updated to explicitly\n> > handle this case.\n> >\n> > Signed-off-by: Justin Tobler <jltobler@gmail.com>\n> > ---\n> > ...\n> > diff --git a/odb/transaction.c b/odb/transaction.c\n> > index b16e07aebf..a5fba7f908 100644\n> > --- a/odb/transaction.c\n> > +++ b/odb/transaction.c\n> > @@ -1,15 +1,20 @@\n> >  #include \"git-compat-util.h\"\n> > +#include \"gettext.h\"\n> >  #include \"odb/source.h\"\n> >  #include \"odb/transaction.h\"\n> >  \n> > -struct odb_transaction *odb_transaction_begin(struct object_database *odb)\n> > +int odb_transaction_begin(struct object_database *odb,\n> > +\t\t\t  struct odb_transaction **out)\n> >  {\n> > +\tint ret;\n> > +\n> >  \tif (odb->transaction)\n> > -\t\treturn NULL;\n> > +\t\treturn error(_(\"object database transaction already pending\"));\n> >  \n> > -\todb_source_begin_transaction(odb->sources, &odb->transaction);\n> > +\tret = odb_source_begin_transaction(odb->sources, out);\n> > +\todb->transaction = *out;\n> \n> Can odb_source_begin_transaction() ever fail?  If so, and when it\n> fails, would *out be left untouched?  \n\nIn this patch there it not yet a way for it to fail, but it can return\nan error later in the series. When an error is encountered though, *out\n_is_ left untouched.\n\n> I am wondering if we want\n> \n> \tif (!(ret = odb_source_begin_transaction(odb->sources, out)))\n>         \todb->transaction = *out;\n> \n> or something like that.\n\nI think it is a good idea to for `odb_transaction_begin()` to ensure the\nrepository transaction is only set on success though. Will update in the\nnext version. Thanks\n\n-Justin\n"},{"id":"547611","messageId":"ak-vJ2hlAHLbg8Zb@denethor","threadId":"65861","inReplyTo":"xmqqbjcgrg0e.fsf@gitster.g","subject":"Re: [PATCH v3 08/11] odb/transaction: add transaction env interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-09T15:02:12Z","receivedAt":"2026-07-09T15:02:18Z","isPatch":true,"body":"On 26/07/08 08:36PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > +static int odb_transaction_files_env(struct odb_transaction *base,\n> > +\t\t\t\t     struct strvec *env)\n> > +{\n> > +\tstruct odb_transaction_files *transaction =\n> > +\t\tcontainer_of(base, struct odb_transaction_files, base);\n> > +\n> > +\todb_transaction_files_prepare(&transaction->base);\n> \n> Can this fail?  The caller of us would not notice that something\n> went wrong, and ...\n> \n> > +\tstrvec_pushv(env, tmp_objdir_env(transaction->objdir));\n> \n> ... happily ends up using transaction->objdir that may not be\n> appropriate for it to use if it fails, no?\n\nYa, `odb_transaction_files_prepare()` can fail here. In practice,\nfailure results in no temporary directory being created which\n`tmp_objdir_env()` does handle gracefully, but we should ideally still\nbe reported the failure back to callers. Will update in the next\nversion.\n\n-Justin\n"},{"id":"547736","messageId":"alEBEbwOMFkVfuk9@denethor","threadId":"65861","inReplyTo":"xmqq33xsrfeu.fsf@gitster.g","subject":"Re: [PATCH v3 11/11] builtin/receive-pack: stage incoming objects via ODB transactions","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T14:37:19Z","receivedAt":"2026-07-10T14:37:25Z","isPatch":true,"body":"On 26/07/08 08:49PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> >  \t\t\tupdate_shallow_info(commands, &si, &ref);\n> >  \t\t}\n> >  \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n> > -\t\texecute_commands(commands, unpack_status, &si,\n> > +\t\texecute_commands(commands, unpack_status, &si, transaction,\n> >  \t\t\t\t &push_options);\n> \n> And in such a case, execute_commands() returns without committing\n> the transaction.  Is there a need to add and make an\n> odb_transaction_abort() call or something in such a case?\n> Everything should be cleaned up upon process exit, and on file based\n> backends, we probably let the tempfile/lockfile API do their thing\n> to clean up, but are there other things we may want to clean up?\n\nAs you mentioned, if we exit before committing the ODB transaction, the\ntemporary directory will get cleaned up when the process exits. I don't\nthink there is anything else we need to cleanup that wouldn't be handled\nat exit though. Regardless, I do plan to add `odb_transaction_abort()`\nin a followup series and I think it would be nice to have an explicit\n\"abort\" here when we know that we are not going to commit anyways. I\nwould like to defer this to my next series though.\n\n-Justin\n"},{"id":"547748","messageId":"20260710163722.2962278-1-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260708235925.3992097-1-jltobler@gmail.com","subject":"[PATCH v4 00/11] receive-pack: use ODB transactions to stage object writes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:11Z","receivedAt":"2026-07-10T16:37:28Z","isPatch":true,"body":"Greetings,\n\nThis patch series replaces direct usage of the `tmp_objdir` interfaces\nin git-receive-pack(1) to instead use the `odb_transaction` interfaces\nto create/manage a staging area to write objects to. The purpose of this\nchange is to get git-receive-pack(1) one step closer to being ODB\nbackend agnostic. For now, the object writes themselves are still\n\"files\" backend specific due to being handled by the git-index-pack(1)\nand git-unpack-objects(1) child processes. This will be tackled in a\nseparate series though.\n\nChanges since V3:\n  - Removed ugly line break in commit message to prevent eye strain.\n  - `odb_transaction_begin()` now only sets the repository transaction\n    on success.\n  - `odb_transaction_env()` now bubbles up error when failing to create\n    the temporary directory.\n\nChanges since V2:\n  - Clarified commit log reasoning for embedding\n    `flush_loose_object_transaction()` logic in commit function.\n  - Started printed some error messages on transaction errors.\n  - Removed include statement.\n  - Fixed transaction leak on `odb_transaction_commit()` error.\n\nChanges since V1:\n  - Adapted other \"file\" ODB transaction helpers to be more consistent\n    with current naming scheme.\n  - Removed redundant NULL transaction handling from\n    `odb_transaction_files_begin()`.\n  - `odb_transaction_begin()` now returns an error if there is already\n    an inflight transaction pending instead of setting the `out` pointer\n    to NULL.\n  - Updated `odb_transaction_env()` to return an error code and append\n    environment variables to a strvec provided as an argument.\n  - Removed redundant setting of tmpdir environment variables for child\n    processes after tmpdir has been migrated.\n  - Split changes adding ODB transaction flags into a separate commit.\n  - Consistently wire the ODB transaction throughout git-receive-pack\n    code instead of reading it from `the_repository`.\n  - Updated user facing error message.\n  - Updated some comments to better document functions/flags.\n  - Clarified some commit messages.\n  - Fixed typos.\n\nThanks,\n-Justin\n\nJustin Tobler (11):\n  object-file: rename files transaction prepare function\n  object-file: rename files transaction fsync function\n  object-file: embed transaction flush logic in commit function\n  object-file: drop check for inflight transactions\n  object-file: propagate files transaction errors\n  odb/transaction: propagate begin errors\n  odb/transaction: propagate commit errors\n  odb/transaction: add transaction env interface\n  odb/transaction: introduce ODB transaction flags\n  builtin/receive-pack: drop redundant tmpdir env\n  builtin/receive-pack: stage incoming objects via ODB transactions\n\n builtin/add.c            |   2 +-\n builtin/receive-pack.c   |  69 ++++++++---------\n builtin/unpack-objects.c |   2 +-\n builtin/update-index.c   |   2 +-\n cache-tree.c             |   7 +-\n object-file.c            | 161 +++++++++++++++++++++++++--------------\n object-file.h            |   8 +-\n odb/source-files.c       |   9 +--\n odb/source-inmemory.c    |   3 +-\n odb/source-loose.c       |   3 +-\n odb/source.h             |   9 ++-\n odb/transaction.c        |  33 ++++++--\n odb/transaction.h        |  59 +++++++++++---\n read-cache.c             |   7 +-\n 14 files changed, 244 insertions(+), 130 deletions(-)\n\nRange-diff against v3:\n 1:  9c14b219ad =  1:  9c14b219ad object-file: rename files transaction prepare function\n 2:  5703a9e93b =  2:  5703a9e93b object-file: rename files transaction fsync function\n 3:  76204847f2 !  3:  70267741b0 object-file: embed transaction flush logic in commit function\n    @@ Commit message\n         subsequent commit, the transaction temporary directory is used to stage\n         packfiles and not just loose objects anymore.\n     \n    -    Lift the helper function logic directly into\n    -    `odb_transaction_files_commit()` to more accurately signal to readers\n    -    the operation being performed.\n    +    Lift the helper function logic into `odb_transaction_files_commit()` to\n    +    more accurately signal to readers the operation being performed.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n 4:  c97eb7763f =  4:  34cd3822c5 object-file: drop check for inflight transactions\n 5:  1f3a1f7714 =  5:  240aa3475f object-file: propagate files transaction errors\n 6:  09d13272d5 !  6:  0d91310fac odb/transaction: propagate begin errors\n    @@ odb/transaction.c\n      \n     -\todb_source_begin_transaction(odb->sources, &odb->transaction);\n     +\tret = odb_source_begin_transaction(odb->sources, out);\n    -+\todb->transaction = *out;\n    ++\tif (!ret)\n    ++\t\todb->transaction = *out;\n      \n     -\treturn odb->transaction;\n     +\treturn ret;\n 7:  12833d6773 =  7:  5e4680ed75 odb/transaction: propagate commit errors\n 8:  f2586f2f34 !  8:  babcf6b156 odb/transaction: add transaction env interface\n    @@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b\n     +{\n     +\tstruct odb_transaction_files *transaction =\n     +\t\tcontainer_of(base, struct odb_transaction_files, base);\n    ++\tint ret;\n     +\n    -+\todb_transaction_files_prepare(&transaction->base);\n    -+\tstrvec_pushv(env, tmp_objdir_env(transaction->objdir));\n    ++\tret = odb_transaction_files_prepare(&transaction->base);\n    ++\tif (!ret)\n    ++\t\tstrvec_pushv(env, tmp_objdir_env(transaction->objdir));\n     +\n    -+\treturn 0;\n    ++\treturn ret;\n     +}\n     +\n      int odb_transaction_files_begin(struct odb_source *source,\n 9:  9d082b5e47 !  9:  96f2a21eec odb/transaction: introduce ODB transaction flags\n    @@ odb/transaction.c\n      \n     -\tret = odb_source_begin_transaction(odb->sources, out);\n     +\tret = odb_source_begin_transaction(odb->sources, out, flags);\n    - \todb->transaction = *out;\n    + \tif (!ret)\n    + \t\todb->transaction = *out;\n      \n    - \treturn ret;\n     \n      ## odb/transaction.h ##\n     @@\n10:  e11d8a6676 = 10:  56718f1190 builtin/receive-pack: drop redundant tmpdir env\n11:  fee57c2817 = 11:  5197a19fbf builtin/receive-pack: stage incoming objects via ODB transactions\n\nbase-commit: ab776a62a78576513ee121424adb19597fbb7613\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547749","messageId":"20260710163722.2962278-2-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"[PATCH v4 01/11] object-file: rename files transaction prepare function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:12Z","receivedAt":"2026-07-10T16:37:29Z","isPatch":true,"body":"The \"files\" ODB transaction backend lazily creates a temporary object\ndirectory when the first loose object is written to the transaction via\n`prepare_loose_object_transaction()`. In a subsequent commit, the\ntemporary directory is used to also write packfiles to.\n\nRename the function to `odb_transaction_files_prepare()` accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex e3d92bbda2..a3eb8d71dd 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -499,7 +499,7 @@ struct odb_transaction_files {\n \tstruct transaction_packfile packfile;\n };\n \n-static void prepare_loose_object_transaction(struct odb_transaction *base)\n+static void 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@@ -761,7 +761,7 @@ int write_loose_object(struct odb_source_loose *loose,\n \tstatic struct strbuf filename = STRBUF_INIT;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\n+\t\todb_transaction_files_prepare(loose->base.odb->transaction);\n \n \todb_loose_path(loose, &filename, oid);\n \n@@ -825,7 +825,7 @@ int odb_source_loose_write_stream(struct odb_source_loose *loose,\n \tint hdrlen;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tprepare_loose_object_transaction(loose->base.odb->transaction);\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-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547750","messageId":"20260710163722.2962278-3-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"[PATCH v4 02/11] object-file: rename files transaction fsync function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:13Z","receivedAt":"2026-07-10T16:37:30Z","isPatch":true,"body":"When writing an object to a \"files\" ODB transaction, a full hardware\nflush is not initially performed during the fsync in\n`fsync_loose_object_transaction()` and instead delayed until the\ntransaction is later committed.\n\nTo be more consistent with other \"files\" ODB transaction helpers, rename\nthe function to `odb_transaction_files_fsync()` accordingly. The\nconditional in the helper is also slightly restructured to improve\nclarity to readers.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 16 ++++++++++------\n 1 file changed, 10 insertions(+), 6 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex a3eb8d71dd..d68824bb44 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -518,12 +518,17 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n \t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n }\n \n-static void fsync_loose_object_transaction(struct odb_transaction *base,\n-\t\t\t\t\t   int fd, const char *filename)\n+static void odb_transaction_files_fsync(struct odb_transaction *base,\n+\t\t\t\t\tint fd, const char *filename)\n {\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of_or_null(base, struct odb_transaction_files, base);\n \n+\tif (!transaction || !transaction->objdir) {\n+\t\tfsync_or_die(fd, filename);\n+\t\treturn;\n+\t}\n+\n \t/*\n \t * If we have an active ODB transaction, we issue a call that\n \t * cleans the filesystem page cache but avoids a hardware flush\n@@ -531,8 +536,7 @@ static void fsync_loose_object_transaction(struct odb_transaction *base,\n \t * before renaming the objects to their final names as part of\n \t * flush_batch_fsync.\n \t */\n-\tif (!transaction || !transaction->objdir ||\n-\t    git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) {\n+\tif (git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) {\n \t\tif (errno == ENOSYS)\n \t\t\twarning(_(\"core.fsyncMethod = batch is unsupported on this platform\"));\n \t\tfsync_or_die(fd, filename);\n@@ -553,7 +557,7 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac\n \t/*\n \t * Issue a full hardware flush against a temporary file to ensure\n \t * that all objects are durable before any renames occur. The code in\n-\t * fsync_loose_object_transaction has already issued a writeout\n+\t * odb_transaction_files_fsync has already issued a writeout\n \t * request, but it has not flushed any writeback cache in the storage\n \t * hardware or any filesystem logs. This fsync call acts as a barrier\n \t * to ensure that the data in each new object file is durable before\n@@ -582,7 +586,7 @@ static void close_loose_object(struct odb_source_loose *loose,\n \t\tgoto out;\n \n \tif (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))\n-\t\tfsync_loose_object_transaction(loose->base.odb->transaction, fd, filename);\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-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547753","messageId":"20260710163722.2962278-4-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"[PATCH v4 03/11] object-file: embed transaction flush logic in commit function","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:14Z","receivedAt":"2026-07-10T16:37:30Z","isPatch":true,"body":"When a \"files\" transaction is committed,\n`flush_loose_object_transaction()` is invoked to handle performing a\nhardware flush along with migrating the temporary object directory into\nthe primary and configuring the repository ODB source accordingly. The\nfunction name here is a bit misleading because the helper is doing a bit\nmore than just \"flushing\" the transaction contents. Also, in a\nsubsequent commit, the transaction temporary directory is used to stage\npackfiles and not just loose objects anymore.\n\nLift the helper function logic into `odb_transaction_files_commit()` to\nmore accurately signal to readers the operation being performed.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 64 ++++++++++++++++++++++-----------------------------\n 1 file changed, 28 insertions(+), 36 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex d68824bb44..33bd6c6810 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -543,41 +543,6 @@ static void odb_transaction_files_fsync(struct odb_transaction *base,\n \t}\n }\n \n-/*\n- * Cleanup after batch-mode fsync_object_files.\n- */\n-static void flush_loose_object_transaction(struct odb_transaction_files *transaction)\n-{\n-\tstruct strbuf temp_path = STRBUF_INIT;\n-\tstruct tempfile *temp;\n-\n-\tif (!transaction->objdir)\n-\t\treturn;\n-\n-\t/*\n-\t * Issue a full hardware flush against a temporary file to ensure\n-\t * that all objects are durable before any renames occur. The code in\n-\t * odb_transaction_files_fsync has already issued a writeout\n-\t * request, but it has not flushed any writeback cache in the storage\n-\t * hardware or any filesystem logs. This fsync call acts as a barrier\n-\t * to ensure that the data in each new object file is durable before\n-\t * the final name is visible.\n-\t */\n-\tstrbuf_addf(&temp_path, \"%s/bulk_fsync_XXXXXX\",\n-\t\t    repo_get_object_directory(transaction->base.source->odb->repo));\n-\ttemp = xmks_tempfile(temp_path.buf);\n-\tfsync_or_die(get_tempfile_fd(temp), get_tempfile_path(temp));\n-\tdelete_tempfile(&temp);\n-\tstrbuf_release(&temp_path);\n-\n-\t/*\n-\t * Make the object files visible in the primary ODB after their data is\n-\t * fully durable.\n-\t */\n-\ttmp_objdir_migrate(transaction->objdir);\n-\ttransaction->objdir = NULL;\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@@ -1679,7 +1644,34 @@ static void odb_transaction_files_commit(struct odb_transaction *base)\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of(base, struct odb_transaction_files, base);\n \n-\tflush_loose_object_transaction(transaction);\n+\tif (transaction->objdir) {\n+\t\tstruct strbuf temp_path = STRBUF_INIT;\n+\t\tstruct tempfile *temp;\n+\n+\t\t/*\n+\t\t * Issue a full hardware flush against a temporary file to ensure\n+\t\t * that all objects are durable before any renames occur. The code in\n+\t\t * odb_transaction_files_fsync has already issued a writeout\n+\t\t * request, but it has not flushed any writeback cache in the storage\n+\t\t * hardware or any filesystem logs. This fsync call acts as a barrier\n+\t\t * to ensure that the data in each new object file is durable before\n+\t\t * the final name is visible.\n+\t\t */\n+\t\tstrbuf_addf(&temp_path, \"%s/bulk_fsync_XXXXXX\",\n+\t\t\t    repo_get_object_directory(transaction->base.source->odb->repo));\n+\t\ttemp = xmks_tempfile(temp_path.buf);\n+\t\tfsync_or_die(get_tempfile_fd(temp), get_tempfile_path(temp));\n+\t\tdelete_tempfile(&temp);\n+\t\tstrbuf_release(&temp_path);\n+\n+\t\t/*\n+\t\t * Make the object files visible in the primary ODB after their data is\n+\t\t * fully durable.\n+\t\t */\n+\t\ttmp_objdir_migrate(transaction->objdir);\n+\t\ttransaction->objdir = NULL;\n+\t}\n+\n \tflush_packfile_transaction(transaction);\n }\n \n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547752","messageId":"20260710163722.2962278-5-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"[PATCH v4 04/11] object-file: drop check for inflight transactions","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:15Z","receivedAt":"2026-07-10T16:37:31Z","isPatch":true,"body":"ODB transactions are started via `odb_transaction_begin()` and contain\nvalidation to avoid starting multiple transactions at the same time. The\n\"files\" backend also has the same logic, but is redundant due to the\ngeneric layer already handling it. Drop this validation from the \"files\"\nbackend accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 4 ----\n object-file.h | 3 +--\n 2 files changed, 1 insertion(+), 6 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 33bd6c6810..e51389833a 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1678,10 +1678,6 @@ static void odb_transaction_files_commit(struct odb_transaction *base)\n struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n {\n \tstruct odb_transaction_files *transaction;\n-\tstruct object_database *odb = source->odb;\n-\n-\tif (odb->transaction)\n-\t\treturn NULL;\n \n \ttransaction = xcalloc(1, sizeof(*transaction));\n \ttransaction->base.source = source;\ndiff --git a/object-file.h b/object-file.h\nindex 528c4e6e69..ea43d818f0 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -194,8 +194,7 @@ struct odb_transaction;\n /*\n  * Tell the object database to optimize for adding\n  * multiple objects. odb_transaction_files_commit must be called\n- * to make new objects visible. If a transaction is already\n- * pending, NULL is returned.\n+ * to make new objects visible.\n  */\n struct odb_transaction *odb_transaction_files_begin(struct odb_source *source);\n \n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547755","messageId":"20260710163722.2962278-6-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"[PATCH v4 05/11] object-file: propagate files transaction errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:16Z","receivedAt":"2026-07-10T16:37:32Z","isPatch":true,"body":"The \"files\" transaction backend may encounter errors related to managing\nthe temporary directory used to stage objects, but silently ignores\nthese errors. Instead return errors encountered in the\n`odb_transaction_files_{prepare,begin,commit}()` interfaces to allow\ncallers to handle them as needed.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c      | 26 ++++++++++++++++++--------\n object-file.h      |  3 ++-\n odb/source-files.c |  6 +-----\n odb/transaction.h  |  7 +++++--\n 4 files changed, 26 insertions(+), 16 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex e51389833a..3651605ea2 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -499,7 +499,7 @@ struct odb_transaction_files {\n \tstruct transaction_packfile packfile;\n };\n \n-static void odb_transaction_files_prepare(struct odb_transaction *base)\n+static 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@@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base)\n \t * added at the time they call odb_transaction_files_begin.\n \t */\n \tif (!transaction || transaction->objdir)\n-\t\treturn;\n+\t\treturn 0;\n \n \ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n-\tif (transaction->objdir)\n-\t\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n+\tif (!transaction->objdir)\n+\t\treturn error(_(\"unable to create temporary object directory\"));\n+\n+\ttmp_objdir_replace_primary_odb(transaction->objdir, 0);\n+\n+\treturn 0;\n }\n \n static void odb_transaction_files_fsync(struct odb_transaction *base,\n@@ -1639,7 +1643,7 @@ int read_loose_object(struct repository *repo,\n \treturn ret;\n }\n \n-static void odb_transaction_files_commit(struct odb_transaction *base)\n+static int odb_transaction_files_commit(struct odb_transaction *base)\n {\n \tstruct odb_transaction_files *transaction =\n \t\tcontainer_of(base, struct odb_transaction_files, base);\n@@ -1668,14 +1672,19 @@ static void odb_transaction_files_commit(struct odb_transaction *base)\n \t\t * Make the object files visible in the primary ODB after their data is\n \t\t * fully durable.\n \t\t */\n-\t\ttmp_objdir_migrate(transaction->objdir);\n+\t\tif (tmp_objdir_migrate(transaction->objdir))\n+\t\t\treturn error(_(\"unable to migrate temporary objects\"));\n+\n \t\ttransaction->objdir = NULL;\n \t}\n \n \tflush_packfile_transaction(transaction);\n+\n+\treturn 0;\n }\n \n-struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n+int odb_transaction_files_begin(struct odb_source *source,\n+\t\t\t\tstruct odb_transaction **out)\n {\n \tstruct odb_transaction_files *transaction;\n \n@@ -1683,6 +1692,7 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source)\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\t*out = &transaction->base;\n \n-\treturn &transaction->base;\n+\treturn 0;\n }\ndiff --git a/object-file.h b/object-file.h\nindex ea43d818f0..1a023226ac 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -196,6 +196,7 @@ struct odb_transaction;\n  * multiple objects. odb_transaction_files_commit must be called\n  * to make new objects visible.\n  */\n-struct odb_transaction *odb_transaction_files_begin(struct odb_source *source);\n+int odb_transaction_files_begin(struct odb_source *source,\n+\t\t\t\tstruct odb_transaction **out);\n \n #endif /* OBJECT_FILE_H */\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 5bdd042922..2545bd81d4 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -182,11 +182,7 @@ static int odb_source_files_write_object_stream(struct odb_source *source,\n static int odb_source_files_begin_transaction(struct odb_source *source,\n \t\t\t\t\t      struct odb_transaction **out)\n {\n-\tstruct odb_transaction *tx = odb_transaction_files_begin(source);\n-\tif (!tx)\n-\t\treturn -1;\n-\t*out = tx;\n-\treturn 0;\n+\treturn odb_transaction_files_begin(source, out);\n }\n \n static int odb_source_files_read_alternates(struct odb_source *source,\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 854fda06f5..d52f0533ce 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -16,8 +16,11 @@ struct odb_transaction {\n \t/* The ODB source the transaction is opened against. */\n \tstruct odb_source *source;\n \n-\t/* The ODB source specific callback invoked to commit a transaction. */\n-\tvoid (*commit)(struct odb_transaction *transaction);\n+\t/*\n+\t * The ODB source specific callback invoked to commit a transaction.\n+\t * Returns 0 on success, a negative error code otherwise.\n+\t */\n+\tint (*commit)(struct odb_transaction *transaction);\n \n \t/*\n \t * This callback is expected to write the given object stream into\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547756","messageId":"20260710163722.2962278-7-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"[PATCH v4 06/11] odb/transaction: propagate begin errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:17Z","receivedAt":"2026-07-10T16:37:33Z","isPatch":true,"body":"When `odb_transaction_begin()` is invoked, the function returns the\ntransaction pointer directly. There is no way for the backend to\nsignal that it failed to set up its state, such as when creating the\ntemporary object directory backing the transaction.\n\nIn a subsequent commit, git-receive-pack(1) starts using ODB\ntransactions and needs to be able to report such failures rather\nthan silently ignore them. Refactor `odb_transaction_begin()` to\nreturn an int error code and write the resulting transaction into an\nout parameter. Also introduce `odb_transaction_begin_or_die()` as a\nconvenience for callsites that do not need to handle errors\nexplicitly.\n\nNote that `odb_transaction_begin()` now returns an error when the ODB\nalready has an inflight transaction pending. ODB transaction call sites\nthat may encounter an inflight transaction are updated to explicitly\nhandle this case.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  2 +-\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  2 +-\n cache-tree.c             |  7 +++++--\n object-file.c            | 10 +++++++---\n odb/transaction.c        | 14 ++++++++++----\n odb/transaction.h        | 19 +++++++++++++++----\n read-cache.c             |  7 +++++--\n 8 files changed, 45 insertions(+), 18 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex c859f66519..3d5d9cfdb9 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -581,7 +581,7 @@ int cmd_add(int argc,\n \t\tstring_list_clear(&only_match_skip_worktree, 0);\n \t}\n \n-\ttransaction = odb_transaction_begin(repo->objects);\n+\todb_transaction_begin_or_die(repo->objects, &transaction);\n \n \tps_matched = xcalloc(pathspec.nr, 1);\n \tif (add_renormalize)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex f3849bb654..d0136cdd99 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -598,7 +598,7 @@ static void unpack_all(void)\n \t\tprogress = start_progress(the_repository,\n \t\t\t\t\t  _(\"Unpacking objects\"), nr_objects);\n \tCALLOC_ARRAY(obj_list, nr_objects);\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \tfor (i = 0; i < nr_objects; i++) {\n \t\tunpack_one(i);\n \t\tdisplay_progress(progress, i + 1);\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 3d6646c318..17f3ea284c 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1124,7 +1124,7 @@ int cmd_update_index(int argc,\n \t * Allow the object layer to optimize adding multiple objects in\n \t * a batch.\n \t */\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \twhile (ctx.argc) {\n \t\tif (parseopt_state != PARSE_OPT_DONE)\n \t\t\tparseopt_state = parse_options_step(&ctx, options,\ndiff --git a/cache-tree.c b/cache-tree.c\nindex 184f7e2635..8eec1d4d52 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -474,6 +474,7 @@ static int update_one(struct cache_tree *it,\n \n int cache_tree_update(struct index_state *istate, int flags)\n {\n+\tint inflight = !!the_repository->objects->transaction;\n \tstruct odb_transaction *transaction;\n \tint skip, i;\n \n@@ -490,10 +491,12 @@ int cache_tree_update(struct index_state *istate, int flags)\n \n \ttrace_performance_enter();\n \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n-\ttransaction = odb_transaction_begin(the_repository->objects);\n+\tif (!inflight)\n+\t\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n \t\t       \"\", 0, &skip, flags);\n-\todb_transaction_commit(transaction);\n+\tif (!inflight)\n+\t\todb_transaction_commit(transaction);\n \ttrace2_region_leave(\"cache_tree\", \"update\", istate->repo);\n \ttrace_performance_leave(\"cache_tree_update\");\n \tif (i < 0)\ndiff --git a/object-file.c b/object-file.c\nindex 3651605ea2..358684beae 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -1354,13 +1354,17 @@ int index_fd(struct index_state *istate, struct object_id *oid,\n \n \t\tif (flags & INDEX_WRITE_OBJECT) {\n \t\t\tstruct object_database *odb = the_repository->objects;\n-\t\t\tstruct odb_transaction *transaction = odb_transaction_begin(odb);\n+\t\t\tstruct odb_transaction *transaction = odb->transaction;\n+\t\t\tint inflight = !!transaction;\n \n-\t\t\tret = odb_transaction_write_object_stream(odb->transaction,\n+\t\t\tif (!inflight)\n+\t\t\t\todb_transaction_begin_or_die(odb, &transaction);\n+\t\t\tret = odb_transaction_write_object_stream(transaction,\n \t\t\t\t\t\t\t\t  &stream,\n \t\t\t\t\t\t\t\t  xsize_t(st->st_size),\n \t\t\t\t\t\t\t\t  oid);\n-\t\t\todb_transaction_commit(transaction);\n+\t\t\tif (!inflight)\n+\t\t\t\todb_transaction_commit(transaction);\n \t\t} else {\n \t\t\tret = hash_blob_stream(&stream,\n \t\t\t\t\t       the_repository->hash_algo, oid,\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex b16e07aebf..b6da4a3942 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -1,15 +1,21 @@\n #include \"git-compat-util.h\"\n+#include \"gettext.h\"\n #include \"odb/source.h\"\n #include \"odb/transaction.h\"\n \n-struct odb_transaction *odb_transaction_begin(struct object_database *odb)\n+int odb_transaction_begin(struct object_database *odb,\n+\t\t\t  struct odb_transaction **out)\n {\n+\tint ret;\n+\n \tif (odb->transaction)\n-\t\treturn NULL;\n+\t\treturn error(_(\"object database transaction already pending\"));\n \n-\todb_source_begin_transaction(odb->sources, &odb->transaction);\n+\tret = odb_source_begin_transaction(odb->sources, out);\n+\tif (!ret)\n+\t\todb->transaction = *out;\n \n-\treturn odb->transaction;\n+\treturn ret;\n }\n \n void odb_transaction_commit(struct odb_transaction *transaction)\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex d52f0533ce..f5c43187c9 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -1,6 +1,7 @@\n #ifndef ODB_TRANSACTION_H\n #define ODB_TRANSACTION_H\n \n+#include \"gettext.h\"\n #include \"odb.h\"\n #include \"odb/source.h\"\n \n@@ -36,11 +37,21 @@ struct odb_transaction {\n };\n \n /*\n- * Starts an ODB transaction. Subsequent objects are written to the transaction\n- * and not committed until odb_transaction_commit() is invoked on the\n- * transaction. If the ODB already has a pending transaction, NULL is returned.\n+ * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n+ * written to the transaction and not committed until odb_transaction_commit()\n+ * is invoked on the transaction. Returns 0 on success and a negative value on\n+ * error. Note that it is considered an error to start a new transaction if the\n+ * ODB already has an inflight transaction pending.\n  */\n-struct odb_transaction *odb_transaction_begin(struct object_database *odb);\n+int odb_transaction_begin(struct object_database *odb,\n+\t\t\t  struct odb_transaction **out);\n+\n+static inline void odb_transaction_begin_or_die(struct object_database *odb,\n+\t\t\t\t\t\tstruct odb_transaction **out)\n+{\n+\tif (odb_transaction_begin(odb, out))\n+\t\tdie(_(\"failed to start ODB transaction\"));\n+}\n \n /*\n  * Commits an ODB transaction making the written objects visible. If the\ndiff --git a/read-cache.c b/read-cache.c\nindex 21ca58beea..d511d25834 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4012,6 +4012,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \t\t       const struct pathspec *pathspec, char *ps_matched,\n \t\t       int include_sparse, int flags, int ignored_too )\n {\n+\tint inflight = !!repo->objects->transaction;\n \tstruct odb_transaction *transaction;\n \tstruct update_callback_data data;\n \tstruct rev_info rev;\n@@ -4042,9 +4043,11 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \t * This function is invoked from commands other than 'add', which\n \t * may not have their own transaction active.\n \t */\n-\ttransaction = odb_transaction_begin(repo->objects);\n+\tif (!inflight)\n+\t\todb_transaction_begin_or_die(repo->objects, &transaction);\n \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n-\todb_transaction_commit(transaction);\n+\tif (!inflight)\n+\t\todb_transaction_commit(transaction);\n \n \trelease_revisions(&rev);\n \treturn !!data.add_errors;\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547757","messageId":"20260710163722.2962278-8-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"[PATCH v4 07/11] odb/transaction: propagate commit errors","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:18Z","receivedAt":"2026-07-10T16:37:33Z","isPatch":true,"body":"When `odb_transaction_commit()` is invoked, the return value of the\nbackend commit callback is silently discarded. A backend has no way\nto signal that committing failed, such as when the \"files\" backend\ncannot migrate its temporary object directory into the permanent\nODB.\n\nIn a subsequent commit, git-receive-pack(1) starts using ODB transaction\nto stage objects and consequently cares about such failures so it can\nhandle the error appropriately. Change the commit callback signature to\nreturn an int error code and have `odb_transaction_commit()` forward it\naccordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n odb/transaction.c | 10 +++++++---\n odb/transaction.h |  7 ++++---\n 2 files changed, 11 insertions(+), 6 deletions(-)\n\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex b6da4a3942..249ef4d9b7 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -18,19 +18,23 @@ int odb_transaction_begin(struct object_database *odb,\n \treturn ret;\n }\n \n-void odb_transaction_commit(struct odb_transaction *transaction)\n+int odb_transaction_commit(struct odb_transaction *transaction)\n {\n+\tint ret;\n+\n \tif (!transaction)\n-\t\treturn;\n+\t\treturn 0;\n \n \t/*\n \t * Ensure the transaction ending matches the pending transaction.\n \t */\n \tASSERT(transaction == transaction->source->odb->transaction);\n \n-\ttransaction->commit(transaction);\n+\tret = transaction->commit(transaction);\n \ttransaction->source->odb->transaction = NULL;\n \tfree(transaction);\n+\n+\treturn ret;\n }\n \n int odb_transaction_write_object_stream(struct odb_transaction *transaction,\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex f5c43187c9..3b0a5a78e5 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -54,10 +54,11 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n }\n \n /*\n- * Commits an ODB transaction making the written objects visible. If the\n- * specified transaction is NULL, the function is a no-op.\n+ * Commits an ODB transaction making the written objects visible. Returns 0 on\n+ * success, a negative error code otherwise. Note that, if the specified\n+ * transaction is NULL, the function is a no-op and no error is returned.\n  */\n-void odb_transaction_commit(struct odb_transaction *transaction);\n+int odb_transaction_commit(struct odb_transaction *transaction);\n \n /*\n  * Writes the object in the provided stream into the transaction. The resulting\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547758","messageId":"20260710163722.2962278-9-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"[PATCH v4 08/11] odb/transaction: add transaction env interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:19Z","receivedAt":"2026-07-10T16:37:34Z","isPatch":true,"body":"The ODB transaction backend is responsible for creating/managing its own\nstaging area for writing objects. Other child processes spawned by Git\nmay need access to uncommitted objects or write new objects in the\nstaging area though.\n\nIntroduce `odb_transaction_env()` which is expected to provide the set\nof environment variables needed by a child process to access the\ntransaction's staging area.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c     | 16 ++++++++++++++++\n odb/transaction.c |  8 ++++++++\n odb/transaction.h | 17 +++++++++++++++++\n 3 files changed, 41 insertions(+)\n\ndiff --git a/object-file.c b/object-file.c\nindex 358684beae..39b92e275c 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -27,6 +27,7 @@\n #include \"path.h\"\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n+#include \"strvec.h\"\n #include \"tempfile.h\"\n #include \"tmp-objdir.h\"\n \n@@ -1687,6 +1688,20 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n \treturn 0;\n }\n \n+static int odb_transaction_files_env(struct odb_transaction *base,\n+\t\t\t\t     struct strvec *env)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\tint ret;\n+\n+\tret = odb_transaction_files_prepare(&transaction->base);\n+\tif (!ret)\n+\t\tstrvec_pushv(env, tmp_objdir_env(transaction->objdir));\n+\n+\treturn ret;\n+}\n+\n int odb_transaction_files_begin(struct odb_source *source,\n \t\t\t\tstruct odb_transaction **out)\n {\n@@ -1696,6 +1711,7 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\ttransaction->base.env = odb_transaction_files_env;\n \t*out = &transaction->base;\n \n \treturn 0;\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex 249ef4d9b7..92ec8786a1 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -43,3 +43,11 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n {\n \treturn transaction->write_object_stream(transaction, stream, len, oid);\n }\n+\n+int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env)\n+{\n+\tif (!transaction)\n+\t\treturn 0;\n+\n+\treturn transaction->env(transaction, env);\n+}\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 3b0a5a78e5..5e51ce5ca4 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -34,6 +34,14 @@ struct odb_transaction {\n \tint (*write_object_stream)(struct odb_transaction *transaction,\n \t\t\t\t   struct odb_write_stream *stream, size_t len,\n \t\t\t\t   struct object_id *oid);\n+\n+\t/*\n+\t * This callback is expected to populate the provided strvec with the\n+\t * environment variables that a child process should inherit so that its\n+\t * object writes participate in the transaction. Returns 0 on success, a\n+\t * negative error code otherwise.\n+\t */\n+\tint (*env)(struct odb_transaction *transaction, struct strvec *env);\n };\n \n /*\n@@ -69,4 +77,13 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n \t\t\t\t\tstruct odb_write_stream *stream,\n \t\t\t\t\tsize_t len, struct object_id *oid);\n \n+/*\n+ * Populates the provided strvec with the environment variables that a child\n+ * process should inherit so that its object writes participate in the\n+ * transaction, suitable for using via child_process.env. Returns 0 on success,\n+ * a negative error code otherwise. Note that, if the specified transaction is\n+ * NULL, the function is a no-op and no error is returned.\n+ */\n+int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env);\n+\n #endif\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547759","messageId":"20260710163722.2962278-11-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"[PATCH v4 10/11] builtin/receive-pack: drop redundant tmpdir env","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:21Z","receivedAt":"2026-07-10T16:37:35Z","isPatch":true,"body":"When performing the connectivity checks for a shallow ref in\n`update_shallow_ref()`, the child process environment variables are\npopulated via `tmp_objdir_env()`. This is unnecessary though as\n`update_shallow_ref()` is only reached after `tmp_objdir_migrate()` has\nbeen performed which means there is no longer a temporary directory that\nneeds to be shared with child processes.\n\nDrop the call to `tmp_objdir_env()` accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 1 -\n 1 file changed, 1 deletion(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 19eb6a1b61..50bc05c70c 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1363,7 +1363,6 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si)\n \t\t    !delayed_reachability_test(si, i))\n \t\t\toid_array_append(&extra, &si->shallow->oid[i]);\n \n-\topt.env = tmp_objdir_env(tmp_objdir);\n \tsetup_alternate_shallow(&shallow_lock, &opt.shallow_file, &extra);\n \tif (check_connected(command_singleton_iterator, cmd, &opt)) {\n \t\trollback_shallow_file(the_repository, &shallow_lock);\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547760","messageId":"20260710163722.2962278-10-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"[PATCH v4 09/11] odb/transaction: introduce ODB transaction flags","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:20Z","receivedAt":"2026-07-10T16:37:35Z","isPatch":true,"body":"The temporary directory used by git-receive-pack(1) to write objects is\nmanaged slightly differently than how it is done via ODB transactions:\n\n  - The temporary directory is eagerly created upfront, instead of\n    waiting for the first object write.\n\n  - The prefix name of the temporary directory is \"incoming\" instead of\n    \"bulk-fsync\".\n\nIn a subsequent commit, git-receive-pack(1) will use ODB transactions\ninstead of `tmp_objdir` directly. To provide a means to configure the\nsame transaction behavior, introduce `enum odb_transaction_flags` and\nthe ODB_TRANSACTION_RECEIVE flag intended as a signal for ODB\ntransactions using the \"files\" backend to be set up for\ngit-receive-pack(1). Transaction call sites are updated accordingly to\nprovide the required flag parameter.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  2 +-\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  2 +-\n cache-tree.c             |  2 +-\n object-file.c            | 29 ++++++++++++++++++++++++++---\n object-file.h            |  4 +++-\n odb/source-files.c       |  5 +++--\n odb/source-inmemory.c    |  3 ++-\n odb/source-loose.c       |  3 ++-\n odb/source.h             |  9 ++++++---\n odb/transaction.c        |  5 +++--\n odb/transaction.h        | 15 +++++++++++----\n read-cache.c             |  2 +-\n 13 files changed, 61 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 3d5d9cfdb9..60ffbede2b 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -581,7 +581,7 @@ int cmd_add(int argc,\n \t\tstring_list_clear(&only_match_skip_worktree, 0);\n \t}\n \n-\todb_transaction_begin_or_die(repo->objects, &transaction);\n+\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \n \tps_matched = xcalloc(pathspec.nr, 1);\n \tif (add_renormalize)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex d0136cdd99..c3d0fc7507 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -598,7 +598,7 @@ static void unpack_all(void)\n \t\tprogress = start_progress(the_repository,\n \t\t\t\t\t  _(\"Unpacking objects\"), nr_objects);\n \tCALLOC_ARRAY(obj_list, nr_objects);\n-\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \tfor (i = 0; i < nr_objects; i++) {\n \t\tunpack_one(i);\n \t\tdisplay_progress(progress, i + 1);\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 17f3ea284c..bf6ea60ef4 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1124,7 +1124,7 @@ int cmd_update_index(int argc,\n \t * Allow the object layer to optimize adding multiple objects in\n \t * a batch.\n \t */\n-\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \twhile (ctx.argc) {\n \t\tif (parseopt_state != PARSE_OPT_DONE)\n \t\t\tparseopt_state = parse_options_step(&ctx, options,\ndiff --git a/cache-tree.c b/cache-tree.c\nindex 8eec1d4d52..99c6a0a7d0 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -492,7 +492,7 @@ int cache_tree_update(struct index_state *istate, int flags)\n \ttrace_performance_enter();\n \ttrace2_region_enter(\"cache_tree\", \"update\", istate->repo);\n \tif (!inflight)\n-\t\todb_transaction_begin_or_die(the_repository->objects, &transaction);\n+\t\todb_transaction_begin_or_die(the_repository->objects, &transaction, 0);\n \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n \t\t       \"\", 0, &skip, flags);\n \tif (!inflight)\ndiff --git a/object-file.c b/object-file.c\nindex 39b92e275c..0640a22009 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -498,6 +498,7 @@ struct odb_transaction_files {\n \n \tstruct tmp_objdir *objdir;\n \tstruct transaction_packfile packfile;\n+\tconst char *prefix;\n };\n \n static int odb_transaction_files_prepare(struct odb_transaction *base)\n@@ -514,7 +515,7 @@ static int odb_transaction_files_prepare(struct odb_transaction *base)\n \tif (!transaction || transaction->objdir)\n \t\treturn 0;\n \n-\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, \"bulk-fsync\");\n+\ttransaction->objdir = tmp_objdir_create(base->source->odb->repo, transaction->prefix);\n \tif (!transaction->objdir)\n \t\treturn error(_(\"unable to create temporary object directory\"));\n \n@@ -1359,7 +1360,7 @@ int index_fd(struct index_state *istate, struct object_id *oid,\n \t\t\tint inflight = !!transaction;\n \n \t\t\tif (!inflight)\n-\t\t\t\todb_transaction_begin_or_die(odb, &transaction);\n+\t\t\t\todb_transaction_begin_or_die(odb, &transaction, 0);\n \t\t\tret = odb_transaction_write_object_stream(transaction,\n \t\t\t\t\t\t\t\t  &stream,\n \t\t\t\t\t\t\t\t  xsize_t(st->st_size),\n@@ -1703,7 +1704,8 @@ static int odb_transaction_files_env(struct odb_transaction *base,\n }\n \n int odb_transaction_files_begin(struct odb_source *source,\n-\t\t\t\tstruct odb_transaction **out)\n+\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\tenum odb_transaction_flags flags)\n {\n \tstruct odb_transaction_files *transaction;\n \n@@ -1712,6 +1714,27 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction->base.commit = odb_transaction_files_commit;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n \ttransaction->base.env = odb_transaction_files_env;\n+\n+\ttransaction->prefix = \"bulk-fsync\";\n+\tif (flags & ODB_TRANSACTION_RECEIVE) {\n+\t\t/*\n+\t\t * ODB transactions for git-receive-pack(1) eagerly create a\n+\t\t * temporary directory and use a different temporary directory\n+\t\t * prefix.\n+\t\t *\n+\t\t * NEEDSWORK: This transaction flag is only used by the \"files\"\n+\t\t * backend to special case temporary directory set up and\n+\t\t * handling. Ideally transaction users should not have to care\n+\t\t * though. To avoid this, we could eagerly create the temporary\n+\t\t * directory and use the same prefix name for all transactions.\n+\t\t */\n+\t\ttransaction->prefix = \"incoming\";\n+\t\tif (odb_transaction_files_prepare(&transaction->base)) {\n+\t\t\tfree(transaction);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\n \t*out = &transaction->base;\n \n \treturn 0;\ndiff --git a/object-file.h b/object-file.h\nindex 1a023226ac..bdd2d67a2e 100644\n--- a/object-file.h\n+++ b/object-file.h\n@@ -5,6 +5,7 @@\n #include \"object.h\"\n #include \"odb.h\"\n #include \"odb/source-loose.h\"\n+#include \"odb/transaction.h\"\n \n /* The maximum size for an object header. */\n #define MAX_HEADER_LEN 32\n@@ -197,6 +198,7 @@ struct odb_transaction;\n  * to make new objects visible.\n  */\n int odb_transaction_files_begin(struct odb_source *source,\n-\t\t\t\tstruct odb_transaction **out);\n+\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\tenum odb_transaction_flags flags);\n \n #endif /* OBJECT_FILE_H */\ndiff --git a/odb/source-files.c b/odb/source-files.c\nindex 2545bd81d4..534f48aad9 100644\n--- a/odb/source-files.c\n+++ b/odb/source-files.c\n@@ -180,9 +180,10 @@ static int odb_source_files_write_object_stream(struct odb_source *source,\n }\n \n static int odb_source_files_begin_transaction(struct odb_source *source,\n-\t\t\t\t\t      struct odb_transaction **out)\n+\t\t\t\t\t      struct odb_transaction **out,\n+\t\t\t\t\t      enum odb_transaction_flags flags)\n {\n-\treturn odb_transaction_files_begin(source, out);\n+\treturn odb_transaction_files_begin(source, out, flags);\n }\n \n static int odb_source_files_read_alternates(struct odb_source *source,\ndiff --git a/odb/source-inmemory.c b/odb/source-inmemory.c\nindex e004566d76..9644d9d474 100644\n--- a/odb/source-inmemory.c\n+++ b/odb/source-inmemory.c\n@@ -304,7 +304,8 @@ static int odb_source_inmemory_freshen_object(struct odb_source *source,\n }\n \n static int odb_source_inmemory_begin_transaction(struct odb_source *source UNUSED,\n-\t\t\t\t\t\t struct odb_transaction **out UNUSED)\n+\t\t\t\t\t\t struct odb_transaction **out UNUSED,\n+\t\t\t\t\t\t enum odb_transaction_flags flags UNUSED)\n {\n \treturn error(\"in-memory source does not support transactions\");\n }\ndiff --git a/odb/source-loose.c b/odb/source-loose.c\nindex 66e6bb8d3f..57c91986b4 100644\n--- a/odb/source-loose.c\n+++ b/odb/source-loose.c\n@@ -638,7 +638,8 @@ static int odb_source_loose_write_object_stream(struct odb_source *source,\n }\n \n static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED,\n-\t\t\t\t\t      struct odb_transaction **out UNUSED)\n+\t\t\t\t\t      struct odb_transaction **out UNUSED,\n+\t\t\t\t\t      enum odb_transaction_flags flags UNUSED)\n {\n \t/* TODO: this is a known omission that we'll want to address eventually. */\n \treturn error(\"loose source does not support transactions\");\ndiff --git a/odb/source.h b/odb/source.h\nindex 2192a101b8..3790d03ff2 100644\n--- a/odb/source.h\n+++ b/odb/source.h\n@@ -3,6 +3,7 @@\n \n #include \"object.h\"\n #include \"odb.h\"\n+#include \"odb/transaction.h\"\n \n enum odb_source_type {\n \t/*\n@@ -228,7 +229,8 @@ struct odb_source {\n \t * negative error code otherwise.\n \t */\n \tint (*begin_transaction)(struct odb_source *source,\n-\t\t\t\t struct odb_transaction **out);\n+\t\t\t\t struct odb_transaction **out,\n+\t\t\t\t enum odb_transaction_flags flags);\n \n \t/*\n \t * This callback is expected to read the list of alternate object\n@@ -467,9 +469,10 @@ static inline int odb_source_write_alternate(struct odb_source *source,\n  * Returns 0 on success, a negative error code otherwise.\n  */\n static inline int odb_source_begin_transaction(struct odb_source *source,\n-\t\t\t\t\t       struct odb_transaction **out)\n+\t\t\t\t\t       struct odb_transaction **out,\n+\t\t\t\t\t       enum odb_transaction_flags flags)\n {\n-\treturn source->begin_transaction(source, out);\n+\treturn source->begin_transaction(source, out, flags);\n }\n \n #endif\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex 92ec8786a1..dab7da6a9a 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -4,14 +4,15 @@\n #include \"odb/transaction.h\"\n \n int odb_transaction_begin(struct object_database *odb,\n-\t\t\t  struct odb_transaction **out)\n+\t\t\t  struct odb_transaction **out,\n+\t\t\t  enum odb_transaction_flags flags)\n {\n \tint ret;\n \n \tif (odb->transaction)\n \t\treturn error(_(\"object database transaction already pending\"));\n \n-\tret = odb_source_begin_transaction(odb->sources, out);\n+\tret = odb_source_begin_transaction(odb->sources, out, flags);\n \tif (!ret)\n \t\todb->transaction = *out;\n \ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 5e51ce5ca4..4cb2eafcbf 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -3,7 +3,6 @@\n \n #include \"gettext.h\"\n #include \"odb.h\"\n-#include \"odb/source.h\"\n \n /*\n  * A transaction may be started for an object database prior to writing new\n@@ -44,6 +43,12 @@ struct odb_transaction {\n \tint (*env)(struct odb_transaction *transaction, struct strvec *env);\n };\n \n+/* Flags used to configure an ODB transaction. */\n+enum odb_transaction_flags {\n+\t/* Configures the transaction for use with git-receive-pack(1). */\n+\tODB_TRANSACTION_RECEIVE = (1 << 0),\n+};\n+\n /*\n  * Starts an ODB transaction and returns it via `out`. Subsequent objects are\n  * written to the transaction and not committed until odb_transaction_commit()\n@@ -52,12 +57,14 @@ struct odb_transaction {\n  * ODB already has an inflight transaction pending.\n  */\n int odb_transaction_begin(struct object_database *odb,\n-\t\t\t  struct odb_transaction **out);\n+\t\t\t  struct odb_transaction **out,\n+\t\t\t  enum odb_transaction_flags flags);\n \n static inline void odb_transaction_begin_or_die(struct object_database *odb,\n-\t\t\t\t\t\tstruct odb_transaction **out)\n+\t\t\t\t\t\tstruct odb_transaction **out,\n+\t\t\t\t\t\tenum odb_transaction_flags flags)\n {\n-\tif (odb_transaction_begin(odb, out))\n+\tif (odb_transaction_begin(odb, out, flags))\n \t\tdie(_(\"failed to start ODB transaction\"));\n }\n \ndiff --git a/read-cache.c b/read-cache.c\nindex d511d25834..50e2320c8d 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4044,7 +4044,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \t * may not have their own transaction active.\n \t */\n \tif (!inflight)\n-\t\todb_transaction_begin_or_die(repo->objects, &transaction);\n+\t\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n \tif (!inflight)\n \t\todb_transaction_commit(transaction);\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547761","messageId":"20260710163722.2962278-12-jltobler@gmail.com","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"[PATCH v4 11/11] builtin/receive-pack: stage incoming objects via ODB transactions","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-07-10T16:37:22Z","receivedAt":"2026-07-10T16:37:36Z","isPatch":true,"body":"Objects received by git-receive-pack(1) are quarantined in a temporary\n\"incoming\" directory and migrated into the object database prior to the\nreference updates. The quarantine is currently managed through\n`tmp_objdir` directly. In a pluggable ODB future, how exactly an object\ngets written to a transaction may vary for a given ODB source. Refactor\ngit-receive-pack(1) to use the ODB transaction interfaces to manage the\nobject staging area in a more agnostic manner accordingly.\n\nNote that the ODB transaction is now responsible for managing the\nprimary and alternate ODBs for the repository. One small change as a\nresult is that the temporary directory is now applied as the primary ODB\nin the main process instead of an alternate. This does not change\nanything for git-receive-pack(1) though because it only needs access to\nthe newly written objects and doesn't care how exactly it is set up.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 68 ++++++++++++++++++++++--------------------\n 1 file changed, 35 insertions(+), 33 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 50bc05c70c..8b8c20dc1a 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -37,7 +37,6 @@\n #include \"sigchain.h\"\n #include \"string-list.h\"\n #include \"strvec.h\"\n-#include \"tmp-objdir.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n #include \"version.h\"\n@@ -112,8 +111,6 @@ static enum {\n } use_keepalive;\n static int keepalive_in_sec = 5;\n \n-static struct tmp_objdir *tmp_objdir;\n-\n static struct proc_receive_ref {\n \tunsigned int want_add:1,\n \t\t     want_delete:1,\n@@ -926,6 +923,7 @@ static void receive_hook_feed_state_free(void *data)\n static int run_receive_hook(struct command *commands,\n \t\t\t    const char *hook_name,\n \t\t\t    int skip_broken,\n+\t\t\t    struct odb_transaction *transaction,\n \t\t\t    const struct string_list *push_options)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n@@ -959,8 +957,8 @@ static int run_receive_hook(struct command *commands,\n \t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n \t}\n \n-\tif (tmp_objdir)\n-\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n+\tif (transaction)\n+\t\todb_transaction_env(transaction, &opt.env);\n \n \tprepare_push_cert_sha1(&opt);\n \n@@ -1789,24 +1787,30 @@ static const struct object_id *command_singleton_iterator(void *cb_data)\n }\n \n static void set_connectivity_errors(struct command *commands,\n-\t\t\t\t    struct shallow_info *si)\n+\t\t\t\t    struct shallow_info *si,\n+\t\t\t\t    struct odb_transaction *transaction)\n {\n \tstruct command *cmd;\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tstruct command *singleton = cmd;\n \t\tstruct check_connected_options opt = CHECK_CONNECTED_INIT;\n+\t\tstruct strvec env = STRVEC_INIT;\n \n \t\tif (shallow_update && si->shallow_ref[cmd->index])\n \t\t\t/* to be checked in update_shallow_ref() */\n \t\t\tcontinue;\n \n-\t\topt.env = tmp_objdir_env(tmp_objdir);\n+\t\todb_transaction_env(transaction, &env);\n+\t\topt.env = env.v;\n+\n \t\tif (!check_connected(command_singleton_iterator, &singleton,\n \t\t\t\t     &opt))\n \t\t\tcontinue;\n \n \t\tcmd->error_string = \"missing necessary objects\";\n+\n+\t\tstrvec_clear(&env);\n \t}\n }\n \n@@ -2027,6 +2031,7 @@ static void execute_commands_atomic(struct command *commands,\n static void execute_commands(struct command *commands,\n \t\t\t     const char *unpacker_error,\n \t\t\t     struct shallow_info *si,\n+\t\t\t     struct odb_transaction *transaction,\n \t\t\t     const struct string_list *push_options)\n {\n \tstruct check_connected_options opt = CHECK_CONNECTED_INIT;\n@@ -2043,6 +2048,8 @@ static void execute_commands(struct command *commands,\n \t}\n \n \tif (!skip_connectivity_check) {\n+\t\tstruct strvec env = STRVEC_INIT;\n+\n \t\tif (use_sideband) {\n \t\t\tmemset(&muxer, 0, sizeof(muxer));\n \t\t\tmuxer.proc = copy_to_sideband;\n@@ -2056,14 +2063,17 @@ static void execute_commands(struct command *commands,\n \t\tdata.si = si;\n \t\topt.err_fd = err_fd;\n \t\topt.progress = err_fd && !quiet;\n-\t\topt.env = tmp_objdir_env(tmp_objdir);\n+\t\todb_transaction_env(transaction, &env);\n+\t\topt.env = env.v;\n \t\topt.exclude_hidden_refs_section = \"receive\";\n \n \t\tif (check_connected(iterate_receive_command_list, &data, &opt))\n-\t\t\tset_connectivity_errors(commands, si);\n+\t\t\tset_connectivity_errors(commands, si, transaction);\n \n \t\tif (use_sideband)\n \t\t\tfinish_async(&muxer);\n+\n+\t\tstrvec_clear(&env);\n \t}\n \n \treject_updates_to_hidden(commands);\n@@ -2084,7 +2094,7 @@ static void execute_commands(struct command *commands,\n \t\t}\n \t}\n \n-\tif (run_receive_hook(commands, \"pre-receive\", 0, push_options)) {\n+\tif (run_receive_hook(commands, \"pre-receive\", 0, transaction, push_options)) {\n \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\t\tif (!cmd->error_string)\n \t\t\t\tcmd->error_string = \"pre-receive hook declined\";\n@@ -2105,14 +2115,13 @@ static void execute_commands(struct command *commands,\n \t * Now we'll start writing out refs, which means the objects need\n \t * to be in their final positions so that other processes can see them.\n \t */\n-\tif (tmp_objdir_migrate(tmp_objdir) < 0) {\n+\tif (odb_transaction_commit(transaction)) {\n \t\tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\t\tif (!cmd->error_string)\n \t\t\t\tcmd->error_string = \"unable to migrate objects to permanent storage\";\n \t\t}\n \t\treturn;\n \t}\n-\ttmp_objdir = NULL;\n \n \tcheck_aliased_updates(commands);\n \n@@ -2325,7 +2334,8 @@ static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n \t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n }\n \n-static const char *unpack(int err_fd, struct shallow_info *si)\n+static const char *unpack(int err_fd, struct shallow_info *si,\n+\t\t\t  struct odb_transaction *transaction)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2350,20 +2360,7 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n \t\tstrvec_push(&child.args, alt_shallow_file);\n \t}\n \n-\ttmp_objdir = tmp_objdir_create(the_repository, \"incoming\");\n-\tif (!tmp_objdir) {\n-\t\tif (err_fd > 0)\n-\t\t\tclose(err_fd);\n-\t\treturn \"unable to create temporary object directory\";\n-\t}\n-\tstrvec_pushv(&child.env, tmp_objdir_env(tmp_objdir));\n-\n-\t/*\n-\t * Normally we just pass the tmp_objdir environment to the child\n-\t * processes that do the heavy lifting, but we may need to see these\n-\t * objects ourselves to set up shallow information.\n-\t */\n-\ttmp_objdir_add_as_alternate(tmp_objdir);\n+\todb_transaction_env(transaction, &child.env);\n \n \tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n@@ -2430,13 +2427,14 @@ static const char *unpack(int err_fd, struct shallow_info *si)\n \treturn NULL;\n }\n \n-static const char *unpack_with_sideband(struct shallow_info *si)\n+static const char *unpack_with_sideband(struct shallow_info *si,\n+\t\t\t\t\tstruct odb_transaction *transaction)\n {\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(0, si);\n+\t\treturn unpack(0, si, transaction);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2445,7 +2443,7 @@ static const char *unpack_with_sideband(struct shallow_info *si)\n \tif (start_async(&muxer))\n \t\treturn NULL;\n \n-\tret = unpack(muxer.in, si);\n+\tret = unpack(muxer.in, si, transaction);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2622,6 +2620,7 @@ int cmd_receive_pack(int argc,\n \tstruct oid_array ref = OID_ARRAY_INIT;\n \tstruct shallow_info si;\n \tstruct packet_reader reader;\n+\tstruct odb_transaction *transaction = NULL;\n \n \tstruct option options[] = {\n \t\tOPT__QUIET(&quiet, N_(\"quiet\")),\n@@ -2706,11 +2705,14 @@ int cmd_receive_pack(int argc,\n \t\tif (!si.nr_ours && !si.nr_theirs)\n \t\t\tshallow_update = 0;\n \t\tif (!delete_only(commands)) {\n-\t\t\tunpack_status = unpack_with_sideband(&si);\n+\t\t\tif (odb_transaction_begin(the_repository->objects, &transaction, ODB_TRANSACTION_RECEIVE))\n+\t\t\t\tunpack_status = \"unable to start object transaction\";\n+\t\t\telse\n+\t\t\t\tunpack_status = unpack_with_sideband(&si, transaction);\n \t\t\tupdate_shallow_info(commands, &si, &ref);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n-\t\texecute_commands(commands, unpack_status, &si,\n+\t\texecute_commands(commands, unpack_status, &si, transaction,\n \t\t\t\t &push_options);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n@@ -2719,7 +2721,7 @@ int cmd_receive_pack(int argc,\n \t\telse if (report_status)\n \t\t\treport(commands, unpack_status);\n \t\tsigchain_pop(SIGPIPE);\n-\t\trun_receive_hook(commands, \"post-receive\", 1,\n+\t\trun_receive_hook(commands, \"post-receive\", 1, NULL,\n \t\t\t\t &push_options);\n \t\trun_update_post_hook(commands);\n \t\tfree_commands(commands);\n-- \n2.55.0.122.gf85a7e6620\n\n"},{"id":"547777","messageId":"xmqqtsq6dbyb.fsf@gitster.g","threadId":"65861","inReplyTo":"alEBEbwOMFkVfuk9@denethor","subject":"Re: [PATCH v3 11/11] builtin/receive-pack: stage incoming objects via ODB transactions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-10T16:52:12Z","receivedAt":"2026-07-10T16:52:14Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> On 26/07/08 08:49PM, Junio C Hamano wrote:\n>> Justin Tobler <jltobler@gmail.com> writes:\n>> >  \t\t\tupdate_shallow_info(commands, &si, &ref);\n>> >  \t\t}\n>> >  \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n>> > -\t\texecute_commands(commands, unpack_status, &si,\n>> > +\t\texecute_commands(commands, unpack_status, &si, transaction,\n>> >  \t\t\t\t &push_options);\n>> \n>> And in such a case, execute_commands() returns without committing\n>> the transaction.  Is there a need to add and make an\n>> odb_transaction_abort() call or something in such a case?\n>> Everything should be cleaned up upon process exit, and on file based\n>> backends, we probably let the tempfile/lockfile API do their thing\n>> to clean up, but are there other things we may want to clean up?\n>\n> As you mentioned, if we exit before committing the ODB transaction, the\n> temporary directory will get cleaned up when the process exits. I don't\n> think there is anything else we need to cleanup that wouldn't be handled\n> at exit though. Regardless, I do plan to add `odb_transaction_abort()`\n> in a followup series and I think it would be nice to have an explicit\n> \"abort\" here when we know that we are not going to commit anyways. I\n> would like to defer this to my next series though.\n\nSounds good.  We cannot trigger receive-pack as a subroutine call in\na long running daemon until that happens, but that is OK for now.\nOne step at a time.\n"},{"id":"547939","messageId":"alR1P-RGZNmjyiUE@pks.im","threadId":"65861","inReplyTo":"20260710163722.2962278-1-jltobler@gmail.com","subject":"Re: [PATCH v4 00/11] receive-pack: use ODB transactions to stage object writes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-13T05:18:55Z","receivedAt":"2026-07-13T05:19:01Z","isPatch":true,"body":"On Fri, Jul 10, 2026 at 11:37:11AM -0500, Justin Tobler wrote:\n> Changes since V3:\n>   - Removed ugly line break in commit message to prevent eye strain.\n>   - `odb_transaction_begin()` now only sets the repository transaction\n>     on success.\n>   - `odb_transaction_env()` now bubbles up error when failing to create\n>     the temporary directory.\n\nThanks, all the changes here look good to me and I'm happy with the\nstate of this patch series.\n\nPatrick\n"}]}