{"thread":{"id":"66133","subject":"[PATCH 0/6] builtin/receive-pack: support pluggable packfile writes","startedAt":"2026-08-06T21:39:06Z","lastAt":"2026-08-21T15:05:40Z","messageCount":80,"participants":["Justin Tobler","Patrick Steinhardt","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":6},"messages":[{"id":"549891","messageId":"20260806213859.816157-1-jltobler@gmail.com","threadId":"66133","inReplyTo":null,"subject":"[PATCH 0/6] builtin/receive-pack: support pluggable packfile writes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-06T21:38:53Z","receivedAt":"2026-08-06T21:39:06Z","isPatch":true,"body":"Greetings,\n\nWith bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\ntransactions, 2026-07-10), git-receive-pack(1) started using the ODB\ntransaction interfaces to stage incoming objects. While this brought the\ncommand closer to being ODB backend agnostic, the underlying\ngit-index-pack(1) and git-unpack-objects(1) processes used to actually\nwrite the objects to the transaction are still fundamentally tied to the\n\"files\" backend.\n\nThis series aims to address this by introducing a generic\n`odb_transaction_write_pack()` transaction interface to handle writing\nthe incoming packfile to the transaction. The existing logic in\ngit-receive-pack(1) that spawns the child processes to write the\npackfile becomes the \"files\" backend implementation of this interface.\n\nAs part of this series, the first patch also introduces the\n`odb_transaction_release()` transaction interface. This is done to\ndecouple freeing the transaction from committing it and is used later in\nthe series to allow the post-commit cleanup of the \".keep\" lockfile to\nbe deffered until after references have been updated. I'm a bit\nuncertain as to whether the \"release\" part of the transaction lifecyle\nis really the appropriate spot for such logic though. An alternative\ncould be to introduce a separate post-commit transaction interface that\nwould exist to remove any lockfiles after reference updates have been\nperformed. I am not certain such an explicit transaction interface is\nalso the best route either. In this version, I've opted to keep it\nsimple for now and tie the lockfile cleanup to transaction release, but\nI am open to change based on feedback. :)\n\nMost of the other patches are just structural refactorings to prepare\ngit-receive-pack(1) to eventually use `odb_transaction_write_pack()`.\nThe final patch makes the switch in git-receive-pack(1).\n\nThanks for the review,\n-Justin\n\nJustin Tobler (6):\n  odb/transaction: add transaction release interface\n  builtin/receive-pack: pass shallow file explicitly\n  builtin/receive-pack: lift global state out of unpack()\n  builtin/receive-pack: report unpack errors via strbuf\n  builtin/receive-pack: explicitly pass packfile fd\n  odb/transaction: add transaction interface to write packfiles\n\n builtin/add.c            |   3 +-\n builtin/receive-pack.c   | 192 ++++++++++-----------------------------\n builtin/unpack-objects.c |   1 +\n builtin/update-index.c   |   2 +\n cache-tree.c             |   4 +-\n object-file.c            | 153 ++++++++++++++++++++++++++++++-\n odb/transaction.c        |  19 +++-\n odb/transaction.h        |  77 ++++++++++++++++\n read-cache.c             |   4 +-\n 9 files changed, 305 insertions(+), 150 deletions(-)\n\n\nbase-commit: 2c78326f810173a4f3aefd8021f1e07575412481\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"549892","messageId":"20260806213859.816157-2-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260806213859.816157-1-jltobler@gmail.com","subject":"[PATCH 1/6] odb/transaction: add transaction release interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-06T21:38:54Z","receivedAt":"2026-08-06T21:39:07Z","isPatch":true,"body":"When committing an ODB transaction via `odb_transaction_commit()`, the\nstaged objects are made visible and the underlying transaction is freed\nat the same time. Coupling these two steps does not leave room for any\npost-commit transaction operations to be introduced though. Such a\ncapability is useful if an ODB transaction backend needs to hold on to\nlockfiles after transaction commit until references are updated, as is\nthe case with the existing \"files\" backend in git-receive-pack(1).\n\nStop freeing the transaction in `odb_transaction_commit()` and introduce\n`odb_transaction_release()` to explicitly clean up the transaction\naccordingly. Note that the release interface also provides an optional\ncallback for any backend-specific deferred cleanup. In a subsequent\ncommit, the \"files\" transaction backend will use this to remove \".keep\"\nfiles generated for packfiles received via git-receive-pack(1) after\nreferences have been updated.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  3 ++-\n builtin/receive-pack.c   |  1 +\n builtin/unpack-objects.c |  1 +\n builtin/update-index.c   |  2 ++\n cache-tree.c             |  4 +++-\n object-file.c            |  4 +++-\n odb/transaction.c        | 12 +++++++++++-\n odb/transaction.h        | 14 ++++++++++++++\n read-cache.c             |  4 +++-\n 9 files changed, 40 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 60ffbede2b..037491a51e 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -393,7 +393,7 @@ int cmd_add(int argc,\n \tchar *seen = NULL;\n \tchar *ps_matched = NULL;\n \tstruct lock_file lock_file = LOCK_INIT;\n-\tstruct odb_transaction *transaction;\n+\tstruct odb_transaction *transaction = NULL;\n \n \trepo_config(repo, add_config, NULL);\n \n@@ -610,5 +610,6 @@ int cmd_add(int argc,\n \tfree(ps_matched);\n \tdir_clear(&dir);\n \tclear_pathspec(&pathspec);\n+\todb_transaction_release(transaction);\n \treturn exit_status;\n }\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 86933d8d7e..420de9aa7f 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2714,6 +2714,7 @@ int cmd_receive_pack(int argc,\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n \t\texecute_commands(commands, unpack_status, &si, transaction,\n \t\t\t\t &push_options);\n+\t\todb_transaction_release(transaction);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex 4263edfbec..13d4b7f1ad 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -604,6 +604,7 @@ static void unpack_all(void)\n \t\tdisplay_progress(progress, i + 1);\n \t}\n \todb_transaction_commit(transaction);\n+\todb_transaction_release(transaction);\n \tstop_progress(&progress);\n \n \tif (delta_list)\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 241abd4332..1484835ef0 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1157,6 +1157,7 @@ int cmd_update_index(int argc,\n \t\t\t */\n \t\t\tif (transaction && verbose) {\n \t\t\t\todb_transaction_commit(transaction);\n+\t\t\t\todb_transaction_release(transaction);\n \t\t\t\ttransaction = NULL;\n \t\t\t}\n \n@@ -1225,6 +1226,7 @@ int cmd_update_index(int argc,\n \t * By now we have added all of the new objects\n \t */\n \todb_transaction_commit(transaction);\n+\todb_transaction_release(transaction);\n \n \tif (split_index > 0) {\n \t\tif (repo_config_get_split_index(the_repository) == 0)\ndiff --git a/cache-tree.c b/cache-tree.c\nindex d92f513286..5a2fa7f22d 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -537,8 +537,10 @@ int cache_tree_update(struct index_state *istate, int flags)\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)\n+\tif (!inflight) {\n \t\todb_transaction_commit(transaction);\n+\t\todb_transaction_release(transaction);\n+\t}\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 ec35c318bc..30b4717d3e 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -964,8 +964,10 @@ int index_fd(struct index_state *istate, struct object_id *oid,\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\tif (!inflight)\n+\t\t\tif (!inflight) {\n \t\t\t\todb_transaction_commit(transaction);\n+\t\t\t\todb_transaction_release(transaction);\n+\t\t\t}\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 dab7da6a9a..ce1e24f3ed 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -33,11 +33,21 @@ int odb_transaction_commit(struct odb_transaction *transaction)\n \n \tret = transaction->commit(transaction);\n \ttransaction->source->odb->transaction = NULL;\n-\tfree(transaction);\n \n \treturn ret;\n }\n \n+void odb_transaction_release(struct odb_transaction *transaction)\n+{\n+\tif (!transaction)\n+\t\treturn;\n+\n+\tif (transaction->release)\n+\t\ttransaction->release(transaction);\n+\n+\tfree(transaction);\n+}\n+\n 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)\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 4cb2eafcbf..ec0b27c449 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -22,6 +22,13 @@ struct odb_transaction {\n \t */\n \tint (*commit)(struct odb_transaction *transaction);\n \n+\t/*\n+\t * Optional ODB source specific callback invoked when the transaction\n+\t * needs to perform any deferred cleanup after objects have been\n+\t * committed.\n+\t */\n+\tvoid (*release)(struct odb_transaction *transaction);\n+\n \t/*\n \t * This callback is expected to write the given object stream into\n \t * the ODB transaction. Note that for now, only blobs support streaming.\n@@ -75,6 +82,13 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n  */\n int odb_transaction_commit(struct odb_transaction *transaction);\n \n+/*\n+ * Releases an ODB transaction, performing any deferred cleanup and freeing it.\n+ * Must be called for every successfully started transaction. Note that, if the\n+ * specified transaction is NULL, the function is a no-op.\n+ */\n+void odb_transaction_release(struct odb_transaction *transaction);\n+\n /*\n  * Writes the object in the provided stream into the transaction. The resulting\n  * object ID is written into the out pointer. Returns 0 on success, a negative\ndiff --git a/read-cache.c b/read-cache.c\nindex 6c449f393d..42623f6e10 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4048,8 +4048,10 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \tif (!inflight)\n \t\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n-\tif (!inflight)\n+\tif (!inflight) {\n \t\todb_transaction_commit(transaction);\n+\t\todb_transaction_release(transaction);\n+\t}\n \n \trelease_revisions(&rev);\n \treturn !!data.add_errors;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"549895","messageId":"20260806213859.816157-3-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260806213859.816157-1-jltobler@gmail.com","subject":"[PATCH 2/6] builtin/receive-pack: pass shallow file explicitly","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-06T21:38:55Z","receivedAt":"2026-08-06T21:39:07Z","isPatch":true,"body":"If shallow information is provided during `unpack()`, a temporary\nshallow file is created and stored in global state. In a subsequent\ncommit, the `unpack()` logic is moved behind a generic ODB transaction\ninterface to handle writing packfiles and thus can no longer rely on\nsuch global state. Lift the setup of the temporary shallow file out of\n`unpack()` and wire it through to its call sites explicitly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 38 ++++++++++++++++++++++----------------\n 1 file changed, 22 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 420de9aa7f..6da854fca2 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -86,7 +86,6 @@ static const char *head_name;\n static void *head_name_to_free;\n static int sent_capabilities;\n static int shallow_update;\n-static const char *alt_shallow_file;\n static struct strbuf push_cert = STRBUF_INIT;\n static struct object_id push_cert_oid;\n static struct signature_check sigcheck;\n@@ -2334,8 +2333,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-\t\t\t  struct odb_transaction *transaction)\n+static const char *unpack(struct odb_transaction *transaction,\n+\t\t\t  const char *shallow_file, int err_fd)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2354,10 +2353,9 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n \t\treturn hdr_err;\n \t}\n \n-\tif (si->nr_ours || si->nr_theirs) {\n-\t\talt_shallow_file = setup_temporary_shallow(si->shallow);\n+\tif (shallow_file) {\n \t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, alt_shallow_file);\n+\t\tstrvec_push(&child.args, shallow_file);\n \t}\n \n \todb_transaction_env(transaction, &child.env);\n@@ -2427,14 +2425,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-\t\t\t\t\tstruct odb_transaction *transaction)\n+static const char *unpack_with_sideband(struct odb_transaction *transaction,\n+\t\t\t\t\tconst char *shallow_file)\n {\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(0, si, transaction);\n+\t\treturn unpack(transaction, shallow_file, 0);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2443,13 +2441,14 @@ 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, transaction);\n+\tret = unpack(transaction, shallow_file, muxer.in);\n \n \tfinish_async(&muxer);\n \treturn ret;\n }\n \n-static void prepare_shallow_update(struct shallow_info *si)\n+static void prepare_shallow_update(struct shallow_info *si,\n+\t\t\t\t   const char *shallow_file)\n {\n \tint i, j, k, bitmap_size = DIV_ROUND_UP(si->ref->nr, 32);\n \n@@ -2489,12 +2488,13 @@ static void prepare_shallow_update(struct shallow_info *si)\n \t * command. check_connected() will be done with\n \t * true .git/shallow though.\n \t */\n-\tsetenv(GIT_SHALLOW_FILE_ENVIRONMENT, alt_shallow_file, 1);\n+\tsetenv(GIT_SHALLOW_FILE_ENVIRONMENT, shallow_file, 1);\n }\n \n static void update_shallow_info(struct command *commands,\n \t\t\t\tstruct shallow_info *si,\n-\t\t\t\tstruct oid_array *ref)\n+\t\t\t\tstruct oid_array *ref,\n+\t\t\t\tconst char *shallow_file)\n {\n \tstruct command *cmd;\n \tint *ref_status;\n@@ -2513,7 +2513,7 @@ static void update_shallow_info(struct command *commands,\n \tsi->ref = ref;\n \n \tif (shallow_update) {\n-\t\tprepare_shallow_update(si);\n+\t\tprepare_shallow_update(si, shallow_file);\n \t\treturn;\n \t}\n \n@@ -2705,11 +2705,17 @@ 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\tconst char *alt_shallow_file = NULL;\n+\n+\t\t\tif (si.nr_ours || si.nr_theirs)\n+\t\t\t\talt_shallow_file = setup_temporary_shallow(si.shallow);\n+\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\t\tunpack_status = unpack_with_sideband(transaction, alt_shallow_file);\n+\n+\t\t\tupdate_shallow_info(commands, &si, &ref, alt_shallow_file);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n \t\texecute_commands(commands, unpack_status, &si, transaction,\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"549893","messageId":"20260806213859.816157-5-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260806213859.816157-1-jltobler@gmail.com","subject":"[PATCH 4/6] builtin/receive-pack: report unpack errors via strbuf","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-06T21:38:57Z","receivedAt":"2026-08-06T21:39:08Z","isPatch":true,"body":"When writing packfiles via `unpack()`, error messages are returned\ndirectly by the function. In preparation for `unpack()` logic being\nmoved behind a generic ODB transaction interface, update the function to\ninstead write any error messages to a caller provided strbuf and return\na negative value on error. Call sites are updated to use the error\nstrbuf accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 63 ++++++++++++++++++++++++------------------\n 1 file changed, 36 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 8c2d6e5789..7635b82bd3 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2028,7 +2028,7 @@ static void execute_commands_atomic(struct command *commands,\n }\n \n static void execute_commands(struct command *commands,\n-\t\t\t     const char *unpacker_error,\n+\t\t\t     int 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@@ -2344,8 +2344,8 @@ struct unpack_opts {\n \tint quiet;\n };\n \n-static const char *unpack(struct odb_transaction *transaction,\n-\t\t\t  const struct unpack_opts *opts)\n+static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n+\t\t  const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2357,7 +2357,8 @@ static const char *unpack(struct odb_transaction *transaction,\n \tif (hdr_err) {\n \t\tif (err_fd > 0)\n \t\t\tclose(err_fd);\n-\t\treturn hdr_err;\n+\t\tstrbuf_addstr(err_msg, hdr_err);\n+\t\treturn -1;\n \t}\n \n \tif (opts->shallow_file) {\n@@ -2382,8 +2383,10 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = run_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"unpack-objects abnormal exit\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n \t} else {\n \t\tchar hostname[HOST_NAME_MAX + 1];\n \t\tchar *lockfile;\n@@ -2414,8 +2417,10 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = start_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"index-pack fork failed\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n+\t\t\treturn -1;\n+\t\t}\n \n \t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n \t\tif (lockfile) {\n@@ -2425,15 +2430,18 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tclose(child.out);\n \n \t\tstatus = finish_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"index-pack abnormal exit\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n \t\todb_reprepare(the_repository->objects);\n \t}\n-\treturn NULL;\n+\treturn 0;\n }\n \n-static const char *unpack_with_sideband(struct odb_transaction *transaction,\n-\t\t\t\t\tconst char *shallow_file)\n+static int unpack_with_sideband(struct odb_transaction *transaction,\n+\t\t\t\tconst char *shallow_file,\n+\t\t\t\tstruct strbuf *err_msg)\n {\n \tstruct unpack_opts opts = {\n \t\t.fsck_objects = (receive_fsck_objects >= 0\n@@ -2449,20 +2457,20 @@ static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \t\t.quiet = quiet,\n \t};\n \tstruct async muxer;\n-\tconst char *ret;\n+\tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, &opts);\n+\t\treturn unpack(transaction, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n \tmuxer.proc = copy_to_sideband;\n \tmuxer.in = -1;\n \tif (start_async(&muxer))\n-\t\treturn NULL;\n+\t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, &opts);\n+\tret = unpack(transaction, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2551,13 +2559,13 @@ static void update_shallow_info(struct command *commands,\n \tfree(ref_status);\n }\n \n-static void report(struct command *commands, const char *unpack_status)\n+static void report(struct command *commands, struct strbuf *unpack_status)\n {\n \tstruct command *cmd;\n \tstruct strbuf buf = STRBUF_INIT;\n \n \tpacket_buf_write(&buf, \"unpack %s\\n\",\n-\t\t\t unpack_status ? unpack_status : \"ok\");\n+\t\t\t unpack_status->len ? unpack_status->buf : \"ok\");\n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (!cmd->error_string)\n \t\t\tpacket_buf_write(&buf, \"ok %s\\n\",\n@@ -2575,14 +2583,14 @@ static void report(struct command *commands, const char *unpack_status)\n \tstrbuf_release(&buf);\n }\n \n-static void report_v2(struct command *commands, const char *unpack_status)\n+static void report_v2(struct command *commands, struct strbuf *unpack_status)\n {\n \tstruct command *cmd;\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct ref_push_report *report;\n \n \tpacket_buf_write(&buf, \"unpack %s\\n\",\n-\t\t\t unpack_status ? unpack_status : \"ok\");\n+\t\t\t unpack_status->len ? unpack_status->buf : \"ok\");\n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tint count = 0;\n \n@@ -2711,8 +2719,8 @@ int cmd_receive_pack(int argc,\n \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\n \n \tif ((commands = read_head_info(&reader, &shallow))) {\n-\t\tconst char *unpack_status = NULL;\n \t\tstruct string_list push_options = STRING_LIST_INIT_DUP;\n+\t\tstruct strbuf unpack_status = STRBUF_INIT;\n \n \t\tif (use_push_options)\n \t\t\tread_push_options(&reader, &push_options);\n@@ -2732,22 +2740,22 @@ int cmd_receive_pack(int argc,\n \t\t\t\talt_shallow_file = setup_temporary_shallow(si.shallow);\n \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\t\tstrbuf_addstr(&unpack_status, \"unable to start object transaction\");\n \t\t\telse\n-\t\t\t\tunpack_status = unpack_with_sideband(transaction, alt_shallow_file);\n+\t\t\t\tunpack_with_sideband(transaction, alt_shallow_file, &unpack_status);\n \n \t\t\tupdate_shallow_info(commands, &si, &ref, alt_shallow_file);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n-\t\texecute_commands(commands, unpack_status, &si, transaction,\n+\t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n \t\t\t\t &push_options);\n \t\todb_transaction_release(transaction);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\n-\t\t\treport_v2(commands, unpack_status);\n+\t\t\treport_v2(commands, &unpack_status);\n \t\telse if (report_status)\n-\t\t\treport(commands, unpack_status);\n+\t\t\treport(commands, &unpack_status);\n \t\tsigchain_pop(SIGPIPE);\n \t\trun_receive_hook(commands, \"post-receive\", 1, NULL,\n \t\t\t\t &push_options);\n@@ -2772,6 +2780,7 @@ int cmd_receive_pack(int argc,\n \t\tif (auto_update_server_info)\n \t\t\tupdate_server_info(the_repository, 0);\n \t\tclear_shallow_info(&si);\n+\t\tstrbuf_release(&unpack_status);\n \t}\n \tif (use_sideband)\n \t\tpacket_flush(1);\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"549894","messageId":"20260806213859.816157-4-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260806213859.816157-1-jltobler@gmail.com","subject":"[PATCH 3/6] builtin/receive-pack: lift global state out of unpack()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-06T21:38:56Z","receivedAt":"2026-08-06T21:39:08Z","isPatch":true,"body":"In git-receive-pack(1), writing the packfile to the transaction is\nhandled via `unpack()` which relies on global variables to decide how to\ninvoke the underlying git-index-pack(1) or git-unpack-objects(1) child\nprocesses. In a subsequent commit, the `unpack()` logic is moved behind\na generic ODB transaction interface to handle writing packfiles and thus\ncan no rely on these globals.\n\nLift the global state out of `unpack()` by instead storing this state in\na `struct unpack_opts` that gets passed to the function explicitly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 67 +++++++++++++++++++++++++++---------------\n 1 file changed, 44 insertions(+), 23 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 6da854fca2..8c2d6e5789 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2333,18 +2333,25 @@ 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+struct unpack_opts {\n+\tconst char *fsck_msg_types;\n+\tconst char *shallow_file;\n+\toff_t max_input_size;\n+\tint fsck_objects;\n+\tint unpack_limit;\n+\tint reject_thin;\n+\tint err_fd;\n+\tint quiet;\n+};\n+\n static const char *unpack(struct odb_transaction *transaction,\n-\t\t\t  const char *shallow_file, int err_fd)\n+\t\t\t  const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n \tint status;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint fsck_objects = (receive_fsck_objects >= 0\n-\t\t\t    ? receive_fsck_objects\n-\t\t\t    : transfer_fsck_objects >= 0\n-\t\t\t    ? transfer_fsck_objects\n-\t\t\t    : 0);\n+\tint err_fd = opts->err_fd;\n \n \thdr_err = parse_pack_header(&hdr);\n \tif (hdr_err) {\n@@ -2353,24 +2360,24 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\treturn hdr_err;\n \t}\n \n-\tif (shallow_file) {\n+\tif (opts->shallow_file) {\n \t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, shallow_file);\n+\t\tstrvec_push(&child.args, opts->shallow_file);\n \t}\n \n \todb_transaction_env(transaction, &child.env);\n \n-\tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n+\tif (ntohl(hdr.hdr_entries) < opts->unpack_limit) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n \t\tpush_header_arg(&child.args, &hdr);\n-\t\tif (quiet)\n+\t\tif (opts->quiet)\n \t\t\tstrvec_push(&child.args, \"-q\");\n-\t\tif (fsck_objects)\n+\t\tif (opts->fsck_objects)\n \t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     fsck_msg_types.buf);\n-\t\tif (max_input_size)\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (opts->max_input_size)\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)max_input_size);\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.no_stdout = 1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n@@ -2391,18 +2398,18 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\t\t     (uintmax_t)getpid(),\n \t\t\t     hostname);\n \n-\t\tif (!quiet && err_fd)\n+\t\tif (!opts->quiet && err_fd)\n \t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n-\t\tif (use_sideband)\n+\t\tif (err_fd)\n \t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n-\t\tif (fsck_objects)\n+\t\tif (opts->fsck_objects)\n \t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     fsck_msg_types.buf);\n-\t\tif (!reject_thin)\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (!opts->reject_thin)\n \t\t\tstrvec_push(&child.args, \"--fix-thin\");\n-\t\tif (max_input_size)\n+\t\tif (opts->max_input_size)\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)max_input_size);\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.out = -1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n@@ -2428,11 +2435,24 @@ static const char *unpack(struct odb_transaction *transaction,\n static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \t\t\t\t\tconst char *shallow_file)\n {\n+\tstruct unpack_opts opts = {\n+\t\t.fsck_objects = (receive_fsck_objects >= 0\n+\t\t\t\t ? receive_fsck_objects\n+\t\t\t\t : transfer_fsck_objects >= 0\n+\t\t\t\t ? transfer_fsck_objects\n+\t\t\t\t : 0),\n+\t\t.fsck_msg_types = fsck_msg_types.buf,\n+\t\t.max_input_size = max_input_size,\n+\t\t.shallow_file = shallow_file,\n+\t\t.unpack_limit = unpack_limit,\n+\t\t.reject_thin = reject_thin,\n+\t\t.quiet = quiet,\n+\t};\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, shallow_file, 0);\n+\t\treturn unpack(transaction, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2441,7 +2461,8 @@ static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \tif (start_async(&muxer))\n \t\treturn NULL;\n \n-\tret = unpack(transaction, shallow_file, muxer.in);\n+\topts.err_fd = muxer.in;\n+\tret = unpack(transaction, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"549896","messageId":"20260806213859.816157-6-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260806213859.816157-1-jltobler@gmail.com","subject":"[PATCH 5/6] builtin/receive-pack: explicitly pass packfile fd","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-06T21:38:58Z","receivedAt":"2026-08-06T21:39:10Z","isPatch":true,"body":"When processing the incoming packfile in git-receive-pack(1), `unpack()`\nassumes it should always read it from stdin. In preparation for\n`unpack()` logic being moved behind a generic ODB transaction interface,\nupdate the function signature to take the an explicit fd provided by\ncallers to read the incoming packfile from instead. Call sites are\nupdated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 16 +++++++++-------\n 1 file changed, 9 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 7635b82bd3..743005f1f5 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2305,9 +2305,9 @@ static void read_push_options(struct packet_reader *reader,\n \t}\n }\n \n-static const char *parse_pack_header(struct pack_header *hdr)\n+static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n {\n-\tswitch (read_pack_header(0, hdr)) {\n+\tswitch (read_pack_header(pack_fd, hdr)) {\n \tcase PH_ERROR_EOF:\n \t\treturn \"eof before pack header was fully read\";\n \n@@ -2344,8 +2344,8 @@ struct unpack_opts {\n \tint quiet;\n };\n \n-static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n-\t\t  const struct unpack_opts *opts)\n+static int unpack(struct odb_transaction *transaction, int pack_fd,\n+\t\t  struct strbuf *err_msg, const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2353,7 +2353,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tint err_fd = opts->err_fd;\n \n-\thdr_err = parse_pack_header(&hdr);\n+\thdr_err = parse_pack_header(&hdr, pack_fd);\n \tif (hdr_err) {\n \t\tif (err_fd > 0)\n \t\t\tclose(err_fd);\n@@ -2380,6 +2380,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n \t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.no_stdout = 1;\n+\t\tchild.in = pack_fd;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = run_command(&child);\n@@ -2414,6 +2415,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n \t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.out = -1;\n+\t\tchild.in = pack_fd;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = start_command(&child);\n@@ -2460,7 +2462,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, err_msg, &opts);\n+\t\treturn unpack(transaction, 0, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2470,7 +2472,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, err_msg, &opts);\n+\tret = unpack(transaction, 0, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"549897","messageId":"20260806213859.816157-7-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260806213859.816157-1-jltobler@gmail.com","subject":"[PATCH 6/6] odb/transaction: add transaction interface to write packfiles","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-06T21:38:59Z","receivedAt":"2026-08-06T21:39:10Z","isPatch":true,"body":"In git-receive-pack(1), the incoming packfile is written to the ODB via\n`unpack()`, which spawns git-index-pack(1) or git-unpack-objects(1)\ndirectly. With pluggable object databases, an alternative backend may\nneed to handle writing packfile data differently though.\n\nIntroduce `odb_transaction_write_pack()` as a generic interface to\nhandle writing a packfile to a transaction and use the logic from\n`unpack()` as the \"files\" backend implementation. Note that a packfile\nwritten via git-index-pack(1) is kept in place by a \".keep\" lockfile\nthat must be retained until references are updated. To faciliate this in\nan ODB backend agnostic manner, the \"files\" transaction backend takes\nownership of these lockfiles and removes them post-commit through its\nrelease callback.\n\nCall sites in git-receive-pack(1) are updated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 143 +--------------------------------------\n object-file.c          | 149 +++++++++++++++++++++++++++++++++++++++++\n odb/transaction.c      |   7 ++\n odb/transaction.h      |  63 +++++++++++++++++\n 4 files changed, 222 insertions(+), 140 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 743005f1f5..3069b53509 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2305,147 +2305,11 @@ static void read_push_options(struct packet_reader *reader,\n \t}\n }\n \n-static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n-{\n-\tswitch (read_pack_header(pack_fd, hdr)) {\n-\tcase PH_ERROR_EOF:\n-\t\treturn \"eof before pack header was fully read\";\n-\n-\tcase PH_ERROR_PACK_SIGNATURE:\n-\t\treturn \"protocol error (pack signature mismatch detected)\";\n-\n-\tcase PH_ERROR_PROTOCOL:\n-\t\treturn \"protocol error (pack version unsupported)\";\n-\n-\tdefault:\n-\t\treturn \"unknown error in parse_pack_header\";\n-\n-\tcase 0:\n-\t\treturn NULL;\n-\t}\n-}\n-\n-static struct tempfile *pack_lockfile;\n-\n-static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n-{\n-\tstrvec_pushf(args, \"--pack_header=%\"PRIu32\",%\"PRIu32,\n-\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n-}\n-\n-struct unpack_opts {\n-\tconst char *fsck_msg_types;\n-\tconst char *shallow_file;\n-\toff_t max_input_size;\n-\tint fsck_objects;\n-\tint unpack_limit;\n-\tint reject_thin;\n-\tint err_fd;\n-\tint quiet;\n-};\n-\n-static int unpack(struct odb_transaction *transaction, int pack_fd,\n-\t\t  struct strbuf *err_msg, const struct unpack_opts *opts)\n-{\n-\tstruct pack_header hdr;\n-\tconst char *hdr_err;\n-\tint status;\n-\tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint err_fd = opts->err_fd;\n-\n-\thdr_err = parse_pack_header(&hdr, pack_fd);\n-\tif (hdr_err) {\n-\t\tif (err_fd > 0)\n-\t\t\tclose(err_fd);\n-\t\tstrbuf_addstr(err_msg, hdr_err);\n-\t\treturn -1;\n-\t}\n-\n-\tif (opts->shallow_file) {\n-\t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, opts->shallow_file);\n-\t}\n-\n-\todb_transaction_env(transaction, &child.env);\n-\n-\tif (ntohl(hdr.hdr_entries) < opts->unpack_limit) {\n-\t\tstrvec_push(&child.args, \"unpack-objects\");\n-\t\tpush_header_arg(&child.args, &hdr);\n-\t\tif (opts->quiet)\n-\t\t\tstrvec_push(&child.args, \"-q\");\n-\t\tif (opts->fsck_objects)\n-\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     opts->fsck_msg_types);\n-\t\tif (opts->max_input_size)\n-\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)opts->max_input_size);\n-\t\tchild.no_stdout = 1;\n-\t\tchild.in = pack_fd;\n-\t\tchild.err = err_fd;\n-\t\tchild.git_cmd = 1;\n-\t\tstatus = run_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n-\t\t\treturn -1;\n-\t\t}\n-\t} else {\n-\t\tchar hostname[HOST_NAME_MAX + 1];\n-\t\tchar *lockfile;\n-\n-\t\tstrvec_pushl(&child.args, \"index-pack\", \"--stdin\", NULL);\n-\t\tpush_header_arg(&child.args, &hdr);\n-\n-\t\tif (xgethostname(hostname, sizeof(hostname)))\n-\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n-\t\tstrvec_pushf(&child.args,\n-\t\t\t     \"--keep=receive-pack %\"PRIuMAX\" on %s\",\n-\t\t\t     (uintmax_t)getpid(),\n-\t\t\t     hostname);\n-\n-\t\tif (!opts->quiet && err_fd)\n-\t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n-\t\tif (err_fd)\n-\t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n-\t\tif (opts->fsck_objects)\n-\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     opts->fsck_msg_types);\n-\t\tif (!opts->reject_thin)\n-\t\t\tstrvec_push(&child.args, \"--fix-thin\");\n-\t\tif (opts->max_input_size)\n-\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)opts->max_input_size);\n-\t\tchild.out = -1;\n-\t\tchild.in = pack_fd;\n-\t\tchild.err = err_fd;\n-\t\tchild.git_cmd = 1;\n-\t\tstatus = start_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n-\t\tif (lockfile) {\n-\t\t\tpack_lockfile = register_tempfile(lockfile);\n-\t\t\tfree(lockfile);\n-\t\t}\n-\t\tclose(child.out);\n-\n-\t\tstatus = finish_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n-\t\t\treturn -1;\n-\t\t}\n-\t\todb_reprepare(the_repository->objects);\n-\t}\n-\treturn 0;\n-}\n-\n static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\t\t\tconst char *shallow_file,\n \t\t\t\tstruct strbuf *err_msg)\n {\n-\tstruct unpack_opts opts = {\n+\tstruct odb_transaction_write_pack_opts opts = {\n \t\t.fsck_objects = (receive_fsck_objects >= 0\n \t\t\t\t ? receive_fsck_objects\n \t\t\t\t : transfer_fsck_objects >= 0\n@@ -2462,7 +2326,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, 0, err_msg, &opts);\n+\t\treturn odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2472,7 +2336,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, 0, err_msg, &opts);\n+\tret = odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2752,7 +2616,6 @@ int cmd_receive_pack(int argc,\n \t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n \t\t\t\t &push_options);\n \t\todb_transaction_release(transaction);\n-\t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\n \t\t\treport_v2(commands, &unpack_status);\ndiff --git a/object-file.c b/object-file.c\nindex 30b4717d3e..ec3b9a185e 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -26,6 +26,7 @@\n #include \"packfile.h\"\n #include \"path.h\"\n #include \"read-cache-ll.h\"\n+#include \"run-command.h\"\n #include \"setup.h\"\n #include \"strvec.h\"\n #include \"tempfile.h\"\n@@ -487,6 +488,10 @@ struct odb_transaction_files {\n \tstruct tmp_objdir *objdir;\n \tstruct transaction_packfile packfile;\n \tconst char *prefix;\n+\n+\tstruct tempfile **pack_lockfiles;\n+\tsize_t pack_lockfiles_nr;\n+\tsize_t pack_lockfiles_alloc;\n };\n \n int odb_transaction_files_prepare(struct odb_transaction *base)\n@@ -1292,6 +1297,148 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n \treturn 0;\n }\n \n+static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n+{\n+\tswitch (read_pack_header(pack_fd, hdr)) {\n+\tcase PH_ERROR_EOF:\n+\t\treturn \"eof before pack header was fully read\";\n+\n+\tcase PH_ERROR_PACK_SIGNATURE:\n+\t\treturn \"protocol error (pack signature mismatch detected)\";\n+\n+\tcase PH_ERROR_PROTOCOL:\n+\t\treturn \"protocol error (pack version unsupported)\";\n+\n+\tdefault:\n+\t\treturn \"unknown error in parse_pack_header\";\n+\n+\tcase 0:\n+\t\treturn NULL;\n+\t}\n+}\n+\n+static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n+{\n+\tstrvec_pushf(args, \"--pack_header=%\"PRIu32\",%\"PRIu32,\n+\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n+}\n+\n+static int odb_transaction_files_write_pack(struct odb_transaction *base,\n+\t\t\t\t\t    int pack_fd, struct strbuf *err_msg,\n+\t\t\t\t\t    const struct odb_transaction_write_pack_opts *opts)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\tstruct repository *repo = base->source->odb->repo;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tstruct pack_header hdr;\n+\tconst char *hdr_err;\n+\tint err_fd = opts->err_fd;\n+\tint status;\n+\n+\thdr_err = parse_pack_header(&hdr, pack_fd);\n+\tif (hdr_err) {\n+\t\tif (err_fd > 0)\n+\t\t\tclose(err_fd);\n+\t\tstrbuf_addstr(err_msg, hdr_err);\n+\t\treturn -1;\n+\t}\n+\n+\tif (opts->shallow_file) {\n+\t\tstrvec_push(&child.args, \"--shallow-file\");\n+\t\tstrvec_push(&child.args, opts->shallow_file);\n+\t}\n+\n+\todb_transaction_env(base, &child.env);\n+\n+\tif (ntohl(hdr.hdr_entries) < (unsigned int)opts->unpack_limit) {\n+\t\tstrvec_push(&child.args, \"unpack-objects\");\n+\t\tpush_header_arg(&child.args, &hdr);\n+\t\tif (opts->quiet)\n+\t\t\tstrvec_push(&child.args, \"-q\");\n+\t\tif (opts->fsck_objects)\n+\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (opts->max_input_size)\n+\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n+\t\tchild.no_stdout = 1;\n+\t\tchild.in = pack_fd;\n+\t\tchild.err = err_fd;\n+\t\tchild.git_cmd = 1;\n+\t\tstatus = run_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n+\t} else {\n+\t\tchar hostname[HOST_NAME_MAX + 1];\n+\t\tchar *lockfile;\n+\n+\t\tstrvec_pushl(&child.args, \"index-pack\", \"--stdin\", NULL);\n+\t\tpush_header_arg(&child.args, &hdr);\n+\n+\t\tif (xgethostname(hostname, sizeof(hostname)))\n+\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n+\t\tstrvec_pushf(&child.args,\n+\t\t\t     \"--keep=receive-pack %\"PRIuMAX\" on %s\",\n+\t\t\t     (uintmax_t)getpid(),\n+\t\t\t     hostname);\n+\n+\t\tif (!opts->quiet && err_fd)\n+\t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n+\t\tif (err_fd)\n+\t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n+\t\tif (opts->fsck_objects)\n+\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (!opts->reject_thin)\n+\t\t\tstrvec_push(&child.args, \"--fix-thin\");\n+\t\tif (opts->max_input_size)\n+\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n+\t\tchild.out = -1;\n+\t\tchild.in = pack_fd;\n+\t\tchild.err = err_fd;\n+\t\tchild.git_cmd = 1;\n+\t\tstatus = start_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tlockfile = index_pack_lockfile(repo, child.out, NULL);\n+\t\tif (lockfile) {\n+\t\t\tALLOC_GROW(transaction->pack_lockfiles,\n+\t\t\t\t   transaction->pack_lockfiles_nr + 1,\n+\t\t\t\t   transaction->pack_lockfiles_alloc);\n+\t\t\ttransaction->pack_lockfiles[transaction->pack_lockfiles_nr++] =\n+\t\t\t\tregister_tempfile(lockfile);\n+\t\t\tfree(lockfile);\n+\t\t}\n+\t\tclose(child.out);\n+\n+\t\tstatus = finish_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n+\t\todb_reprepare(repo->objects);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void odb_transaction_files_release(struct odb_transaction *base)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\n+\tfor (size_t i = 0; i < transaction->pack_lockfiles_nr; i++)\n+\t\tdelete_tempfile(&transaction->pack_lockfiles[i]);\n+\tfree(transaction->pack_lockfiles);\n+}\n+\n static int odb_transaction_files_env(struct odb_transaction *base,\n \t\t\t\t     struct strvec *env)\n {\n@@ -1315,7 +1462,9 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction = xcalloc(1, sizeof(*transaction));\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n+\ttransaction->base.release = odb_transaction_files_release;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\ttransaction->base.write_pack = odb_transaction_files_write_pack;\n \ttransaction->base.env = odb_transaction_files_env;\n \n \ttransaction->prefix = \"bulk-fsync\";\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex ce1e24f3ed..de03116ca0 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -55,6 +55,13 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n \treturn transaction->write_object_stream(transaction, stream, len, oid);\n }\n \n+int odb_transaction_write_pack(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t       struct strbuf *err_msg,\n+\t\t\t       const struct odb_transaction_write_pack_opts *opts)\n+{\n+\treturn transaction->write_pack(transaction, pack_fd, err_msg, opts);\n+}\n+\n int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env)\n {\n \tif (!transaction)\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex ec0b27c449..491026e815 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -4,6 +4,51 @@\n #include \"gettext.h\"\n #include \"odb.h\"\n \n+/*\n+ * Options controlling how odb_transaction_write_pack() ingests a packfile.\n+ */\n+struct odb_transaction_write_pack_opts {\n+\t/*\n+\t * Optional fsck severity configuration to apply when incoming objects\n+\t * are verified.\n+\t */\n+\tconst char *fsck_msg_types;\n+\t/*\n+\t * Path to an alternative shallow file describing the shallow boundaries\n+\t * to honor while ingesting the pack.\n+\t */\n+\tconst char *shallow_file;\n+\t/*\n+\t * The max size in bytes of the incoming packfile allowed. No limit is\n+\t * enforced when set to 0.\n+\t */\n+\toff_t max_input_size;\n+\t/*\n+\t * Whether the validity of incoming objects should be verified.\n+\t */\n+\tint fsck_objects;\n+\t/*\n+\t * The threshold for the number of incoming objects required to store\n+\t * the objects in a packfile. This option may not be relevant to\n+\t * backends that do not store obejcts in loose/packed formats and can be\n+\t * ignored.\n+\t */\n+\tint unpack_limit;\n+\t/*\n+\t * Whether to reject an incoming packfile if it is \"thin\".\n+\t */\n+\tint reject_thin;\n+\t/*\n+\t * Optional file descriptor for reporting progress and errors. Set to 0\n+\t * for none.\n+\t */\n+\tint err_fd;\n+\t/*\n+\t * Suppresses progress reporting.\n+\t */\n+\tint quiet;\n+};\n+\n /*\n  * A transaction may be started for an object database prior to writing new\n  * objects via odb_transaction_begin(). These objects are not committed until\n@@ -40,6 +85,15 @@ 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+\t/*\n+\t * This callback is expected to ingest the packfile readable via\n+\t * `pack_fd` into the transaction. Returns 0 on success, a negative\n+\t * error code otherwise. On failure, a human-readable description is\n+\t * appended to `err_msg`.\n+\t */\n+\tint (*write_pack)(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t  struct strbuf *err_msg,\n+\t\t\t  const struct odb_transaction_write_pack_opts *opts);\n \n \t/*\n \t * This callback is expected to populate the provided strvec with the\n@@ -98,6 +152,15 @@ 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+ * Ingests the packfile readable via `pack_fd` into the transaction. Returns 0\n+ * on success, a negative error code otherwise. On failure, a human-readable\n+ * description is appended to `err_msg`.\n+ */\n+int odb_transaction_write_pack(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t       struct strbuf *err_msg,\n+\t\t\t       const struct odb_transaction_write_pack_opts *opts);\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-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"549940","messageId":"anWDKwkRp1EK9NRi@pks.im","threadId":"66133","inReplyTo":"20260806213859.816157-2-jltobler@gmail.com","subject":"Re: [PATCH 1/6] odb/transaction: add transaction release interface","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-07T07:03:23Z","receivedAt":"2026-08-07T07:03:34Z","isPatch":true,"body":"On Thu, Aug 06, 2026 at 04:38:54PM -0500, Justin Tobler wrote:\n> When committing an ODB transaction via `odb_transaction_commit()`, the\n> staged objects are made visible and the underlying transaction is freed\n> at the same time. Coupling these two steps does not leave room for any\n> post-commit transaction operations to be introduced though. Such a\n> capability is useful if an ODB transaction backend needs to hold on to\n> lockfiles after transaction commit until references are updated, as is\n> the case with the existing \"files\" backend in git-receive-pack(1).\n\nRight. We don't want to remove \".keep\" files until references have been\nupdated so that the potentially still unreachable objects won't get\npruned. And consequently we have to introduce an additional phase after\nthe transaction has been committed but before the refs were updated.\n\n> Stop freeing the transaction in `odb_transaction_commit()` and introduce\n> `odb_transaction_release()` to explicitly clean up the transaction\n> accordingly. Note that the release interface also provides an optional\n> callback for any backend-specific deferred cleanup. In a subsequent\n> commit, the \"files\" transaction backend will use this to remove \".keep\"\n> files generated for packfiles received via git-receive-pack(1) after\n> references have been updated.\n\nI'm not a 100% sure whether I like \"release\" as a name, as it typically\nindicates that we release memory and other resources hold on by Git. On\nthe other hand we also kind of release state in this case here, but it\nfeels like the consequence of that is broader than it usually is.\n\nHow about we call this \"finalize\" instead?\n\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index 86933d8d7e..420de9aa7f 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -2714,6 +2714,7 @@ int cmd_receive_pack(int argc,\n>  \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n>  \t\texecute_commands(commands, unpack_status, &si, transaction,\n>  \t\t\t\t &push_options);\n> +\t\todb_transaction_release(transaction);\n>  \t\tdelete_tempfile(&pack_lockfile);\n>  \t\tsigchain_push(SIGPIPE, SIG_IGN);\n>  \t\tif (report_status_v2)\n\nI think this here is the only caller that we care about where we release\nthe transaction not immediately after committing it. This is because\n`execute_commands()` is the function that's responsible for updating the\nreferences, and thus we don't want to delete the \".keep\" files before\nit.\n\nIt would make sense to single out this caller in the commit message.\n\n> diff --git a/odb/transaction.h b/odb/transaction.h\n> index 4cb2eafcbf..ec0b27c449 100644\n> --- a/odb/transaction.h\n> +++ b/odb/transaction.h\n> @@ -75,6 +82,13 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n>   */\n>  int odb_transaction_commit(struct odb_transaction *transaction);\n>  \n> +/*\n> + * Releases an ODB transaction, performing any deferred cleanup and freeing it.\n> + * Must be called for every successfully started transaction. Note that, if the\n> + * specified transaction is NULL, the function is a no-op.\n> + */\n> +void odb_transaction_release(struct odb_transaction *transaction);\n\nShould this function be able to report errors? Cleaning up \".keep\" files\ncan fail, and I'm not sure whether we should simply ignore those.\n\nPatrick\n"},{"id":"549941","messageId":"anWDRVA0mSQva2QX@pks.im","threadId":"66133","inReplyTo":"20260806213859.816157-3-jltobler@gmail.com","subject":"Re: [PATCH 2/6] builtin/receive-pack: pass shallow file explicitly","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-07T07:03:33Z","receivedAt":"2026-08-07T07:03:38Z","isPatch":true,"body":"On Thu, Aug 06, 2026 at 04:38:55PM -0500, Justin Tobler wrote:\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index 420de9aa7f..6da854fca2 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -86,7 +86,6 @@ static const char *head_name;\n>  static void *head_name_to_free;\n>  static int sent_capabilities;\n>  static int shallow_update;\n> -static const char *alt_shallow_file;\n>  static struct strbuf push_cert = STRBUF_INIT;\n>  static struct object_id push_cert_oid;\n>  static struct signature_check sigcheck;\n\nI always like seeing less global state.\n\n> @@ -2354,10 +2353,9 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n>  \t\treturn hdr_err;\n>  \t}\n>  \n> -\tif (si->nr_ours || si->nr_theirs) {\n> -\t\talt_shallow_file = setup_temporary_shallow(si->shallow);\n> +\tif (shallow_file) {\n>  \t\tstrvec_push(&child.args, \"--shallow-file\");\n> -\t\tstrvec_push(&child.args, alt_shallow_file);\n> +\t\tstrvec_push(&child.args, shallow_file);\n>  \t}\n>  \n>  \todb_transaction_env(transaction, &child.env);\n\nOkay, so instead of creating the shallow file here, ...\n\n> @@ -2705,11 +2705,17 @@ 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\tconst char *alt_shallow_file = NULL;\n> +\n> +\t\t\tif (si.nr_ours || si.nr_theirs)\n> +\t\t\t\talt_shallow_file = setup_temporary_shallow(si.shallow);\n> +\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\t\tunpack_status = unpack_with_sideband(transaction, alt_shallow_file);\n> +\n> +\t\t\tupdate_shallow_info(commands, &si, &ref, alt_shallow_file);\n>  \t\t}\n\n... we create it in a transitive caller and then pass it down the stack.\nMakes sense.\n\nIt's nice that we don't have to pass the shallow information at all\nanymore as a consequence.\n\nPatrick\n"},{"id":"549942","messageId":"anWDSt155Y9hzHGM@pks.im","threadId":"66133","inReplyTo":"20260806213859.816157-4-jltobler@gmail.com","subject":"Re: [PATCH 3/6] builtin/receive-pack: lift global state out of unpack()","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-07T07:03:38Z","receivedAt":"2026-08-07T07:03:43Z","isPatch":true,"body":"On Thu, Aug 06, 2026 at 04:38:56PM -0500, Justin Tobler wrote:\n> In git-receive-pack(1), writing the packfile to the transaction is\n> handled via `unpack()` which relies on global variables to decide how to\n> invoke the underlying git-index-pack(1) or git-unpack-objects(1) child\n> processes. In a subsequent commit, the `unpack()` logic is moved behind\n> a generic ODB transaction interface to handle writing packfiles and thus\n> can no rely on these globals.\n\nNit: either \"can not\" or \"can no longer\".\n\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index 6da854fca2..8c2d6e5789 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -2333,18 +2333,25 @@ 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> +struct unpack_opts {\n> +\tconst char *fsck_msg_types;\n> +\tconst char *shallow_file;\n> +\toff_t max_input_size;\n> +\tint fsck_objects;\n> +\tint unpack_limit;\n> +\tint reject_thin;\n> +\tint err_fd;\n> +\tint quiet;\n> +};\n> +\n>  static const char *unpack(struct odb_transaction *transaction,\n> -\t\t\t  const char *shallow_file, int err_fd)\n> +\t\t\t  const struct unpack_opts *opts)\n>  {\n>  \tstruct pack_header hdr;\n>  \tconst char *hdr_err;\n>  \tint status;\n>  \tstruct child_process child = CHILD_PROCESS_INIT;\n> -\tint fsck_objects = (receive_fsck_objects >= 0\n> -\t\t\t    ? receive_fsck_objects\n> -\t\t\t    : transfer_fsck_objects >= 0\n> -\t\t\t    ? transfer_fsck_objects\n> -\t\t\t    : 0);\n> +\tint err_fd = opts->err_fd;\n>  \n>  \thdr_err = parse_pack_header(&hdr);\n>  \tif (hdr_err) {\n\nIt's quite hard to see that the function indeed doesn't rely on the\nglobal variables anymore, and I'm quite certain that I'd not spot cases\nthat you forgot to convert to use the options structure instead. But I\nassume that the function will move into a different file in a subsequent\ncommit, so we'd notice in that patch.\n\n> @@ -2428,11 +2435,24 @@ static const char *unpack(struct odb_transaction *transaction,\n>  static const char *unpack_with_sideband(struct odb_transaction *transaction,\n>  \t\t\t\t\tconst char *shallow_file)\n>  {\n> +\tstruct unpack_opts opts = {\n> +\t\t.fsck_objects = (receive_fsck_objects >= 0\n> +\t\t\t\t ? receive_fsck_objects\n> +\t\t\t\t : transfer_fsck_objects >= 0\n> +\t\t\t\t ? transfer_fsck_objects\n> +\t\t\t\t : 0),\n\nThis looks quite ugly, but it's no more ugly than the previous code it\nreplaces.\n\n> @@ -2441,7 +2461,8 @@ static const char *unpack_with_sideband(struct odb_transaction *transaction,\n>  \tif (start_async(&muxer))\n>  \t\treturn NULL;\n>  \n> -\tret = unpack(transaction, shallow_file, muxer.in);\n> +\topts.err_fd = muxer.in;\n> +\tret = unpack(transaction, &opts);\n\nHm, okay. I guess this here is because we only want to manually read\nstderr in case we use the sideband. It's a bit unfortunate that this\nrequires us to modify the passed-in options structure, but I guess I can\nlive with that.\n\nPatrick\n"},{"id":"549943","messageId":"anWDTwCwMn5wEdIQ@pks.im","threadId":"66133","inReplyTo":"20260806213859.816157-5-jltobler@gmail.com","subject":"Re: [PATCH 4/6] builtin/receive-pack: report unpack errors via strbuf","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-07T07:03:43Z","receivedAt":"2026-08-07T07:03:48Z","isPatch":true,"body":"On Thu, Aug 06, 2026 at 04:38:57PM -0500, Justin Tobler wrote:\n> When writing packfiles via `unpack()`, error messages are returned\n> directly by the function. In preparation for `unpack()` logic being\n> moved behind a generic ODB transaction interface, update the function to\n> instead write any error messages to a caller provided strbuf and return\n> a negative value on error. Call sites are updated to use the error\n> strbuf accordingly.\n\nIf only Git had a structured error type, than we wouldn't have to have\nsuch ugly workarounds. Anyway, this is a deeper issue and nothing we can\nblame on this patch series.\n\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index 8c2d6e5789..7635b82bd3 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -2344,8 +2344,8 @@ struct unpack_opts {\n>  \tint quiet;\n>  };\n>  \n> -static const char *unpack(struct odb_transaction *transaction,\n> -\t\t\t  const struct unpack_opts *opts)\n> +static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n> +\t\t  const struct unpack_opts *opts)\n>  {\n>  \tstruct pack_header hdr;\n>  \tconst char *hdr_err;\n\nWhile I'm not a huge fan of error message parameters like this, this\nchange does make the calling convention more straight-forward. A reader\nprobably wouldn't have known beforehand what to do with the return value\nwithout reading through docs.\n\nAlso, we cannot just return the equivalent of `return error(\"msg\")`, as\nwe do want to use and munge the error message as part of the status\nreport we send to the client.\n\n> @@ -2551,13 +2559,13 @@ static void update_shallow_info(struct command *commands,\n>  \tfree(ref_status);\n>  }\n>  \n> -static void report(struct command *commands, const char *unpack_status)\n> +static void report(struct command *commands, struct strbuf *unpack_status)\n\nShould we mark this parameter as `const`?\n\n> @@ -2575,14 +2583,14 @@ static void report(struct command *commands, const char *unpack_status)\n>  \tstrbuf_release(&buf);\n>  }\n>  \n> -static void report_v2(struct command *commands, const char *unpack_status)\n> +static void report_v2(struct command *commands, struct strbuf *unpack_status)\n\nAnd here, as well?\n\n> @@ -2711,8 +2719,8 @@ int cmd_receive_pack(int argc,\n>  \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\n>  \n>  \tif ((commands = read_head_info(&reader, &shallow))) {\n> -\t\tconst char *unpack_status = NULL;\n>  \t\tstruct string_list push_options = STRING_LIST_INIT_DUP;\n> +\t\tstruct strbuf unpack_status = STRBUF_INIT;\n\nCan't we reuse this buffer and reset it on every run to save some memory\nallocations?\n\nPatrick\n"},{"id":"549944","messageId":"anWDVFL6OjX2xdR-@pks.im","threadId":"66133","inReplyTo":"20260806213859.816157-7-jltobler@gmail.com","subject":"Re: [PATCH 6/6] odb/transaction: add transaction interface to write packfiles","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-07T07:03:48Z","receivedAt":"2026-08-07T07:03:52Z","isPatch":true,"body":"On Thu, Aug 06, 2026 at 04:38:59PM -0500, Justin Tobler wrote:\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index 743005f1f5..3069b53509 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n[snip]\n>  static int unpack_with_sideband(struct odb_transaction *transaction,\n>  \t\t\t\tconst char *shallow_file,\n>  \t\t\t\tstruct strbuf *err_msg)\n>  {\n> -\tstruct unpack_opts opts = {\n> +\tstruct odb_transaction_write_pack_opts opts = {\n>  \t\t.fsck_objects = (receive_fsck_objects >= 0\n>  \t\t\t\t ? receive_fsck_objects\n>  \t\t\t\t : transfer_fsck_objects >= 0\n> @@ -2462,7 +2326,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n>  \tint ret;\n>  \n>  \tif (!use_sideband)\n> -\t\treturn unpack(transaction, 0, err_msg, &opts);\n> +\t\treturn odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n>  \n>  \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n>  \tmemset(&muxer, 0, sizeof(muxer));\n> @@ -2472,7 +2336,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n>  \t\treturn 0;\n>  \n>  \topts.err_fd = muxer.in;\n> -\tret = unpack(transaction, 0, err_msg, &opts);\n> +\tret = odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n>  \n>  \tfinish_async(&muxer);\n>  \treturn ret;\n\nNicely done. All we need to do now is to rename the structure and the\nparameters, and everything else was already taken care of in the\npreceding commits.\n\n> diff --git a/object-file.c b/object-file.c\n> index 30b4717d3e..ec3b9a185e 100644\n> --- a/object-file.c\n> +++ b/object-file.c\n> @@ -1292,6 +1297,148 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n[snip]\n> +static int odb_transaction_files_write_pack(struct odb_transaction *base,\n> +\t\t\t\t\t    int pack_fd, struct strbuf *err_msg,\n> +\t\t\t\t\t    const struct odb_transaction_write_pack_opts *opts)\n> +{\n> +\tstruct odb_transaction_files *transaction =\n> +\t\tcontainer_of(base, struct odb_transaction_files, base);\n> +\tstruct repository *repo = base->source->odb->repo;\n> +\tstruct child_process child = CHILD_PROCESS_INIT;\n> +\tstruct pack_header hdr;\n> +\tconst char *hdr_err;\n> +\tint err_fd = opts->err_fd;\n> +\tint status;\n> +\n> +\thdr_err = parse_pack_header(&hdr, pack_fd);\n> +\tif (hdr_err) {\n> +\t\tif (err_fd > 0)\n> +\t\t\tclose(err_fd);\n> +\t\tstrbuf_addstr(err_msg, hdr_err);\n> +\t\treturn -1;\n> +\t}\n> +\n> +\tif (opts->shallow_file) {\n> +\t\tstrvec_push(&child.args, \"--shallow-file\");\n> +\t\tstrvec_push(&child.args, opts->shallow_file);\n> +\t}\n> +\n> +\todb_transaction_env(base, &child.env);\n> +\n> +\tif (ntohl(hdr.hdr_entries) < (unsigned int)opts->unpack_limit) {\n> +\t\tstrvec_push(&child.args, \"unpack-objects\");\n> +\t\tpush_header_arg(&child.args, &hdr);\n> +\t\tif (opts->quiet)\n> +\t\t\tstrvec_push(&child.args, \"-q\");\n> +\t\tif (opts->fsck_objects)\n> +\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n> +\t\t\t\t     opts->fsck_msg_types);\n> +\t\tif (opts->max_input_size)\n> +\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n> +\t\t\t\t     (uintmax_t)opts->max_input_size);\n> +\t\tchild.no_stdout = 1;\n> +\t\tchild.in = pack_fd;\n> +\t\tchild.err = err_fd;\n> +\t\tchild.git_cmd = 1;\n> +\t\tstatus = run_command(&child);\n> +\t\tif (status) {\n> +\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n> +\t\t\treturn -1;\n> +\t\t}\n> +\t} else {\n> +\t\tchar hostname[HOST_NAME_MAX + 1];\n> +\t\tchar *lockfile;\n> +\n> +\t\tstrvec_pushl(&child.args, \"index-pack\", \"--stdin\", NULL);\n> +\t\tpush_header_arg(&child.args, &hdr);\n> +\n> +\t\tif (xgethostname(hostname, sizeof(hostname)))\n> +\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n> +\t\tstrvec_pushf(&child.args,\n> +\t\t\t     \"--keep=receive-pack %\"PRIuMAX\" on %s\",\n> +\t\t\t     (uintmax_t)getpid(),\n> +\t\t\t     hostname);\n> +\n> +\t\tif (!opts->quiet && err_fd)\n> +\t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n> +\t\tif (err_fd)\n> +\t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n> +\t\tif (opts->fsck_objects)\n> +\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n> +\t\t\t\t     opts->fsck_msg_types);\n> +\t\tif (!opts->reject_thin)\n> +\t\t\tstrvec_push(&child.args, \"--fix-thin\");\n> +\t\tif (opts->max_input_size)\n> +\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n> +\t\t\t\t     (uintmax_t)opts->max_input_size);\n> +\t\tchild.out = -1;\n> +\t\tchild.in = pack_fd;\n> +\t\tchild.err = err_fd;\n> +\t\tchild.git_cmd = 1;\n> +\t\tstatus = start_command(&child);\n> +\t\tif (status) {\n> +\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n> +\t\t\treturn -1;\n> +\t\t}\n> +\n> +\t\tlockfile = index_pack_lockfile(repo, child.out, NULL);\n> +\t\tif (lockfile) {\n> +\t\t\tALLOC_GROW(transaction->pack_lockfiles,\n> +\t\t\t\t   transaction->pack_lockfiles_nr + 1,\n> +\t\t\t\t   transaction->pack_lockfiles_alloc);\n> +\t\t\ttransaction->pack_lockfiles[transaction->pack_lockfiles_nr++] =\n> +\t\t\t\tregister_tempfile(lockfile);\n> +\t\t\tfree(lockfile);\n> +\t\t}\n> +\t\tclose(child.out);\n\nA `git diff --color-moved` shows that almost all of the code was simply\nmoved around. The biggest change is this part here, where we now\nregister the packfiles as part of the transactions. Makes sense.\n\n> +\t\tstatus = finish_command(&child);\n> +\t\tif (status) {\n> +\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n> +\t\t\treturn -1;\n> +\t\t}\n> +\t\todb_reprepare(repo->objects);\n\nNow that this is part of the ODB transaction, do we really have to\nreprepare the whole object database? Shouldn't it suffice to reprepare\njust the one source that we've created the transaction for?\n\n> diff --git a/odb/transaction.h b/odb/transaction.h\n> index ec0b27c449..491026e815 100644\n> --- a/odb/transaction.h\n> +++ b/odb/transaction.h\n> @@ -4,6 +4,51 @@\n>  #include \"gettext.h\"\n>  #include \"odb.h\"\n>  \n> +/*\n> + * Options controlling how odb_transaction_write_pack() ingests a packfile.\n> + */\n> +struct odb_transaction_write_pack_opts {\n> +\t/*\n> +\t * Optional fsck severity configuration to apply when incoming objects\n> +\t * are verified.\n> +\t */\n> +\tconst char *fsck_msg_types;\n> +\t/*\n> +\t * Path to an alternative shallow file describing the shallow boundaries\n> +\t * to honor while ingesting the pack.\n> +\t */\n> +\tconst char *shallow_file;\n> +\t/*\n> +\t * The max size in bytes of the incoming packfile allowed. No limit is\n> +\t * enforced when set to 0.\n> +\t */\n> +\toff_t max_input_size;\n> +\t/*\n> +\t * Whether the validity of incoming objects should be verified.\n> +\t */\n> +\tint fsck_objects;\n> +\t/*\n> +\t * The threshold for the number of incoming objects required to store\n> +\t * the objects in a packfile. This option may not be relevant to\n> +\t * backends that do not store obejcts in loose/packed formats and can be\n> +\t * ignored.\n> +\t */\n> +\tint unpack_limit;\n\nI wonder whether this option should rather be handled internal in the\nbackend itself, as it very likely doesn't apply to alternative backends\nanyway. I don't think we allow command line options to override this, so\nthe backend could just read the configuration manually.\n\n> +\t/*\n> +\t * Whether to reject an incoming packfile if it is \"thin\".\n> +\t */\n> +\tint reject_thin;\n> +\t/*\n> +\t * Optional file descriptor for reporting progress and errors. Set to 0\n> +\t * for none.\n> +\t */\n> +\tint err_fd;\n> +\t/*\n> +\t * Suppresses progress reporting.\n> +\t */\n> +\tint quiet;\n> +};\n\nNit: I think having some spacing between the different options would\nmake this a bit easier to grok.\n\nPatrick\n"},{"id":"550011","messageId":"anX0NDfcaKGFOTjS@denethor","threadId":"66133","inReplyTo":"anWDKwkRp1EK9NRi@pks.im","subject":"Re: [PATCH 1/6] odb/transaction: add transaction release interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-07T15:11:44Z","receivedAt":"2026-08-07T15:11:50Z","isPatch":true,"body":"On 26/08/07 09:03AM, Patrick Steinhardt wrote:\n> On Thu, Aug 06, 2026 at 04:38:54PM -0500, Justin Tobler wrote:\n> I'm not a 100% sure whether I like \"release\" as a name, as it typically\n> indicates that we release memory and other resources hold on by Git. On\n> the other hand we also kind of release state in this case here, but it\n> feels like the consequence of that is broader than it usually is.\n> \n> How about we call this \"finalize\" instead?\n\nYa, that is fair. If we keep freeing the transaction and removing\nlockfiles in the same lifecycle phase, \"finalize\" is probably a better\nname. Will update in the next version.\n\n> > diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> > index 86933d8d7e..420de9aa7f 100644\n> > --- a/builtin/receive-pack.c\n> > +++ b/builtin/receive-pack.c\n> > @@ -2714,6 +2714,7 @@ int cmd_receive_pack(int argc,\n> >  \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n> >  \t\texecute_commands(commands, unpack_status, &si, transaction,\n> >  \t\t\t\t &push_options);\n> > +\t\todb_transaction_release(transaction);\n> >  \t\tdelete_tempfile(&pack_lockfile);\n> >  \t\tsigchain_push(SIGPIPE, SIG_IGN);\n> >  \t\tif (report_status_v2)\n> \n> I think this here is the only caller that we care about where we release\n> the transaction not immediately after committing it. This is because\n> `execute_commands()` is the function that's responsible for updating the\n> references, and thus we don't want to delete the \".keep\" files before\n> it.\n> \n> It would make sense to single out this caller in the commit message.\n\nThat is correct, git-receive-pack(1) is the only ODB transaction user\ncurrently that cares about this. At this point in the series,\n`odb_transaction_release()` is not yet cleaning up any lockfiles yet,\nbut will later on in the series. I'll explain this in the commit\nmessage.\n\n> > diff --git a/odb/transaction.h b/odb/transaction.h\n> > index 4cb2eafcbf..ec0b27c449 100644\n> > --- a/odb/transaction.h\n> > +++ b/odb/transaction.h\n> > @@ -75,6 +82,13 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n> >   */\n> >  int odb_transaction_commit(struct odb_transaction *transaction);\n> >  \n> > +/*\n> > + * Releases an ODB transaction, performing any deferred cleanup and freeing it.\n> > + * Must be called for every successfully started transaction. Note that, if the\n> > + * specified transaction is NULL, the function is a no-op.\n> > + */\n> > +void odb_transaction_release(struct odb_transaction *transaction);\n> \n> Should this function be able to report errors? Cleaning up \".keep\" files\n> can fail, and I'm not sure whether we should simply ignore those.\n\nGood point. Will update in the next version.\n\n-Justin\n"},{"id":"550017","messageId":"anX5PxN15qvtfFEX@denethor","threadId":"66133","inReplyTo":"anWDSt155Y9hzHGM@pks.im","subject":"Re: [PATCH 3/6] builtin/receive-pack: lift global state out of unpack()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-07T15:33:12Z","receivedAt":"2026-08-07T15:33:16Z","isPatch":true,"body":"On 26/08/07 09:03AM, Patrick Steinhardt wrote:\n> On Thu, Aug 06, 2026 at 04:38:56PM -0500, Justin Tobler wrote:\n> > In git-receive-pack(1), writing the packfile to the transaction is\n> > handled via `unpack()` which relies on global variables to decide how to\n> > invoke the underlying git-index-pack(1) or git-unpack-objects(1) child\n> > processes. In a subsequent commit, the `unpack()` logic is moved behind\n> > a generic ODB transaction interface to handle writing packfiles and thus\n> > can no rely on these globals.\n> \n> Nit: either \"can not\" or \"can no longer\".\n\nWill fix.\n\n> > diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> > index 6da854fca2..8c2d6e5789 100644\n> > --- a/builtin/receive-pack.c\n> > +++ b/builtin/receive-pack.c\n> > @@ -2333,18 +2333,25 @@ 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> > +struct unpack_opts {\n> > +\tconst char *fsck_msg_types;\n> > +\tconst char *shallow_file;\n> > +\toff_t max_input_size;\n> > +\tint fsck_objects;\n> > +\tint unpack_limit;\n> > +\tint reject_thin;\n> > +\tint err_fd;\n> > +\tint quiet;\n> > +};\n> > +\n> >  static const char *unpack(struct odb_transaction *transaction,\n> > -\t\t\t  const char *shallow_file, int err_fd)\n> > +\t\t\t  const struct unpack_opts *opts)\n> >  {\n> >  \tstruct pack_header hdr;\n> >  \tconst char *hdr_err;\n> >  \tint status;\n> >  \tstruct child_process child = CHILD_PROCESS_INIT;\n> > -\tint fsck_objects = (receive_fsck_objects >= 0\n> > -\t\t\t    ? receive_fsck_objects\n> > -\t\t\t    : transfer_fsck_objects >= 0\n> > -\t\t\t    ? transfer_fsck_objects\n> > -\t\t\t    : 0);\n> > +\tint err_fd = opts->err_fd;\n> >  \n> >  \thdr_err = parse_pack_header(&hdr);\n> >  \tif (hdr_err) {\n> \n> It's quite hard to see that the function indeed doesn't rely on the\n> global variables anymore, and I'm quite certain that I'd not spot cases\n> that you forgot to convert to use the options structure instead. But I\n> assume that the function will move into a different file in a subsequent\n> commit, so we'd notice in that patch.\n\nYa, that is indeed the plan. :)\n\n> > @@ -2428,11 +2435,24 @@ static const char *unpack(struct odb_transaction *transaction,\n> >  static const char *unpack_with_sideband(struct odb_transaction *transaction,\n> >  \t\t\t\t\tconst char *shallow_file)\n> >  {\n> > +\tstruct unpack_opts opts = {\n> > +\t\t.fsck_objects = (receive_fsck_objects >= 0\n> > +\t\t\t\t ? receive_fsck_objects\n> > +\t\t\t\t : transfer_fsck_objects >= 0\n> > +\t\t\t\t ? transfer_fsck_objects\n> > +\t\t\t\t : 0),\n> \n> This looks quite ugly, but it's no more ugly than the previous code it\n> replaces.\n\nIn a different version of this patch, I modified the fsck objects field\nafter initialization and dropped the single statement here, but I\nultimately didn't think that looked much better either.\n\n-Justin\n"},{"id":"550019","messageId":"anX6w0tFV6pCu1ux@denethor","threadId":"66133","inReplyTo":"anWDTwCwMn5wEdIQ@pks.im","subject":"Re: [PATCH 4/6] builtin/receive-pack: report unpack errors via strbuf","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-07T15:36:09Z","receivedAt":"2026-08-07T15:36:11Z","isPatch":true,"body":"On 26/08/07 09:03AM, Patrick Steinhardt wrote:\n> On Thu, Aug 06, 2026 at 04:38:57PM -0500, Justin Tobler wrote:\n> > @@ -2551,13 +2559,13 @@ static void update_shallow_info(struct command *commands,\n> >  \tfree(ref_status);\n> >  }\n> >  \n> > -static void report(struct command *commands, const char *unpack_status)\n> > +static void report(struct command *commands, struct strbuf *unpack_status)\n> \n> Should we mark this parameter as `const`?\n\nYes, will do in the next version.\n\n> \n> > @@ -2575,14 +2583,14 @@ static void report(struct command *commands, const char *unpack_status)\n> >  \tstrbuf_release(&buf);\n> >  }\n> >  \n> > -static void report_v2(struct command *commands, const char *unpack_status)\n> > +static void report_v2(struct command *commands, struct strbuf *unpack_status)\n> \n> And here, as well?\n\nWill do.\n\n> > @@ -2711,8 +2719,8 @@ int cmd_receive_pack(int argc,\n> >  \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\n> >  \n> >  \tif ((commands = read_head_info(&reader, &shallow))) {\n> > -\t\tconst char *unpack_status = NULL;\n> >  \t\tstruct string_list push_options = STRING_LIST_INIT_DUP;\n> > +\t\tstruct strbuf unpack_status = STRBUF_INIT;\n> \n> Can't we reuse this buffer and reset it on every run to save some memory\n> allocations?\n\nGood suggestion. I'll lift this up in the next version so we can reuse\nit for each iteration.\n\n-Justin\n"},{"id":"550026","messageId":"anX7baSyrG2dvFDk@denethor","threadId":"66133","inReplyTo":"anWDVFL6OjX2xdR-@pks.im","subject":"Re: [PATCH 6/6] odb/transaction: add transaction interface to write packfiles","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-07T16:01:35Z","receivedAt":"2026-08-07T16:01:39Z","isPatch":true,"body":"On 26/08/07 09:03AM, Patrick Steinhardt wrote:\n> On Thu, Aug 06, 2026 at 04:38:59PM -0500, Justin Tobler wrote:\n> > +\t\tstatus = finish_command(&child);\n> > +\t\tif (status) {\n> > +\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n> > +\t\t\treturn -1;\n> > +\t\t}\n> > +\t\todb_reprepare(repo->objects);\n> \n> Now that this is part of the ODB transaction, do we really have to\n> reprepare the whole object database? Shouldn't it suffice to reprepare\n> just the one source that we've created the transaction for?\n\nYa, this is a good suggestion. At this point, the packfile has only been\nwritten to the transaction source, so it should be fine to just prepare\nthat source. Will do in the next version.\n\n> > diff --git a/odb/transaction.h b/odb/transaction.h\n> > index ec0b27c449..491026e815 100644\n> > --- a/odb/transaction.h\n> > +++ b/odb/transaction.h\n> > @@ -4,6 +4,51 @@\n> >  #include \"gettext.h\"\n> >  #include \"odb.h\"\n> >  \n> > +/*\n> > + * Options controlling how odb_transaction_write_pack() ingests a packfile.\n> > + */\n> > +struct odb_transaction_write_pack_opts {\n> > +\t/*\n> > +\t * Optional fsck severity configuration to apply when incoming objects\n> > +\t * are verified.\n> > +\t */\n> > +\tconst char *fsck_msg_types;\n> > +\t/*\n> > +\t * Path to an alternative shallow file describing the shallow boundaries\n> > +\t * to honor while ingesting the pack.\n> > +\t */\n> > +\tconst char *shallow_file;\n> > +\t/*\n> > +\t * The max size in bytes of the incoming packfile allowed. No limit is\n> > +\t * enforced when set to 0.\n> > +\t */\n> > +\toff_t max_input_size;\n> > +\t/*\n> > +\t * Whether the validity of incoming objects should be verified.\n> > +\t */\n> > +\tint fsck_objects;\n> > +\t/*\n> > +\t * The threshold for the number of incoming objects required to store\n> > +\t * the objects in a packfile. This option may not be relevant to\n> > +\t * backends that do not store obejcts in loose/packed formats and can be\n> > +\t * ignored.\n> > +\t */\n> > +\tint unpack_limit;\n> \n> I wonder whether this option should rather be handled internal in the\n> backend itself, as it very likely doesn't apply to alternative backends\n> anyway. I don't think we allow command line options to override this, so\n> the backend could just read the configuration manually.\n\nThis was something I was also considering initially. This option doesn't\nreally make much sense to have as part of the generic interface though.\nI'll update in the next version to have the backend read this\nconfiguration manually.\n\n> > +\t/*\n> > +\t * Whether to reject an incoming packfile if it is \"thin\".\n> > +\t */\n> > +\tint reject_thin;\n> > +\t/*\n> > +\t * Optional file descriptor for reporting progress and errors. Set to 0\n> > +\t * for none.\n> > +\t */\n> > +\tint err_fd;\n> > +\t/*\n> > +\t * Suppresses progress reporting.\n> > +\t */\n> > +\tint quiet;\n> > +};\n> \n> Nit: I think having some spacing between the different options would\n> make this a bit easier to grok.\n\nWill do.\n\n-Justin\n"},{"id":"550123","messageId":"ani4GoefzYFWjTMl@denethor","threadId":"66133","inReplyTo":"anX6w0tFV6pCu1ux@denethor","subject":"Re: [PATCH 4/6] builtin/receive-pack: report unpack errors via strbuf","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-09T19:00:52Z","receivedAt":"2026-08-09T19:00:57Z","isPatch":true,"body":"On 26/08/07 10:36AM, Justin Tobler wrote:\n> On 26/08/07 09:03AM, Patrick Steinhardt wrote:\n> > > @@ -2711,8 +2719,8 @@ int cmd_receive_pack(int argc,\n> > >  \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\n> > >  \n> > >  \tif ((commands = read_head_info(&reader, &shallow))) {\n> > > -\t\tconst char *unpack_status = NULL;\n> > >  \t\tstruct string_list push_options = STRING_LIST_INIT_DUP;\n> > > +\t\tstruct strbuf unpack_status = STRBUF_INIT;\n> > \n> > Can't we reuse this buffer and reset it on every run to save some memory\n> > allocations?\n\nLooking at this more closely, there isn't actually any loop we are\nrunning this in so I don't think there is any need to change how\n`unpack_status` is set up here.\n\n-Justin\n"},{"id":"550124","messageId":"20260809190106.1565882-1-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260806213859.816157-1-jltobler@gmail.com","subject":"[PATCH v2 0/7] builtin/receive-pack: support pluggable packfile writes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-09T19:00:59Z","receivedAt":"2026-08-09T19:01:11Z","isPatch":true,"body":"Greetings,\n\nWith bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\ntransactions, 2026-07-10), git-receive-pack(1) started using the ODB\ntransaction interfaces to stage incoming objects. While this brought the\ncommand closer to being ODB backend agnostic, the underlying\ngit-index-pack(1) and git-unpack-objects(1) processes used to actually\nwrite the objects to the transaction are still fundamentally tied to the\n\"files\" backend.\n\nThis series aims to address this by introducing a generic\n`odb_transaction_write_pack()` transaction interface to handle writing\nthe incoming packfile to the transaction. The existing logic in\ngit-receive-pack(1) that spawns the child processes to write the\npackfile becomes the \"files\" backend implementation of this interface.\n\nChanges since V1:\n- Changed the \"release\" interface name to \"finalize\" and updated it to\n  return error codes.\n- Marked some function parameters as const.\n- Unpack limit configuration is now resolved in the ODB transaction\n  backend instead of wiring it through the interface.\n- When writing a packfile to the transaction, now only the transaction\n  source is prepared.\n- Updated some commit messages.\n- Updated some code formatting.\n\nThanks for the review,\n-Justin\n\nJustin Tobler (7):\n  odb/transaction: add transaction finalize interface\n  builtin/receive-pack: pass shallow file explicitly\n  builtin/receive-pack: read unpack limit config lazily\n  builtin/receive-pack: lift global state out of unpack()\n  builtin/receive-pack: report unpack errors via strbuf\n  builtin/receive-pack: explicitly pass packfile fd\n  odb/transaction: add transaction interface to write packfiles\n\n builtin/add.c            |   3 +-\n builtin/receive-pack.c   | 211 +++++++++------------------------------\n builtin/unpack-objects.c |   1 +\n builtin/update-index.c   |   2 +\n cache-tree.c             |   4 +-\n object-file.c            | 184 +++++++++++++++++++++++++++++++++-\n odb/transaction.c        |  21 ++++\n odb/transaction.h        |  78 +++++++++++++++\n read-cache.c             |   4 +-\n 9 files changed, 339 insertions(+), 169 deletions(-)\n\nRange-diff against v1:\n1:  d0a4b632bd ! 1:  10efcc22e4 odb/transaction: add transaction release interface\n    @@ Metadata\n     Author: Justin Tobler <jltobler@gmail.com>\n     \n      ## Commit message ##\n    -    odb/transaction: add transaction release interface\n    +    odb/transaction: add transaction finalize interface\n     \n         When committing an ODB transaction via `odb_transaction_commit()`, the\n         staged objects are made visible and the underlying transaction is freed\n    @@ Commit message\n         the case with the existing \"files\" backend in git-receive-pack(1).\n     \n         Stop freeing the transaction in `odb_transaction_commit()` and introduce\n    -    `odb_transaction_release()` to explicitly clean up the transaction\n    -    accordingly. Note that the release interface also provides an optional\n    +    `odb_transaction_finalize()` to explicitly clean up the transaction\n    +    accordingly. Note that the finalize interface also provides an optional\n         callback for any backend-specific deferred cleanup. In a subsequent\n         commit, the \"files\" transaction backend will use this to remove \".keep\"\n         files generated for packfiles received via git-receive-pack(1) after\n    -    references have been updated.\n    +    references have been updated. In preparation for this, the\n    +    `odb_transaction_finalize()` call site in git-receive-pack(1) is made\n    +    after the reference updates are finished.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n    @@ builtin/add.c: int cmd_add(int argc,\n      \tfree(ps_matched);\n      \tdir_clear(&dir);\n      \tclear_pathspec(&pathspec);\n    -+\todb_transaction_release(transaction);\n    ++\todb_transaction_finalize(transaction);\n      \treturn exit_status;\n      }\n     \n    @@ builtin/receive-pack.c: int cmd_receive_pack(int argc,\n      \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n      \t\texecute_commands(commands, unpack_status, &si, transaction,\n      \t\t\t\t &push_options);\n    -+\t\todb_transaction_release(transaction);\n    ++\t\todb_transaction_finalize(transaction);\n      \t\tdelete_tempfile(&pack_lockfile);\n      \t\tsigchain_push(SIGPIPE, SIG_IGN);\n      \t\tif (report_status_v2)\n    @@ builtin/unpack-objects.c: static void unpack_all(void)\n      \t\tdisplay_progress(progress, i + 1);\n      \t}\n      \todb_transaction_commit(transaction);\n    -+\todb_transaction_release(transaction);\n    ++\todb_transaction_finalize(transaction);\n      \tstop_progress(&progress);\n      \n      \tif (delta_list)\n    @@ builtin/update-index.c: int cmd_update_index(int argc,\n      \t\t\t */\n      \t\t\tif (transaction && verbose) {\n      \t\t\t\todb_transaction_commit(transaction);\n    -+\t\t\t\todb_transaction_release(transaction);\n    ++\t\t\t\todb_transaction_finalize(transaction);\n      \t\t\t\ttransaction = NULL;\n      \t\t\t}\n      \n    @@ builtin/update-index.c: int cmd_update_index(int argc,\n      \t * By now we have added all of the new objects\n      \t */\n      \todb_transaction_commit(transaction);\n    -+\todb_transaction_release(transaction);\n    ++\todb_transaction_finalize(transaction);\n      \n      \tif (split_index > 0) {\n      \t\tif (repo_config_get_split_index(the_repository) == 0)\n    @@ cache-tree.c: int cache_tree_update(struct index_state *istate, int flags)\n     -\tif (!inflight)\n     +\tif (!inflight) {\n      \t\todb_transaction_commit(transaction);\n    -+\t\todb_transaction_release(transaction);\n    ++\t\todb_transaction_finalize(transaction);\n     +\t}\n      \ttrace2_region_leave(\"cache_tree\", \"update\", istate->repo);\n      \ttrace_performance_leave(\"cache_tree_update\");\n    @@ object-file.c: int index_fd(struct index_state *istate, struct object_id *oid,\n     -\t\t\tif (!inflight)\n     +\t\t\tif (!inflight) {\n      \t\t\t\todb_transaction_commit(transaction);\n    -+\t\t\t\todb_transaction_release(transaction);\n    ++\t\t\t\todb_transaction_finalize(transaction);\n     +\t\t\t}\n      \t\t} else {\n      \t\t\tret = hash_blob_stream(&stream,\n    @@ odb/transaction.c: int odb_transaction_commit(struct odb_transaction *transactio\n      \n      \tret = transaction->commit(transaction);\n      \ttransaction->source->odb->transaction = NULL;\n    --\tfree(transaction);\n    - \n    - \treturn ret;\n    - }\n    - \n    -+void odb_transaction_release(struct odb_transaction *transaction)\n    ++\n    ++\treturn ret;\n    ++}\n    ++\n    ++int odb_transaction_finalize(struct odb_transaction *transaction)\n     +{\n    -+\tif (!transaction)\n    -+\t\treturn;\n    ++\tint ret = 0;\n     +\n    -+\tif (transaction->release)\n    -+\t\ttransaction->release(transaction);\n    ++\tif (!transaction)\n    ++\t\treturn 0;\n     +\n    -+\tfree(transaction);\n    -+}\n    ++\tif (transaction->finalize)\n    ++\t\tret = transaction->finalize(transaction);\n     +\n    - 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    + \tfree(transaction);\n    + \n    + \treturn ret;\n     \n      ## odb/transaction.h ##\n     @@ odb/transaction.h: struct odb_transaction {\n    @@ odb/transaction.h: struct odb_transaction {\n     +\t/*\n     +\t * Optional ODB source specific callback invoked when the transaction\n     +\t * needs to perform any deferred cleanup after objects have been\n    -+\t * committed.\n    ++\t * committed. Returns 0 on success, a negative error code otherwise.\n     +\t */\n    -+\tvoid (*release)(struct odb_transaction *transaction);\n    ++\tint (*finalize)(struct odb_transaction *transaction);\n     +\n      \t/*\n      \t * This callback is expected to write the given object stream into\n    @@ odb/transaction.h: static inline void odb_transaction_begin_or_die(struct object\n      int odb_transaction_commit(struct odb_transaction *transaction);\n      \n     +/*\n    -+ * Releases an ODB transaction, performing any deferred cleanup and freeing it.\n    ++ * Finalizes an ODB transaction, performing any deferred cleanup and freeing it.\n     + * Must be called for every successfully started transaction. Note that, if the\n    -+ * specified transaction is NULL, the function is a no-op.\n    ++ * specified transaction is NULL, the function is a no-op. Returns 0 on success,\n    ++ * a negative error code otherwise.\n     + */\n    -+void odb_transaction_release(struct odb_transaction *transaction);\n    ++int odb_transaction_finalize(struct odb_transaction *transaction);\n     +\n      /*\n       * Writes the object in the provided stream into the transaction. The resulting\n    @@ read-cache.c: int add_files_to_cache(struct repository *repo, const char *prefix\n     -\tif (!inflight)\n     +\tif (!inflight) {\n      \t\todb_transaction_commit(transaction);\n    -+\t\todb_transaction_release(transaction);\n    ++\t\todb_transaction_finalize(transaction);\n     +\t}\n      \n      \trelease_revisions(&rev);\n2:  0aff7f769e = 2:  e1903ac32f builtin/receive-pack: pass shallow file explicitly\n-:  ---------- > 3:  e4950c0abe builtin/receive-pack: read unpack limit config lazily\n3:  61bac2a56f ! 4:  c9b4ff73ba builtin/receive-pack: lift global state out of unpack()\n    @@ Commit message\n         invoke the underlying git-index-pack(1) or git-unpack-objects(1) child\n         processes. In a subsequent commit, the `unpack()` logic is moved behind\n         a generic ODB transaction interface to handle writing packfiles and thus\n    -    can no rely on these globals.\n    +    can no longer rely on these globals.\n     \n         Lift the global state out of `unpack()` by instead storing this state in\n         a `struct unpack_opts` that gets passed to the function explicitly.\n    @@ Commit message\n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n      ## builtin/receive-pack.c ##\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    +@@ builtin/receive-pack.c: static int get_unpack_limit(struct repository *repo)\n    + \treturn limit;\n      }\n      \n     +struct unpack_opts {\n    @@ builtin/receive-pack.c: static void push_header_arg(struct strvec *args, struct\n     +\tconst char *shallow_file;\n     +\toff_t max_input_size;\n     +\tint fsck_objects;\n    -+\tint unpack_limit;\n     +\tint reject_thin;\n     +\tint err_fd;\n     +\tint quiet;\n    @@ builtin/receive-pack.c: static const char *unpack(struct odb_transaction *transa\n      \t}\n      \n      \todb_transaction_env(transaction, &child.env);\n    - \n    --\tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n    -+\tif (ntohl(hdr.hdr_entries) < opts->unpack_limit) {\n    +@@ builtin/receive-pack.c: static const char *unpack(struct odb_transaction *transaction,\n    + \tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n      \t\tstrvec_push(&child.args, \"unpack-objects\");\n      \t\tpush_header_arg(&child.args, &hdr);\n     -\t\tif (quiet)\n    @@ builtin/receive-pack.c: static const char *unpack(struct odb_transaction *transa\n     +\t\t.fsck_msg_types = fsck_msg_types.buf,\n     +\t\t.max_input_size = max_input_size,\n     +\t\t.shallow_file = shallow_file,\n    -+\t\t.unpack_limit = unpack_limit,\n     +\t\t.reject_thin = reject_thin,\n     +\t\t.quiet = quiet,\n     +\t};\n4:  12b83ee3bc ! 5:  7be990c2c2 builtin/receive-pack: report unpack errors via strbuf\n    @@ builtin/receive-pack.c: static void update_shallow_info(struct command *commands\n      }\n      \n     -static void report(struct command *commands, const char *unpack_status)\n    -+static void report(struct command *commands, struct strbuf *unpack_status)\n    ++static void report(struct command *commands, const struct strbuf *unpack_status)\n      {\n      \tstruct command *cmd;\n      \tstruct strbuf buf = STRBUF_INIT;\n    @@ builtin/receive-pack.c: static void report(struct command *commands, const char\n      }\n      \n     -static void report_v2(struct command *commands, const char *unpack_status)\n    -+static void report_v2(struct command *commands, struct strbuf *unpack_status)\n    ++static void report_v2(struct command *commands, const struct strbuf *unpack_status)\n      {\n      \tstruct command *cmd;\n      \tstruct strbuf buf = STRBUF_INIT;\n    @@ builtin/receive-pack.c: int cmd_receive_pack(int argc,\n     -\t\texecute_commands(commands, unpack_status, &si, transaction,\n     +\t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n      \t\t\t\t &push_options);\n    - \t\todb_transaction_release(transaction);\n    + \t\todb_transaction_finalize(transaction);\n      \t\tdelete_tempfile(&pack_lockfile);\n      \t\tsigchain_push(SIGPIPE, SIG_IGN);\n      \t\tif (report_status_v2)\n5:  8678f4cd45 = 6:  742c724943 builtin/receive-pack: explicitly pass packfile fd\n6:  c390f59367 ! 7:  7743cf242a odb/transaction: add transaction interface to write packfiles\n    @@ Commit message\n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n      ## builtin/receive-pack.c ##\n    +@@\n    + #include \"gpg-interface.h\"\n    + #include \"hex.h\"\n    + #include \"hook.h\"\n    +-#include \"lockfile.h\"\n    + #include \"object.h\"\n    + #include \"object-file.h\"\n    + #include \"object-name.h\"\n    +@@\n    + #include \"oid-array.h\"\n    + #include \"oidset.h\"\n    + #include \"pack.h\"\n    +-#include \"packfile.h\"\n    + #include \"parse-options.h\"\n    + #include \"pkt-line.h\"\n    + #include \"protocol.h\"\n     @@ builtin/receive-pack.c: static void read_push_options(struct packet_reader *reader,\n      \t}\n      }\n    @@ builtin/receive-pack.c: static void read_push_options(struct packet_reader *read\n     -\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n     -}\n     -\n    +-static int get_unpack_limit(struct repository *repo)\n    +-{\n    +-\tstatic int limit = -1;\n    +-\n    +-\tif (limit < 0) {\n    +-\t\tint receive_limit = -1;\n    +-\t\tint transfer_limit = -1;\n    +-\n    +-\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n    +-\t\t\t\t    &receive_limit);\n    +-\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n    +-\t\t\t\t    &transfer_limit);\n    +-\n    +-\t\tif (receive_limit >= 0)\n    +-\t\t\tlimit = receive_limit;\n    +-\t\telse if (transfer_limit >= 0)\n    +-\t\t\tlimit = transfer_limit;\n    +-\t\telse\n    +-\t\t\tlimit = 100;\n    +-\t}\n    +-\n    +-\treturn limit;\n    +-}\n    +-\n     -struct unpack_opts {\n     -\tconst char *fsck_msg_types;\n     -\tconst char *shallow_file;\n     -\toff_t max_input_size;\n     -\tint fsck_objects;\n    --\tint unpack_limit;\n     -\tint reject_thin;\n     -\tint err_fd;\n     -\tint quiet;\n    @@ builtin/receive-pack.c: static void read_push_options(struct packet_reader *read\n     -\n     -\todb_transaction_env(transaction, &child.env);\n     -\n    --\tif (ntohl(hdr.hdr_entries) < opts->unpack_limit) {\n    +-\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n     -\t\tstrvec_push(&child.args, \"unpack-objects\");\n     -\t\tpush_header_arg(&child.args, &hdr);\n     -\t\tif (opts->quiet)\n    @@ builtin/receive-pack.c: static int unpack_with_sideband(struct odb_transaction *\n     @@ builtin/receive-pack.c: int cmd_receive_pack(int argc,\n      \t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n      \t\t\t\t &push_options);\n    - \t\todb_transaction_release(transaction);\n    + \t\todb_transaction_finalize(transaction);\n     -\t\tdelete_tempfile(&pack_lockfile);\n      \t\tsigchain_push(SIGPIPE, SIG_IGN);\n      \t\tif (report_status_v2)\n      \t\t\treport_v2(commands, &unpack_status);\n     \n      ## object-file.c ##\n    +@@\n    + #define USE_THE_REPOSITORY_VARIABLE\n    + \n    + #include \"git-compat-util.h\"\n    ++#include \"config.h\"\n    + #include \"convert.h\"\n    + #include \"dir.h\"\n    + #include \"environment.h\"\n     @@\n      #include \"packfile.h\"\n      #include \"path.h\"\n    @@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b\n     +\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n     +}\n     +\n    ++static int get_unpack_limit(struct repository *repo)\n    ++{\n    ++\tstatic int limit = -1;\n    ++\n    ++\tif (limit < 0) {\n    ++\t\tint receive_limit = -1;\n    ++\t\tint transfer_limit = -1;\n    ++\n    ++\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n    ++\t\t\t\t    &receive_limit);\n    ++\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n    ++\t\t\t\t    &transfer_limit);\n    ++\n    ++\t\tif (receive_limit >= 0)\n    ++\t\t\tlimit = receive_limit;\n    ++\t\telse if (transfer_limit >= 0)\n    ++\t\t\tlimit = transfer_limit;\n    ++\t\telse\n    ++\t\t\tlimit = 100;\n    ++\t}\n    ++\n    ++\treturn limit;\n    ++}\n    ++\n     +static int odb_transaction_files_write_pack(struct odb_transaction *base,\n     +\t\t\t\t\t    int pack_fd, struct strbuf *err_msg,\n     +\t\t\t\t\t    const struct odb_transaction_write_pack_opts *opts)\n    @@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b\n     +\n     +\todb_transaction_env(base, &child.env);\n     +\n    -+\tif (ntohl(hdr.hdr_entries) < (unsigned int)opts->unpack_limit) {\n    ++\tif (ntohl(hdr.hdr_entries) < (unsigned int)get_unpack_limit(repo)) {\n     +\t\tstrvec_push(&child.args, \"unpack-objects\");\n     +\t\tpush_header_arg(&child.args, &hdr);\n     +\t\tif (opts->quiet)\n    @@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b\n     +\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n     +\t\t\treturn -1;\n     +\t\t}\n    -+\t\todb_reprepare(repo->objects);\n    ++\n    ++\t\todb_source_prepare(repo->objects->sources,\n    ++\t\t\t\t   ODB_PREPARE_FLUSH_CACHES);\n     +\t}\n     +\n     +\treturn 0;\n     +}\n     +\n    -+static void odb_transaction_files_release(struct odb_transaction *base)\n    ++static int odb_transaction_files_finalize(struct odb_transaction *base)\n     +{\n     +\tstruct odb_transaction_files *transaction =\n     +\t\tcontainer_of(base, struct odb_transaction_files, base);\n    ++\tint ret = 0;\n     +\n     +\tfor (size_t i = 0; i < transaction->pack_lockfiles_nr; i++)\n    -+\t\tdelete_tempfile(&transaction->pack_lockfiles[i]);\n    ++\t\tret |= delete_tempfile(&transaction->pack_lockfiles[i]);\n    ++\n     +\tfree(transaction->pack_lockfiles);\n    ++\n    ++\treturn ret;\n     +}\n     +\n      static int odb_transaction_files_env(struct odb_transaction *base,\n    @@ object-file.c: int odb_transaction_files_begin(struct odb_source *source,\n      \ttransaction = xcalloc(1, sizeof(*transaction));\n      \ttransaction->base.source = source;\n      \ttransaction->base.commit = odb_transaction_files_commit;\n    -+\ttransaction->base.release = odb_transaction_files_release;\n    ++\ttransaction->base.finalize = odb_transaction_files_finalize;\n      \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n     +\ttransaction->base.write_pack = odb_transaction_files_write_pack;\n      \ttransaction->base.env = odb_transaction_files_env;\n    @@ odb/transaction.h\n     +\t * are verified.\n     +\t */\n     +\tconst char *fsck_msg_types;\n    ++\n     +\t/*\n     +\t * Path to an alternative shallow file describing the shallow boundaries\n     +\t * to honor while ingesting the pack.\n     +\t */\n     +\tconst char *shallow_file;\n    ++\n     +\t/*\n     +\t * The max size in bytes of the incoming packfile allowed. No limit is\n     +\t * enforced when set to 0.\n     +\t */\n    ++\n     +\toff_t max_input_size;\n    ++\n     +\t/*\n     +\t * Whether the validity of incoming objects should be verified.\n     +\t */\n     +\tint fsck_objects;\n    -+\t/*\n    -+\t * The threshold for the number of incoming objects required to store\n    -+\t * the objects in a packfile. This option may not be relevant to\n    -+\t * backends that do not store obejcts in loose/packed formats and can be\n    -+\t * ignored.\n    -+\t */\n    -+\tint unpack_limit;\n    ++\n     +\t/*\n     +\t * Whether to reject an incoming packfile if it is \"thin\".\n     +\t */\n     +\tint reject_thin;\n    ++\n     +\t/*\n     +\t * Optional file descriptor for reporting progress and errors. Set to 0\n     +\t * for none.\n     +\t */\n     +\tint err_fd;\n    ++\n     +\t/*\n     +\t * Suppresses progress reporting.\n     +\t */\n\nbase-commit: 2c78326f810173a4f3aefd8021f1e07575412481\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550125","messageId":"20260809190106.1565882-2-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260809190106.1565882-1-jltobler@gmail.com","subject":"[PATCH v2 1/7] odb/transaction: add transaction finalize interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-09T19:01:00Z","receivedAt":"2026-08-09T19:01:12Z","isPatch":true,"body":"When committing an ODB transaction via `odb_transaction_commit()`, the\nstaged objects are made visible and the underlying transaction is freed\nat the same time. Coupling these two steps does not leave room for any\npost-commit transaction operations to be introduced though. Such a\ncapability is useful if an ODB transaction backend needs to hold on to\nlockfiles after transaction commit until references are updated, as is\nthe case with the existing \"files\" backend in git-receive-pack(1).\n\nStop freeing the transaction in `odb_transaction_commit()` and introduce\n`odb_transaction_finalize()` to explicitly clean up the transaction\naccordingly. Note that the finalize interface also provides an optional\ncallback for any backend-specific deferred cleanup. In a subsequent\ncommit, the \"files\" transaction backend will use this to remove \".keep\"\nfiles generated for packfiles received via git-receive-pack(1) after\nreferences have been updated. In preparation for this, the\n`odb_transaction_finalize()` call site in git-receive-pack(1) is made\nafter the reference updates are finished.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  3 ++-\n builtin/receive-pack.c   |  1 +\n builtin/unpack-objects.c |  1 +\n builtin/update-index.c   |  2 ++\n cache-tree.c             |  4 +++-\n object-file.c            |  4 +++-\n odb/transaction.c        | 14 ++++++++++++++\n odb/transaction.h        | 15 +++++++++++++++\n read-cache.c             |  4 +++-\n 9 files changed, 44 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 60ffbede2b..501e114ed5 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -393,7 +393,7 @@ int cmd_add(int argc,\n \tchar *seen = NULL;\n \tchar *ps_matched = NULL;\n \tstruct lock_file lock_file = LOCK_INIT;\n-\tstruct odb_transaction *transaction;\n+\tstruct odb_transaction *transaction = NULL;\n \n \trepo_config(repo, add_config, NULL);\n \n@@ -610,5 +610,6 @@ int cmd_add(int argc,\n \tfree(ps_matched);\n \tdir_clear(&dir);\n \tclear_pathspec(&pathspec);\n+\todb_transaction_finalize(transaction);\n \treturn exit_status;\n }\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 86933d8d7e..8720281250 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2714,6 +2714,7 @@ int cmd_receive_pack(int argc,\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n \t\texecute_commands(commands, unpack_status, &si, transaction,\n \t\t\t\t &push_options);\n+\t\todb_transaction_finalize(transaction);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex 4263edfbec..aee68dc42d 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -604,6 +604,7 @@ static void unpack_all(void)\n \t\tdisplay_progress(progress, i + 1);\n \t}\n \todb_transaction_commit(transaction);\n+\todb_transaction_finalize(transaction);\n \tstop_progress(&progress);\n \n \tif (delta_list)\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 241abd4332..e422342f52 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1157,6 +1157,7 @@ int cmd_update_index(int argc,\n \t\t\t */\n \t\t\tif (transaction && verbose) {\n \t\t\t\todb_transaction_commit(transaction);\n+\t\t\t\todb_transaction_finalize(transaction);\n \t\t\t\ttransaction = NULL;\n \t\t\t}\n \n@@ -1225,6 +1226,7 @@ int cmd_update_index(int argc,\n \t * By now we have added all of the new objects\n \t */\n \todb_transaction_commit(transaction);\n+\todb_transaction_finalize(transaction);\n \n \tif (split_index > 0) {\n \t\tif (repo_config_get_split_index(the_repository) == 0)\ndiff --git a/cache-tree.c b/cache-tree.c\nindex d92f513286..cff1d4fd48 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -537,8 +537,10 @@ int cache_tree_update(struct index_state *istate, int flags)\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)\n+\tif (!inflight) {\n \t\todb_transaction_commit(transaction);\n+\t\todb_transaction_finalize(transaction);\n+\t}\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 ec35c318bc..f993d58056 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -964,8 +964,10 @@ int index_fd(struct index_state *istate, struct object_id *oid,\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\tif (!inflight)\n+\t\t\tif (!inflight) {\n \t\t\t\todb_transaction_commit(transaction);\n+\t\t\t\todb_transaction_finalize(transaction);\n+\t\t\t}\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 dab7da6a9a..9e9a982778 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -33,6 +33,20 @@ int odb_transaction_commit(struct odb_transaction *transaction)\n \n \tret = transaction->commit(transaction);\n \ttransaction->source->odb->transaction = NULL;\n+\n+\treturn ret;\n+}\n+\n+int odb_transaction_finalize(struct odb_transaction *transaction)\n+{\n+\tint ret = 0;\n+\n+\tif (!transaction)\n+\t\treturn 0;\n+\n+\tif (transaction->finalize)\n+\t\tret = transaction->finalize(transaction);\n+\n \tfree(transaction);\n \n \treturn ret;\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 4cb2eafcbf..89f6902caf 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -22,6 +22,13 @@ struct odb_transaction {\n \t */\n \tint (*commit)(struct odb_transaction *transaction);\n \n+\t/*\n+\t * Optional ODB source specific callback invoked when the transaction\n+\t * needs to perform any deferred cleanup after objects have been\n+\t * committed. Returns 0 on success, a negative error code otherwise.\n+\t */\n+\tint (*finalize)(struct odb_transaction *transaction);\n+\n \t/*\n \t * This callback is expected to write the given object stream into\n \t * the ODB transaction. Note that for now, only blobs support streaming.\n@@ -75,6 +82,14 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n  */\n int odb_transaction_commit(struct odb_transaction *transaction);\n \n+/*\n+ * Finalizes an ODB transaction, performing any deferred cleanup and freeing it.\n+ * Must be called for every successfully started transaction. Note that, if the\n+ * specified transaction is NULL, the function is a no-op. Returns 0 on success,\n+ * a negative error code otherwise.\n+ */\n+int odb_transaction_finalize(struct odb_transaction *transaction);\n+\n /*\n  * Writes the object in the provided stream into the transaction. The resulting\n  * object ID is written into the out pointer. Returns 0 on success, a negative\ndiff --git a/read-cache.c b/read-cache.c\nindex 6c449f393d..9a3ac4646f 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4048,8 +4048,10 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\n \tif (!inflight)\n \t\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n-\tif (!inflight)\n+\tif (!inflight) {\n \t\todb_transaction_commit(transaction);\n+\t\todb_transaction_finalize(transaction);\n+\t}\n \n \trelease_revisions(&rev);\n \treturn !!data.add_errors;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550126","messageId":"20260809190106.1565882-3-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260809190106.1565882-1-jltobler@gmail.com","subject":"[PATCH v2 2/7] builtin/receive-pack: pass shallow file explicitly","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-09T19:01:01Z","receivedAt":"2026-08-09T19:01:13Z","isPatch":true,"body":"If shallow information is provided during `unpack()`, a temporary\nshallow file is created and stored in global state. In a subsequent\ncommit, the `unpack()` logic is moved behind a generic ODB transaction\ninterface to handle writing packfiles and thus can no longer rely on\nsuch global state. Lift the setup of the temporary shallow file out of\n`unpack()` and wire it through to its call sites explicitly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 38 ++++++++++++++++++++++----------------\n 1 file changed, 22 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 8720281250..78d2911c00 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -86,7 +86,6 @@ static const char *head_name;\n static void *head_name_to_free;\n static int sent_capabilities;\n static int shallow_update;\n-static const char *alt_shallow_file;\n static struct strbuf push_cert = STRBUF_INIT;\n static struct object_id push_cert_oid;\n static struct signature_check sigcheck;\n@@ -2334,8 +2333,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-\t\t\t  struct odb_transaction *transaction)\n+static const char *unpack(struct odb_transaction *transaction,\n+\t\t\t  const char *shallow_file, int err_fd)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2354,10 +2353,9 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n \t\treturn hdr_err;\n \t}\n \n-\tif (si->nr_ours || si->nr_theirs) {\n-\t\talt_shallow_file = setup_temporary_shallow(si->shallow);\n+\tif (shallow_file) {\n \t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, alt_shallow_file);\n+\t\tstrvec_push(&child.args, shallow_file);\n \t}\n \n \todb_transaction_env(transaction, &child.env);\n@@ -2427,14 +2425,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-\t\t\t\t\tstruct odb_transaction *transaction)\n+static const char *unpack_with_sideband(struct odb_transaction *transaction,\n+\t\t\t\t\tconst char *shallow_file)\n {\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(0, si, transaction);\n+\t\treturn unpack(transaction, shallow_file, 0);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2443,13 +2441,14 @@ 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, transaction);\n+\tret = unpack(transaction, shallow_file, muxer.in);\n \n \tfinish_async(&muxer);\n \treturn ret;\n }\n \n-static void prepare_shallow_update(struct shallow_info *si)\n+static void prepare_shallow_update(struct shallow_info *si,\n+\t\t\t\t   const char *shallow_file)\n {\n \tint i, j, k, bitmap_size = DIV_ROUND_UP(si->ref->nr, 32);\n \n@@ -2489,12 +2488,13 @@ static void prepare_shallow_update(struct shallow_info *si)\n \t * command. check_connected() will be done with\n \t * true .git/shallow though.\n \t */\n-\tsetenv(GIT_SHALLOW_FILE_ENVIRONMENT, alt_shallow_file, 1);\n+\tsetenv(GIT_SHALLOW_FILE_ENVIRONMENT, shallow_file, 1);\n }\n \n static void update_shallow_info(struct command *commands,\n \t\t\t\tstruct shallow_info *si,\n-\t\t\t\tstruct oid_array *ref)\n+\t\t\t\tstruct oid_array *ref,\n+\t\t\t\tconst char *shallow_file)\n {\n \tstruct command *cmd;\n \tint *ref_status;\n@@ -2513,7 +2513,7 @@ static void update_shallow_info(struct command *commands,\n \tsi->ref = ref;\n \n \tif (shallow_update) {\n-\t\tprepare_shallow_update(si);\n+\t\tprepare_shallow_update(si, shallow_file);\n \t\treturn;\n \t}\n \n@@ -2705,11 +2705,17 @@ 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\tconst char *alt_shallow_file = NULL;\n+\n+\t\t\tif (si.nr_ours || si.nr_theirs)\n+\t\t\t\talt_shallow_file = setup_temporary_shallow(si.shallow);\n+\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\t\tunpack_status = unpack_with_sideband(transaction, alt_shallow_file);\n+\n+\t\t\tupdate_shallow_info(commands, &si, &ref, alt_shallow_file);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n \t\texecute_commands(commands, unpack_status, &si, transaction,\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550127","messageId":"20260809190106.1565882-4-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260809190106.1565882-1-jltobler@gmail.com","subject":"[PATCH v2 3/7] builtin/receive-pack: read unpack limit config lazily","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-09T19:01:02Z","receivedAt":"2026-08-09T19:01:14Z","isPatch":true,"body":"In git-receive-pack(1), the `receive.unpackLimit` and\n`transfer.unpackLimit` configuration decides whether an incoming\npackfile should be exploded into loose objects or kept as a packfile\non-disk. In a subsequent commit, the logic to write the incoming\npackfile is made ODB backend agnostic and moved behind a pluggable ODB\ntransaction interface. Consequently, whether to explode a packfile is a\ndetail of how a particular backend stores objects and should not be a\npart of the generic interface itself.\n\nIn preparation for this, instead resolve the unpack limit lazily inside\n`unpack()` by reading the configuration directly. The now-unused unpack\nlimit globals are dropped accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 44 ++++++++++++++++++++++++------------------\n 1 file changed, 25 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 78d2911c00..5264d70467 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -62,12 +62,9 @@ static enum deny_action deny_delete_current = DENY_UNCONFIGURED;\n static int receive_fsck_objects = -1;\n static int transfer_fsck_objects = -1;\n static struct strbuf fsck_msg_types = STRBUF_INIT;\n-static int receive_unpack_limit = -1;\n-static int transfer_unpack_limit = -1;\n static int advertise_atomic_push = 1;\n static int advertise_push_options;\n static int advertise_sid;\n-static int unpack_limit = 100;\n static off_t max_input_size;\n static int report_status;\n static int report_status_v2;\n@@ -157,16 +154,6 @@ static int receive_pack_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (strcmp(var, \"receive.unpacklimit\") == 0) {\n-\t\treceive_unpack_limit = git_config_int(var, value, ctx->kvi);\n-\t\treturn 0;\n-\t}\n-\n-\tif (strcmp(var, \"transfer.unpacklimit\") == 0) {\n-\t\ttransfer_unpack_limit = git_config_int(var, value, ctx->kvi);\n-\t\treturn 0;\n-\t}\n-\n \tif (strcmp(var, \"receive.fsck.skiplist\") == 0) {\n \t\tchar *path;\n \n@@ -2333,6 +2320,30 @@ 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 int get_unpack_limit(struct repository *repo)\n+{\n+\tstatic int limit = -1;\n+\n+\tif (limit < 0) {\n+\t\tint receive_limit = -1;\n+\t\tint transfer_limit = -1;\n+\n+\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n+\t\t\t\t    &receive_limit);\n+\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n+\t\t\t\t    &transfer_limit);\n+\n+\t\tif (receive_limit >= 0)\n+\t\t\tlimit = receive_limit;\n+\t\telse if (transfer_limit >= 0)\n+\t\t\tlimit = transfer_limit;\n+\t\telse\n+\t\t\tlimit = 100;\n+\t}\n+\n+\treturn limit;\n+}\n+\n static const char *unpack(struct odb_transaction *transaction,\n \t\t\t  const char *shallow_file, int err_fd)\n {\n@@ -2360,7 +2371,7 @@ static const char *unpack(struct odb_transaction *transaction,\n \n \todb_transaction_env(transaction, &child.env);\n \n-\tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n+\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n \t\tpush_header_arg(&child.args, &hdr);\n \t\tif (quiet)\n@@ -2652,11 +2663,6 @@ int cmd_receive_pack(int argc,\n \tif (cert_nonce_seed)\n \t\tpush_cert_nonce = prepare_push_cert_nonce(service_dir, time(NULL));\n \n-\tif (0 <= receive_unpack_limit)\n-\t\tunpack_limit = receive_unpack_limit;\n-\telse if (0 <= transfer_unpack_limit)\n-\t\tunpack_limit = transfer_unpack_limit;\n-\n \tswitch (determine_protocol_version_server()) {\n \tcase protocol_v2:\n \t\t/*\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550128","messageId":"20260809190106.1565882-5-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260809190106.1565882-1-jltobler@gmail.com","subject":"[PATCH v2 4/7] builtin/receive-pack: lift global state out of unpack()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-09T19:01:03Z","receivedAt":"2026-08-09T19:01:15Z","isPatch":true,"body":"In git-receive-pack(1), writing the packfile to the transaction is\nhandled via `unpack()` which relies on global variables to decide how to\ninvoke the underlying git-index-pack(1) or git-unpack-objects(1) child\nprocesses. In a subsequent commit, the `unpack()` logic is moved behind\na generic ODB transaction interface to handle writing packfiles and thus\ncan no longer rely on these globals.\n\nLift the global state out of `unpack()` by instead storing this state in\na `struct unpack_opts` that gets passed to the function explicitly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 63 +++++++++++++++++++++++++++---------------\n 1 file changed, 41 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 5264d70467..21dab851ad 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2344,18 +2344,24 @@ static int get_unpack_limit(struct repository *repo)\n \treturn limit;\n }\n \n+struct unpack_opts {\n+\tconst char *fsck_msg_types;\n+\tconst char *shallow_file;\n+\toff_t max_input_size;\n+\tint fsck_objects;\n+\tint reject_thin;\n+\tint err_fd;\n+\tint quiet;\n+};\n+\n static const char *unpack(struct odb_transaction *transaction,\n-\t\t\t  const char *shallow_file, int err_fd)\n+\t\t\t  const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n \tint status;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint fsck_objects = (receive_fsck_objects >= 0\n-\t\t\t    ? receive_fsck_objects\n-\t\t\t    : transfer_fsck_objects >= 0\n-\t\t\t    ? transfer_fsck_objects\n-\t\t\t    : 0);\n+\tint err_fd = opts->err_fd;\n \n \thdr_err = parse_pack_header(&hdr);\n \tif (hdr_err) {\n@@ -2364,9 +2370,9 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\treturn hdr_err;\n \t}\n \n-\tif (shallow_file) {\n+\tif (opts->shallow_file) {\n \t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, shallow_file);\n+\t\tstrvec_push(&child.args, opts->shallow_file);\n \t}\n \n \todb_transaction_env(transaction, &child.env);\n@@ -2374,14 +2380,14 @@ static const char *unpack(struct odb_transaction *transaction,\n \tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n \t\tpush_header_arg(&child.args, &hdr);\n-\t\tif (quiet)\n+\t\tif (opts->quiet)\n \t\t\tstrvec_push(&child.args, \"-q\");\n-\t\tif (fsck_objects)\n+\t\tif (opts->fsck_objects)\n \t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     fsck_msg_types.buf);\n-\t\tif (max_input_size)\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (opts->max_input_size)\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)max_input_size);\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.no_stdout = 1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n@@ -2402,18 +2408,18 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\t\t     (uintmax_t)getpid(),\n \t\t\t     hostname);\n \n-\t\tif (!quiet && err_fd)\n+\t\tif (!opts->quiet && err_fd)\n \t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n-\t\tif (use_sideband)\n+\t\tif (err_fd)\n \t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n-\t\tif (fsck_objects)\n+\t\tif (opts->fsck_objects)\n \t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     fsck_msg_types.buf);\n-\t\tif (!reject_thin)\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (!opts->reject_thin)\n \t\t\tstrvec_push(&child.args, \"--fix-thin\");\n-\t\tif (max_input_size)\n+\t\tif (opts->max_input_size)\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)max_input_size);\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.out = -1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n@@ -2439,11 +2445,23 @@ static const char *unpack(struct odb_transaction *transaction,\n static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \t\t\t\t\tconst char *shallow_file)\n {\n+\tstruct unpack_opts opts = {\n+\t\t.fsck_objects = (receive_fsck_objects >= 0\n+\t\t\t\t ? receive_fsck_objects\n+\t\t\t\t : transfer_fsck_objects >= 0\n+\t\t\t\t ? transfer_fsck_objects\n+\t\t\t\t : 0),\n+\t\t.fsck_msg_types = fsck_msg_types.buf,\n+\t\t.max_input_size = max_input_size,\n+\t\t.shallow_file = shallow_file,\n+\t\t.reject_thin = reject_thin,\n+\t\t.quiet = quiet,\n+\t};\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, shallow_file, 0);\n+\t\treturn unpack(transaction, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2452,7 +2470,8 @@ static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \tif (start_async(&muxer))\n \t\treturn NULL;\n \n-\tret = unpack(transaction, shallow_file, muxer.in);\n+\topts.err_fd = muxer.in;\n+\tret = unpack(transaction, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550129","messageId":"20260809190106.1565882-6-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260809190106.1565882-1-jltobler@gmail.com","subject":"[PATCH v2 5/7] builtin/receive-pack: report unpack errors via strbuf","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-09T19:01:04Z","receivedAt":"2026-08-09T19:01:16Z","isPatch":true,"body":"When writing packfiles via `unpack()`, error messages are returned\ndirectly by the function. In preparation for `unpack()` logic being\nmoved behind a generic ODB transaction interface, update the function to\ninstead write any error messages to a caller provided strbuf and return\na negative value on error. Call sites are updated to use the error\nstrbuf accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 63 ++++++++++++++++++++++++------------------\n 1 file changed, 36 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 21dab851ad..896439d46d 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2015,7 +2015,7 @@ static void execute_commands_atomic(struct command *commands,\n }\n \n static void execute_commands(struct command *commands,\n-\t\t\t     const char *unpacker_error,\n+\t\t\t     int 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@@ -2354,8 +2354,8 @@ struct unpack_opts {\n \tint quiet;\n };\n \n-static const char *unpack(struct odb_transaction *transaction,\n-\t\t\t  const struct unpack_opts *opts)\n+static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n+\t\t  const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2367,7 +2367,8 @@ static const char *unpack(struct odb_transaction *transaction,\n \tif (hdr_err) {\n \t\tif (err_fd > 0)\n \t\t\tclose(err_fd);\n-\t\treturn hdr_err;\n+\t\tstrbuf_addstr(err_msg, hdr_err);\n+\t\treturn -1;\n \t}\n \n \tif (opts->shallow_file) {\n@@ -2392,8 +2393,10 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = run_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"unpack-objects abnormal exit\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n \t} else {\n \t\tchar hostname[HOST_NAME_MAX + 1];\n \t\tchar *lockfile;\n@@ -2424,8 +2427,10 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = start_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"index-pack fork failed\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n+\t\t\treturn -1;\n+\t\t}\n \n \t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n \t\tif (lockfile) {\n@@ -2435,15 +2440,18 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tclose(child.out);\n \n \t\tstatus = finish_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"index-pack abnormal exit\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n \t\todb_reprepare(the_repository->objects);\n \t}\n-\treturn NULL;\n+\treturn 0;\n }\n \n-static const char *unpack_with_sideband(struct odb_transaction *transaction,\n-\t\t\t\t\tconst char *shallow_file)\n+static int unpack_with_sideband(struct odb_transaction *transaction,\n+\t\t\t\tconst char *shallow_file,\n+\t\t\t\tstruct strbuf *err_msg)\n {\n \tstruct unpack_opts opts = {\n \t\t.fsck_objects = (receive_fsck_objects >= 0\n@@ -2458,20 +2466,20 @@ static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \t\t.quiet = quiet,\n \t};\n \tstruct async muxer;\n-\tconst char *ret;\n+\tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, &opts);\n+\t\treturn unpack(transaction, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n \tmuxer.proc = copy_to_sideband;\n \tmuxer.in = -1;\n \tif (start_async(&muxer))\n-\t\treturn NULL;\n+\t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, &opts);\n+\tret = unpack(transaction, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2560,13 +2568,13 @@ static void update_shallow_info(struct command *commands,\n \tfree(ref_status);\n }\n \n-static void report(struct command *commands, const char *unpack_status)\n+static void report(struct command *commands, const struct strbuf *unpack_status)\n {\n \tstruct command *cmd;\n \tstruct strbuf buf = STRBUF_INIT;\n \n \tpacket_buf_write(&buf, \"unpack %s\\n\",\n-\t\t\t unpack_status ? unpack_status : \"ok\");\n+\t\t\t unpack_status->len ? unpack_status->buf : \"ok\");\n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (!cmd->error_string)\n \t\t\tpacket_buf_write(&buf, \"ok %s\\n\",\n@@ -2584,14 +2592,14 @@ static void report(struct command *commands, const char *unpack_status)\n \tstrbuf_release(&buf);\n }\n \n-static void report_v2(struct command *commands, const char *unpack_status)\n+static void report_v2(struct command *commands, const struct strbuf *unpack_status)\n {\n \tstruct command *cmd;\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct ref_push_report *report;\n \n \tpacket_buf_write(&buf, \"unpack %s\\n\",\n-\t\t\t unpack_status ? unpack_status : \"ok\");\n+\t\t\t unpack_status->len ? unpack_status->buf : \"ok\");\n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tint count = 0;\n \n@@ -2715,8 +2723,8 @@ int cmd_receive_pack(int argc,\n \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\n \n \tif ((commands = read_head_info(&reader, &shallow))) {\n-\t\tconst char *unpack_status = NULL;\n \t\tstruct string_list push_options = STRING_LIST_INIT_DUP;\n+\t\tstruct strbuf unpack_status = STRBUF_INIT;\n \n \t\tif (use_push_options)\n \t\t\tread_push_options(&reader, &push_options);\n@@ -2736,22 +2744,22 @@ int cmd_receive_pack(int argc,\n \t\t\t\talt_shallow_file = setup_temporary_shallow(si.shallow);\n \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\t\tstrbuf_addstr(&unpack_status, \"unable to start object transaction\");\n \t\t\telse\n-\t\t\t\tunpack_status = unpack_with_sideband(transaction, alt_shallow_file);\n+\t\t\t\tunpack_with_sideband(transaction, alt_shallow_file, &unpack_status);\n \n \t\t\tupdate_shallow_info(commands, &si, &ref, alt_shallow_file);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n-\t\texecute_commands(commands, unpack_status, &si, transaction,\n+\t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n \t\t\t\t &push_options);\n \t\todb_transaction_finalize(transaction);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\n-\t\t\treport_v2(commands, unpack_status);\n+\t\t\treport_v2(commands, &unpack_status);\n \t\telse if (report_status)\n-\t\t\treport(commands, unpack_status);\n+\t\t\treport(commands, &unpack_status);\n \t\tsigchain_pop(SIGPIPE);\n \t\trun_receive_hook(commands, \"post-receive\", 1, NULL,\n \t\t\t\t &push_options);\n@@ -2776,6 +2784,7 @@ int cmd_receive_pack(int argc,\n \t\tif (auto_update_server_info)\n \t\t\tupdate_server_info(the_repository, 0);\n \t\tclear_shallow_info(&si);\n+\t\tstrbuf_release(&unpack_status);\n \t}\n \tif (use_sideband)\n \t\tpacket_flush(1);\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550130","messageId":"20260809190106.1565882-7-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260809190106.1565882-1-jltobler@gmail.com","subject":"[PATCH v2 6/7] builtin/receive-pack: explicitly pass packfile fd","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-09T19:01:05Z","receivedAt":"2026-08-09T19:01:17Z","isPatch":true,"body":"When processing the incoming packfile in git-receive-pack(1), `unpack()`\nassumes it should always read it from stdin. In preparation for\n`unpack()` logic being moved behind a generic ODB transaction interface,\nupdate the function signature to take the an explicit fd provided by\ncallers to read the incoming packfile from instead. Call sites are\nupdated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 16 +++++++++-------\n 1 file changed, 9 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 896439d46d..76e8f4216c 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2292,9 +2292,9 @@ static void read_push_options(struct packet_reader *reader,\n \t}\n }\n \n-static const char *parse_pack_header(struct pack_header *hdr)\n+static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n {\n-\tswitch (read_pack_header(0, hdr)) {\n+\tswitch (read_pack_header(pack_fd, hdr)) {\n \tcase PH_ERROR_EOF:\n \t\treturn \"eof before pack header was fully read\";\n \n@@ -2354,8 +2354,8 @@ struct unpack_opts {\n \tint quiet;\n };\n \n-static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n-\t\t  const struct unpack_opts *opts)\n+static int unpack(struct odb_transaction *transaction, int pack_fd,\n+\t\t  struct strbuf *err_msg, const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2363,7 +2363,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tint err_fd = opts->err_fd;\n \n-\thdr_err = parse_pack_header(&hdr);\n+\thdr_err = parse_pack_header(&hdr, pack_fd);\n \tif (hdr_err) {\n \t\tif (err_fd > 0)\n \t\t\tclose(err_fd);\n@@ -2390,6 +2390,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n \t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.no_stdout = 1;\n+\t\tchild.in = pack_fd;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = run_command(&child);\n@@ -2424,6 +2425,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n \t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.out = -1;\n+\t\tchild.in = pack_fd;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = start_command(&child);\n@@ -2469,7 +2471,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, err_msg, &opts);\n+\t\treturn unpack(transaction, 0, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2479,7 +2481,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, err_msg, &opts);\n+\tret = unpack(transaction, 0, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550131","messageId":"20260809190106.1565882-8-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260809190106.1565882-1-jltobler@gmail.com","subject":"[PATCH v2 7/7] odb/transaction: add transaction interface to write packfiles","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-09T19:01:06Z","receivedAt":"2026-08-09T19:01:18Z","isPatch":true,"body":"In git-receive-pack(1), the incoming packfile is written to the ODB via\n`unpack()`, which spawns git-index-pack(1) or git-unpack-objects(1)\ndirectly. With pluggable object databases, an alternative backend may\nneed to handle writing packfile data differently though.\n\nIntroduce `odb_transaction_write_pack()` as a generic interface to\nhandle writing a packfile to a transaction and use the logic from\n`unpack()` as the \"files\" backend implementation. Note that a packfile\nwritten via git-index-pack(1) is kept in place by a \".keep\" lockfile\nthat must be retained until references are updated. To faciliate this in\nan ODB backend agnostic manner, the \"files\" transaction backend takes\nownership of these lockfiles and removes them post-commit through its\nrelease callback.\n\nCall sites in git-receive-pack(1) are updated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 168 +-------------------------------------\n object-file.c          | 180 +++++++++++++++++++++++++++++++++++++++++\n odb/transaction.c      |   7 ++\n odb/transaction.h      |  63 +++++++++++++++\n 4 files changed, 253 insertions(+), 165 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 76e8f4216c..e6e54ba55f 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -15,7 +15,6 @@\n #include \"gpg-interface.h\"\n #include \"hex.h\"\n #include \"hook.h\"\n-#include \"lockfile.h\"\n #include \"object.h\"\n #include \"object-file.h\"\n #include \"object-name.h\"\n@@ -23,7 +22,6 @@\n #include \"oid-array.h\"\n #include \"oidset.h\"\n #include \"pack.h\"\n-#include \"packfile.h\"\n #include \"parse-options.h\"\n #include \"pkt-line.h\"\n #include \"protocol.h\"\n@@ -2292,170 +2290,11 @@ static void read_push_options(struct packet_reader *reader,\n \t}\n }\n \n-static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n-{\n-\tswitch (read_pack_header(pack_fd, hdr)) {\n-\tcase PH_ERROR_EOF:\n-\t\treturn \"eof before pack header was fully read\";\n-\n-\tcase PH_ERROR_PACK_SIGNATURE:\n-\t\treturn \"protocol error (pack signature mismatch detected)\";\n-\n-\tcase PH_ERROR_PROTOCOL:\n-\t\treturn \"protocol error (pack version unsupported)\";\n-\n-\tdefault:\n-\t\treturn \"unknown error in parse_pack_header\";\n-\n-\tcase 0:\n-\t\treturn NULL;\n-\t}\n-}\n-\n-static struct tempfile *pack_lockfile;\n-\n-static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n-{\n-\tstrvec_pushf(args, \"--pack_header=%\"PRIu32\",%\"PRIu32,\n-\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n-}\n-\n-static int get_unpack_limit(struct repository *repo)\n-{\n-\tstatic int limit = -1;\n-\n-\tif (limit < 0) {\n-\t\tint receive_limit = -1;\n-\t\tint transfer_limit = -1;\n-\n-\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n-\t\t\t\t    &receive_limit);\n-\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n-\t\t\t\t    &transfer_limit);\n-\n-\t\tif (receive_limit >= 0)\n-\t\t\tlimit = receive_limit;\n-\t\telse if (transfer_limit >= 0)\n-\t\t\tlimit = transfer_limit;\n-\t\telse\n-\t\t\tlimit = 100;\n-\t}\n-\n-\treturn limit;\n-}\n-\n-struct unpack_opts {\n-\tconst char *fsck_msg_types;\n-\tconst char *shallow_file;\n-\toff_t max_input_size;\n-\tint fsck_objects;\n-\tint reject_thin;\n-\tint err_fd;\n-\tint quiet;\n-};\n-\n-static int unpack(struct odb_transaction *transaction, int pack_fd,\n-\t\t  struct strbuf *err_msg, const struct unpack_opts *opts)\n-{\n-\tstruct pack_header hdr;\n-\tconst char *hdr_err;\n-\tint status;\n-\tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint err_fd = opts->err_fd;\n-\n-\thdr_err = parse_pack_header(&hdr, pack_fd);\n-\tif (hdr_err) {\n-\t\tif (err_fd > 0)\n-\t\t\tclose(err_fd);\n-\t\tstrbuf_addstr(err_msg, hdr_err);\n-\t\treturn -1;\n-\t}\n-\n-\tif (opts->shallow_file) {\n-\t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, opts->shallow_file);\n-\t}\n-\n-\todb_transaction_env(transaction, &child.env);\n-\n-\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n-\t\tstrvec_push(&child.args, \"unpack-objects\");\n-\t\tpush_header_arg(&child.args, &hdr);\n-\t\tif (opts->quiet)\n-\t\t\tstrvec_push(&child.args, \"-q\");\n-\t\tif (opts->fsck_objects)\n-\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     opts->fsck_msg_types);\n-\t\tif (opts->max_input_size)\n-\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)opts->max_input_size);\n-\t\tchild.no_stdout = 1;\n-\t\tchild.in = pack_fd;\n-\t\tchild.err = err_fd;\n-\t\tchild.git_cmd = 1;\n-\t\tstatus = run_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n-\t\t\treturn -1;\n-\t\t}\n-\t} else {\n-\t\tchar hostname[HOST_NAME_MAX + 1];\n-\t\tchar *lockfile;\n-\n-\t\tstrvec_pushl(&child.args, \"index-pack\", \"--stdin\", NULL);\n-\t\tpush_header_arg(&child.args, &hdr);\n-\n-\t\tif (xgethostname(hostname, sizeof(hostname)))\n-\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n-\t\tstrvec_pushf(&child.args,\n-\t\t\t     \"--keep=receive-pack %\"PRIuMAX\" on %s\",\n-\t\t\t     (uintmax_t)getpid(),\n-\t\t\t     hostname);\n-\n-\t\tif (!opts->quiet && err_fd)\n-\t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n-\t\tif (err_fd)\n-\t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n-\t\tif (opts->fsck_objects)\n-\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     opts->fsck_msg_types);\n-\t\tif (!opts->reject_thin)\n-\t\t\tstrvec_push(&child.args, \"--fix-thin\");\n-\t\tif (opts->max_input_size)\n-\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)opts->max_input_size);\n-\t\tchild.out = -1;\n-\t\tchild.in = pack_fd;\n-\t\tchild.err = err_fd;\n-\t\tchild.git_cmd = 1;\n-\t\tstatus = start_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n-\t\tif (lockfile) {\n-\t\t\tpack_lockfile = register_tempfile(lockfile);\n-\t\t\tfree(lockfile);\n-\t\t}\n-\t\tclose(child.out);\n-\n-\t\tstatus = finish_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n-\t\t\treturn -1;\n-\t\t}\n-\t\todb_reprepare(the_repository->objects);\n-\t}\n-\treturn 0;\n-}\n-\n static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\t\t\tconst char *shallow_file,\n \t\t\t\tstruct strbuf *err_msg)\n {\n-\tstruct unpack_opts opts = {\n+\tstruct odb_transaction_write_pack_opts opts = {\n \t\t.fsck_objects = (receive_fsck_objects >= 0\n \t\t\t\t ? receive_fsck_objects\n \t\t\t\t : transfer_fsck_objects >= 0\n@@ -2471,7 +2310,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, 0, err_msg, &opts);\n+\t\treturn odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2481,7 +2320,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, 0, err_msg, &opts);\n+\tret = odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2756,7 +2595,6 @@ int cmd_receive_pack(int argc,\n \t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n \t\t\t\t &push_options);\n \t\todb_transaction_finalize(transaction);\n-\t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\n \t\t\treport_v2(commands, &unpack_status);\ndiff --git a/object-file.c b/object-file.c\nindex f993d58056..424f5f148c 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -10,6 +10,7 @@\n #define USE_THE_REPOSITORY_VARIABLE\n \n #include \"git-compat-util.h\"\n+#include \"config.h\"\n #include \"convert.h\"\n #include \"dir.h\"\n #include \"environment.h\"\n@@ -26,6 +27,7 @@\n #include \"packfile.h\"\n #include \"path.h\"\n #include \"read-cache-ll.h\"\n+#include \"run-command.h\"\n #include \"setup.h\"\n #include \"strvec.h\"\n #include \"tempfile.h\"\n@@ -487,6 +489,10 @@ struct odb_transaction_files {\n \tstruct tmp_objdir *objdir;\n \tstruct transaction_packfile packfile;\n \tconst char *prefix;\n+\n+\tstruct tempfile **pack_lockfiles;\n+\tsize_t pack_lockfiles_nr;\n+\tsize_t pack_lockfiles_alloc;\n };\n \n int odb_transaction_files_prepare(struct odb_transaction *base)\n@@ -1292,6 +1298,178 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n \treturn 0;\n }\n \n+static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n+{\n+\tswitch (read_pack_header(pack_fd, hdr)) {\n+\tcase PH_ERROR_EOF:\n+\t\treturn \"eof before pack header was fully read\";\n+\n+\tcase PH_ERROR_PACK_SIGNATURE:\n+\t\treturn \"protocol error (pack signature mismatch detected)\";\n+\n+\tcase PH_ERROR_PROTOCOL:\n+\t\treturn \"protocol error (pack version unsupported)\";\n+\n+\tdefault:\n+\t\treturn \"unknown error in parse_pack_header\";\n+\n+\tcase 0:\n+\t\treturn NULL;\n+\t}\n+}\n+\n+static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n+{\n+\tstrvec_pushf(args, \"--pack_header=%\"PRIu32\",%\"PRIu32,\n+\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n+}\n+\n+static int get_unpack_limit(struct repository *repo)\n+{\n+\tstatic int limit = -1;\n+\n+\tif (limit < 0) {\n+\t\tint receive_limit = -1;\n+\t\tint transfer_limit = -1;\n+\n+\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n+\t\t\t\t    &receive_limit);\n+\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n+\t\t\t\t    &transfer_limit);\n+\n+\t\tif (receive_limit >= 0)\n+\t\t\tlimit = receive_limit;\n+\t\telse if (transfer_limit >= 0)\n+\t\t\tlimit = transfer_limit;\n+\t\telse\n+\t\t\tlimit = 100;\n+\t}\n+\n+\treturn limit;\n+}\n+\n+static int odb_transaction_files_write_pack(struct odb_transaction *base,\n+\t\t\t\t\t    int pack_fd, struct strbuf *err_msg,\n+\t\t\t\t\t    const struct odb_transaction_write_pack_opts *opts)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\tstruct repository *repo = base->source->odb->repo;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tstruct pack_header hdr;\n+\tconst char *hdr_err;\n+\tint err_fd = opts->err_fd;\n+\tint status;\n+\n+\thdr_err = parse_pack_header(&hdr, pack_fd);\n+\tif (hdr_err) {\n+\t\tif (err_fd > 0)\n+\t\t\tclose(err_fd);\n+\t\tstrbuf_addstr(err_msg, hdr_err);\n+\t\treturn -1;\n+\t}\n+\n+\tif (opts->shallow_file) {\n+\t\tstrvec_push(&child.args, \"--shallow-file\");\n+\t\tstrvec_push(&child.args, opts->shallow_file);\n+\t}\n+\n+\todb_transaction_env(base, &child.env);\n+\n+\tif (ntohl(hdr.hdr_entries) < (unsigned int)get_unpack_limit(repo)) {\n+\t\tstrvec_push(&child.args, \"unpack-objects\");\n+\t\tpush_header_arg(&child.args, &hdr);\n+\t\tif (opts->quiet)\n+\t\t\tstrvec_push(&child.args, \"-q\");\n+\t\tif (opts->fsck_objects)\n+\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (opts->max_input_size)\n+\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n+\t\tchild.no_stdout = 1;\n+\t\tchild.in = pack_fd;\n+\t\tchild.err = err_fd;\n+\t\tchild.git_cmd = 1;\n+\t\tstatus = run_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n+\t} else {\n+\t\tchar hostname[HOST_NAME_MAX + 1];\n+\t\tchar *lockfile;\n+\n+\t\tstrvec_pushl(&child.args, \"index-pack\", \"--stdin\", NULL);\n+\t\tpush_header_arg(&child.args, &hdr);\n+\n+\t\tif (xgethostname(hostname, sizeof(hostname)))\n+\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n+\t\tstrvec_pushf(&child.args,\n+\t\t\t     \"--keep=receive-pack %\"PRIuMAX\" on %s\",\n+\t\t\t     (uintmax_t)getpid(),\n+\t\t\t     hostname);\n+\n+\t\tif (!opts->quiet && err_fd)\n+\t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n+\t\tif (err_fd)\n+\t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n+\t\tif (opts->fsck_objects)\n+\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (!opts->reject_thin)\n+\t\t\tstrvec_push(&child.args, \"--fix-thin\");\n+\t\tif (opts->max_input_size)\n+\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n+\t\tchild.out = -1;\n+\t\tchild.in = pack_fd;\n+\t\tchild.err = err_fd;\n+\t\tchild.git_cmd = 1;\n+\t\tstatus = start_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tlockfile = index_pack_lockfile(repo, child.out, NULL);\n+\t\tif (lockfile) {\n+\t\t\tALLOC_GROW(transaction->pack_lockfiles,\n+\t\t\t\t   transaction->pack_lockfiles_nr + 1,\n+\t\t\t\t   transaction->pack_lockfiles_alloc);\n+\t\t\ttransaction->pack_lockfiles[transaction->pack_lockfiles_nr++] =\n+\t\t\t\tregister_tempfile(lockfile);\n+\t\t\tfree(lockfile);\n+\t\t}\n+\t\tclose(child.out);\n+\n+\t\tstatus = finish_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\todb_source_prepare(repo->objects->sources,\n+\t\t\t\t   ODB_PREPARE_FLUSH_CACHES);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int odb_transaction_files_finalize(struct odb_transaction *base)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\tint ret = 0;\n+\n+\tfor (size_t i = 0; i < transaction->pack_lockfiles_nr; i++)\n+\t\tret |= delete_tempfile(&transaction->pack_lockfiles[i]);\n+\n+\tfree(transaction->pack_lockfiles);\n+\n+\treturn ret;\n+}\n+\n static int odb_transaction_files_env(struct odb_transaction *base,\n \t\t\t\t     struct strvec *env)\n {\n@@ -1315,7 +1493,9 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction = xcalloc(1, sizeof(*transaction));\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n+\ttransaction->base.finalize = odb_transaction_files_finalize;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\ttransaction->base.write_pack = odb_transaction_files_write_pack;\n \ttransaction->base.env = odb_transaction_files_env;\n \n \ttransaction->prefix = \"bulk-fsync\";\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex 9e9a982778..c9144e6cd6 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -59,6 +59,13 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n \treturn transaction->write_object_stream(transaction, stream, len, oid);\n }\n \n+int odb_transaction_write_pack(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t       struct strbuf *err_msg,\n+\t\t\t       const struct odb_transaction_write_pack_opts *opts)\n+{\n+\treturn transaction->write_pack(transaction, pack_fd, err_msg, opts);\n+}\n+\n int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env)\n {\n \tif (!transaction)\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 89f6902caf..e77807c593 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -4,6 +4,51 @@\n #include \"gettext.h\"\n #include \"odb.h\"\n \n+/*\n+ * Options controlling how odb_transaction_write_pack() ingests a packfile.\n+ */\n+struct odb_transaction_write_pack_opts {\n+\t/*\n+\t * Optional fsck severity configuration to apply when incoming objects\n+\t * are verified.\n+\t */\n+\tconst char *fsck_msg_types;\n+\n+\t/*\n+\t * Path to an alternative shallow file describing the shallow boundaries\n+\t * to honor while ingesting the pack.\n+\t */\n+\tconst char *shallow_file;\n+\n+\t/*\n+\t * The max size in bytes of the incoming packfile allowed. No limit is\n+\t * enforced when set to 0.\n+\t */\n+\n+\toff_t max_input_size;\n+\n+\t/*\n+\t * Whether the validity of incoming objects should be verified.\n+\t */\n+\tint fsck_objects;\n+\n+\t/*\n+\t * Whether to reject an incoming packfile if it is \"thin\".\n+\t */\n+\tint reject_thin;\n+\n+\t/*\n+\t * Optional file descriptor for reporting progress and errors. Set to 0\n+\t * for none.\n+\t */\n+\tint err_fd;\n+\n+\t/*\n+\t * Suppresses progress reporting.\n+\t */\n+\tint quiet;\n+};\n+\n /*\n  * A transaction may be started for an object database prior to writing new\n  * objects via odb_transaction_begin(). These objects are not committed until\n@@ -40,6 +85,15 @@ 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+\t/*\n+\t * This callback is expected to ingest the packfile readable via\n+\t * `pack_fd` into the transaction. Returns 0 on success, a negative\n+\t * error code otherwise. On failure, a human-readable description is\n+\t * appended to `err_msg`.\n+\t */\n+\tint (*write_pack)(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t  struct strbuf *err_msg,\n+\t\t\t  const struct odb_transaction_write_pack_opts *opts);\n \n \t/*\n \t * This callback is expected to populate the provided strvec with the\n@@ -99,6 +153,15 @@ 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+ * Ingests the packfile readable via `pack_fd` into the transaction. Returns 0\n+ * on success, a negative error code otherwise. On failure, a human-readable\n+ * description is appended to `err_msg`.\n+ */\n+int odb_transaction_write_pack(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t       struct strbuf *err_msg,\n+\t\t\t       const struct odb_transaction_write_pack_opts *opts);\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-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550146","messageId":"xmqqa4qu91vi.fsf@gitster.g","threadId":"66133","inReplyTo":"20260809190106.1565882-8-jltobler@gmail.com","subject":"Re: [PATCH v2 7/7] odb/transaction: add transaction interface to write packfiles","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-10T01:54:09Z","receivedAt":"2026-08-10T01:54:12Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> In git-receive-pack(1), the incoming packfile is written to the ODB via\n> `unpack()`, which spawns git-index-pack(1) or git-unpack-objects(1)\n> directly. With pluggable object databases, an alternative backend may\n> need to handle writing packfile data differently though.\n>\n> Introduce `odb_transaction_write_pack()` as a generic interface to\n> handle writing a packfile to a transaction and use the logic from\n> `unpack()` as the \"files\" backend implementation. Note that a packfile\n> written via git-index-pack(1) is kept in place by a \".keep\" lockfile\n> that must be retained until references are updated. To faciliate this in\n> an ODB backend agnostic manner, the \"files\" transaction backend takes\n> ownership of these lockfiles and removes them post-commit through its\n> release callback.\n> ...\n> +static int odb_transaction_files_write_pack(struct odb_transaction *base,\n> +\t\t\t\t\t    int pack_fd, struct strbuf *err_msg,\n> +\t\t\t\t\t    const struct odb_transaction_write_pack_opts *opts)\n> +{\n> + ...\n> +\t\todb_source_prepare(repo->objects->sources,\n> +\t\t\t\t   ODB_PREPARE_FLUSH_CACHES);\n> +\t}\n\nThis assumes that we are working with the first entry in the\n'repo->objects->sources' linked list.  Should we not use the source\nactually associated with the current transaction (I am guessing that\nit is 'base->source' but I may be wrong)?\n"},{"id":"550147","messageId":"xmqq33wm8x20.fsf@gitster.g","threadId":"66133","inReplyTo":"20260809190106.1565882-2-jltobler@gmail.com","subject":"Re: [PATCH v2 1/7] odb/transaction: add transaction finalize interface","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-10T03:38:15Z","receivedAt":"2026-08-10T03:38:18Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> diff --git a/builtin/add.c b/builtin/add.c\n> index 60ffbede2b..501e114ed5 100644\n> --- a/builtin/add.c\n> +++ b/builtin/add.c\n> @@ -393,7 +393,7 @@ int cmd_add(int argc,\n>  \tchar *seen = NULL;\n>  \tchar *ps_matched = NULL;\n>  \tstruct lock_file lock_file = LOCK_INIT;\n> -\tstruct odb_transaction *transaction;\n> +\tstruct odb_transaction *transaction = NULL;\n>  \n>  \trepo_config(repo, add_config, NULL);\n>  \n> @@ -610,5 +610,6 @@ int cmd_add(int argc,\n>  \tfree(ps_matched);\n>  \tdir_clear(&dir);\n>  \tclear_pathspec(&pathspec);\n> +\todb_transaction_finalize(transaction);\n>  \treturn exit_status;\n>  }\n\nThere is only one non-local exit between transation-begin and\ntransaction-finalize, which is a call ot report_path_error()\nfollowed by exit(128).  Will _finalize() stay to be just freeing\nmemory and nothing else?  It may be conceptually cleaner to jump to\nthe bottom to make sure the clean-up sequence will always happen.\n\nThe same comment applies to other codepaths to which this patch adds\n_finalize() calls.\n\n> diff --git a/odb/transaction.c b/odb/transaction.c\n> index dab7da6a9a..9e9a982778 100644\n> --- a/odb/transaction.c\n> +++ b/odb/transaction.c\n> @@ -33,6 +33,20 @@ int odb_transaction_commit(struct odb_transaction *transaction)\n>  \n>  \tret = transaction->commit(transaction);\n>  \ttransaction->source->odb->transaction = NULL;\n> +\n> +\treturn ret;\n> +}\n> +\n> +int odb_transaction_finalize(struct odb_transaction *transaction)\n> +{\n\nCuriously no callers added by this patch checks the return value\nof this function.  Intended or just sloppy?  If the former, perhaps\nthis wants to return void instead?\n\nThe same can be said for _commit(), by the way.\n"},{"id":"550148","messageId":"xmqqwlty7hdz.fsf@gitster.g","threadId":"66133","inReplyTo":"20260809190106.1565882-8-jltobler@gmail.com","subject":"Re: [PATCH v2 7/7] odb/transaction: add transaction interface to write packfiles","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-10T04:02:00Z","receivedAt":"2026-08-10T04:02:03Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> .... Note that a packfile\n> written via git-index-pack(1) is kept in place by a \".keep\" lockfile\n> that must be retained until references are updated. To faciliate this in\n> an ODB backend agnostic manner, the \"files\" transaction backend takes\n> ownership of these lockfiles and removes them post-commit through its\n> release callback.\n\nThe above is confusing and I am lost.  Care to explain a bit more?\n\n> +\t\tstatus = start_command(&child);\n> +\t\tif (status) {\n> +\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n> +\t\t\treturn -1;\n> +\t\t}\n> +\n> +\t\tlockfile = index_pack_lockfile(repo, child.out, NULL);\n> +\t\tif (lockfile) {\n> +\t\t\tALLOC_GROW(transaction->pack_lockfiles,\n> +\t\t\t\t   transaction->pack_lockfiles_nr + 1,\n> +\t\t\t\t   transaction->pack_lockfiles_alloc);\n> +\t\t\ttransaction->pack_lockfiles[transaction->pack_lockfiles_nr++] =\n> +\t\t\t\tregister_tempfile(lockfile);\n> +\t\t\tfree(lockfile);\n> +\t\t}\n\nHere we add the .keep file to the list of lockfiles.  We have\nfinalization step laer in odb_transaction_files_finalize() that\ndeletes the tempfiles when we are done, which comes after\nthe transaction is committed.\n\nBut isn't the odb_transaction_files_commit() where the migration of\ntmp_objdir_migrate() happens?  Everything in the quarantine directory\nincluding these .keep files are \"migrated\" (either link-to-the-new\nfollowed by unlink-of-the-old, or rename-old-to-new) there.\n\nAnd then ...\n\n> +static int odb_transaction_files_finalize(struct odb_transaction *base)\n> +{\n> +\tstruct odb_transaction_files *transaction =\n> +\t\tcontainer_of(base, struct odb_transaction_files, base);\n> +\tint ret = 0;\n> +\n> +\tfor (size_t i = 0; i < transaction->pack_lockfiles_nr; i++)\n> +\t\tret |= delete_tempfile(&transaction->pack_lockfiles[i]);\n> +\n> +\tfree(transaction->pack_lockfiles);\n> +\n> +\treturn ret;\n> +}\n\n... we do the deletion of tempfile but has anybody migrated the path\nto these files recorded in the lockfile structure?  How are we\nremoving the .keep files that were \"migrated\" when the transaction\nwas committed?\n\nPuzzled and confused...\n\n"},{"id":"550150","messageId":"anlefHYq57Rz5oW9@pks.im","threadId":"66133","inReplyTo":"ani4GoefzYFWjTMl@denethor","subject":"Re: [PATCH 4/6] builtin/receive-pack: report unpack errors via strbuf","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-10T05:15:40Z","receivedAt":"2026-08-10T05:15:48Z","isPatch":true,"body":"On Sun, Aug 09, 2026 at 02:00:52PM -0500, Justin Tobler wrote:\n> On 26/08/07 10:36AM, Justin Tobler wrote:\n> > On 26/08/07 09:03AM, Patrick Steinhardt wrote:\n> > > > @@ -2711,8 +2719,8 @@ int cmd_receive_pack(int argc,\n> > > >  \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\n> > > >  \n> > > >  \tif ((commands = read_head_info(&reader, &shallow))) {\n> > > > -\t\tconst char *unpack_status = NULL;\n> > > >  \t\tstruct string_list push_options = STRING_LIST_INIT_DUP;\n> > > > +\t\tstruct strbuf unpack_status = STRBUF_INIT;\n> > > \n> > > Can't we reuse this buffer and reset it on every run to save some memory\n> > > allocations?\n> \n> Looking at this more closely, there isn't actually any loop we are\n> running this in so I don't think there is any need to change how\n> `unpack_status` is set up here.\n\nOh, right. I think I saw the loop in `read_head_info()` itself. My\nmistake.\n\nPatrick\n"},{"id":"550151","messageId":"anlegs6zfUysbx0C@pks.im","threadId":"66133","inReplyTo":"20260809190106.1565882-4-jltobler@gmail.com","subject":"Re: [PATCH v2 3/7] builtin/receive-pack: read unpack limit config lazily","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-10T05:15:46Z","receivedAt":"2026-08-10T05:15:51Z","isPatch":true,"body":"On Sun, Aug 09, 2026 at 02:01:02PM -0500, Justin Tobler wrote:\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index 78d2911c00..5264d70467 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -2333,6 +2320,30 @@ 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 int get_unpack_limit(struct repository *repo)\n\nShouldn't the function return `unsigned int`? We always expect it to be\na positiv value, and in the final commit we have to add a cast because\nof that.\n\n> +{\n> +\tstatic int limit = -1;\n\nIs it really necessary to have this be a static variable? As far as I\ncan see we'd only call `unpack()` once. Also, the cache would become\nstale if we ever tried to read the limit for multiple different repos.\n\n> +\tif (limit < 0) {\n> +\t\tint receive_limit = -1;\n> +\t\tint transfer_limit = -1;\n> +\n> +\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n> +\t\t\t\t    &receive_limit);\n> +\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n> +\t\t\t\t    &transfer_limit);\n> +\n> +\t\tif (receive_limit >= 0)\n> +\t\t\tlimit = receive_limit;\n> +\t\telse if (transfer_limit >= 0)\n> +\t\t\tlimit = transfer_limit;\n> +\t\telse\n> +\t\t\tlimit = 100;\n> +\t}\n> +\n> +\treturn limit;\n> +}\n\nSo how about something like this instead?\n\n\tstatic unsigned int get_unpack_limit(struct repository *repo)\n\t{\n\t\tunsigned int limit = 100;\n\t\tif (!repo_config_get_uint(repo, \"receive.unpacklimit\", &receive_limit) ||\n\t\t    !repo_config_get_uint(repo, \"receive.unpacklimit\", &receive_limit))\n\t\t\t/* do nothing */;\n\t\treturn limit;\n\t}\n\nPatrick\n"},{"id":"550198","messageId":"annwLmJ08J7q9GoW@denethor","threadId":"66133","inReplyTo":"anlegs6zfUysbx0C@pks.im","subject":"Re: [PATCH v2 3/7] builtin/receive-pack: read unpack limit config lazily","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-10T15:42:41Z","receivedAt":"2026-08-10T15:42:46Z","isPatch":true,"body":"On 26/08/10 07:15AM, Patrick Steinhardt wrote:\n> On Sun, Aug 09, 2026 at 02:01:02PM -0500, Justin Tobler wrote:\n> > diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> > index 78d2911c00..5264d70467 100644\n> > --- a/builtin/receive-pack.c\n> > +++ b/builtin/receive-pack.c\n> > @@ -2333,6 +2320,30 @@ 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 int get_unpack_limit(struct repository *repo)\n> \n> Shouldn't the function return `unsigned int`? We always expect it to be\n> a positiv value, and in the final commit we have to add a cast because\n> of that.\n\nWill update.\n\n> > +{\n> > +\tstatic int limit = -1;\n> \n> Is it really necessary to have this be a static variable? As far as I\n> can see we'd only call `unpack()` once. Also, the cache would become\n> stale if we ever tried to read the limit for multiple different repos.\n\nGood point and yes in practice we really will only be reading this once\nso there isn't really point to caching it. Will update.\n\n> > +\tif (limit < 0) {\n> > +\t\tint receive_limit = -1;\n> > +\t\tint transfer_limit = -1;\n> > +\n> > +\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n> > +\t\t\t\t    &receive_limit);\n> > +\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n> > +\t\t\t\t    &transfer_limit);\n> > +\n> > +\t\tif (receive_limit >= 0)\n> > +\t\t\tlimit = receive_limit;\n> > +\t\telse if (transfer_limit >= 0)\n> > +\t\t\tlimit = transfer_limit;\n> > +\t\telse\n> > +\t\t\tlimit = 100;\n> > +\t}\n> > +\n> > +\treturn limit;\n> > +}\n> \n> So how about something like this instead?\n> \n> \tstatic unsigned int get_unpack_limit(struct repository *repo)\n> \t{\n> \t\tunsigned int limit = 100;\n> \t\tif (!repo_config_get_uint(repo, \"receive.unpacklimit\", &receive_limit) ||\n> \t\t    !repo_config_get_uint(repo, \"receive.unpacklimit\", &receive_limit))\n> \t\t\t/* do nothing */;\n> \t\treturn limit;\n> \t}\n\nGood suggestion. Because `repo_config_get_uint()` leaves the value\nuntouched if no config is set, we could probably simplify even further\nto something like this:\n\n  static unsigned int get_unpack_limit(struct repository *repo)\n  {\n  \tunsigned int limit = 100;\n  \n  \trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n  \trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n  \n  \treturn limit;\n  }\n\nThanks,\n-Justin\n"},{"id":"550212","messageId":"xmqqzeyt6ets.fsf@gitster.g","threadId":"66133","inReplyTo":"20260809190106.1565882-4-jltobler@gmail.com","subject":"Re: [PATCH v2 3/7] builtin/receive-pack: read unpack limit config lazily","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-10T17:54:55Z","receivedAt":"2026-08-10T17:54:57Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> +static int get_unpack_limit(struct repository *repo)\n> +{\n> +\tstatic int limit = -1;\n> +\n> +\tif (limit < 0) {\n> +\t\tint receive_limit = -1;\n> +\t\tint transfer_limit = -1;\n> +\n> +\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n> +\t\t\t\t    &receive_limit);\n> +\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n> +\t\t\t\t    &transfer_limit);\n> +\n> +\t\tif (receive_limit >= 0)\n> +\t\t\tlimit = receive_limit;\n> +\t\telse if (transfer_limit >= 0)\n> +\t\t\tlimit = transfer_limit;\n> +\t\telse\n> +\t\t\tlimit = 100;\n> +\t}\n> +\n> +\treturn limit;\n> +}\n\nI am not sure whether this is progress.\n\nA function that defines a 'static int' internally and sets it only\nonce is akin to using a global variable.  I wonder whether it would\nbe too much work to add a new member to either 'repo->settings' or\n'repo->config_values' to make the setting truly per-repository.\n"},{"id":"550215","messageId":"ann3abcyJD0KwuHx@denethor","threadId":"66133","inReplyTo":"xmqq33wm8x20.fsf@gitster.g","subject":"Re: [PATCH v2 1/7] odb/transaction: add transaction finalize interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-10T19:10:13Z","receivedAt":"2026-08-10T19:10:19Z","isPatch":true,"body":"On 26/08/09 08:38PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > diff --git a/builtin/add.c b/builtin/add.c\n> > index 60ffbede2b..501e114ed5 100644\n> > --- a/builtin/add.c\n> > +++ b/builtin/add.c\n> > @@ -393,7 +393,7 @@ int cmd_add(int argc,\n> >  \tchar *seen = NULL;\n> >  \tchar *ps_matched = NULL;\n> >  \tstruct lock_file lock_file = LOCK_INIT;\n> > -\tstruct odb_transaction *transaction;\n> > +\tstruct odb_transaction *transaction = NULL;\n> >  \n> >  \trepo_config(repo, add_config, NULL);\n> >  \n> > @@ -610,5 +610,6 @@ int cmd_add(int argc,\n> >  \tfree(ps_matched);\n> >  \tdir_clear(&dir);\n> >  \tclear_pathspec(&pathspec);\n> > +\todb_transaction_finalize(transaction);\n> >  \treturn exit_status;\n> >  }\n> \n> There is only one non-local exit between transation-begin and\n> transaction-finalize, which is a call ot report_path_error()\n> followed by exit(128).  Will _finalize() stay to be just freeing\n> memory and nothing else?  It may be conceptually cleaner to jump to\n> the bottom to make sure the clean-up sequence will always happen.\n\nIn practice, only call sites that invoke `odb_transaction_write_pack()`\n(only git-receive-pack(1) for now) actually need to be concerned about\nany deferred clean up outside of just freeing some memory. Conceptually\nthis is a bit messy though and callers shouldn't ideally have to be\naware of such specifics.\n\nIt may make sense to align the clean-up as you suggested above. I will\nexplore in the next version.\n\n> The same comment applies to other codepaths to which this patch adds\n> _finalize() calls.\n> \n> > diff --git a/odb/transaction.c b/odb/transaction.c\n> > index dab7da6a9a..9e9a982778 100644\n> > --- a/odb/transaction.c\n> > +++ b/odb/transaction.c\n> > @@ -33,6 +33,20 @@ int odb_transaction_commit(struct odb_transaction *transaction)\n> >  \n> >  \tret = transaction->commit(transaction);\n> >  \ttransaction->source->odb->transaction = NULL;\n> > +\n> > +\treturn ret;\n> > +}\n> > +\n> > +int odb_transaction_finalize(struct odb_transaction *transaction)\n> > +{\n> \n> Curiously no callers added by this patch checks the return value\n> of this function.  Intended or just sloppy?  If the former, perhaps\n> this wants to return void instead?\n\nIn version 1 I did keep `odb_transaction_finalize()` void, but decided\nto at least provide the option for callers to check for errors if they\nwished. The existing callers don't, but there isn't a reason most of the\ncouldn't be more strict here. In the next version, similar to\n`odb_transaction_begin_or_die()`, I may add an\n`odb_transaction_finalize_or_die()` helper and adapt some of the\nexisting callers.\n\n> The same can be said for _commit(), by the way.\n\nThere is one `odb_transaction_commit()` caller in\n\"builtin/receive-pack.c\" that does check for errors, but ya all other\ncallers simply ignore them. For the same reasons mentioned above, I\nopted to follow the existing behavior of ignoring temporary directory\nrelated errors, but include error reporting as part of the interface in\ncase callers wanted to check. I could also add an\n`odb_transaction_commit_or_die()` helper here too and adapt callers\nwhere it is reasonable to be more strict.\n\n-Justin\n"},{"id":"550217","messageId":"anoi42_kmpc13Axd@denethor","threadId":"66133","inReplyTo":"xmqqzeyt6ets.fsf@gitster.g","subject":"Re: [PATCH v2 3/7] builtin/receive-pack: read unpack limit config lazily","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-10T19:16:29Z","receivedAt":"2026-08-10T19:16:31Z","isPatch":true,"body":"On 26/08/10 10:54AM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > +static int get_unpack_limit(struct repository *repo)\n> > +{\n> > +\tstatic int limit = -1;\n> > +\n> > +\tif (limit < 0) {\n> > +\t\tint receive_limit = -1;\n> > +\t\tint transfer_limit = -1;\n> > +\n> > +\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n> > +\t\t\t\t    &receive_limit);\n> > +\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n> > +\t\t\t\t    &transfer_limit);\n> > +\n> > +\t\tif (receive_limit >= 0)\n> > +\t\t\tlimit = receive_limit;\n> > +\t\telse if (transfer_limit >= 0)\n> > +\t\t\tlimit = transfer_limit;\n> > +\t\telse\n> > +\t\t\tlimit = 100;\n> > +\t}\n> > +\n> > +\treturn limit;\n> > +}\n> \n> I am not sure whether this is progress.\n> \n> A function that defines a 'static int' internally and sets it only\n> once is akin to using a global variable.  I wonder whether it would\n> be too much work to add a new member to either 'repo->settings' or\n> 'repo->config_values' to make the setting truly per-repository.\n\nYa, as Patrick mentioned in [1], making it static probably isn't even\nreally required because in practice we just check the unpack limit once.\nFor now, it may just be sufficient to fetch the unpack limit value on\ndemand.\n\n-Justin\n\n[1]: <anlegs6zfUysbx0C@pks.im>\n"},{"id":"550219","messageId":"anojlSEahYWzFOBV@denethor","threadId":"66133","inReplyTo":"xmqqa4qu91vi.fsf@gitster.g","subject":"Re: [PATCH v2 7/7] odb/transaction: add transaction interface to write packfiles","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-10T19:29:33Z","receivedAt":"2026-08-10T19:29:37Z","isPatch":true,"body":"On 26/08/09 06:54PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > In git-receive-pack(1), the incoming packfile is written to the ODB via\n> > `unpack()`, which spawns git-index-pack(1) or git-unpack-objects(1)\n> > directly. With pluggable object databases, an alternative backend may\n> > need to handle writing packfile data differently though.\n> >\n> > Introduce `odb_transaction_write_pack()` as a generic interface to\n> > handle writing a packfile to a transaction and use the logic from\n> > `unpack()` as the \"files\" backend implementation. Note that a packfile\n> > written via git-index-pack(1) is kept in place by a \".keep\" lockfile\n> > that must be retained until references are updated. To faciliate this in\n> > an ODB backend agnostic manner, the \"files\" transaction backend takes\n> > ownership of these lockfiles and removes them post-commit through its\n> > release callback.\n> > ...\n> > +static int odb_transaction_files_write_pack(struct odb_transaction *base,\n> > +\t\t\t\t\t    int pack_fd, struct strbuf *err_msg,\n> > +\t\t\t\t\t    const struct odb_transaction_write_pack_opts *opts)\n> > +{\n> > + ...\n> > +\t\todb_source_prepare(repo->objects->sources,\n> > +\t\t\t\t   ODB_PREPARE_FLUSH_CACHES);\n> > +\t}\n> \n> This assumes that we are working with the first entry in the\n> 'repo->objects->sources' linked list.  Should we not use the source\n> actually associated with the current transaction (I am guessing that\n> it is 'base->source' but I may be wrong)?\n\nCurrently tracking of the transaction's ODB source relies on reading the\nfirst source in this list which is rather awkward in my opinion and\nrather fragile. The ODB source specified by `base->source` here is\nactually the main ODB source that the transaction is created against so\nit can't be used here.\n\nIn a future series, my plan is to stop reordering the source list when\nstarting a transaction and instead track the source separately which\nshould simplify some of this. It may be a good idea for the ODB\ntransaction itself to track its own source regardless though and may be\nsomething nice to add as a part of this series. I'll explore this in my\nnext version of the series.\n\nThanks,\n-Justin\n"},{"id":"550221","messageId":"anomr5jpSGlrTX2m@denethor","threadId":"66133","inReplyTo":"xmqqwlty7hdz.fsf@gitster.g","subject":"Re: [PATCH v2 7/7] odb/transaction: add transaction interface to write packfiles","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-10T19:54:22Z","receivedAt":"2026-08-10T19:54:27Z","isPatch":true,"body":"On 26/08/09 09:02PM, Junio C Hamano wrote:\n> Justin Tobler <jltobler@gmail.com> writes:\n> \n> > .... Note that a packfile\n> > written via git-index-pack(1) is kept in place by a \".keep\" lockfile\n> > that must be retained until references are updated. To faciliate this in\n> > an ODB backend agnostic manner, the \"files\" transaction backend takes\n> > ownership of these lockfiles and removes them post-commit through its\n> > release callback.\n> \n> The above is confusing and I am lost.  Care to explain a bit more?\n\nIt should say \"finalize callback\" instead of \"release callback\". The\nidea here though is that \".keep\" files are likely an implementation\ndetail of the \"files\" backend, but we still need a way to clean them up\nafter a transaction is committed and reference updates have been\nperformed via the generic ODB transaction interface.\n\nSo after `odb_transaction_commit()`, the \".keep\" files are tracked by\nthe \"files\" transaction and only removed once\n`odb_transaction_finalize()` is invoked. It would be up to callers to\nensure that reference updates are performed as required prior to\nfinalize being invoked.\n\nI'll try to explain this better in the next version.\n\n> > +\t\tstatus = start_command(&child);\n> > +\t\tif (status) {\n> > +\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n> > +\t\t\treturn -1;\n> > +\t\t}\n> > +\n> > +\t\tlockfile = index_pack_lockfile(repo, child.out, NULL);\n> > +\t\tif (lockfile) {\n> > +\t\t\tALLOC_GROW(transaction->pack_lockfiles,\n> > +\t\t\t\t   transaction->pack_lockfiles_nr + 1,\n> > +\t\t\t\t   transaction->pack_lockfiles_alloc);\n> > +\t\t\ttransaction->pack_lockfiles[transaction->pack_lockfiles_nr++] =\n> > +\t\t\t\tregister_tempfile(lockfile);\n> > +\t\t\tfree(lockfile);\n> > +\t\t}\n> \n> Here we add the .keep file to the list of lockfiles.  We have\n> finalization step laer in odb_transaction_files_finalize() that\n> deletes the tempfiles when we are done, which comes after\n> the transaction is committed.\n> \n> But isn't the odb_transaction_files_commit() where the migration of\n> tmp_objdir_migrate() happens?  Everything in the quarantine directory\n> including these .keep files are \"migrated\" (either link-to-the-new\n> followed by unlink-of-the-old, or rename-old-to-new) there.\n> \n> And then ...\n> \n> > +static int odb_transaction_files_finalize(struct odb_transaction *base)\n> > +{\n> > +\tstruct odb_transaction_files *transaction =\n> > +\t\tcontainer_of(base, struct odb_transaction_files, base);\n> > +\tint ret = 0;\n> > +\n> > +\tfor (size_t i = 0; i < transaction->pack_lockfiles_nr; i++)\n> > +\t\tret |= delete_tempfile(&transaction->pack_lockfiles[i]);\n> > +\n> > +\tfree(transaction->pack_lockfiles);\n> > +\n> > +\treturn ret;\n> > +}\n> \n> ... we do the deletion of tempfile but has anybody migrated the path\n> to these files recorded in the lockfile structure?  How are we\n> removing the .keep files that were \"migrated\" when the transaction\n> was committed?\n\nThe filepath recorded by the \".keep\" tempfile structure is _supposed_ to\nbe the final path of the \".keep\" file post-commit that way it knows its\nlocation after its been migrated and can remove it. The path is\ngenerated by `index_pack_lockfile()` and is supposed to use the real ODB\nsource path and not the transaction ODB source path... but this is not\nhappening anymore now that we are using ODB transactions in\ngit-receive-pack(1) which reorders the ODB source list order when the\ntransaction is applied...\n\nThis is a bug and needs to be fixed. The fix itself should be fairly\nstraightforward, we just need to tell `index_pack_lockfile()` the\ncorrect ODB source it should be using. I'll send a patch later today\ncorrect this.\n\nThanks,\n-Justin\n"},{"id":"550308","messageId":"20260811175415.2044235-1-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260809190106.1565882-1-jltobler@gmail.com","subject":"[PATCH v3 0/9] builtin/receive-pack: support pluggable packfile writes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-11T17:54:06Z","receivedAt":"2026-08-11T17:54:24Z","isPatch":true,"body":"Greetings,\n\nWith bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\ntransactions, 2026-07-10), git-receive-pack(1) started using the ODB\ntransaction interfaces to stage incoming objects. While this brought the\ncommand closer to being ODB backend agnostic, the underlying\ngit-index-pack(1) and git-unpack-objects(1) processes used to actually\nwrite the objects to the transaction are still fundamentally tied to the\n\"files\" backend.\n\nThis series aims to address this by introducing a generic\n`odb_transaction_write_pack()` transaction interface to handle writing\nthe incoming packfile to the transaction. The existing logic in\ngit-receive-pack(1) that spawns the child processes to write the\npackfile becomes the \"files\" backend implementation of this interface.\n\nChanges since V2:\n- Added a patch to address a bug causing \".keep\" files from not being\n  removed.\n- Started handling errors at transaction commit and finalize call sites\n  instead of ignoring them. We also make sure\n  `odb_transaction_finalize()` runs after every successful commit\n  callsite to ensure proper cleanup.\n- Updated the code handling lazy loading of unpack limit configuration\n  to not longer cache the value.\n- Added a patch to begin explictly tracking the ODB source used by the\n  \"files\" transaction to avoid relying on the ordering of the ODB source\n  list.\n- Updated some commit messages to improve clarity.\n\nChanges since V1:\n- Changed the \"release\" interface name to \"finalize\" and updated it to\n  return error codes.\n- Marked some function parameters as const.\n- Unpack limit configuration is now resolved in the ODB transaction\n  backend instead of wiring it through the interface.\n- When writing a packfile to the transaction, now only the transaction\n  source is prepared.\n- Updated some commit messages.\n- Updated some code formatting.\n\nThanks for the review,\n-Justin\n\nJustin Tobler (9):\n  builtin/receive-pack: properly clean up keep files\n  odb/transaction: add transaction finalize interface\n  builtin/receive-pack: pass shallow file explicitly\n  builtin/receive-pack: read unpack limit config lazily\n  builtin/receive-pack: lift global state out of unpack()\n  builtin/receive-pack: report unpack errors via strbuf\n  builtin/receive-pack: explicitly pass packfile fd\n  odb: return temporary ODB source when set\n  odb/transaction: add transaction interface to write packfiles\n\n builtin/add.c              |   4 +-\n builtin/receive-pack.c     | 211 ++++++++-----------------------------\n builtin/unpack-objects.c   |   2 +-\n builtin/update-index.c     |   4 +-\n cache-tree.c               |   2 +-\n fetch-pack.c               |   2 +-\n object-file.c              | 177 ++++++++++++++++++++++++++++++-\n odb.c                      |   9 +-\n odb.h                      |   6 +-\n odb/transaction.c          |  21 ++++\n odb/transaction.h          |  85 +++++++++++++++\n pack-write.c               |   7 +-\n pack.h                     |   4 +-\n read-cache.c               |   2 +-\n t/t5547-push-quarantine.sh |  14 +++\n tmp-objdir.c               |   8 +-\n tmp-objdir.h               |   6 +-\n 17 files changed, 376 insertions(+), 188 deletions(-)\n\nRange-diff against v2:\n -:  ---------- >  1:  58569303f9 builtin/receive-pack: properly clean up keep files\n 1:  10efcc22e4 !  2:  dba9696866 odb/transaction: add transaction finalize interface\n    @@ Commit message\n         `odb_transaction_finalize()` call site in git-receive-pack(1) is made\n         after the reference updates are finished.\n     \n    +    All other callers commit a transaction and immediately finalize it with\n    +    no work in between and cannot meaningfully recover should either step\n    +    fail, so introduce an `odb_transaction_commit_and_finalize_or_die()`\n    +    helper that performs both and dies on error. Call sites are updated\n    +    accordingly.\n    +\n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n      ## builtin/add.c ##\n    @@ builtin/add.c: int cmd_add(int argc,\n      \trepo_config(repo, add_config, NULL);\n      \n     @@ builtin/add.c: int cmd_add(int argc,\n    - \tfree(ps_matched);\n    - \tdir_clear(&dir);\n    - \tclear_pathspec(&pathspec);\n    -+\todb_transaction_finalize(transaction);\n    - \treturn exit_status;\n    - }\n    + \n    + \tif (chmod_arg && pathspec.nr)\n    + \t\texit_status |= chmod_pathspec(repo, &pathspec, chmod_arg[0], show_only);\n    +-\todb_transaction_commit(transaction);\n    ++\todb_transaction_commit_and_finalize_or_die(transaction);\n    + \n    + finish:\n    + \tif (write_locked_index(repo->index, &lock_file,\n     \n      ## builtin/receive-pack.c ##\n     @@ builtin/receive-pack.c: int cmd_receive_pack(int argc,\n    @@ builtin/receive-pack.c: int cmd_receive_pack(int argc,\n     \n      ## builtin/unpack-objects.c ##\n     @@ builtin/unpack-objects.c: static void unpack_all(void)\n    + \t\tunpack_one(i);\n      \t\tdisplay_progress(progress, i + 1);\n      \t}\n    - \todb_transaction_commit(transaction);\n    -+\todb_transaction_finalize(transaction);\n    +-\todb_transaction_commit(transaction);\n    ++\todb_transaction_commit_and_finalize_or_die(transaction);\n      \tstop_progress(&progress);\n      \n      \tif (delta_list)\n     \n      ## builtin/update-index.c ##\n     @@ builtin/update-index.c: int cmd_update_index(int argc,\n    + \t\t\t * a transaction.\n      \t\t\t */\n      \t\t\tif (transaction && verbose) {\n    - \t\t\t\todb_transaction_commit(transaction);\n    -+\t\t\t\todb_transaction_finalize(transaction);\n    +-\t\t\t\todb_transaction_commit(transaction);\n    ++\t\t\t\todb_transaction_commit_and_finalize_or_die(transaction);\n      \t\t\t\ttransaction = NULL;\n      \t\t\t}\n      \n     @@ builtin/update-index.c: int cmd_update_index(int argc,\n    + \t/*\n      \t * By now we have added all of the new objects\n      \t */\n    - \todb_transaction_commit(transaction);\n    -+\todb_transaction_finalize(transaction);\n    +-\todb_transaction_commit(transaction);\n    ++\todb_transaction_commit_and_finalize_or_die(transaction);\n      \n      \tif (split_index > 0) {\n      \t\tif (repo_config_get_split_index(the_repository) == 0)\n     \n      ## cache-tree.c ##\n     @@ cache-tree.c: int cache_tree_update(struct index_state *istate, int flags)\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)\n    -+\tif (!inflight) {\n    - \t\todb_transaction_commit(transaction);\n    -+\t\todb_transaction_finalize(transaction);\n    -+\t}\n    + \tif (!inflight)\n    +-\t\todb_transaction_commit(transaction);\n    ++\t\todb_transaction_commit_and_finalize_or_die(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    - \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\tif (!inflight)\n    -+\t\t\tif (!inflight) {\n    - \t\t\t\todb_transaction_commit(transaction);\n    -+\t\t\t\todb_transaction_finalize(transaction);\n    -+\t\t\t}\n    + \t\t\tif (!inflight)\n    +-\t\t\t\todb_transaction_commit(transaction);\n    ++\t\t\t\todb_transaction_commit_and_finalize_or_die(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    @@ odb/transaction.h: static inline void odb_transaction_begin_or_die(struct object\n     + * a negative error code otherwise.\n     + */\n     +int odb_transaction_finalize(struct odb_transaction *transaction);\n    ++\n    ++static inline void odb_transaction_commit_and_finalize_or_die(struct odb_transaction *transaction)\n    ++{\n    ++\tif (odb_transaction_commit(transaction))\n    ++\t\tdie(_(\"failed to commit ODB transaction\"));\n    ++\tif (odb_transaction_finalize(transaction))\n    ++\t\tdie(_(\"failed to finalize ODB transaction\"));\n    ++}\n     +\n      /*\n       * Writes the object in the provided stream into the transaction. The resulting\n    @@ odb/transaction.h: static inline void odb_transaction_begin_or_die(struct object\n     \n      ## read-cache.c ##\n     @@ read-cache.c: int add_files_to_cache(struct repository *repo, const char *prefix,\n    - \tif (!inflight)\n      \t\todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n      \trun_diff_files(&rev, DIFF_RACY_IS_MODIFIED);\n    --\tif (!inflight)\n    -+\tif (!inflight) {\n    - \t\todb_transaction_commit(transaction);\n    -+\t\todb_transaction_finalize(transaction);\n    -+\t}\n    + \tif (!inflight)\n    +-\t\todb_transaction_commit(transaction);\n    ++\t\todb_transaction_commit_and_finalize_or_die(transaction);\n      \n      \trelease_revisions(&rev);\n      \treturn !!data.add_errors;\n 2:  e1903ac32f =  3:  09bc00a070 builtin/receive-pack: pass shallow file explicitly\n 3:  e4950c0abe !  4:  2586ea4041 builtin/receive-pack: read unpack limit config lazily\n    @@ builtin/receive-pack.c: static void push_header_arg(struct strvec *args, struct\n      \t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n      }\n      \n    -+static int get_unpack_limit(struct repository *repo)\n    ++static unsigned int get_unpack_limit(struct repository *repo)\n     +{\n    -+\tstatic int limit = -1;\n    ++\tunsigned int limit = 100;\n     +\n    -+\tif (limit < 0) {\n    -+\t\tint receive_limit = -1;\n    -+\t\tint transfer_limit = -1;\n    -+\n    -+\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n    -+\t\t\t\t    &receive_limit);\n    -+\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n    -+\t\t\t\t    &transfer_limit);\n    -+\n    -+\t\tif (receive_limit >= 0)\n    -+\t\t\tlimit = receive_limit;\n    -+\t\telse if (transfer_limit >= 0)\n    -+\t\t\tlimit = transfer_limit;\n    -+\t\telse\n    -+\t\t\tlimit = 100;\n    -+\t}\n    ++\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n    ++\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n     +\n     +\treturn limit;\n     +}\n 4:  c9b4ff73ba !  5:  adf325095e builtin/receive-pack: lift global state out of unpack()\n    @@ Commit message\n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n      ## builtin/receive-pack.c ##\n    -@@ builtin/receive-pack.c: static int get_unpack_limit(struct repository *repo)\n    +@@ builtin/receive-pack.c: static unsigned int get_unpack_limit(struct repository *repo)\n      \treturn limit;\n      }\n      \n 5:  7be990c2c2 !  6:  29f407bf36 builtin/receive-pack: report unpack errors via strbuf\n    @@ builtin/receive-pack.c: static const char *unpack(struct odb_transaction *transa\n     +\t\t\treturn -1;\n     +\t\t}\n      \n    - \t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n    - \t\tif (lockfile) {\n    + \t\t/*\n    + \t\t * The lockfile filepath is expected to be the final location of\n     @@ builtin/receive-pack.c: static const char *unpack(struct odb_transaction *transaction,\n      \t\tclose(child.out);\n      \n 6:  742c724943 =  7:  b85f5e868c builtin/receive-pack: explicitly pass packfile fd\n -:  ---------- >  8:  620eafe035 odb: return temporary ODB source when set\n 7:  7743cf242a !  9:  2e75a8bd6c odb/transaction: add transaction interface to write packfiles\n    @@ Commit message\n     \n         Introduce `odb_transaction_write_pack()` as a generic interface to\n         handle writing a packfile to a transaction and use the logic from\n    -    `unpack()` as the \"files\" backend implementation. Note that a packfile\n    -    written via git-index-pack(1) is kept in place by a \".keep\" lockfile\n    -    that must be retained until references are updated. To faciliate this in\n    -    an ODB backend agnostic manner, the \"files\" transaction backend takes\n    -    ownership of these lockfiles and removes them post-commit through its\n    -    release callback.\n    +    `unpack()` as the \"files\" backend implementation. Note that when storing\n    +    the objects as a packfile, git-index-pack(1) also writes a \".keep\"\n    +    lockfile next to it to prevent a concurrent repack from removing the new\n    +    pack prior to reference updates being performed. The \"files\" transaction\n    +    backend is responsible for managing these \".keep\" files and removes them\n    +    post-commit once the transaction is finalized.\n     \n         Call sites in git-receive-pack(1) are updated accordingly.\n     \n    @@ builtin/receive-pack.c: static void read_push_options(struct packet_reader *read\n     -\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n     -}\n     -\n    --static int get_unpack_limit(struct repository *repo)\n    +-static unsigned int get_unpack_limit(struct repository *repo)\n     -{\n    --\tstatic int limit = -1;\n    +-\tunsigned int limit = 100;\n     -\n    --\tif (limit < 0) {\n    --\t\tint receive_limit = -1;\n    --\t\tint transfer_limit = -1;\n    --\n    --\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n    --\t\t\t\t    &receive_limit);\n    --\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n    --\t\t\t\t    &transfer_limit);\n    --\n    --\t\tif (receive_limit >= 0)\n    --\t\t\tlimit = receive_limit;\n    --\t\telse if (transfer_limit >= 0)\n    --\t\t\tlimit = transfer_limit;\n    --\t\telse\n    --\t\t\tlimit = 100;\n    --\t}\n    +-\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n    +-\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n     -\n     -\treturn limit;\n     -}\n    @@ builtin/receive-pack.c: static void read_push_options(struct packet_reader *read\n     -\t\t\treturn -1;\n     -\t\t}\n     -\n    --\t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n    +-\t\t/*\n    +-\t\t * The lockfile filepath is expected to be the final location of\n    +-\t\t * the \".keep\" file after being migrated to the main ODB source.\n    +-\t\t * This ensures the lockfile can be found and removed later\n    +-\t\t * after the ODB transaction has been committed.\n    +-\t\t */\n    +-\t\tlockfile = index_pack_lockfile(transaction->source, child.out, NULL);\n     -\t\tif (lockfile) {\n     -\t\t\tpack_lockfile = register_tempfile(lockfile);\n     -\t\t\tfree(lockfile);\n    @@ object-file.c\n      #include \"strvec.h\"\n      #include \"tempfile.h\"\n     @@ object-file.c: struct odb_transaction_files {\n    - \tstruct tmp_objdir *objdir;\n    + \tstruct odb_source *quarantine;\n      \tstruct transaction_packfile packfile;\n      \tconst char *prefix;\n     +\n    @@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b\n     +\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n     +}\n     +\n    -+static int get_unpack_limit(struct repository *repo)\n    ++static unsigned int get_unpack_limit(struct repository *repo)\n     +{\n    -+\tstatic int limit = -1;\n    -+\n    -+\tif (limit < 0) {\n    -+\t\tint receive_limit = -1;\n    -+\t\tint transfer_limit = -1;\n    -+\n    -+\t\trepo_config_get_int(repo, \"receive.unpacklimit\",\n    -+\t\t\t\t    &receive_limit);\n    -+\t\trepo_config_get_int(repo, \"transfer.unpacklimit\",\n    -+\t\t\t\t    &transfer_limit);\n    -+\n    -+\t\tif (receive_limit >= 0)\n    -+\t\t\tlimit = receive_limit;\n    -+\t\telse if (transfer_limit >= 0)\n    -+\t\t\tlimit = transfer_limit;\n    -+\t\telse\n    -+\t\t\tlimit = 100;\n    -+\t}\n    ++\tunsigned int limit = 100;\n    ++\n    ++\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n    ++\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n     +\n     +\treturn limit;\n     +}\n    @@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b\n     +\n     +\todb_transaction_env(base, &child.env);\n     +\n    -+\tif (ntohl(hdr.hdr_entries) < (unsigned int)get_unpack_limit(repo)) {\n    ++\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(repo)) {\n     +\t\tstrvec_push(&child.args, \"unpack-objects\");\n     +\t\tpush_header_arg(&child.args, &hdr);\n     +\t\tif (opts->quiet)\n    @@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b\n     +\t\t\treturn -1;\n     +\t\t}\n     +\n    -+\t\tlockfile = index_pack_lockfile(repo, child.out, NULL);\n    ++\t\t/*\n    ++\t\t * The lockfile filepath is expected to be the final location of\n    ++\t\t * the \".keep\" file after being migrated to the main ODB source.\n    ++\t\t * This ensures the lockfile can be found and removed later\n    ++\t\t * after the ODB transaction has been committed.\n    ++\t\t */\n    ++\t\tlockfile = index_pack_lockfile(base->source, child.out, NULL);\n     +\t\tif (lockfile) {\n     +\t\t\tALLOC_GROW(transaction->pack_lockfiles,\n     +\t\t\t\t   transaction->pack_lockfiles_nr + 1,\n    @@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b\n     +\t\t\treturn -1;\n     +\t\t}\n     +\n    -+\t\todb_source_prepare(repo->objects->sources,\n    ++\t\todb_source_prepare(transaction->quarantine,\n     +\t\t\t\t   ODB_PREPARE_FLUSH_CACHES);\n     +\t}\n     +\n    @@ odb/transaction.h\n     +\t * The max size in bytes of the incoming packfile allowed. No limit is\n     +\t * enforced when set to 0.\n     +\t */\n    -+\n     +\toff_t max_input_size;\n     +\n     +\t/*\n\nbase-commit: 2c78326f810173a4f3aefd8021f1e07575412481\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550309","messageId":"20260811175415.2044235-2-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260811175415.2044235-1-jltobler@gmail.com","subject":"[PATCH v3 1/9] builtin/receive-pack: properly clean up keep files","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-11T17:54:07Z","receivedAt":"2026-08-11T17:54:25Z","isPatch":true,"body":"When git-receive-pack(1) stores an incoming packfile with\ngit-index-pack(1), a \".keep\" file is written alongside it to hold the\npack in place until the references have been updated, and is removed\nafterwards. The path used to remove it is derived via\n`index_pack_lockfile()` from the repository's primary object directory.\n\nIn bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\ntransactions, 2026-07-10), git-receive-pack(1) started using the ODB\ntransaction interfaces instead of managing a temporary directory\ndirectly. When starting an ODB transaction, the sources list is\nreordered to insert the newly created transaction source first as the\nprimary to ensure writes are routed to it accordingly.\n\nPrior to using ODB transactions, git-receive-pack(1) would only set the\ntemporary directory as the primary source for the child\ngit-index-pack(1) and git-unpack-objects(1) processes it spawned and the\nparent process would set the temporary directory set as an alternate\nonly. By using ODB transactions, the ODB source list is also reordered\nfor the parent process which results in `index_pack_lockfile()` deriving\nthe \".keep\" path relative to the temporary directory instead the actual\nmain ODB source path. Consequently, this prevents the \".keep\" file from\nbeing properly removed after being migrated into the main ODB source\npost-commit.\n\nUpdate `index_pack_lockfile()` to operate on an ODB source explicitly\nprovided to it and update call sites accordingly to pass the expected\nODB source.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c     |  8 +++++++-\n fetch-pack.c               |  2 +-\n pack-write.c               |  7 ++++---\n pack.h                     |  4 +++-\n t/t5547-push-quarantine.sh | 14 ++++++++++++++\n 5 files changed, 29 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 86933d8d7e..d74b787148 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2412,7 +2412,13 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n \t\tif (status)\n \t\t\treturn \"index-pack fork failed\";\n \n-\t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n+\t\t/*\n+\t\t * The lockfile filepath is expected to be the final location of\n+\t\t * the \".keep\" file after being migrated to the main ODB source.\n+\t\t * This ensures the lockfile can be found and removed later\n+\t\t * after the ODB transaction has been committed.\n+\t\t */\n+\t\tlockfile = index_pack_lockfile(transaction->source, child.out, NULL);\n \t\tif (lockfile) {\n \t\t\tpack_lockfile = register_tempfile(lockfile);\n \t\t\tfree(lockfile);\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 922a9b2581..6df5813b33 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -1075,7 +1075,7 @@ static int get_pack(struct fetch_pack_args *args,\n \t\tdie(_(\"fetch-pack: unable to fork off %s\"), cmd_name);\n \tif (do_keep && (pack_lockfiles || fsck_objects)) {\n \t\tint is_well_formed;\n-\t\tchar *pack_lockfile = index_pack_lockfile(the_repository,\n+\t\tchar *pack_lockfile = index_pack_lockfile(the_repository->objects->sources,\n \t\t\t\t\t\t\t  cmd.out,\n \t\t\t\t\t\t\t  &is_well_formed);\n \ndiff --git a/pack-write.c b/pack-write.c\nindex 24033a9101..85674e4b72 100644\n--- a/pack-write.c\n+++ b/pack-write.c\n@@ -469,10 +469,11 @@ void fixup_pack_header_footer(const struct git_hash_algo *hash_algo,\n \tfsync_component_or_die(FSYNC_COMPONENT_PACK, pack_fd, pack_name);\n }\n \n-char *index_pack_lockfile(struct repository *r, int ip_out, int *is_well_formed)\n+char *index_pack_lockfile(struct odb_source *source, int ip_out,\n+\t\t\t  int *is_well_formed)\n {\n \tchar packname[GIT_MAX_HEXSZ + 6];\n-\tconst int len = r->hash_algo->hexsz + 6;\n+\tconst int len = source->odb->repo->hash_algo->hexsz + 6;\n \n \t/*\n \t * The first thing we expect from index-pack's output\n@@ -489,7 +490,7 @@ char *index_pack_lockfile(struct repository *r, int ip_out, int *is_well_formed)\n \t\tpackname[len-1] = 0;\n \t\tif (skip_prefix(packname, \"keep\\t\", &name))\n \t\t\treturn xstrfmt(\"%s/pack/pack-%s.keep\",\n-\t\t\t\t       repo_get_object_directory(r), name);\n+\t\t\t\t       source->path, name);\n \t\treturn NULL;\n \t}\n \tif (is_well_formed)\ndiff --git a/pack.h b/pack.h\nindex 1cde92082b..68dcf08cf3 100644\n--- a/pack.h\n+++ b/pack.h\n@@ -3,6 +3,7 @@\n \n #include \"object.h\"\n #include \"csum-file.h\"\n+#include \"odb/source.h\"\n \n struct packed_git;\n struct pack_window;\n@@ -105,7 +106,8 @@ off_t write_pack_header(struct hashfile *f, uint32_t);\n void fixup_pack_header_footer(const struct git_hash_algo *, int,\n \t\t\t      unsigned char *, const char *, uint32_t,\n \t\t\t      unsigned char *, off_t);\n-char *index_pack_lockfile(struct repository *r, int fd, int *is_well_formed);\n+char *index_pack_lockfile(struct odb_source *source, int fd,\n+\t\t\t  int *is_well_formed);\n \n struct ref;\n \ndiff --git a/t/t5547-push-quarantine.sh b/t/t5547-push-quarantine.sh\nindex 0798ddab02..400a597606 100755\n--- a/t/t5547-push-quarantine.sh\n+++ b/t/t5547-push-quarantine.sh\n@@ -70,4 +70,18 @@ test_expect_success 'updating a ref from quarantine is forbidden' '\n \tgit -C update.git fsck\n '\n \n+test_expect_success '.keep file is removed after push' '\n+\ttest_when_finished rm -rf keep.git &&\n+\tgit init --bare keep.git &&\n+\n+\tgit -C keep.git config set receive.unpackLimit 0 &&\n+\ttest_commit foo &&\n+\tgit push keep.git HEAD &&\n+\tpack=\"$(ls keep.git/objects/pack/pack-*.pack)\" &&\n+\tkeep=\"${pack%.pack}.keep\" &&\n+\n+\ttest_path_is_file \"$pack\" &&\n+\ttest_path_is_missing \"$keep\"\n+'\n+\n test_done\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550310","messageId":"20260811175415.2044235-3-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260811175415.2044235-1-jltobler@gmail.com","subject":"[PATCH v3 2/9] odb/transaction: add transaction finalize interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-11T17:54:08Z","receivedAt":"2026-08-11T17:54:26Z","isPatch":true,"body":"When committing an ODB transaction via `odb_transaction_commit()`, the\nstaged objects are made visible and the underlying transaction is freed\nat the same time. Coupling these two steps does not leave room for any\npost-commit transaction operations to be introduced though. Such a\ncapability is useful if an ODB transaction backend needs to hold on to\nlockfiles after transaction commit until references are updated, as is\nthe case with the existing \"files\" backend in git-receive-pack(1).\n\nStop freeing the transaction in `odb_transaction_commit()` and introduce\n`odb_transaction_finalize()` to explicitly clean up the transaction\naccordingly. Note that the finalize interface also provides an optional\ncallback for any backend-specific deferred cleanup. In a subsequent\ncommit, the \"files\" transaction backend will use this to remove \".keep\"\nfiles generated for packfiles received via git-receive-pack(1) after\nreferences have been updated. In preparation for this, the\n`odb_transaction_finalize()` call site in git-receive-pack(1) is made\nafter the reference updates are finished.\n\nAll other callers commit a transaction and immediately finalize it with\nno work in between and cannot meaningfully recover should either step\nfail, so introduce an `odb_transaction_commit_and_finalize_or_die()`\nhelper that performs both and dies on error. Call sites are updated\naccordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  4 ++--\n builtin/receive-pack.c   |  1 +\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  4 ++--\n cache-tree.c             |  2 +-\n object-file.c            |  2 +-\n odb/transaction.c        | 14 ++++++++++++++\n odb/transaction.h        | 23 +++++++++++++++++++++++\n read-cache.c             |  2 +-\n 9 files changed, 46 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 60ffbede2b..ad418a5952 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -393,7 +393,7 @@ int cmd_add(int argc,\n \tchar *seen = NULL;\n \tchar *ps_matched = NULL;\n \tstruct lock_file lock_file = LOCK_INIT;\n-\tstruct odb_transaction *transaction;\n+\tstruct odb_transaction *transaction = NULL;\n \n \trepo_config(repo, add_config, NULL);\n \n@@ -600,7 +600,7 @@ int cmd_add(int argc,\n \n \tif (chmod_arg && pathspec.nr)\n \t\texit_status |= chmod_pathspec(repo, &pathspec, chmod_arg[0], show_only);\n-\todb_transaction_commit(transaction);\n+\todb_transaction_commit_and_finalize_or_die(transaction);\n \n finish:\n \tif (write_locked_index(repo->index, &lock_file,\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex d74b787148..ed1edcbe93 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2720,6 +2720,7 @@ int cmd_receive_pack(int argc,\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n \t\texecute_commands(commands, unpack_status, &si, transaction,\n \t\t\t\t &push_options);\n+\t\todb_transaction_finalize(transaction);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex 4263edfbec..d6a2d616d9 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -603,7 +603,7 @@ static void unpack_all(void)\n \t\tunpack_one(i);\n \t\tdisplay_progress(progress, i + 1);\n \t}\n-\todb_transaction_commit(transaction);\n+\todb_transaction_commit_and_finalize_or_die(transaction);\n \tstop_progress(&progress);\n \n \tif (delta_list)\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 241abd4332..b25d4ecb10 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1156,7 +1156,7 @@ int cmd_update_index(int argc,\n \t\t\t * a transaction.\n \t\t\t */\n \t\t\tif (transaction && verbose) {\n-\t\t\t\todb_transaction_commit(transaction);\n+\t\t\t\todb_transaction_commit_and_finalize_or_die(transaction);\n \t\t\t\ttransaction = NULL;\n \t\t\t}\n \n@@ -1224,7 +1224,7 @@ int cmd_update_index(int argc,\n \t/*\n \t * By now we have added all of the new objects\n \t */\n-\todb_transaction_commit(transaction);\n+\todb_transaction_commit_and_finalize_or_die(transaction);\n \n \tif (split_index > 0) {\n \t\tif (repo_config_get_split_index(the_repository) == 0)\ndiff --git a/cache-tree.c b/cache-tree.c\nindex d92f513286..a220372a42 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -538,7 +538,7 @@ int cache_tree_update(struct index_state *istate, int flags)\n \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n \t\t       \"\", 0, &skip, flags);\n \tif (!inflight)\n-\t\todb_transaction_commit(transaction);\n+\t\todb_transaction_commit_and_finalize_or_die(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 ec35c318bc..4d03c167d5 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -965,7 +965,7 @@ int index_fd(struct index_state *istate, struct object_id *oid,\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\tif (!inflight)\n-\t\t\t\todb_transaction_commit(transaction);\n+\t\t\t\todb_transaction_commit_and_finalize_or_die(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 dab7da6a9a..9e9a982778 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -33,6 +33,20 @@ int odb_transaction_commit(struct odb_transaction *transaction)\n \n \tret = transaction->commit(transaction);\n \ttransaction->source->odb->transaction = NULL;\n+\n+\treturn ret;\n+}\n+\n+int odb_transaction_finalize(struct odb_transaction *transaction)\n+{\n+\tint ret = 0;\n+\n+\tif (!transaction)\n+\t\treturn 0;\n+\n+\tif (transaction->finalize)\n+\t\tret = transaction->finalize(transaction);\n+\n \tfree(transaction);\n \n \treturn ret;\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 4cb2eafcbf..6ed39b3d0e 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -22,6 +22,13 @@ struct odb_transaction {\n \t */\n \tint (*commit)(struct odb_transaction *transaction);\n \n+\t/*\n+\t * Optional ODB source specific callback invoked when the transaction\n+\t * needs to perform any deferred cleanup after objects have been\n+\t * committed. Returns 0 on success, a negative error code otherwise.\n+\t */\n+\tint (*finalize)(struct odb_transaction *transaction);\n+\n \t/*\n \t * This callback is expected to write the given object stream into\n \t * the ODB transaction. Note that for now, only blobs support streaming.\n@@ -75,6 +82,22 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n  */\n int odb_transaction_commit(struct odb_transaction *transaction);\n \n+/*\n+ * Finalizes an ODB transaction, performing any deferred cleanup and freeing it.\n+ * Must be called for every successfully started transaction. Note that, if the\n+ * specified transaction is NULL, the function is a no-op. Returns 0 on success,\n+ * a negative error code otherwise.\n+ */\n+int odb_transaction_finalize(struct odb_transaction *transaction);\n+\n+static inline void odb_transaction_commit_and_finalize_or_die(struct odb_transaction *transaction)\n+{\n+\tif (odb_transaction_commit(transaction))\n+\t\tdie(_(\"failed to commit ODB transaction\"));\n+\tif (odb_transaction_finalize(transaction))\n+\t\tdie(_(\"failed to finalize ODB transaction\"));\n+}\n+\n /*\n  * Writes the object in the provided stream into the transaction. The resulting\n  * object ID is written into the out pointer. Returns 0 on success, a negative\ndiff --git a/read-cache.c b/read-cache.c\nindex 6c449f393d..0cd0ef85ec 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4049,7 +4049,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\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+\t\todb_transaction_commit_and_finalize_or_die(transaction);\n \n \trelease_revisions(&rev);\n \treturn !!data.add_errors;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550311","messageId":"20260811175415.2044235-4-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260811175415.2044235-1-jltobler@gmail.com","subject":"[PATCH v3 3/9] builtin/receive-pack: pass shallow file explicitly","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-11T17:54:09Z","receivedAt":"2026-08-11T17:54:27Z","isPatch":true,"body":"If shallow information is provided during `unpack()`, a temporary\nshallow file is created and stored in global state. In a subsequent\ncommit, the `unpack()` logic is moved behind a generic ODB transaction\ninterface to handle writing packfiles and thus can no longer rely on\nsuch global state. Lift the setup of the temporary shallow file out of\n`unpack()` and wire it through to its call sites explicitly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 38 ++++++++++++++++++++++----------------\n 1 file changed, 22 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex ed1edcbe93..135105deae 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -86,7 +86,6 @@ static const char *head_name;\n static void *head_name_to_free;\n static int sent_capabilities;\n static int shallow_update;\n-static const char *alt_shallow_file;\n static struct strbuf push_cert = STRBUF_INIT;\n static struct object_id push_cert_oid;\n static struct signature_check sigcheck;\n@@ -2334,8 +2333,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-\t\t\t  struct odb_transaction *transaction)\n+static const char *unpack(struct odb_transaction *transaction,\n+\t\t\t  const char *shallow_file, int err_fd)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2354,10 +2353,9 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n \t\treturn hdr_err;\n \t}\n \n-\tif (si->nr_ours || si->nr_theirs) {\n-\t\talt_shallow_file = setup_temporary_shallow(si->shallow);\n+\tif (shallow_file) {\n \t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, alt_shallow_file);\n+\t\tstrvec_push(&child.args, shallow_file);\n \t}\n \n \todb_transaction_env(transaction, &child.env);\n@@ -2433,14 +2431,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-\t\t\t\t\tstruct odb_transaction *transaction)\n+static const char *unpack_with_sideband(struct odb_transaction *transaction,\n+\t\t\t\t\tconst char *shallow_file)\n {\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(0, si, transaction);\n+\t\treturn unpack(transaction, shallow_file, 0);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2449,13 +2447,14 @@ 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, transaction);\n+\tret = unpack(transaction, shallow_file, muxer.in);\n \n \tfinish_async(&muxer);\n \treturn ret;\n }\n \n-static void prepare_shallow_update(struct shallow_info *si)\n+static void prepare_shallow_update(struct shallow_info *si,\n+\t\t\t\t   const char *shallow_file)\n {\n \tint i, j, k, bitmap_size = DIV_ROUND_UP(si->ref->nr, 32);\n \n@@ -2495,12 +2494,13 @@ static void prepare_shallow_update(struct shallow_info *si)\n \t * command. check_connected() will be done with\n \t * true .git/shallow though.\n \t */\n-\tsetenv(GIT_SHALLOW_FILE_ENVIRONMENT, alt_shallow_file, 1);\n+\tsetenv(GIT_SHALLOW_FILE_ENVIRONMENT, shallow_file, 1);\n }\n \n static void update_shallow_info(struct command *commands,\n \t\t\t\tstruct shallow_info *si,\n-\t\t\t\tstruct oid_array *ref)\n+\t\t\t\tstruct oid_array *ref,\n+\t\t\t\tconst char *shallow_file)\n {\n \tstruct command *cmd;\n \tint *ref_status;\n@@ -2519,7 +2519,7 @@ static void update_shallow_info(struct command *commands,\n \tsi->ref = ref;\n \n \tif (shallow_update) {\n-\t\tprepare_shallow_update(si);\n+\t\tprepare_shallow_update(si, shallow_file);\n \t\treturn;\n \t}\n \n@@ -2711,11 +2711,17 @@ 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\tconst char *alt_shallow_file = NULL;\n+\n+\t\t\tif (si.nr_ours || si.nr_theirs)\n+\t\t\t\talt_shallow_file = setup_temporary_shallow(si.shallow);\n+\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\t\tunpack_status = unpack_with_sideband(transaction, alt_shallow_file);\n+\n+\t\t\tupdate_shallow_info(commands, &si, &ref, alt_shallow_file);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n \t\texecute_commands(commands, unpack_status, &si, transaction,\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550312","messageId":"20260811175415.2044235-5-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260811175415.2044235-1-jltobler@gmail.com","subject":"[PATCH v3 4/9] builtin/receive-pack: read unpack limit config lazily","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-11T17:54:10Z","receivedAt":"2026-08-11T17:54:28Z","isPatch":true,"body":"In git-receive-pack(1), the `receive.unpackLimit` and\n`transfer.unpackLimit` configuration decides whether an incoming\npackfile should be exploded into loose objects or kept as a packfile\non-disk. In a subsequent commit, the logic to write the incoming\npackfile is made ODB backend agnostic and moved behind a pluggable ODB\ntransaction interface. Consequently, whether to explode a packfile is a\ndetail of how a particular backend stores objects and should not be a\npart of the generic interface itself.\n\nIn preparation for this, instead resolve the unpack limit lazily inside\n`unpack()` by reading the configuration directly. The now-unused unpack\nlimit globals are dropped accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 30 +++++++++++-------------------\n 1 file changed, 11 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 135105deae..971dc3f52e 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -62,12 +62,9 @@ static enum deny_action deny_delete_current = DENY_UNCONFIGURED;\n static int receive_fsck_objects = -1;\n static int transfer_fsck_objects = -1;\n static struct strbuf fsck_msg_types = STRBUF_INIT;\n-static int receive_unpack_limit = -1;\n-static int transfer_unpack_limit = -1;\n static int advertise_atomic_push = 1;\n static int advertise_push_options;\n static int advertise_sid;\n-static int unpack_limit = 100;\n static off_t max_input_size;\n static int report_status;\n static int report_status_v2;\n@@ -157,16 +154,6 @@ static int receive_pack_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (strcmp(var, \"receive.unpacklimit\") == 0) {\n-\t\treceive_unpack_limit = git_config_int(var, value, ctx->kvi);\n-\t\treturn 0;\n-\t}\n-\n-\tif (strcmp(var, \"transfer.unpacklimit\") == 0) {\n-\t\ttransfer_unpack_limit = git_config_int(var, value, ctx->kvi);\n-\t\treturn 0;\n-\t}\n-\n \tif (strcmp(var, \"receive.fsck.skiplist\") == 0) {\n \t\tchar *path;\n \n@@ -2333,6 +2320,16 @@ 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 unsigned int get_unpack_limit(struct repository *repo)\n+{\n+\tunsigned int limit = 100;\n+\n+\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n+\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n+\n+\treturn limit;\n+}\n+\n static const char *unpack(struct odb_transaction *transaction,\n \t\t\t  const char *shallow_file, int err_fd)\n {\n@@ -2360,7 +2357,7 @@ static const char *unpack(struct odb_transaction *transaction,\n \n \todb_transaction_env(transaction, &child.env);\n \n-\tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n+\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n \t\tpush_header_arg(&child.args, &hdr);\n \t\tif (quiet)\n@@ -2658,11 +2655,6 @@ int cmd_receive_pack(int argc,\n \tif (cert_nonce_seed)\n \t\tpush_cert_nonce = prepare_push_cert_nonce(service_dir, time(NULL));\n \n-\tif (0 <= receive_unpack_limit)\n-\t\tunpack_limit = receive_unpack_limit;\n-\telse if (0 <= transfer_unpack_limit)\n-\t\tunpack_limit = transfer_unpack_limit;\n-\n \tswitch (determine_protocol_version_server()) {\n \tcase protocol_v2:\n \t\t/*\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550313","messageId":"20260811175415.2044235-6-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260811175415.2044235-1-jltobler@gmail.com","subject":"[PATCH v3 5/9] builtin/receive-pack: lift global state out of unpack()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-11T17:54:11Z","receivedAt":"2026-08-11T17:54:30Z","isPatch":true,"body":"In git-receive-pack(1), writing the packfile to the transaction is\nhandled via `unpack()` which relies on global variables to decide how to\ninvoke the underlying git-index-pack(1) or git-unpack-objects(1) child\nprocesses. In a subsequent commit, the `unpack()` logic is moved behind\na generic ODB transaction interface to handle writing packfiles and thus\ncan no longer rely on these globals.\n\nLift the global state out of `unpack()` by instead storing this state in\na `struct unpack_opts` that gets passed to the function explicitly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 63 +++++++++++++++++++++++++++---------------\n 1 file changed, 41 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 971dc3f52e..f062b93b8d 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2330,18 +2330,24 @@ static unsigned int get_unpack_limit(struct repository *repo)\n \treturn limit;\n }\n \n+struct unpack_opts {\n+\tconst char *fsck_msg_types;\n+\tconst char *shallow_file;\n+\toff_t max_input_size;\n+\tint fsck_objects;\n+\tint reject_thin;\n+\tint err_fd;\n+\tint quiet;\n+};\n+\n static const char *unpack(struct odb_transaction *transaction,\n-\t\t\t  const char *shallow_file, int err_fd)\n+\t\t\t  const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n \tint status;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint fsck_objects = (receive_fsck_objects >= 0\n-\t\t\t    ? receive_fsck_objects\n-\t\t\t    : transfer_fsck_objects >= 0\n-\t\t\t    ? transfer_fsck_objects\n-\t\t\t    : 0);\n+\tint err_fd = opts->err_fd;\n \n \thdr_err = parse_pack_header(&hdr);\n \tif (hdr_err) {\n@@ -2350,9 +2356,9 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\treturn hdr_err;\n \t}\n \n-\tif (shallow_file) {\n+\tif (opts->shallow_file) {\n \t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, shallow_file);\n+\t\tstrvec_push(&child.args, opts->shallow_file);\n \t}\n \n \todb_transaction_env(transaction, &child.env);\n@@ -2360,14 +2366,14 @@ static const char *unpack(struct odb_transaction *transaction,\n \tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n \t\tpush_header_arg(&child.args, &hdr);\n-\t\tif (quiet)\n+\t\tif (opts->quiet)\n \t\t\tstrvec_push(&child.args, \"-q\");\n-\t\tif (fsck_objects)\n+\t\tif (opts->fsck_objects)\n \t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     fsck_msg_types.buf);\n-\t\tif (max_input_size)\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (opts->max_input_size)\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)max_input_size);\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.no_stdout = 1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n@@ -2388,18 +2394,18 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\t\t     (uintmax_t)getpid(),\n \t\t\t     hostname);\n \n-\t\tif (!quiet && err_fd)\n+\t\tif (!opts->quiet && err_fd)\n \t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n-\t\tif (use_sideband)\n+\t\tif (err_fd)\n \t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n-\t\tif (fsck_objects)\n+\t\tif (opts->fsck_objects)\n \t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     fsck_msg_types.buf);\n-\t\tif (!reject_thin)\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (!opts->reject_thin)\n \t\t\tstrvec_push(&child.args, \"--fix-thin\");\n-\t\tif (max_input_size)\n+\t\tif (opts->max_input_size)\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)max_input_size);\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.out = -1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n@@ -2431,11 +2437,23 @@ static const char *unpack(struct odb_transaction *transaction,\n static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \t\t\t\t\tconst char *shallow_file)\n {\n+\tstruct unpack_opts opts = {\n+\t\t.fsck_objects = (receive_fsck_objects >= 0\n+\t\t\t\t ? receive_fsck_objects\n+\t\t\t\t : transfer_fsck_objects >= 0\n+\t\t\t\t ? transfer_fsck_objects\n+\t\t\t\t : 0),\n+\t\t.fsck_msg_types = fsck_msg_types.buf,\n+\t\t.max_input_size = max_input_size,\n+\t\t.shallow_file = shallow_file,\n+\t\t.reject_thin = reject_thin,\n+\t\t.quiet = quiet,\n+\t};\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, shallow_file, 0);\n+\t\treturn unpack(transaction, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2444,7 +2462,8 @@ static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \tif (start_async(&muxer))\n \t\treturn NULL;\n \n-\tret = unpack(transaction, shallow_file, muxer.in);\n+\topts.err_fd = muxer.in;\n+\tret = unpack(transaction, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550314","messageId":"20260811175415.2044235-7-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260811175415.2044235-1-jltobler@gmail.com","subject":"[PATCH v3 6/9] builtin/receive-pack: report unpack errors via strbuf","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-11T17:54:12Z","receivedAt":"2026-08-11T17:54:31Z","isPatch":true,"body":"When writing packfiles via `unpack()`, error messages are returned\ndirectly by the function. In preparation for `unpack()` logic being\nmoved behind a generic ODB transaction interface, update the function to\ninstead write any error messages to a caller provided strbuf and return\na negative value on error. Call sites are updated to use the error\nstrbuf accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 63 ++++++++++++++++++++++++------------------\n 1 file changed, 36 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex f062b93b8d..6df872697b 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2015,7 +2015,7 @@ static void execute_commands_atomic(struct command *commands,\n }\n \n static void execute_commands(struct command *commands,\n-\t\t\t     const char *unpacker_error,\n+\t\t\t     int 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@@ -2340,8 +2340,8 @@ struct unpack_opts {\n \tint quiet;\n };\n \n-static const char *unpack(struct odb_transaction *transaction,\n-\t\t\t  const struct unpack_opts *opts)\n+static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n+\t\t  const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2353,7 +2353,8 @@ static const char *unpack(struct odb_transaction *transaction,\n \tif (hdr_err) {\n \t\tif (err_fd > 0)\n \t\t\tclose(err_fd);\n-\t\treturn hdr_err;\n+\t\tstrbuf_addstr(err_msg, hdr_err);\n+\t\treturn -1;\n \t}\n \n \tif (opts->shallow_file) {\n@@ -2378,8 +2379,10 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = run_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"unpack-objects abnormal exit\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n \t} else {\n \t\tchar hostname[HOST_NAME_MAX + 1];\n \t\tchar *lockfile;\n@@ -2410,8 +2413,10 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = start_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"index-pack fork failed\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n+\t\t\treturn -1;\n+\t\t}\n \n \t\t/*\n \t\t * The lockfile filepath is expected to be the final location of\n@@ -2427,15 +2432,18 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tclose(child.out);\n \n \t\tstatus = finish_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"index-pack abnormal exit\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n \t\todb_reprepare(the_repository->objects);\n \t}\n-\treturn NULL;\n+\treturn 0;\n }\n \n-static const char *unpack_with_sideband(struct odb_transaction *transaction,\n-\t\t\t\t\tconst char *shallow_file)\n+static int unpack_with_sideband(struct odb_transaction *transaction,\n+\t\t\t\tconst char *shallow_file,\n+\t\t\t\tstruct strbuf *err_msg)\n {\n \tstruct unpack_opts opts = {\n \t\t.fsck_objects = (receive_fsck_objects >= 0\n@@ -2450,20 +2458,20 @@ static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \t\t.quiet = quiet,\n \t};\n \tstruct async muxer;\n-\tconst char *ret;\n+\tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, &opts);\n+\t\treturn unpack(transaction, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n \tmuxer.proc = copy_to_sideband;\n \tmuxer.in = -1;\n \tif (start_async(&muxer))\n-\t\treturn NULL;\n+\t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, &opts);\n+\tret = unpack(transaction, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2552,13 +2560,13 @@ static void update_shallow_info(struct command *commands,\n \tfree(ref_status);\n }\n \n-static void report(struct command *commands, const char *unpack_status)\n+static void report(struct command *commands, const struct strbuf *unpack_status)\n {\n \tstruct command *cmd;\n \tstruct strbuf buf = STRBUF_INIT;\n \n \tpacket_buf_write(&buf, \"unpack %s\\n\",\n-\t\t\t unpack_status ? unpack_status : \"ok\");\n+\t\t\t unpack_status->len ? unpack_status->buf : \"ok\");\n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (!cmd->error_string)\n \t\t\tpacket_buf_write(&buf, \"ok %s\\n\",\n@@ -2576,14 +2584,14 @@ static void report(struct command *commands, const char *unpack_status)\n \tstrbuf_release(&buf);\n }\n \n-static void report_v2(struct command *commands, const char *unpack_status)\n+static void report_v2(struct command *commands, const struct strbuf *unpack_status)\n {\n \tstruct command *cmd;\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct ref_push_report *report;\n \n \tpacket_buf_write(&buf, \"unpack %s\\n\",\n-\t\t\t unpack_status ? unpack_status : \"ok\");\n+\t\t\t unpack_status->len ? unpack_status->buf : \"ok\");\n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tint count = 0;\n \n@@ -2707,8 +2715,8 @@ int cmd_receive_pack(int argc,\n \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\n \n \tif ((commands = read_head_info(&reader, &shallow))) {\n-\t\tconst char *unpack_status = NULL;\n \t\tstruct string_list push_options = STRING_LIST_INIT_DUP;\n+\t\tstruct strbuf unpack_status = STRBUF_INIT;\n \n \t\tif (use_push_options)\n \t\t\tread_push_options(&reader, &push_options);\n@@ -2728,22 +2736,22 @@ int cmd_receive_pack(int argc,\n \t\t\t\talt_shallow_file = setup_temporary_shallow(si.shallow);\n \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\t\tstrbuf_addstr(&unpack_status, \"unable to start object transaction\");\n \t\t\telse\n-\t\t\t\tunpack_status = unpack_with_sideband(transaction, alt_shallow_file);\n+\t\t\t\tunpack_with_sideband(transaction, alt_shallow_file, &unpack_status);\n \n \t\t\tupdate_shallow_info(commands, &si, &ref, alt_shallow_file);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n-\t\texecute_commands(commands, unpack_status, &si, transaction,\n+\t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n \t\t\t\t &push_options);\n \t\todb_transaction_finalize(transaction);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\n-\t\t\treport_v2(commands, unpack_status);\n+\t\t\treport_v2(commands, &unpack_status);\n \t\telse if (report_status)\n-\t\t\treport(commands, unpack_status);\n+\t\t\treport(commands, &unpack_status);\n \t\tsigchain_pop(SIGPIPE);\n \t\trun_receive_hook(commands, \"post-receive\", 1, NULL,\n \t\t\t\t &push_options);\n@@ -2768,6 +2776,7 @@ int cmd_receive_pack(int argc,\n \t\tif (auto_update_server_info)\n \t\t\tupdate_server_info(the_repository, 0);\n \t\tclear_shallow_info(&si);\n+\t\tstrbuf_release(&unpack_status);\n \t}\n \tif (use_sideband)\n \t\tpacket_flush(1);\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550315","messageId":"20260811175415.2044235-9-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260811175415.2044235-1-jltobler@gmail.com","subject":"[PATCH v3 8/9] odb: return temporary ODB source when set","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-11T17:54:14Z","receivedAt":"2026-08-11T17:54:32Z","isPatch":true,"body":"When invoked, `odb_set_temporary_primary_source()` installs a temporary\nobject directory as the new primary ODB source. A caller that wants to\noperate on the ODB source of the open transaction must assume that it is\nthe first entry in the ODB source list which is a bit awkward and\nfragile.\n\nInstead, return the newly installed source directly and report the\nprevious primary source via a new `prev_source` out parameter. Propagate\nthe installed source through `tmp_objdir_replace_primary_odb()` and\nstart storing it in the \"files\" ODB transaction so a subsequent commit\ncan easily access it without relying on the ODB source list ordering.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 3 ++-\n odb.c         | 9 +++++++--\n odb.h         | 6 ++++--\n tmp-objdir.c  | 8 +++++---\n tmp-objdir.h  | 6 ++++--\n 5 files changed, 22 insertions(+), 10 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 4d03c167d5..db63587f6d 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -485,6 +485,7 @@ struct odb_transaction_files {\n \tstruct odb_transaction base;\n \n \tstruct tmp_objdir *objdir;\n+\tstruct odb_source *quarantine;\n \tstruct transaction_packfile packfile;\n \tconst char *prefix;\n };\n@@ -507,7 +508,7 @@ int odb_transaction_files_prepare(struct odb_transaction *base)\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+\ttransaction->quarantine = tmp_objdir_replace_primary_odb(transaction->objdir, 0);\n \n \treturn 0;\n }\ndiff --git a/odb.c b/odb.c\nindex caf1d0f542..8afcb6b637 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -226,7 +226,8 @@ struct odb_source *odb_add_to_alternates_memory(struct object_database *odb,\n }\n \n struct odb_source *odb_set_temporary_primary_source(struct object_database *odb,\n-\t\t\t\t\t\t    const char *dir, int will_destroy)\n+\t\t\t\t\t\t    const char *dir, int will_destroy,\n+\t\t\t\t\t\t    struct odb_source **prev_source)\n {\n \tstruct odb_source *source;\n \n@@ -250,7 +251,11 @@ struct odb_source *odb_set_temporary_primary_source(struct object_database *odb,\n \tsource->will_destroy = will_destroy;\n \tsource->next = odb->sources;\n \todb->sources = source;\n-\treturn source->next;\n+\n+\tif (prev_source)\n+\t\t*prev_source = source->next;\n+\n+\treturn source;\n }\n \n void odb_restore_primary_source(struct object_database *odb,\ndiff --git a/odb.h b/odb.h\nindex fca67e8253..bdfcb9509a 100644\n--- a/odb.h\n+++ b/odb.h\n@@ -199,10 +199,12 @@ struct odb_source *odb_find_source_or_die(struct object_database *odb, const cha\n \n /*\n  * Replace the current writable object directory with the specified temporary\n- * object directory; returns the former primary source.\n+ * object directory and return the newly installed primary source. The former\n+ * primary source is reported via `prev_source` when non-NULL.\n  */\n struct odb_source *odb_set_temporary_primary_source(struct object_database *odb,\n-\t\t\t\t\t\t    const char *dir, int will_destroy);\n+\t\t\t\t\t\t    const char *dir, int will_destroy,\n+\t\t\t\t\t\t    struct odb_source **prev_source);\n \n /*\n  * Restore the primary source that was previously replaced by\ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex d199d39e7c..e633d97e0e 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -327,11 +327,13 @@ void tmp_objdir_add_as_alternate(const struct tmp_objdir *t)\n \todb_add_to_alternates_memory(t->repo->objects, t->path.buf);\n }\n \n-void tmp_objdir_replace_primary_odb(struct tmp_objdir *t, int will_destroy)\n+struct odb_source *tmp_objdir_replace_primary_odb(struct tmp_objdir *t,\n+\t\t\t\t\t\t  int will_destroy)\n {\n \tif (t->prev_source)\n \t\tBUG(\"the primary object database is already replaced\");\n-\tt->prev_source = odb_set_temporary_primary_source(t->repo->objects,\n-\t\t\t\t\t\t\t  t->path.buf, will_destroy);\n \tt->will_destroy = will_destroy;\n+\n+\treturn odb_set_temporary_primary_source(t->repo->objects, t->path.buf,\n+\t\t\t\t\t\twill_destroy, &t->prev_source);\n }\ndiff --git a/tmp-objdir.h b/tmp-objdir.h\nindex ccf800faa7..81eb927413 100644\n--- a/tmp-objdir.h\n+++ b/tmp-objdir.h\n@@ -64,8 +64,10 @@ void tmp_objdir_add_as_alternate(const struct tmp_objdir *);\n /*\n  * Replaces the writable object store in the current process with the temporary\n  * object directory and makes the former main object store an alternate.\n- * If will_destroy is nonzero, the object directory may not be migrated.\n+ * If will_destroy is nonzero, the object directory may not be migrated. Returns\n+ * the newly installed primary source.\n  */\n-void tmp_objdir_replace_primary_odb(struct tmp_objdir *, int will_destroy);\n+struct odb_source *tmp_objdir_replace_primary_odb(struct tmp_objdir *,\n+\t\t\t\t\t\t  int will_destroy);\n \n #endif /* TMP_OBJDIR_H */\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550316","messageId":"20260811175415.2044235-8-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260811175415.2044235-1-jltobler@gmail.com","subject":"[PATCH v3 7/9] builtin/receive-pack: explicitly pass packfile fd","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-11T17:54:13Z","receivedAt":"2026-08-11T17:54:32Z","isPatch":true,"body":"When processing the incoming packfile in git-receive-pack(1), `unpack()`\nassumes it should always read it from stdin. In preparation for\n`unpack()` logic being moved behind a generic ODB transaction interface,\nupdate the function signature to take the an explicit fd provided by\ncallers to read the incoming packfile from instead. Call sites are\nupdated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 16 +++++++++-------\n 1 file changed, 9 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 6df872697b..b369466783 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2292,9 +2292,9 @@ static void read_push_options(struct packet_reader *reader,\n \t}\n }\n \n-static const char *parse_pack_header(struct pack_header *hdr)\n+static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n {\n-\tswitch (read_pack_header(0, hdr)) {\n+\tswitch (read_pack_header(pack_fd, hdr)) {\n \tcase PH_ERROR_EOF:\n \t\treturn \"eof before pack header was fully read\";\n \n@@ -2340,8 +2340,8 @@ struct unpack_opts {\n \tint quiet;\n };\n \n-static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n-\t\t  const struct unpack_opts *opts)\n+static int unpack(struct odb_transaction *transaction, int pack_fd,\n+\t\t  struct strbuf *err_msg, const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2349,7 +2349,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tint err_fd = opts->err_fd;\n \n-\thdr_err = parse_pack_header(&hdr);\n+\thdr_err = parse_pack_header(&hdr, pack_fd);\n \tif (hdr_err) {\n \t\tif (err_fd > 0)\n \t\t\tclose(err_fd);\n@@ -2376,6 +2376,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n \t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.no_stdout = 1;\n+\t\tchild.in = pack_fd;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = run_command(&child);\n@@ -2410,6 +2411,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n \t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.out = -1;\n+\t\tchild.in = pack_fd;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = start_command(&child);\n@@ -2461,7 +2463,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, err_msg, &opts);\n+\t\treturn unpack(transaction, 0, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2471,7 +2473,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, err_msg, &opts);\n+\tret = unpack(transaction, 0, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550317","messageId":"20260811175415.2044235-10-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260811175415.2044235-1-jltobler@gmail.com","subject":"[PATCH v3 9/9] odb/transaction: add transaction interface to write packfiles","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-11T17:54:15Z","receivedAt":"2026-08-11T17:54:33Z","isPatch":true,"body":"In git-receive-pack(1), the incoming packfile is written to the ODB via\n`unpack()`, which spawns git-index-pack(1) or git-unpack-objects(1)\ndirectly. With pluggable object databases, an alternative backend may\nneed to handle writing packfile data differently though.\n\nIntroduce `odb_transaction_write_pack()` as a generic interface to\nhandle writing a packfile to a transaction and use the logic from\n`unpack()` as the \"files\" backend implementation. Note that when storing\nthe objects as a packfile, git-index-pack(1) also writes a \".keep\"\nlockfile next to it to prevent a concurrent repack from removing the new\npack prior to reference updates being performed. The \"files\" transaction\nbackend is responsible for managing these \".keep\" files and removes them\npost-commit once the transaction is finalized.\n\nCall sites in git-receive-pack(1) are updated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 160 +-------------------------------------\n object-file.c          | 172 +++++++++++++++++++++++++++++++++++++++++\n odb/transaction.c      |   7 ++\n odb/transaction.h      |  62 +++++++++++++++\n 4 files changed, 244 insertions(+), 157 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex b369466783..e6e54ba55f 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -15,7 +15,6 @@\n #include \"gpg-interface.h\"\n #include \"hex.h\"\n #include \"hook.h\"\n-#include \"lockfile.h\"\n #include \"object.h\"\n #include \"object-file.h\"\n #include \"object-name.h\"\n@@ -23,7 +22,6 @@\n #include \"oid-array.h\"\n #include \"oidset.h\"\n #include \"pack.h\"\n-#include \"packfile.h\"\n #include \"parse-options.h\"\n #include \"pkt-line.h\"\n #include \"protocol.h\"\n@@ -2292,162 +2290,11 @@ static void read_push_options(struct packet_reader *reader,\n \t}\n }\n \n-static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n-{\n-\tswitch (read_pack_header(pack_fd, hdr)) {\n-\tcase PH_ERROR_EOF:\n-\t\treturn \"eof before pack header was fully read\";\n-\n-\tcase PH_ERROR_PACK_SIGNATURE:\n-\t\treturn \"protocol error (pack signature mismatch detected)\";\n-\n-\tcase PH_ERROR_PROTOCOL:\n-\t\treturn \"protocol error (pack version unsupported)\";\n-\n-\tdefault:\n-\t\treturn \"unknown error in parse_pack_header\";\n-\n-\tcase 0:\n-\t\treturn NULL;\n-\t}\n-}\n-\n-static struct tempfile *pack_lockfile;\n-\n-static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n-{\n-\tstrvec_pushf(args, \"--pack_header=%\"PRIu32\",%\"PRIu32,\n-\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n-}\n-\n-static unsigned int get_unpack_limit(struct repository *repo)\n-{\n-\tunsigned int limit = 100;\n-\n-\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n-\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n-\n-\treturn limit;\n-}\n-\n-struct unpack_opts {\n-\tconst char *fsck_msg_types;\n-\tconst char *shallow_file;\n-\toff_t max_input_size;\n-\tint fsck_objects;\n-\tint reject_thin;\n-\tint err_fd;\n-\tint quiet;\n-};\n-\n-static int unpack(struct odb_transaction *transaction, int pack_fd,\n-\t\t  struct strbuf *err_msg, const struct unpack_opts *opts)\n-{\n-\tstruct pack_header hdr;\n-\tconst char *hdr_err;\n-\tint status;\n-\tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint err_fd = opts->err_fd;\n-\n-\thdr_err = parse_pack_header(&hdr, pack_fd);\n-\tif (hdr_err) {\n-\t\tif (err_fd > 0)\n-\t\t\tclose(err_fd);\n-\t\tstrbuf_addstr(err_msg, hdr_err);\n-\t\treturn -1;\n-\t}\n-\n-\tif (opts->shallow_file) {\n-\t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, opts->shallow_file);\n-\t}\n-\n-\todb_transaction_env(transaction, &child.env);\n-\n-\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n-\t\tstrvec_push(&child.args, \"unpack-objects\");\n-\t\tpush_header_arg(&child.args, &hdr);\n-\t\tif (opts->quiet)\n-\t\t\tstrvec_push(&child.args, \"-q\");\n-\t\tif (opts->fsck_objects)\n-\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     opts->fsck_msg_types);\n-\t\tif (opts->max_input_size)\n-\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)opts->max_input_size);\n-\t\tchild.no_stdout = 1;\n-\t\tchild.in = pack_fd;\n-\t\tchild.err = err_fd;\n-\t\tchild.git_cmd = 1;\n-\t\tstatus = run_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n-\t\t\treturn -1;\n-\t\t}\n-\t} else {\n-\t\tchar hostname[HOST_NAME_MAX + 1];\n-\t\tchar *lockfile;\n-\n-\t\tstrvec_pushl(&child.args, \"index-pack\", \"--stdin\", NULL);\n-\t\tpush_header_arg(&child.args, &hdr);\n-\n-\t\tif (xgethostname(hostname, sizeof(hostname)))\n-\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n-\t\tstrvec_pushf(&child.args,\n-\t\t\t     \"--keep=receive-pack %\"PRIuMAX\" on %s\",\n-\t\t\t     (uintmax_t)getpid(),\n-\t\t\t     hostname);\n-\n-\t\tif (!opts->quiet && err_fd)\n-\t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n-\t\tif (err_fd)\n-\t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n-\t\tif (opts->fsck_objects)\n-\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     opts->fsck_msg_types);\n-\t\tif (!opts->reject_thin)\n-\t\t\tstrvec_push(&child.args, \"--fix-thin\");\n-\t\tif (opts->max_input_size)\n-\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)opts->max_input_size);\n-\t\tchild.out = -1;\n-\t\tchild.in = pack_fd;\n-\t\tchild.err = err_fd;\n-\t\tchild.git_cmd = 1;\n-\t\tstatus = start_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\t/*\n-\t\t * The lockfile filepath is expected to be the final location of\n-\t\t * the \".keep\" file after being migrated to the main ODB source.\n-\t\t * This ensures the lockfile can be found and removed later\n-\t\t * after the ODB transaction has been committed.\n-\t\t */\n-\t\tlockfile = index_pack_lockfile(transaction->source, child.out, NULL);\n-\t\tif (lockfile) {\n-\t\t\tpack_lockfile = register_tempfile(lockfile);\n-\t\t\tfree(lockfile);\n-\t\t}\n-\t\tclose(child.out);\n-\n-\t\tstatus = finish_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n-\t\t\treturn -1;\n-\t\t}\n-\t\todb_reprepare(the_repository->objects);\n-\t}\n-\treturn 0;\n-}\n-\n static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\t\t\tconst char *shallow_file,\n \t\t\t\tstruct strbuf *err_msg)\n {\n-\tstruct unpack_opts opts = {\n+\tstruct odb_transaction_write_pack_opts opts = {\n \t\t.fsck_objects = (receive_fsck_objects >= 0\n \t\t\t\t ? receive_fsck_objects\n \t\t\t\t : transfer_fsck_objects >= 0\n@@ -2463,7 +2310,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, 0, err_msg, &opts);\n+\t\treturn odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2473,7 +2320,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, 0, err_msg, &opts);\n+\tret = odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2748,7 +2595,6 @@ int cmd_receive_pack(int argc,\n \t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n \t\t\t\t &push_options);\n \t\todb_transaction_finalize(transaction);\n-\t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\n \t\t\treport_v2(commands, &unpack_status);\ndiff --git a/object-file.c b/object-file.c\nindex db63587f6d..a957bc126f 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -10,6 +10,7 @@\n #define USE_THE_REPOSITORY_VARIABLE\n \n #include \"git-compat-util.h\"\n+#include \"config.h\"\n #include \"convert.h\"\n #include \"dir.h\"\n #include \"environment.h\"\n@@ -26,6 +27,7 @@\n #include \"packfile.h\"\n #include \"path.h\"\n #include \"read-cache-ll.h\"\n+#include \"run-command.h\"\n #include \"setup.h\"\n #include \"strvec.h\"\n #include \"tempfile.h\"\n@@ -488,6 +490,10 @@ struct odb_transaction_files {\n \tstruct odb_source *quarantine;\n \tstruct transaction_packfile packfile;\n \tconst char *prefix;\n+\n+\tstruct tempfile **pack_lockfiles;\n+\tsize_t pack_lockfiles_nr;\n+\tsize_t pack_lockfiles_alloc;\n };\n \n int odb_transaction_files_prepare(struct odb_transaction *base)\n@@ -1291,6 +1297,170 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n \treturn 0;\n }\n \n+static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n+{\n+\tswitch (read_pack_header(pack_fd, hdr)) {\n+\tcase PH_ERROR_EOF:\n+\t\treturn \"eof before pack header was fully read\";\n+\n+\tcase PH_ERROR_PACK_SIGNATURE:\n+\t\treturn \"protocol error (pack signature mismatch detected)\";\n+\n+\tcase PH_ERROR_PROTOCOL:\n+\t\treturn \"protocol error (pack version unsupported)\";\n+\n+\tdefault:\n+\t\treturn \"unknown error in parse_pack_header\";\n+\n+\tcase 0:\n+\t\treturn NULL;\n+\t}\n+}\n+\n+static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n+{\n+\tstrvec_pushf(args, \"--pack_header=%\"PRIu32\",%\"PRIu32,\n+\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n+}\n+\n+static unsigned int get_unpack_limit(struct repository *repo)\n+{\n+\tunsigned int limit = 100;\n+\n+\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n+\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n+\n+\treturn limit;\n+}\n+\n+static int odb_transaction_files_write_pack(struct odb_transaction *base,\n+\t\t\t\t\t    int pack_fd, struct strbuf *err_msg,\n+\t\t\t\t\t    const struct odb_transaction_write_pack_opts *opts)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\tstruct repository *repo = base->source->odb->repo;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tstruct pack_header hdr;\n+\tconst char *hdr_err;\n+\tint err_fd = opts->err_fd;\n+\tint status;\n+\n+\thdr_err = parse_pack_header(&hdr, pack_fd);\n+\tif (hdr_err) {\n+\t\tif (err_fd > 0)\n+\t\t\tclose(err_fd);\n+\t\tstrbuf_addstr(err_msg, hdr_err);\n+\t\treturn -1;\n+\t}\n+\n+\tif (opts->shallow_file) {\n+\t\tstrvec_push(&child.args, \"--shallow-file\");\n+\t\tstrvec_push(&child.args, opts->shallow_file);\n+\t}\n+\n+\todb_transaction_env(base, &child.env);\n+\n+\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(repo)) {\n+\t\tstrvec_push(&child.args, \"unpack-objects\");\n+\t\tpush_header_arg(&child.args, &hdr);\n+\t\tif (opts->quiet)\n+\t\t\tstrvec_push(&child.args, \"-q\");\n+\t\tif (opts->fsck_objects)\n+\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (opts->max_input_size)\n+\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n+\t\tchild.no_stdout = 1;\n+\t\tchild.in = pack_fd;\n+\t\tchild.err = err_fd;\n+\t\tchild.git_cmd = 1;\n+\t\tstatus = run_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n+\t} else {\n+\t\tchar hostname[HOST_NAME_MAX + 1];\n+\t\tchar *lockfile;\n+\n+\t\tstrvec_pushl(&child.args, \"index-pack\", \"--stdin\", NULL);\n+\t\tpush_header_arg(&child.args, &hdr);\n+\n+\t\tif (xgethostname(hostname, sizeof(hostname)))\n+\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n+\t\tstrvec_pushf(&child.args,\n+\t\t\t     \"--keep=receive-pack %\"PRIuMAX\" on %s\",\n+\t\t\t     (uintmax_t)getpid(),\n+\t\t\t     hostname);\n+\n+\t\tif (!opts->quiet && err_fd)\n+\t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n+\t\tif (err_fd)\n+\t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n+\t\tif (opts->fsck_objects)\n+\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (!opts->reject_thin)\n+\t\t\tstrvec_push(&child.args, \"--fix-thin\");\n+\t\tif (opts->max_input_size)\n+\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n+\t\tchild.out = -1;\n+\t\tchild.in = pack_fd;\n+\t\tchild.err = err_fd;\n+\t\tchild.git_cmd = 1;\n+\t\tstatus = start_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\t/*\n+\t\t * The lockfile filepath is expected to be the final location of\n+\t\t * the \".keep\" file after being migrated to the main ODB source.\n+\t\t * This ensures the lockfile can be found and removed later\n+\t\t * after the ODB transaction has been committed.\n+\t\t */\n+\t\tlockfile = index_pack_lockfile(base->source, child.out, NULL);\n+\t\tif (lockfile) {\n+\t\t\tALLOC_GROW(transaction->pack_lockfiles,\n+\t\t\t\t   transaction->pack_lockfiles_nr + 1,\n+\t\t\t\t   transaction->pack_lockfiles_alloc);\n+\t\t\ttransaction->pack_lockfiles[transaction->pack_lockfiles_nr++] =\n+\t\t\t\tregister_tempfile(lockfile);\n+\t\t\tfree(lockfile);\n+\t\t}\n+\t\tclose(child.out);\n+\n+\t\tstatus = finish_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\todb_source_prepare(transaction->quarantine,\n+\t\t\t\t   ODB_PREPARE_FLUSH_CACHES);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int odb_transaction_files_finalize(struct odb_transaction *base)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\tint ret = 0;\n+\n+\tfor (size_t i = 0; i < transaction->pack_lockfiles_nr; i++)\n+\t\tret |= delete_tempfile(&transaction->pack_lockfiles[i]);\n+\n+\tfree(transaction->pack_lockfiles);\n+\n+\treturn ret;\n+}\n+\n static int odb_transaction_files_env(struct odb_transaction *base,\n \t\t\t\t     struct strvec *env)\n {\n@@ -1314,7 +1484,9 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction = xcalloc(1, sizeof(*transaction));\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n+\ttransaction->base.finalize = odb_transaction_files_finalize;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\ttransaction->base.write_pack = odb_transaction_files_write_pack;\n \ttransaction->base.env = odb_transaction_files_env;\n \n \ttransaction->prefix = \"bulk-fsync\";\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex 9e9a982778..c9144e6cd6 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -59,6 +59,13 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n \treturn transaction->write_object_stream(transaction, stream, len, oid);\n }\n \n+int odb_transaction_write_pack(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t       struct strbuf *err_msg,\n+\t\t\t       const struct odb_transaction_write_pack_opts *opts)\n+{\n+\treturn transaction->write_pack(transaction, pack_fd, err_msg, opts);\n+}\n+\n int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env)\n {\n \tif (!transaction)\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 6ed39b3d0e..8cb06c1191 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -4,6 +4,50 @@\n #include \"gettext.h\"\n #include \"odb.h\"\n \n+/*\n+ * Options controlling how odb_transaction_write_pack() ingests a packfile.\n+ */\n+struct odb_transaction_write_pack_opts {\n+\t/*\n+\t * Optional fsck severity configuration to apply when incoming objects\n+\t * are verified.\n+\t */\n+\tconst char *fsck_msg_types;\n+\n+\t/*\n+\t * Path to an alternative shallow file describing the shallow boundaries\n+\t * to honor while ingesting the pack.\n+\t */\n+\tconst char *shallow_file;\n+\n+\t/*\n+\t * The max size in bytes of the incoming packfile allowed. No limit is\n+\t * enforced when set to 0.\n+\t */\n+\toff_t max_input_size;\n+\n+\t/*\n+\t * Whether the validity of incoming objects should be verified.\n+\t */\n+\tint fsck_objects;\n+\n+\t/*\n+\t * Whether to reject an incoming packfile if it is \"thin\".\n+\t */\n+\tint reject_thin;\n+\n+\t/*\n+\t * Optional file descriptor for reporting progress and errors. Set to 0\n+\t * for none.\n+\t */\n+\tint err_fd;\n+\n+\t/*\n+\t * Suppresses progress reporting.\n+\t */\n+\tint quiet;\n+};\n+\n /*\n  * A transaction may be started for an object database prior to writing new\n  * objects via odb_transaction_begin(). These objects are not committed until\n@@ -40,6 +84,15 @@ 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+\t/*\n+\t * This callback is expected to ingest the packfile readable via\n+\t * `pack_fd` into the transaction. Returns 0 on success, a negative\n+\t * error code otherwise. On failure, a human-readable description is\n+\t * appended to `err_msg`.\n+\t */\n+\tint (*write_pack)(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t  struct strbuf *err_msg,\n+\t\t\t  const struct odb_transaction_write_pack_opts *opts);\n \n \t/*\n \t * This callback is expected to populate the provided strvec with the\n@@ -107,6 +160,15 @@ 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+ * Ingests the packfile readable via `pack_fd` into the transaction. Returns 0\n+ * on success, a negative error code otherwise. On failure, a human-readable\n+ * description is appended to `err_msg`.\n+ */\n+int odb_transaction_write_pack(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t       struct strbuf *err_msg,\n+\t\t\t       const struct odb_transaction_write_pack_opts *opts);\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-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550342","messageId":"anwNonpw5SZuHADv@pks.im","threadId":"66133","inReplyTo":"20260811175415.2044235-2-jltobler@gmail.com","subject":"Re: [PATCH v3 1/9] builtin/receive-pack: properly clean up keep files","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-12T06:07:30Z","receivedAt":"2026-08-12T06:07:37Z","isPatch":true,"body":"On Tue, Aug 11, 2026 at 12:54:07PM -0500, Justin Tobler wrote:\n> When git-receive-pack(1) stores an incoming packfile with\n> git-index-pack(1), a \".keep\" file is written alongside it to hold the\n> pack in place until the references have been updated, and is removed\n> afterwards. The path used to remove it is derived via\n> `index_pack_lockfile()` from the repository's primary object directory.\n> \n> In bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\n> transactions, 2026-07-10), git-receive-pack(1) started using the ODB\n> transaction interfaces instead of managing a temporary directory\n> directly. When starting an ODB transaction, the sources list is\n> reordered to insert the newly created transaction source first as the\n> primary to ensure writes are routed to it accordingly.\n> \n> Prior to using ODB transactions, git-receive-pack(1) would only set the\n> temporary directory as the primary source for the child\n> git-index-pack(1) and git-unpack-objects(1) processes it spawned and the\n> parent process would set the temporary directory set as an alternate\n> only. By using ODB transactions, the ODB source list is also reordered\n> for the parent process which results in `index_pack_lockfile()` deriving\n> the \".keep\" path relative to the temporary directory instead the actual\n\nNit: s/instead/& of/\n\n> main ODB source path. Consequently, this prevents the \".keep\" file from\n> being properly removed after being migrated into the main ODB source\n> post-commit.\n\nHm. Are the temporary packs written into the transaction-managed tempdir\nnow, or do they still end up in the main object directory?\n\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index 86933d8d7e..d74b787148 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -2412,7 +2412,13 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n>  \t\tif (status)\n>  \t\t\treturn \"index-pack fork failed\";\n>  \n> -\t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n> +\t\t/*\n> +\t\t * The lockfile filepath is expected to be the final location of\n> +\t\t * the \".keep\" file after being migrated to the main ODB source.\n> +\t\t * This ensures the lockfile can be found and removed later\n> +\t\t * after the ODB transaction has been committed.\n> +\t\t */\n> +\t\tlockfile = index_pack_lockfile(transaction->source, child.out, NULL);\n>  \t\tif (lockfile) {\n>  \t\t\tpack_lockfile = register_tempfile(lockfile);\n>  \t\t\tfree(lockfile);\n\nOkay. So previously, we wrote the \".keep\" file into the main repository,\nwhereas now we write it into the temporary object directory? Is the\npackfile itself also written in there?\n\nWhat I'm wondering is why we even need a \".keep\" file at all anymore if\nwe're not storing it in the main object directory. It wouldn't help us\nto avoid the race, because after committing the transaction the \".keep\"\nfile would remain in the temporary directory, whereas the packfile would\nhave been migrated to the main object directory. So it doesn't have a\n\".keep\" file at that point, and neither have references been updated to\npoint to the new objects yet.\n\nSo I wonder whether instead, we'd have to:\n\n  1. Start the transaction, creating the temporary object directory.\n  \n  2. Write the packfile into the temporary object directory, but don't\n     create a \".keep\" file.\n\n  3. At commit time, first write a \".keep\" file in the main object\n     directory and then migrate the packfile over.\n\n  4. At finalization time, prune the \".keep\" file from the main object\n     directory.\n\nThat would retain the current properties of the system, but as far as I\ncan see this is not what we're doing here.\n\n> diff --git a/pack.h b/pack.h\n> index 1cde92082b..68dcf08cf3 100644\n> --- a/pack.h\n> +++ b/pack.h\n> @@ -3,6 +3,7 @@\n>  \n>  #include \"object.h\"\n>  #include \"csum-file.h\"\n> +#include \"odb/source.h\"\n>  \n>  struct packed_git;\n>  struct pack_window;\n\nLet's add a forward declaration instead of including this header.\n\n> diff --git a/t/t5547-push-quarantine.sh b/t/t5547-push-quarantine.sh\n> index 0798ddab02..400a597606 100755\n> --- a/t/t5547-push-quarantine.sh\n> +++ b/t/t5547-push-quarantine.sh\n> @@ -70,4 +70,18 @@ test_expect_success 'updating a ref from quarantine is forbidden' '\n>  \tgit -C update.git fsck\n>  '\n>  \n> +test_expect_success '.keep file is removed after push' '\n> +\ttest_when_finished rm -rf keep.git &&\n> +\tgit init --bare keep.git &&\n> +\n> +\tgit -C keep.git config set receive.unpackLimit 0 &&\n> +\ttest_commit foo &&\n> +\tgit push keep.git HEAD &&\n> +\tpack=\"$(ls keep.git/objects/pack/pack-*.pack)\" &&\n> +\tkeep=\"${pack%.pack}.keep\" &&\n> +\n> +\ttest_path_is_file \"$pack\" &&\n> +\ttest_path_is_missing \"$keep\"\n> +'\n\nThis would feel a bit safer if we had a hook that verifies that we\nindeed have the \".keep\" file in the right spot before committing\neverything.\n\nThanks!\n\nPatrick\n"},{"id":"550343","messageId":"anwNp8cbCOOuI7nK@pks.im","threadId":"66133","inReplyTo":"20260811175415.2044235-3-jltobler@gmail.com","subject":"Re: [PATCH v3 2/9] odb/transaction: add transaction finalize interface","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-12T06:07:35Z","receivedAt":"2026-08-12T06:07:41Z","isPatch":true,"body":"On Tue, Aug 11, 2026 at 12:54:08PM -0500, Justin Tobler wrote:\n> When committing an ODB transaction via `odb_transaction_commit()`, the\n> staged objects are made visible and the underlying transaction is freed\n> at the same time. Coupling these two steps does not leave room for any\n> post-commit transaction operations to be introduced though. Such a\n> capability is useful if an ODB transaction backend needs to hold on to\n> lockfiles after transaction commit until references are updated, as is\n> the case with the existing \"files\" backend in git-receive-pack(1).\n> \n> Stop freeing the transaction in `odb_transaction_commit()` and introduce\n> `odb_transaction_finalize()` to explicitly clean up the transaction\n> accordingly. Note that the finalize interface also provides an optional\n> callback for any backend-specific deferred cleanup. In a subsequent\n> commit, the \"files\" transaction backend will use this to remove \".keep\"\n> files generated for packfiles received via git-receive-pack(1) after\n> references have been updated. In preparation for this, the\n> `odb_transaction_finalize()` call site in git-receive-pack(1) is made\n> after the reference updates are finished.\n> \n> All other callers commit a transaction and immediately finalize it with\n> no work in between and cannot meaningfully recover should either step\n> fail, so introduce an `odb_transaction_commit_and_finalize_or_die()`\n\n\"step fail\"? I guess this ought to just read \"fail\"?\n\n> helper that performs both and dies on error. Call sites are updated\n> accordingly.\n> \n> Signed-off-by: Justin Tobler <jltobler@gmail.com>\n> ---\n>  builtin/add.c            |  4 ++--\n>  builtin/receive-pack.c   |  1 +\n>  builtin/unpack-objects.c |  2 +-\n>  builtin/update-index.c   |  4 ++--\n>  cache-tree.c             |  2 +-\n>  object-file.c            |  2 +-\n>  odb/transaction.c        | 14 ++++++++++++++\n>  odb/transaction.h        | 23 +++++++++++++++++++++++\n>  read-cache.c             |  2 +-\n>  9 files changed, 46 insertions(+), 8 deletions(-)\n> \n> diff --git a/builtin/add.c b/builtin/add.c\n> index 60ffbede2b..ad418a5952 100644\n> --- a/builtin/add.c\n> +++ b/builtin/add.c\n> @@ -393,7 +393,7 @@ int cmd_add(int argc,\n>  \tchar *seen = NULL;\n>  \tchar *ps_matched = NULL;\n>  \tstruct lock_file lock_file = LOCK_INIT;\n> -\tstruct odb_transaction *transaction;\n> +\tstruct odb_transaction *transaction = NULL;\n>  \n>  \trepo_config(repo, add_config, NULL);\n>  \n> @@ -600,7 +600,7 @@ int cmd_add(int argc,\n>  \n>  \tif (chmod_arg && pathspec.nr)\n>  \t\texit_status |= chmod_pathspec(repo, &pathspec, chmod_arg[0], show_only);\n> -\todb_transaction_commit(transaction);\n> +\todb_transaction_commit_and_finalize_or_die(transaction);\n>  \n>  finish:\n>  \tif (write_locked_index(repo->index, &lock_file,\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index d74b787148..ed1edcbe93 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -2720,6 +2720,7 @@ int cmd_receive_pack(int argc,\n>  \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n>  \t\texecute_commands(commands, unpack_status, &si, transaction,\n>  \t\t\t\t &push_options);\n> +\t\todb_transaction_finalize(transaction);\n>  \t\tdelete_tempfile(&pack_lockfile);\n>  \t\tsigchain_push(SIGPIPE, SIG_IGN);\n>  \t\tif (report_status_v2)\n> diff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\n> index 4263edfbec..d6a2d616d9 100644\n> --- a/builtin/unpack-objects.c\n> +++ b/builtin/unpack-objects.c\n> @@ -603,7 +603,7 @@ static void unpack_all(void)\n>  \t\tunpack_one(i);\n>  \t\tdisplay_progress(progress, i + 1);\n>  \t}\n> -\todb_transaction_commit(transaction);\n> +\todb_transaction_commit_and_finalize_or_die(transaction);\n>  \tstop_progress(&progress);\n>  \n>  \tif (delta_list)\n> diff --git a/builtin/update-index.c b/builtin/update-index.c\n> index 241abd4332..b25d4ecb10 100644\n> --- a/builtin/update-index.c\n> +++ b/builtin/update-index.c\n> @@ -1156,7 +1156,7 @@ int cmd_update_index(int argc,\n>  \t\t\t * a transaction.\n>  \t\t\t */\n>  \t\t\tif (transaction && verbose) {\n> -\t\t\t\todb_transaction_commit(transaction);\n> +\t\t\t\todb_transaction_commit_and_finalize_or_die(transaction);\n>  \t\t\t\ttransaction = NULL;\n>  \t\t\t}\n>  \n> @@ -1224,7 +1224,7 @@ int cmd_update_index(int argc,\n>  \t/*\n>  \t * By now we have added all of the new objects\n>  \t */\n> -\todb_transaction_commit(transaction);\n> +\todb_transaction_commit_and_finalize_or_die(transaction);\n>  \n>  \tif (split_index > 0) {\n>  \t\tif (repo_config_get_split_index(the_repository) == 0)\n> diff --git a/cache-tree.c b/cache-tree.c\n> index d92f513286..a220372a42 100644\n> --- a/cache-tree.c\n> +++ b/cache-tree.c\n> @@ -538,7 +538,7 @@ int cache_tree_update(struct index_state *istate, int flags)\n>  \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n>  \t\t       \"\", 0, &skip, flags);\n>  \tif (!inflight)\n> -\t\todb_transaction_commit(transaction);\n> +\t\todb_transaction_commit_and_finalize_or_die(transaction);\n>  \ttrace2_region_leave(\"cache_tree\", \"update\", istate->repo);\n>  \ttrace_performance_leave(\"cache_tree_update\");\n>  \tif (i < 0)\n> diff --git a/object-file.c b/object-file.c\n> index ec35c318bc..4d03c167d5 100644\n> --- a/object-file.c\n> +++ b/object-file.c\n> @@ -965,7 +965,7 @@ int index_fd(struct index_state *istate, struct object_id *oid,\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\tif (!inflight)\n> -\t\t\t\todb_transaction_commit(transaction);\n> +\t\t\t\todb_transaction_commit_and_finalize_or_die(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> diff --git a/odb/transaction.c b/odb/transaction.c\n> index dab7da6a9a..9e9a982778 100644\n> --- a/odb/transaction.c\n> +++ b/odb/transaction.c\n> @@ -33,6 +33,20 @@ int odb_transaction_commit(struct odb_transaction *transaction)\n>  \n>  \tret = transaction->commit(transaction);\n>  \ttransaction->source->odb->transaction = NULL;\n> +\n> +\treturn ret;\n> +}\n> +\n> +int odb_transaction_finalize(struct odb_transaction *transaction)\n> +{\n> +\tint ret = 0;\n> +\n> +\tif (!transaction)\n> +\t\treturn 0;\n> +\n> +\tif (transaction->finalize)\n> +\t\tret = transaction->finalize(transaction);\n> +\n>  \tfree(transaction);\n>  \n>  \treturn ret;\n> diff --git a/odb/transaction.h b/odb/transaction.h\n> index 4cb2eafcbf..6ed39b3d0e 100644\n> --- a/odb/transaction.h\n> +++ b/odb/transaction.h\n> @@ -22,6 +22,13 @@ struct odb_transaction {\n>  \t */\n>  \tint (*commit)(struct odb_transaction *transaction);\n>  \n> +\t/*\n> +\t * Optional ODB source specific callback invoked when the transaction\n> +\t * needs to perform any deferred cleanup after objects have been\n> +\t * committed. Returns 0 on success, a negative error code otherwise.\n> +\t */\n> +\tint (*finalize)(struct odb_transaction *transaction);\n> +\n>  \t/*\n>  \t * This callback is expected to write the given object stream into\n>  \t * the ODB transaction. Note that for now, only blobs support streaming.\n> @@ -75,6 +82,22 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n>   */\n>  int odb_transaction_commit(struct odb_transaction *transaction);\n>  \n> +/*\n> + * Finalizes an ODB transaction, performing any deferred cleanup and freeing it.\n> + * Must be called for every successfully started transaction. Note that, if the\n> + * specified transaction is NULL, the function is a no-op. Returns 0 on success,\n> + * a negative error code otherwise.\n> + */\n> +int odb_transaction_finalize(struct odb_transaction *transaction);\n> +\n> +static inline void odb_transaction_commit_and_finalize_or_die(struct odb_transaction *transaction)\n> +{\n> +\tif (odb_transaction_commit(transaction))\n> +\t\tdie(_(\"failed to commit ODB transaction\"));\n> +\tif (odb_transaction_finalize(transaction))\n> +\t\tdie(_(\"failed to finalize ODB transaction\"));\n> +}\n> +\n>  /*\n>   * Writes the object in the provided stream into the transaction. The resulting\n>   * object ID is written into the out pointer. Returns 0 on success, a negative\n> diff --git a/read-cache.c b/read-cache.c\n> index 6c449f393d..0cd0ef85ec 100644\n> --- a/read-cache.c\n> +++ b/read-cache.c\n> @@ -4049,7 +4049,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\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> +\t\todb_transaction_commit_and_finalize_or_die(transaction);\n>  \n>  \trelease_revisions(&rev);\n>  \treturn !!data.add_errors;\n> -- \n> 2.55.0.424.g13c7afec21\n> \n"},{"id":"550344","messageId":"anwNsTpB7XwZa8hh@pks.im","threadId":"66133","inReplyTo":"20260811175415.2044235-9-jltobler@gmail.com","subject":"Re: [PATCH v3 8/9] odb: return temporary ODB source when set","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-12T06:07:45Z","receivedAt":"2026-08-12T06:07:50Z","isPatch":true,"body":"On Tue, Aug 11, 2026 at 12:54:14PM -0500, Justin Tobler wrote:\n> When invoked, `odb_set_temporary_primary_source()` installs a temporary\n> object directory as the new primary ODB source. A caller that wants to\n> operate on the ODB source of the open transaction must assume that it is\n> the first entry in the ODB source list which is a bit awkward and\n> fragile.\n> \n> Instead, return the newly installed source directly and report the\n> previous primary source via a new `prev_source` out parameter. Propagate\n> the installed source through `tmp_objdir_replace_primary_odb()` and\n> start storing it in the \"files\" ODB transaction so a subsequent commit\n> can easily access it without relying on the ODB source list ordering.\n\nMakes sense. I'm looking forward to the day where we get rid of this\nmechanism altogether.\n\nPatrick\n"},{"id":"550582","messageId":"an41gSCa7EFGkB1r@denethor","threadId":"66133","inReplyTo":"anwNonpw5SZuHADv@pks.im","subject":"Re: [PATCH v3 1/9] builtin/receive-pack: properly clean up keep files","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-13T21:45:16Z","receivedAt":"2026-08-13T21:45:21Z","isPatch":true,"body":"On 26/08/12 08:07AM, Patrick Steinhardt wrote:\n> On Tue, Aug 11, 2026 at 12:54:07PM -0500, Justin Tobler wrote:\n> > When git-receive-pack(1) stores an incoming packfile with\n> > git-index-pack(1), a \".keep\" file is written alongside it to hold the\n> > pack in place until the references have been updated, and is removed\n> > afterwards. The path used to remove it is derived via\n> > `index_pack_lockfile()` from the repository's primary object directory.\n> > \n> > In bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\n> > transactions, 2026-07-10), git-receive-pack(1) started using the ODB\n> > transaction interfaces instead of managing a temporary directory\n> > directly. When starting an ODB transaction, the sources list is\n> > reordered to insert the newly created transaction source first as the\n> > primary to ensure writes are routed to it accordingly.\n> > \n> > Prior to using ODB transactions, git-receive-pack(1) would only set the\n> > temporary directory as the primary source for the child\n> > git-index-pack(1) and git-unpack-objects(1) processes it spawned and the\n> > parent process would set the temporary directory set as an alternate\n> > only. By using ODB transactions, the ODB source list is also reordered\n> > for the parent process which results in `index_pack_lockfile()` deriving\n> > the \".keep\" path relative to the temporary directory instead the actual\n> \n> Nit: s/instead/& of/\n\nWill fix.\n\n> > main ODB source path. Consequently, this prevents the \".keep\" file from\n> > being properly removed after being migrated into the main ODB source\n> > post-commit.\n> \n> Hm. Are the temporary packs written into the transaction-managed tempdir\n> now, or do they still end up in the main object directory?\n\nThe packfile and associated \".keep\" lockfiles are both initially written\ninto the temporary directory managed by the ODB transaction. On\ntransaction commit, they are then both migrated to the main ODB.\n\nWhen registering the keep tempfile, we need to record the future\npost-commit location of the keep file that way it can be removed when\n`odb_transaction_finalize()` is invoked. This matches the original\nbehavior prior to ODB transaction being introduced in\ngit-receive-pack(1).\n\n> > diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> > index 86933d8d7e..d74b787148 100644\n> > --- a/builtin/receive-pack.c\n> > +++ b/builtin/receive-pack.c\n> > @@ -2412,7 +2412,13 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n> >  \t\tif (status)\n> >  \t\t\treturn \"index-pack fork failed\";\n> >  \n> > -\t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n> > +\t\t/*\n> > +\t\t * The lockfile filepath is expected to be the final location of\n> > +\t\t * the \".keep\" file after being migrated to the main ODB source.\n> > +\t\t * This ensures the lockfile can be found and removed later\n> > +\t\t * after the ODB transaction has been committed.\n> > +\t\t */\n> > +\t\tlockfile = index_pack_lockfile(transaction->source, child.out, NULL);\n> >  \t\tif (lockfile) {\n> >  \t\t\tpack_lockfile = register_tempfile(lockfile);\n> >  \t\t\tfree(lockfile);\n> \n> Okay. So previously, we wrote the \".keep\" file into the main repository,\n> whereas now we write it into the temporary object directory? Is the\n> packfile itself also written in there?\n\nNot quite, both the packfile and keep file were written to the temporary\ndirectory and continue to do so.\n\nPrior to bdee7b3013 (builtin/receive-pack: stage incoming objects via\nODB transactions, 2026-07-10), the \".keep\" files were also being written\nto the quarantine directory and migrated alongside the packfiles. The\nmain git-receive-pack(1) process always kept the primary ODB as the\nfirst entry in the source list though ensuring that the \"filename\"\nregistered for keep tempfile was the final location. With ODB\ntransactions though, the source list order _does_ get changed and\nresulted in the keep tempfile not knowing about its final location.\nConsequently, it is no longer cleaned up.\n\n> What I'm wondering is why we even need a \".keep\" file at all anymore if\n> we're not storing it in the main object directory. It wouldn't help us\n> to avoid the race, because after committing the transaction the \".keep\"\n> file would remain in the temporary directory, whereas the packfile would\n> have been migrated to the main object directory. So it doesn't have a\n> \".keep\" file at that point, and neither have references been updated to\n> point to the new objects yet.\n\nThe \".keep\" file does end up in the main ODB alongside the packfile when\nthe transaction is committed. The main problem here is that it is not\nbeing cleaned up because the post-migration path does not match what the\nregistered tempfile tracks.\n\n> So I wonder whether instead, we'd have to:\n> \n>   1. Start the transaction, creating the temporary object directory.\n>   \n>   2. Write the packfile into the temporary object directory, but don't\n>      create a \".keep\" file.\n> \n>   3. At commit time, first write a \".keep\" file in the main object\n>      directory and then migrate the packfile over.\n> \n>   4. At finalization time, prune the \".keep\" file from the main object\n>      directory.\n> \n> That would retain the current properties of the system, but as far as I\n> can see this is not what we're doing here.\n\nWith this patch, this is effectly what we are doing already. The main\ndifference is that we are creating the \".keep\" file alongside the\npackfile via git-index-pack(1) and migrating both when\n`odb_transaction_commit()` is invoked.\n\nWe could stop relying on git-index-pack(1) to generate the \".keep\" file\nand instead generate it ourselves during the commit phase as you\nsuggested, but I'm not sure that would really buy us anything right now.\nFor now, I think it would be fine to keep the changes more minimal.\n\nI'll try to clarify the commit message a bit in the next version to\nbetter explain what is happening.\n\n> > diff --git a/pack.h b/pack.h\n> > index 1cde92082b..68dcf08cf3 100644\n> > --- a/pack.h\n> > +++ b/pack.h\n> > @@ -3,6 +3,7 @@\n> >  \n> >  #include \"object.h\"\n> >  #include \"csum-file.h\"\n> > +#include \"odb/source.h\"\n> >  \n> >  struct packed_git;\n> >  struct pack_window;\n> \n> Let's add a forward declaration instead of including this header.\n\nWill do.\n\n> > diff --git a/t/t5547-push-quarantine.sh b/t/t5547-push-quarantine.sh\n> > index 0798ddab02..400a597606 100755\n> > --- a/t/t5547-push-quarantine.sh\n> > +++ b/t/t5547-push-quarantine.sh\n> > @@ -70,4 +70,18 @@ test_expect_success 'updating a ref from quarantine is forbidden' '\n> >  \tgit -C update.git fsck\n> >  '\n> >  \n> > +test_expect_success '.keep file is removed after push' '\n> > +\ttest_when_finished rm -rf keep.git &&\n> > +\tgit init --bare keep.git &&\n> > +\n> > +\tgit -C keep.git config set receive.unpackLimit 0 &&\n> > +\ttest_commit foo &&\n> > +\tgit push keep.git HEAD &&\n> > +\tpack=\"$(ls keep.git/objects/pack/pack-*.pack)\" &&\n> > +\tkeep=\"${pack%.pack}.keep\" &&\n> > +\n> > +\ttest_path_is_file \"$pack\" &&\n> > +\ttest_path_is_missing \"$keep\"\n> > +'\n> \n> This would feel a bit safer if we had a hook that verifies that we\n> indeed have the \".keep\" file in the right spot before committing\n> everything.\n\nI'll try to set something up in the next version. Thanks.\n\n-Justin\n"},{"id":"550596","messageId":"an7H2C3JKqEdbGXQ@pks.im","threadId":"66133","inReplyTo":"an41gSCa7EFGkB1r@denethor","subject":"Re: [PATCH v3 1/9] builtin/receive-pack: properly clean up keep files","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-14T07:46:32Z","receivedAt":"2026-08-14T07:46:42Z","isPatch":true,"body":"On Thu, Aug 13, 2026 at 04:45:16PM -0500, Justin Tobler wrote:\n> On 26/08/12 08:07AM, Patrick Steinhardt wrote:\n> > On Tue, Aug 11, 2026 at 12:54:07PM -0500, Justin Tobler wrote:\n> > > When git-receive-pack(1) stores an incoming packfile with\n> > > git-index-pack(1), a \".keep\" file is written alongside it to hold the\n> > > pack in place until the references have been updated, and is removed\n> > > afterwards. The path used to remove it is derived via\n> > > `index_pack_lockfile()` from the repository's primary object directory.\n> > > \n> > > In bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\n> > > transactions, 2026-07-10), git-receive-pack(1) started using the ODB\n> > > transaction interfaces instead of managing a temporary directory\n> > > directly. When starting an ODB transaction, the sources list is\n> > > reordered to insert the newly created transaction source first as the\n> > > primary to ensure writes are routed to it accordingly.\n> > > \n> > > Prior to using ODB transactions, git-receive-pack(1) would only set the\n> > > temporary directory as the primary source for the child\n> > > git-index-pack(1) and git-unpack-objects(1) processes it spawned and the\n> > > parent process would set the temporary directory set as an alternate\n> > > only. By using ODB transactions, the ODB source list is also reordered\n> > > for the parent process which results in `index_pack_lockfile()` deriving\n> > > the \".keep\" path relative to the temporary directory instead the actual\n> > \n> > Nit: s/instead/& of/\n> \n> Will fix.\n> \n> > > main ODB source path. Consequently, this prevents the \".keep\" file from\n> > > being properly removed after being migrated into the main ODB source\n> > > post-commit.\n> > \n> > Hm. Are the temporary packs written into the transaction-managed tempdir\n> > now, or do they still end up in the main object directory?\n> \n> The packfile and associated \".keep\" lockfiles are both initially written\n> into the temporary directory managed by the ODB transaction. On\n> transaction commit, they are then both migrated to the main ODB.\n> \n> When registering the keep tempfile, we need to record the future\n> post-commit location of the keep file that way it can be removed when\n> `odb_transaction_finalize()` is invoked. This matches the original\n> behavior prior to ODB transaction being introduced in\n> git-receive-pack(1).\n> \n> > > diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> > > index 86933d8d7e..d74b787148 100644\n> > > --- a/builtin/receive-pack.c\n> > > +++ b/builtin/receive-pack.c\n> > > @@ -2412,7 +2412,13 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n> > >  \t\tif (status)\n> > >  \t\t\treturn \"index-pack fork failed\";\n> > >  \n> > > -\t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n> > > +\t\t/*\n> > > +\t\t * The lockfile filepath is expected to be the final location of\n> > > +\t\t * the \".keep\" file after being migrated to the main ODB source.\n> > > +\t\t * This ensures the lockfile can be found and removed later\n> > > +\t\t * after the ODB transaction has been committed.\n> > > +\t\t */\n> > > +\t\tlockfile = index_pack_lockfile(transaction->source, child.out, NULL);\n> > >  \t\tif (lockfile) {\n> > >  \t\t\tpack_lockfile = register_tempfile(lockfile);\n> > >  \t\t\tfree(lockfile);\n> > \n> > Okay. So previously, we wrote the \".keep\" file into the main repository,\n> > whereas now we write it into the temporary object directory? Is the\n> > packfile itself also written in there?\n> \n> Not quite, both the packfile and keep file were written to the temporary\n> directory and continue to do so.\n> \n> Prior to bdee7b3013 (builtin/receive-pack: stage incoming objects via\n> ODB transactions, 2026-07-10), the \".keep\" files were also being written\n> to the quarantine directory and migrated alongside the packfiles. The\n> main git-receive-pack(1) process always kept the primary ODB as the\n> first entry in the source list though ensuring that the \"filename\"\n> registered for keep tempfile was the final location. With ODB\n> transactions though, the source list order _does_ get changed and\n> resulted in the keep tempfile not knowing about its final location.\n> Consequently, it is no longer cleaned up.\n> \n> > What I'm wondering is why we even need a \".keep\" file at all anymore if\n> > we're not storing it in the main object directory. It wouldn't help us\n> > to avoid the race, because after committing the transaction the \".keep\"\n> > file would remain in the temporary directory, whereas the packfile would\n> > have been migrated to the main object directory. So it doesn't have a\n> > \".keep\" file at that point, and neither have references been updated to\n> > point to the new objects yet.\n> \n> The \".keep\" file does end up in the main ODB alongside the packfile when\n> the transaction is committed. The main problem here is that it is not\n> being cleaned up because the post-migration path does not match what the\n> registered tempfile tracks.\n> \n> > So I wonder whether instead, we'd have to:\n> > \n> >   1. Start the transaction, creating the temporary object directory.\n> >   \n> >   2. Write the packfile into the temporary object directory, but don't\n> >      create a \".keep\" file.\n> > \n> >   3. At commit time, first write a \".keep\" file in the main object\n> >      directory and then migrate the packfile over.\n> > \n> >   4. At finalization time, prune the \".keep\" file from the main object\n> >      directory.\n> > \n> > That would retain the current properties of the system, but as far as I\n> > can see this is not what we're doing here.\n> \n> With this patch, this is effectly what we are doing already. The main\n> difference is that we are creating the \".keep\" file alongside the\n> packfile via git-index-pack(1) and migrating both when\n> `odb_transaction_commit()` is invoked.\n> \n> We could stop relying on git-index-pack(1) to generate the \".keep\" file\n> and instead generate it ourselves during the commit phase as you\n> suggested, but I'm not sure that would really buy us anything right now.\n> For now, I think it would be fine to keep the changes more minimal.\n> \n> I'll try to clarify the commit message a bit in the next version to\n> better explain what is happening.\n\nThanks for the explanation, this helped a lot!\n\nPatrick\n"},{"id":"550600","messageId":"an7XAyQr7PrPlAGO@pks.im","threadId":"66133","inReplyTo":"20260811175415.2044235-10-jltobler@gmail.com","subject":"Re: [PATCH v3 9/9] odb/transaction: add transaction interface to write packfiles","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-14T08:51:24Z","receivedAt":"2026-08-14T08:51:36Z","isPatch":true,"body":"On Tue, Aug 11, 2026 at 12:54:15PM -0500, Justin Tobler wrote:\n  \t\t\treport_v2(commands, &unpack_status);\n> diff --git a/object-file.c b/object-file.c\n> index db63587f6d..a957bc126f 100644\n> --- a/object-file.c\n> +++ b/object-file.c\n> @@ -1291,6 +1297,170 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n>  \treturn 0;\n>  }\n>  \n> +static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n> +{\n> +\tswitch (read_pack_header(pack_fd, hdr)) {\n> +\tcase PH_ERROR_EOF:\n> +\t\treturn \"eof before pack header was fully read\";\n> +\n> +\tcase PH_ERROR_PACK_SIGNATURE:\n> +\t\treturn \"protocol error (pack signature mismatch detected)\";\n> +\n> +\tcase PH_ERROR_PROTOCOL:\n> +\t\treturn \"protocol error (pack version unsupported)\";\n> +\n> +\tdefault:\n> +\t\treturn \"unknown error in parse_pack_header\";\n> +\n> +\tcase 0:\n> +\t\treturn NULL;\n> +\t}\n> +}\n> +\n> +static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n> +{\n> +\tstrvec_pushf(args, \"--pack_header=%\"PRIu32\",%\"PRIu32,\n> +\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n> +}\n> +\n> +static unsigned int get_unpack_limit(struct repository *repo)\n> +{\n> +\tunsigned int limit = 100;\n> +\n> +\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n> +\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n> +\n> +\treturn limit;\n> +}\n\nOne thing I noticed just now: as the intention is that `write_pack()`\nwill be called for more use cases than only git-receive-pack(1) we'll\nhave to add a way to tell the callback what scenario they are running\nin. I still think moving the unpack limit into the backend is sensible,\nbut now we're not givint it enough information.\n\nPatrick\n"},{"id":"550613","messageId":"an8YSE8iIDXPSkH8@denethor","threadId":"66133","inReplyTo":"an7XAyQr7PrPlAGO@pks.im","subject":"Re: [PATCH v3 9/9] odb/transaction: add transaction interface to write packfiles","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-14T13:40:32Z","receivedAt":"2026-08-14T13:40:37Z","isPatch":true,"body":"On 26/08/14 10:51AM, Patrick Steinhardt wrote:\n> On Tue, Aug 11, 2026 at 12:54:15PM -0500, Justin Tobler wrote:\n> > +static unsigned int get_unpack_limit(struct repository *repo)\n> > +{\n> > +\tunsigned int limit = 100;\n> > +\n> > +\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n> > +\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n> > +\n> > +\treturn limit;\n> > +}\n> \n> One thing I noticed just now: as the intention is that `write_pack()`\n> will be called for more use cases than only git-receive-pack(1) we'll\n> have to add a way to tell the callback what scenario they are running\n> in. I still think moving the unpack limit into the backend is sensible,\n> but now we're not givint it enough information.\n\nSo we already have transaction flags like ODB_TRANSACTION_RECEIVE that\ncan be used to differentiate certain callers that may require slightly\ndifferent behavior in the backend. \n\nIn a followup series where I expand usage of odb_transaction_write_pack\nto git-fetch-pack(1), I was originally planning on using this flag and\nalso adding ODB_TRANSACTION_FETCH accordingly. It's probably a good idea\nto go ahead though and start using the transaction flags here in this\nseries too. Will update in the next version.\n\n-Justin\n"},{"id":"550678","messageId":"aoKZX-eBZ7xIyHCc@pks.im","threadId":"66133","inReplyTo":"an8YSE8iIDXPSkH8@denethor","subject":"Re: [PATCH v3 9/9] odb/transaction: add transaction interface to write packfiles","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-17T05:17:19Z","receivedAt":"2026-08-17T05:17:27Z","isPatch":true,"body":"On Fri, Aug 14, 2026 at 08:40:32AM -0500, Justin Tobler wrote:\n> On 26/08/14 10:51AM, Patrick Steinhardt wrote:\n> > On Tue, Aug 11, 2026 at 12:54:15PM -0500, Justin Tobler wrote:\n> > > +static unsigned int get_unpack_limit(struct repository *repo)\n> > > +{\n> > > +\tunsigned int limit = 100;\n> > > +\n> > > +\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n> > > +\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n> > > +\n> > > +\treturn limit;\n> > > +}\n> > \n> > One thing I noticed just now: as the intention is that `write_pack()`\n> > will be called for more use cases than only git-receive-pack(1) we'll\n> > have to add a way to tell the callback what scenario they are running\n> > in. I still think moving the unpack limit into the backend is sensible,\n> > but now we're not givint it enough information.\n> \n> So we already have transaction flags like ODB_TRANSACTION_RECEIVE that\n> can be used to differentiate certain callers that may require slightly\n> different behavior in the backend. \n> \n> In a followup series where I expand usage of odb_transaction_write_pack\n> to git-fetch-pack(1), I was originally planning on using this flag and\n> also adding ODB_TRANSACTION_FETCH accordingly. It's probably a good idea\n> to go ahead though and start using the transaction flags here in this\n> series too. Will update in the next version.\n\nAh, that makes sense then. And I agree, introducing that flag now\nalready makes it a bit more obvious for how future series will look\nlike. Thanks!\n\nPatrick\n"},{"id":"550844","messageId":"20260819215311.3880274-1-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260811175415.2044235-1-jltobler@gmail.com","subject":"[PATCH v4 0/9] builtin/receive-pack: support pluggable packfile writes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-19T21:53:02Z","receivedAt":"2026-08-19T21:53:15Z","isPatch":true,"body":"Greetings,\n\nWith bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\ntransactions, 2026-07-10), git-receive-pack(1) started using the ODB\ntransaction interfaces to stage incoming objects. While this brought the\ncommand closer to being ODB backend agnostic, the underlying\ngit-index-pack(1) and git-unpack-objects(1) processes used to actually\nwrite the objects to the transaction are still fundamentally tied to the\n\"files\" backend.\n\nThis series aims to address this by introducing a generic\n`odb_transaction_write_pack()` transaction interface to handle writing\nthe incoming packfile to the transaction. The existing logic in\ngit-receive-pack(1) that spawns the child processes to write the\npackfile becomes the \"files\" backend implementation of this interface.\n\nChances since V3:\n- In preparation for future `odb_transaction_write_pack()` users, the\n  unpack limit takes into consideration odb_transaction_flags to augment\n  configutation.\n- Added additional test assertion in first patch to ensure keep file is\n  generated and placed in quarantine directory.\n- Removed an include statement in favor of just forward declaring a\n  struct.\n- Updated some commit messages.\n\nChanges since V2:\n- Added a patch to address a bug causing \".keep\" files from not being\n  removed.\n- Started handling errors at transaction commit and finalize call sites\n  instead of ignoring them. We also make sure\n  `odb_transaction_finalize()` runs after every successful commit\n  callsite to ensure proper cleanup.\n- Updated the code handling lazy loading of unpack limit configuration\n  to not longer cache the value.\n- Added a patch to begin explictly tracking the ODB source used by the\n  \"files\" transaction to avoid relying on the ordering of the ODB source\n  list.\n- Updated some commit messages to improve clarity.\n\nChanges since V1:\n- Changed the \"release\" interface name to \"finalize\" and updated it to\n  return error codes.\n- Marked some function parameters as const.\n- Unpack limit configuration is now resolved in the ODB transaction\n  backend instead of wiring it through the interface.\n- When writing a packfile to the transaction, now only the transaction\n  source is prepared.\n- Updated some commit messages.\n- Updated some code formatting.\n\nThanks for the review,\n-Justin\n\nJustin Tobler (9):\n  builtin/receive-pack: properly clean up keep files\n  odb/transaction: add transaction finalize interface\n  builtin/receive-pack: pass shallow file explicitly\n  builtin/receive-pack: read unpack limit config lazily\n  builtin/receive-pack: lift global state out of unpack()\n  builtin/receive-pack: report unpack errors via strbuf\n  builtin/receive-pack: explicitly pass packfile fd\n  odb: return temporary ODB source when set\n  odb/transaction: add transaction interface to write packfiles\n\n builtin/add.c              |   4 +-\n builtin/receive-pack.c     | 211 ++++++++-----------------------------\n builtin/unpack-objects.c   |   2 +-\n builtin/update-index.c     |   4 +-\n cache-tree.c               |   2 +-\n fetch-pack.c               |   2 +-\n object-file.c              | 183 +++++++++++++++++++++++++++++++-\n odb.c                      |   9 +-\n odb.h                      |   6 +-\n odb/transaction.c          |  21 ++++\n odb/transaction.h          |  85 +++++++++++++++\n pack-write.c               |   7 +-\n pack.h                     |   4 +-\n read-cache.c               |   2 +-\n t/t5547-push-quarantine.sh |  22 ++++\n tmp-objdir.c               |   8 +-\n tmp-objdir.h               |   6 +-\n 17 files changed, 390 insertions(+), 188 deletions(-)\n\nRange-diff against v3:\n 1:  58569303f9 !  1:  13a57feea7 builtin/receive-pack: properly clean up keep files\n    @@ Commit message\n         builtin/receive-pack: properly clean up keep files\n     \n         When git-receive-pack(1) stores an incoming packfile with\n    -    git-index-pack(1), a \".keep\" file is written alongside it to hold the\n    -    pack in place until the references have been updated, and is removed\n    -    afterwards. The path used to remove it is derived via\n    +    git-index-pack(1), a \".keep\" file is written alongside it in the\n    +    transaction quarantine directory and also gets migrated to the main ODB\n    +    when the ODB transaction is committed. This keep lockfile ensures the\n    +    packfile remains in place until the references have been updated and is\n    +    removed afterwards. The path used to remove it is derived via\n         `index_pack_lockfile()` from the repository's primary object directory.\n     \n         In bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\n    @@ Commit message\n         parent process would set the temporary directory set as an alternate\n         only. By using ODB transactions, the ODB source list is also reordered\n         for the parent process which results in `index_pack_lockfile()` deriving\n    -    the \".keep\" path relative to the temporary directory instead the actual\n    -    main ODB source path. Consequently, this prevents the \".keep\" file from\n    -    being properly removed after being migrated into the main ODB source\n    -    post-commit.\n    +    the \".keep\" path relative to the temporary directory instead of the\n    +    actual main ODB source path. Consequently, this prevents the \".keep\"\n    +    file from being properly removed after being migrated into the main ODB\n    +    source post-commit.\n     \n         Update `index_pack_lockfile()` to operate on an ODB source explicitly\n         provided to it and update call sites accordingly to pass the expected\n    @@ pack-write.c: char *index_pack_lockfile(struct repository *r, int ip_out, int *i\n     \n      ## pack.h ##\n     @@\n    - \n    - #include \"object.h\"\n    - #include \"csum-file.h\"\n    -+#include \"odb/source.h\"\n    - \n      struct packed_git;\n      struct pack_window;\n    + struct repository;\n    ++struct odb_source;\n    + \n    + /*\n    +  * Packed object header\n     @@ pack.h: off_t write_pack_header(struct hashfile *f, uint32_t);\n      void fixup_pack_header_footer(const struct git_hash_algo *, int,\n      \t\t\t      unsigned char *, const char *, uint32_t,\n    @@ t/t5547-push-quarantine.sh: test_expect_success 'updating a ref from quarantine\n     +\tgit init --bare keep.git &&\n     +\n     +\tgit -C keep.git config set receive.unpackLimit 0 &&\n    ++\n    ++\t# While incoming objects are still quarantined, validate that the keep\n    ++\t# lockfile does indeed exist.\n    ++\ttest_hook -C keep.git pre-receive <<-\\EOF &&\n    ++\tkeep=\"$(ls \"$GIT_QUARANTINE_PATH\"/pack/pack-*.keep)\" &&\n    ++\ttest -f \"$keep\"\n    ++\tEOF\n    ++\n     +\ttest_commit foo &&\n     +\tgit push keep.git HEAD &&\n     +\tpack=\"$(ls keep.git/objects/pack/pack-*.pack)\" &&\n 2:  dba9696866 !  2:  49254af71c odb/transaction: add transaction finalize interface\n    @@ Commit message\n         after the reference updates are finished.\n     \n         All other callers commit a transaction and immediately finalize it with\n    -    no work in between and cannot meaningfully recover should either step\n    -    fail, so introduce an `odb_transaction_commit_and_finalize_or_die()`\n    -    helper that performs both and dies on error. Call sites are updated\n    +    no work in between and cannot meaningfully recover should either fail,\n    +    so introduce an `odb_transaction_commit_and_finalize_or_die()` helper\n    +    that performs both and dies on error. Call sites are updated\n         accordingly.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n 3:  09bc00a070 =  3:  882cf06bc3 builtin/receive-pack: pass shallow file explicitly\n 4:  2586ea4041 =  4:  8deec37a09 builtin/receive-pack: read unpack limit config lazily\n 5:  adf325095e =  5:  92d56134f0 builtin/receive-pack: lift global state out of unpack()\n 6:  29f407bf36 =  6:  d614b10715 builtin/receive-pack: report unpack errors via strbuf\n 7:  b85f5e868c =  7:  bc5839ad8e builtin/receive-pack: explicitly pass packfile fd\n 8:  620eafe035 =  8:  13540b91b8 odb: return temporary ODB source when set\n 9:  2e75a8bd6c !  9:  62d46d5c07 odb/transaction: add transaction interface to write packfiles\n    @@ object-file.c\n      #include \"setup.h\"\n      #include \"strvec.h\"\n      #include \"tempfile.h\"\n    -@@ object-file.c: struct odb_transaction_files {\n    +@@ object-file.c: struct transaction_packfile {\n    + \n    + struct odb_transaction_files {\n    + \tstruct odb_transaction base;\n    ++\tenum odb_transaction_flags flags;\n    + \n    + \tstruct tmp_objdir *objdir;\n      \tstruct odb_source *quarantine;\n      \tstruct transaction_packfile packfile;\n      \tconst char *prefix;\n    @@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b\n     +\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n     +}\n     +\n    -+static unsigned int get_unpack_limit(struct repository *repo)\n    ++static unsigned int get_unpack_limit(struct repository *repo,\n    ++\t\t\t\t     enum odb_transaction_flags flags)\n     +{\n    -+\tunsigned int limit = 100;\n    ++\tunsigned int limit = 0;\n     +\n    -+\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n    -+\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n    ++\tif (flags & ODB_TRANSACTION_RECEIVE) {\n    ++\t\tlimit = 100;\n    ++\t\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n    ++\t\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n    ++\t}\n     +\n     +\treturn limit;\n     +}\n    @@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b\n     +\n     +\todb_transaction_env(base, &child.env);\n     +\n    -+\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(repo)) {\n    ++\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(repo, transaction->flags)) {\n     +\t\tstrvec_push(&child.args, \"unpack-objects\");\n     +\t\tpush_header_arg(&child.args, &hdr);\n     +\t\tif (opts->quiet)\n    @@ object-file.c: int odb_transaction_files_begin(struct odb_source *source,\n      \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n     +\ttransaction->base.write_pack = odb_transaction_files_write_pack;\n      \ttransaction->base.env = odb_transaction_files_env;\n    ++\ttransaction->flags = flags;\n      \n      \ttransaction->prefix = \"bulk-fsync\";\n    + \tif (flags & ODB_TRANSACTION_RECEIVE) {\n     \n      ## odb/transaction.c ##\n     @@ odb/transaction.c: int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n\nbase-commit: 2c78326f810173a4f3aefd8021f1e07575412481\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550845","messageId":"20260819215311.3880274-2-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260819215311.3880274-1-jltobler@gmail.com","subject":"[PATCH v4 1/9] builtin/receive-pack: properly clean up keep files","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-19T21:53:03Z","receivedAt":"2026-08-19T21:53:16Z","isPatch":true,"body":"When git-receive-pack(1) stores an incoming packfile with\ngit-index-pack(1), a \".keep\" file is written alongside it in the\ntransaction quarantine directory and also gets migrated to the main ODB\nwhen the ODB transaction is committed. This keep lockfile ensures the\npackfile remains in place until the references have been updated and is\nremoved afterwards. The path used to remove it is derived via\n`index_pack_lockfile()` from the repository's primary object directory.\n\nIn bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\ntransactions, 2026-07-10), git-receive-pack(1) started using the ODB\ntransaction interfaces instead of managing a temporary directory\ndirectly. When starting an ODB transaction, the sources list is\nreordered to insert the newly created transaction source first as the\nprimary to ensure writes are routed to it accordingly.\n\nPrior to using ODB transactions, git-receive-pack(1) would only set the\ntemporary directory as the primary source for the child\ngit-index-pack(1) and git-unpack-objects(1) processes it spawned and the\nparent process would set the temporary directory set as an alternate\nonly. By using ODB transactions, the ODB source list is also reordered\nfor the parent process which results in `index_pack_lockfile()` deriving\nthe \".keep\" path relative to the temporary directory instead of the\nactual main ODB source path. Consequently, this prevents the \".keep\"\nfile from being properly removed after being migrated into the main ODB\nsource post-commit.\n\nUpdate `index_pack_lockfile()` to operate on an ODB source explicitly\nprovided to it and update call sites accordingly to pass the expected\nODB source.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c     |  8 +++++++-\n fetch-pack.c               |  2 +-\n pack-write.c               |  7 ++++---\n pack.h                     |  4 +++-\n t/t5547-push-quarantine.sh | 22 ++++++++++++++++++++++\n 5 files changed, 37 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 86933d8d7e..d74b787148 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2412,7 +2412,13 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n \t\tif (status)\n \t\t\treturn \"index-pack fork failed\";\n \n-\t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n+\t\t/*\n+\t\t * The lockfile filepath is expected to be the final location of\n+\t\t * the \".keep\" file after being migrated to the main ODB source.\n+\t\t * This ensures the lockfile can be found and removed later\n+\t\t * after the ODB transaction has been committed.\n+\t\t */\n+\t\tlockfile = index_pack_lockfile(transaction->source, child.out, NULL);\n \t\tif (lockfile) {\n \t\t\tpack_lockfile = register_tempfile(lockfile);\n \t\t\tfree(lockfile);\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 922a9b2581..6df5813b33 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -1075,7 +1075,7 @@ static int get_pack(struct fetch_pack_args *args,\n \t\tdie(_(\"fetch-pack: unable to fork off %s\"), cmd_name);\n \tif (do_keep && (pack_lockfiles || fsck_objects)) {\n \t\tint is_well_formed;\n-\t\tchar *pack_lockfile = index_pack_lockfile(the_repository,\n+\t\tchar *pack_lockfile = index_pack_lockfile(the_repository->objects->sources,\n \t\t\t\t\t\t\t  cmd.out,\n \t\t\t\t\t\t\t  &is_well_formed);\n \ndiff --git a/pack-write.c b/pack-write.c\nindex 24033a9101..85674e4b72 100644\n--- a/pack-write.c\n+++ b/pack-write.c\n@@ -469,10 +469,11 @@ void fixup_pack_header_footer(const struct git_hash_algo *hash_algo,\n \tfsync_component_or_die(FSYNC_COMPONENT_PACK, pack_fd, pack_name);\n }\n \n-char *index_pack_lockfile(struct repository *r, int ip_out, int *is_well_formed)\n+char *index_pack_lockfile(struct odb_source *source, int ip_out,\n+\t\t\t  int *is_well_formed)\n {\n \tchar packname[GIT_MAX_HEXSZ + 6];\n-\tconst int len = r->hash_algo->hexsz + 6;\n+\tconst int len = source->odb->repo->hash_algo->hexsz + 6;\n \n \t/*\n \t * The first thing we expect from index-pack's output\n@@ -489,7 +490,7 @@ char *index_pack_lockfile(struct repository *r, int ip_out, int *is_well_formed)\n \t\tpackname[len-1] = 0;\n \t\tif (skip_prefix(packname, \"keep\\t\", &name))\n \t\t\treturn xstrfmt(\"%s/pack/pack-%s.keep\",\n-\t\t\t\t       repo_get_object_directory(r), name);\n+\t\t\t\t       source->path, name);\n \t\treturn NULL;\n \t}\n \tif (is_well_formed)\ndiff --git a/pack.h b/pack.h\nindex 1cde92082b..ada506b5c5 100644\n--- a/pack.h\n+++ b/pack.h\n@@ -7,6 +7,7 @@\n struct packed_git;\n struct pack_window;\n struct repository;\n+struct odb_source;\n \n /*\n  * Packed object header\n@@ -105,7 +106,8 @@ off_t write_pack_header(struct hashfile *f, uint32_t);\n void fixup_pack_header_footer(const struct git_hash_algo *, int,\n \t\t\t      unsigned char *, const char *, uint32_t,\n \t\t\t      unsigned char *, off_t);\n-char *index_pack_lockfile(struct repository *r, int fd, int *is_well_formed);\n+char *index_pack_lockfile(struct odb_source *source, int fd,\n+\t\t\t  int *is_well_formed);\n \n struct ref;\n \ndiff --git a/t/t5547-push-quarantine.sh b/t/t5547-push-quarantine.sh\nindex 0798ddab02..3da253cc1a 100755\n--- a/t/t5547-push-quarantine.sh\n+++ b/t/t5547-push-quarantine.sh\n@@ -70,4 +70,26 @@ test_expect_success 'updating a ref from quarantine is forbidden' '\n \tgit -C update.git fsck\n '\n \n+test_expect_success '.keep file is removed after push' '\n+\ttest_when_finished rm -rf keep.git &&\n+\tgit init --bare keep.git &&\n+\n+\tgit -C keep.git config set receive.unpackLimit 0 &&\n+\n+\t# While incoming objects are still quarantined, validate that the keep\n+\t# lockfile does indeed exist.\n+\ttest_hook -C keep.git pre-receive <<-\\EOF &&\n+\tkeep=\"$(ls \"$GIT_QUARANTINE_PATH\"/pack/pack-*.keep)\" &&\n+\ttest -f \"$keep\"\n+\tEOF\n+\n+\ttest_commit foo &&\n+\tgit push keep.git HEAD &&\n+\tpack=\"$(ls keep.git/objects/pack/pack-*.pack)\" &&\n+\tkeep=\"${pack%.pack}.keep\" &&\n+\n+\ttest_path_is_file \"$pack\" &&\n+\ttest_path_is_missing \"$keep\"\n+'\n+\n test_done\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550846","messageId":"20260819215311.3880274-3-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260819215311.3880274-1-jltobler@gmail.com","subject":"[PATCH v4 2/9] odb/transaction: add transaction finalize interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-19T21:53:04Z","receivedAt":"2026-08-19T21:53:16Z","isPatch":true,"body":"When committing an ODB transaction via `odb_transaction_commit()`, the\nstaged objects are made visible and the underlying transaction is freed\nat the same time. Coupling these two steps does not leave room for any\npost-commit transaction operations to be introduced though. Such a\ncapability is useful if an ODB transaction backend needs to hold on to\nlockfiles after transaction commit until references are updated, as is\nthe case with the existing \"files\" backend in git-receive-pack(1).\n\nStop freeing the transaction in `odb_transaction_commit()` and introduce\n`odb_transaction_finalize()` to explicitly clean up the transaction\naccordingly. Note that the finalize interface also provides an optional\ncallback for any backend-specific deferred cleanup. In a subsequent\ncommit, the \"files\" transaction backend will use this to remove \".keep\"\nfiles generated for packfiles received via git-receive-pack(1) after\nreferences have been updated. In preparation for this, the\n`odb_transaction_finalize()` call site in git-receive-pack(1) is made\nafter the reference updates are finished.\n\nAll other callers commit a transaction and immediately finalize it with\nno work in between and cannot meaningfully recover should either fail,\nso introduce an `odb_transaction_commit_and_finalize_or_die()` helper\nthat performs both and dies on error. Call sites are updated\naccordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  4 ++--\n builtin/receive-pack.c   |  1 +\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  4 ++--\n cache-tree.c             |  2 +-\n object-file.c            |  2 +-\n odb/transaction.c        | 14 ++++++++++++++\n odb/transaction.h        | 23 +++++++++++++++++++++++\n read-cache.c             |  2 +-\n 9 files changed, 46 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 60ffbede2b..ad418a5952 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -393,7 +393,7 @@ int cmd_add(int argc,\n \tchar *seen = NULL;\n \tchar *ps_matched = NULL;\n \tstruct lock_file lock_file = LOCK_INIT;\n-\tstruct odb_transaction *transaction;\n+\tstruct odb_transaction *transaction = NULL;\n \n \trepo_config(repo, add_config, NULL);\n \n@@ -600,7 +600,7 @@ int cmd_add(int argc,\n \n \tif (chmod_arg && pathspec.nr)\n \t\texit_status |= chmod_pathspec(repo, &pathspec, chmod_arg[0], show_only);\n-\todb_transaction_commit(transaction);\n+\todb_transaction_commit_and_finalize_or_die(transaction);\n \n finish:\n \tif (write_locked_index(repo->index, &lock_file,\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex d74b787148..ed1edcbe93 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2720,6 +2720,7 @@ int cmd_receive_pack(int argc,\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n \t\texecute_commands(commands, unpack_status, &si, transaction,\n \t\t\t\t &push_options);\n+\t\todb_transaction_finalize(transaction);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex 4263edfbec..d6a2d616d9 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -603,7 +603,7 @@ static void unpack_all(void)\n \t\tunpack_one(i);\n \t\tdisplay_progress(progress, i + 1);\n \t}\n-\todb_transaction_commit(transaction);\n+\todb_transaction_commit_and_finalize_or_die(transaction);\n \tstop_progress(&progress);\n \n \tif (delta_list)\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 241abd4332..b25d4ecb10 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1156,7 +1156,7 @@ int cmd_update_index(int argc,\n \t\t\t * a transaction.\n \t\t\t */\n \t\t\tif (transaction && verbose) {\n-\t\t\t\todb_transaction_commit(transaction);\n+\t\t\t\todb_transaction_commit_and_finalize_or_die(transaction);\n \t\t\t\ttransaction = NULL;\n \t\t\t}\n \n@@ -1224,7 +1224,7 @@ int cmd_update_index(int argc,\n \t/*\n \t * By now we have added all of the new objects\n \t */\n-\todb_transaction_commit(transaction);\n+\todb_transaction_commit_and_finalize_or_die(transaction);\n \n \tif (split_index > 0) {\n \t\tif (repo_config_get_split_index(the_repository) == 0)\ndiff --git a/cache-tree.c b/cache-tree.c\nindex d92f513286..a220372a42 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -538,7 +538,7 @@ int cache_tree_update(struct index_state *istate, int flags)\n \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n \t\t       \"\", 0, &skip, flags);\n \tif (!inflight)\n-\t\todb_transaction_commit(transaction);\n+\t\todb_transaction_commit_and_finalize_or_die(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 ec35c318bc..4d03c167d5 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -965,7 +965,7 @@ int index_fd(struct index_state *istate, struct object_id *oid,\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\tif (!inflight)\n-\t\t\t\todb_transaction_commit(transaction);\n+\t\t\t\todb_transaction_commit_and_finalize_or_die(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 dab7da6a9a..9e9a982778 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -33,6 +33,20 @@ int odb_transaction_commit(struct odb_transaction *transaction)\n \n \tret = transaction->commit(transaction);\n \ttransaction->source->odb->transaction = NULL;\n+\n+\treturn ret;\n+}\n+\n+int odb_transaction_finalize(struct odb_transaction *transaction)\n+{\n+\tint ret = 0;\n+\n+\tif (!transaction)\n+\t\treturn 0;\n+\n+\tif (transaction->finalize)\n+\t\tret = transaction->finalize(transaction);\n+\n \tfree(transaction);\n \n \treturn ret;\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 4cb2eafcbf..6ed39b3d0e 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -22,6 +22,13 @@ struct odb_transaction {\n \t */\n \tint (*commit)(struct odb_transaction *transaction);\n \n+\t/*\n+\t * Optional ODB source specific callback invoked when the transaction\n+\t * needs to perform any deferred cleanup after objects have been\n+\t * committed. Returns 0 on success, a negative error code otherwise.\n+\t */\n+\tint (*finalize)(struct odb_transaction *transaction);\n+\n \t/*\n \t * This callback is expected to write the given object stream into\n \t * the ODB transaction. Note that for now, only blobs support streaming.\n@@ -75,6 +82,22 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n  */\n int odb_transaction_commit(struct odb_transaction *transaction);\n \n+/*\n+ * Finalizes an ODB transaction, performing any deferred cleanup and freeing it.\n+ * Must be called for every successfully started transaction. Note that, if the\n+ * specified transaction is NULL, the function is a no-op. Returns 0 on success,\n+ * a negative error code otherwise.\n+ */\n+int odb_transaction_finalize(struct odb_transaction *transaction);\n+\n+static inline void odb_transaction_commit_and_finalize_or_die(struct odb_transaction *transaction)\n+{\n+\tif (odb_transaction_commit(transaction))\n+\t\tdie(_(\"failed to commit ODB transaction\"));\n+\tif (odb_transaction_finalize(transaction))\n+\t\tdie(_(\"failed to finalize ODB transaction\"));\n+}\n+\n /*\n  * Writes the object in the provided stream into the transaction. The resulting\n  * object ID is written into the out pointer. Returns 0 on success, a negative\ndiff --git a/read-cache.c b/read-cache.c\nindex 6c449f393d..0cd0ef85ec 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4049,7 +4049,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\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+\t\todb_transaction_commit_and_finalize_or_die(transaction);\n \n \trelease_revisions(&rev);\n \treturn !!data.add_errors;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550847","messageId":"20260819215311.3880274-4-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260819215311.3880274-1-jltobler@gmail.com","subject":"[PATCH v4 3/9] builtin/receive-pack: pass shallow file explicitly","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-19T21:53:05Z","receivedAt":"2026-08-19T21:53:17Z","isPatch":true,"body":"If shallow information is provided during `unpack()`, a temporary\nshallow file is created and stored in global state. In a subsequent\ncommit, the `unpack()` logic is moved behind a generic ODB transaction\ninterface to handle writing packfiles and thus can no longer rely on\nsuch global state. Lift the setup of the temporary shallow file out of\n`unpack()` and wire it through to its call sites explicitly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 38 ++++++++++++++++++++++----------------\n 1 file changed, 22 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex ed1edcbe93..135105deae 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -86,7 +86,6 @@ static const char *head_name;\n static void *head_name_to_free;\n static int sent_capabilities;\n static int shallow_update;\n-static const char *alt_shallow_file;\n static struct strbuf push_cert = STRBUF_INIT;\n static struct object_id push_cert_oid;\n static struct signature_check sigcheck;\n@@ -2334,8 +2333,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-\t\t\t  struct odb_transaction *transaction)\n+static const char *unpack(struct odb_transaction *transaction,\n+\t\t\t  const char *shallow_file, int err_fd)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2354,10 +2353,9 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n \t\treturn hdr_err;\n \t}\n \n-\tif (si->nr_ours || si->nr_theirs) {\n-\t\talt_shallow_file = setup_temporary_shallow(si->shallow);\n+\tif (shallow_file) {\n \t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, alt_shallow_file);\n+\t\tstrvec_push(&child.args, shallow_file);\n \t}\n \n \todb_transaction_env(transaction, &child.env);\n@@ -2433,14 +2431,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-\t\t\t\t\tstruct odb_transaction *transaction)\n+static const char *unpack_with_sideband(struct odb_transaction *transaction,\n+\t\t\t\t\tconst char *shallow_file)\n {\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(0, si, transaction);\n+\t\treturn unpack(transaction, shallow_file, 0);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2449,13 +2447,14 @@ 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, transaction);\n+\tret = unpack(transaction, shallow_file, muxer.in);\n \n \tfinish_async(&muxer);\n \treturn ret;\n }\n \n-static void prepare_shallow_update(struct shallow_info *si)\n+static void prepare_shallow_update(struct shallow_info *si,\n+\t\t\t\t   const char *shallow_file)\n {\n \tint i, j, k, bitmap_size = DIV_ROUND_UP(si->ref->nr, 32);\n \n@@ -2495,12 +2494,13 @@ static void prepare_shallow_update(struct shallow_info *si)\n \t * command. check_connected() will be done with\n \t * true .git/shallow though.\n \t */\n-\tsetenv(GIT_SHALLOW_FILE_ENVIRONMENT, alt_shallow_file, 1);\n+\tsetenv(GIT_SHALLOW_FILE_ENVIRONMENT, shallow_file, 1);\n }\n \n static void update_shallow_info(struct command *commands,\n \t\t\t\tstruct shallow_info *si,\n-\t\t\t\tstruct oid_array *ref)\n+\t\t\t\tstruct oid_array *ref,\n+\t\t\t\tconst char *shallow_file)\n {\n \tstruct command *cmd;\n \tint *ref_status;\n@@ -2519,7 +2519,7 @@ static void update_shallow_info(struct command *commands,\n \tsi->ref = ref;\n \n \tif (shallow_update) {\n-\t\tprepare_shallow_update(si);\n+\t\tprepare_shallow_update(si, shallow_file);\n \t\treturn;\n \t}\n \n@@ -2711,11 +2711,17 @@ 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\tconst char *alt_shallow_file = NULL;\n+\n+\t\t\tif (si.nr_ours || si.nr_theirs)\n+\t\t\t\talt_shallow_file = setup_temporary_shallow(si.shallow);\n+\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\t\tunpack_status = unpack_with_sideband(transaction, alt_shallow_file);\n+\n+\t\t\tupdate_shallow_info(commands, &si, &ref, alt_shallow_file);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n \t\texecute_commands(commands, unpack_status, &si, transaction,\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550848","messageId":"20260819215311.3880274-5-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260819215311.3880274-1-jltobler@gmail.com","subject":"[PATCH v4 4/9] builtin/receive-pack: read unpack limit config lazily","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-19T21:53:06Z","receivedAt":"2026-08-19T21:53:18Z","isPatch":true,"body":"In git-receive-pack(1), the `receive.unpackLimit` and\n`transfer.unpackLimit` configuration decides whether an incoming\npackfile should be exploded into loose objects or kept as a packfile\non-disk. In a subsequent commit, the logic to write the incoming\npackfile is made ODB backend agnostic and moved behind a pluggable ODB\ntransaction interface. Consequently, whether to explode a packfile is a\ndetail of how a particular backend stores objects and should not be a\npart of the generic interface itself.\n\nIn preparation for this, instead resolve the unpack limit lazily inside\n`unpack()` by reading the configuration directly. The now-unused unpack\nlimit globals are dropped accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 30 +++++++++++-------------------\n 1 file changed, 11 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 135105deae..971dc3f52e 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -62,12 +62,9 @@ static enum deny_action deny_delete_current = DENY_UNCONFIGURED;\n static int receive_fsck_objects = -1;\n static int transfer_fsck_objects = -1;\n static struct strbuf fsck_msg_types = STRBUF_INIT;\n-static int receive_unpack_limit = -1;\n-static int transfer_unpack_limit = -1;\n static int advertise_atomic_push = 1;\n static int advertise_push_options;\n static int advertise_sid;\n-static int unpack_limit = 100;\n static off_t max_input_size;\n static int report_status;\n static int report_status_v2;\n@@ -157,16 +154,6 @@ static int receive_pack_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (strcmp(var, \"receive.unpacklimit\") == 0) {\n-\t\treceive_unpack_limit = git_config_int(var, value, ctx->kvi);\n-\t\treturn 0;\n-\t}\n-\n-\tif (strcmp(var, \"transfer.unpacklimit\") == 0) {\n-\t\ttransfer_unpack_limit = git_config_int(var, value, ctx->kvi);\n-\t\treturn 0;\n-\t}\n-\n \tif (strcmp(var, \"receive.fsck.skiplist\") == 0) {\n \t\tchar *path;\n \n@@ -2333,6 +2320,16 @@ 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 unsigned int get_unpack_limit(struct repository *repo)\n+{\n+\tunsigned int limit = 100;\n+\n+\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n+\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n+\n+\treturn limit;\n+}\n+\n static const char *unpack(struct odb_transaction *transaction,\n \t\t\t  const char *shallow_file, int err_fd)\n {\n@@ -2360,7 +2357,7 @@ static const char *unpack(struct odb_transaction *transaction,\n \n \todb_transaction_env(transaction, &child.env);\n \n-\tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n+\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n \t\tpush_header_arg(&child.args, &hdr);\n \t\tif (quiet)\n@@ -2658,11 +2655,6 @@ int cmd_receive_pack(int argc,\n \tif (cert_nonce_seed)\n \t\tpush_cert_nonce = prepare_push_cert_nonce(service_dir, time(NULL));\n \n-\tif (0 <= receive_unpack_limit)\n-\t\tunpack_limit = receive_unpack_limit;\n-\telse if (0 <= transfer_unpack_limit)\n-\t\tunpack_limit = transfer_unpack_limit;\n-\n \tswitch (determine_protocol_version_server()) {\n \tcase protocol_v2:\n \t\t/*\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550849","messageId":"20260819215311.3880274-6-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260819215311.3880274-1-jltobler@gmail.com","subject":"[PATCH v4 5/9] builtin/receive-pack: lift global state out of unpack()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-19T21:53:07Z","receivedAt":"2026-08-19T21:53:18Z","isPatch":true,"body":"In git-receive-pack(1), writing the packfile to the transaction is\nhandled via `unpack()` which relies on global variables to decide how to\ninvoke the underlying git-index-pack(1) or git-unpack-objects(1) child\nprocesses. In a subsequent commit, the `unpack()` logic is moved behind\na generic ODB transaction interface to handle writing packfiles and thus\ncan no longer rely on these globals.\n\nLift the global state out of `unpack()` by instead storing this state in\na `struct unpack_opts` that gets passed to the function explicitly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 63 +++++++++++++++++++++++++++---------------\n 1 file changed, 41 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 971dc3f52e..f062b93b8d 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2330,18 +2330,24 @@ static unsigned int get_unpack_limit(struct repository *repo)\n \treturn limit;\n }\n \n+struct unpack_opts {\n+\tconst char *fsck_msg_types;\n+\tconst char *shallow_file;\n+\toff_t max_input_size;\n+\tint fsck_objects;\n+\tint reject_thin;\n+\tint err_fd;\n+\tint quiet;\n+};\n+\n static const char *unpack(struct odb_transaction *transaction,\n-\t\t\t  const char *shallow_file, int err_fd)\n+\t\t\t  const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n \tint status;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint fsck_objects = (receive_fsck_objects >= 0\n-\t\t\t    ? receive_fsck_objects\n-\t\t\t    : transfer_fsck_objects >= 0\n-\t\t\t    ? transfer_fsck_objects\n-\t\t\t    : 0);\n+\tint err_fd = opts->err_fd;\n \n \thdr_err = parse_pack_header(&hdr);\n \tif (hdr_err) {\n@@ -2350,9 +2356,9 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\treturn hdr_err;\n \t}\n \n-\tif (shallow_file) {\n+\tif (opts->shallow_file) {\n \t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, shallow_file);\n+\t\tstrvec_push(&child.args, opts->shallow_file);\n \t}\n \n \todb_transaction_env(transaction, &child.env);\n@@ -2360,14 +2366,14 @@ static const char *unpack(struct odb_transaction *transaction,\n \tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n \t\tpush_header_arg(&child.args, &hdr);\n-\t\tif (quiet)\n+\t\tif (opts->quiet)\n \t\t\tstrvec_push(&child.args, \"-q\");\n-\t\tif (fsck_objects)\n+\t\tif (opts->fsck_objects)\n \t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     fsck_msg_types.buf);\n-\t\tif (max_input_size)\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (opts->max_input_size)\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)max_input_size);\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.no_stdout = 1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n@@ -2388,18 +2394,18 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\t\t     (uintmax_t)getpid(),\n \t\t\t     hostname);\n \n-\t\tif (!quiet && err_fd)\n+\t\tif (!opts->quiet && err_fd)\n \t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n-\t\tif (use_sideband)\n+\t\tif (err_fd)\n \t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n-\t\tif (fsck_objects)\n+\t\tif (opts->fsck_objects)\n \t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     fsck_msg_types.buf);\n-\t\tif (!reject_thin)\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (!opts->reject_thin)\n \t\t\tstrvec_push(&child.args, \"--fix-thin\");\n-\t\tif (max_input_size)\n+\t\tif (opts->max_input_size)\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)max_input_size);\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.out = -1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n@@ -2431,11 +2437,23 @@ static const char *unpack(struct odb_transaction *transaction,\n static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \t\t\t\t\tconst char *shallow_file)\n {\n+\tstruct unpack_opts opts = {\n+\t\t.fsck_objects = (receive_fsck_objects >= 0\n+\t\t\t\t ? receive_fsck_objects\n+\t\t\t\t : transfer_fsck_objects >= 0\n+\t\t\t\t ? transfer_fsck_objects\n+\t\t\t\t : 0),\n+\t\t.fsck_msg_types = fsck_msg_types.buf,\n+\t\t.max_input_size = max_input_size,\n+\t\t.shallow_file = shallow_file,\n+\t\t.reject_thin = reject_thin,\n+\t\t.quiet = quiet,\n+\t};\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, shallow_file, 0);\n+\t\treturn unpack(transaction, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2444,7 +2462,8 @@ static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \tif (start_async(&muxer))\n \t\treturn NULL;\n \n-\tret = unpack(transaction, shallow_file, muxer.in);\n+\topts.err_fd = muxer.in;\n+\tret = unpack(transaction, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550850","messageId":"20260819215311.3880274-7-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260819215311.3880274-1-jltobler@gmail.com","subject":"[PATCH v4 6/9] builtin/receive-pack: report unpack errors via strbuf","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-19T21:53:08Z","receivedAt":"2026-08-19T21:53:19Z","isPatch":true,"body":"When writing packfiles via `unpack()`, error messages are returned\ndirectly by the function. In preparation for `unpack()` logic being\nmoved behind a generic ODB transaction interface, update the function to\ninstead write any error messages to a caller provided strbuf and return\na negative value on error. Call sites are updated to use the error\nstrbuf accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 63 ++++++++++++++++++++++++------------------\n 1 file changed, 36 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex f062b93b8d..6df872697b 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2015,7 +2015,7 @@ static void execute_commands_atomic(struct command *commands,\n }\n \n static void execute_commands(struct command *commands,\n-\t\t\t     const char *unpacker_error,\n+\t\t\t     int 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@@ -2340,8 +2340,8 @@ struct unpack_opts {\n \tint quiet;\n };\n \n-static const char *unpack(struct odb_transaction *transaction,\n-\t\t\t  const struct unpack_opts *opts)\n+static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n+\t\t  const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2353,7 +2353,8 @@ static const char *unpack(struct odb_transaction *transaction,\n \tif (hdr_err) {\n \t\tif (err_fd > 0)\n \t\t\tclose(err_fd);\n-\t\treturn hdr_err;\n+\t\tstrbuf_addstr(err_msg, hdr_err);\n+\t\treturn -1;\n \t}\n \n \tif (opts->shallow_file) {\n@@ -2378,8 +2379,10 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = run_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"unpack-objects abnormal exit\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n \t} else {\n \t\tchar hostname[HOST_NAME_MAX + 1];\n \t\tchar *lockfile;\n@@ -2410,8 +2413,10 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = start_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"index-pack fork failed\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n+\t\t\treturn -1;\n+\t\t}\n \n \t\t/*\n \t\t * The lockfile filepath is expected to be the final location of\n@@ -2427,15 +2432,18 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tclose(child.out);\n \n \t\tstatus = finish_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"index-pack abnormal exit\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n \t\todb_reprepare(the_repository->objects);\n \t}\n-\treturn NULL;\n+\treturn 0;\n }\n \n-static const char *unpack_with_sideband(struct odb_transaction *transaction,\n-\t\t\t\t\tconst char *shallow_file)\n+static int unpack_with_sideband(struct odb_transaction *transaction,\n+\t\t\t\tconst char *shallow_file,\n+\t\t\t\tstruct strbuf *err_msg)\n {\n \tstruct unpack_opts opts = {\n \t\t.fsck_objects = (receive_fsck_objects >= 0\n@@ -2450,20 +2458,20 @@ static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \t\t.quiet = quiet,\n \t};\n \tstruct async muxer;\n-\tconst char *ret;\n+\tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, &opts);\n+\t\treturn unpack(transaction, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n \tmuxer.proc = copy_to_sideband;\n \tmuxer.in = -1;\n \tif (start_async(&muxer))\n-\t\treturn NULL;\n+\t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, &opts);\n+\tret = unpack(transaction, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2552,13 +2560,13 @@ static void update_shallow_info(struct command *commands,\n \tfree(ref_status);\n }\n \n-static void report(struct command *commands, const char *unpack_status)\n+static void report(struct command *commands, const struct strbuf *unpack_status)\n {\n \tstruct command *cmd;\n \tstruct strbuf buf = STRBUF_INIT;\n \n \tpacket_buf_write(&buf, \"unpack %s\\n\",\n-\t\t\t unpack_status ? unpack_status : \"ok\");\n+\t\t\t unpack_status->len ? unpack_status->buf : \"ok\");\n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (!cmd->error_string)\n \t\t\tpacket_buf_write(&buf, \"ok %s\\n\",\n@@ -2576,14 +2584,14 @@ static void report(struct command *commands, const char *unpack_status)\n \tstrbuf_release(&buf);\n }\n \n-static void report_v2(struct command *commands, const char *unpack_status)\n+static void report_v2(struct command *commands, const struct strbuf *unpack_status)\n {\n \tstruct command *cmd;\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct ref_push_report *report;\n \n \tpacket_buf_write(&buf, \"unpack %s\\n\",\n-\t\t\t unpack_status ? unpack_status : \"ok\");\n+\t\t\t unpack_status->len ? unpack_status->buf : \"ok\");\n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tint count = 0;\n \n@@ -2707,8 +2715,8 @@ int cmd_receive_pack(int argc,\n \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\n \n \tif ((commands = read_head_info(&reader, &shallow))) {\n-\t\tconst char *unpack_status = NULL;\n \t\tstruct string_list push_options = STRING_LIST_INIT_DUP;\n+\t\tstruct strbuf unpack_status = STRBUF_INIT;\n \n \t\tif (use_push_options)\n \t\t\tread_push_options(&reader, &push_options);\n@@ -2728,22 +2736,22 @@ int cmd_receive_pack(int argc,\n \t\t\t\talt_shallow_file = setup_temporary_shallow(si.shallow);\n \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\t\tstrbuf_addstr(&unpack_status, \"unable to start object transaction\");\n \t\t\telse\n-\t\t\t\tunpack_status = unpack_with_sideband(transaction, alt_shallow_file);\n+\t\t\t\tunpack_with_sideband(transaction, alt_shallow_file, &unpack_status);\n \n \t\t\tupdate_shallow_info(commands, &si, &ref, alt_shallow_file);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n-\t\texecute_commands(commands, unpack_status, &si, transaction,\n+\t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n \t\t\t\t &push_options);\n \t\todb_transaction_finalize(transaction);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\n-\t\t\treport_v2(commands, unpack_status);\n+\t\t\treport_v2(commands, &unpack_status);\n \t\telse if (report_status)\n-\t\t\treport(commands, unpack_status);\n+\t\t\treport(commands, &unpack_status);\n \t\tsigchain_pop(SIGPIPE);\n \t\trun_receive_hook(commands, \"post-receive\", 1, NULL,\n \t\t\t\t &push_options);\n@@ -2768,6 +2776,7 @@ int cmd_receive_pack(int argc,\n \t\tif (auto_update_server_info)\n \t\t\tupdate_server_info(the_repository, 0);\n \t\tclear_shallow_info(&si);\n+\t\tstrbuf_release(&unpack_status);\n \t}\n \tif (use_sideband)\n \t\tpacket_flush(1);\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550851","messageId":"20260819215311.3880274-8-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260819215311.3880274-1-jltobler@gmail.com","subject":"[PATCH v4 7/9] builtin/receive-pack: explicitly pass packfile fd","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-19T21:53:09Z","receivedAt":"2026-08-19T21:53:20Z","isPatch":true,"body":"When processing the incoming packfile in git-receive-pack(1), `unpack()`\nassumes it should always read it from stdin. In preparation for\n`unpack()` logic being moved behind a generic ODB transaction interface,\nupdate the function signature to take the an explicit fd provided by\ncallers to read the incoming packfile from instead. Call sites are\nupdated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 16 +++++++++-------\n 1 file changed, 9 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 6df872697b..b369466783 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2292,9 +2292,9 @@ static void read_push_options(struct packet_reader *reader,\n \t}\n }\n \n-static const char *parse_pack_header(struct pack_header *hdr)\n+static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n {\n-\tswitch (read_pack_header(0, hdr)) {\n+\tswitch (read_pack_header(pack_fd, hdr)) {\n \tcase PH_ERROR_EOF:\n \t\treturn \"eof before pack header was fully read\";\n \n@@ -2340,8 +2340,8 @@ struct unpack_opts {\n \tint quiet;\n };\n \n-static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n-\t\t  const struct unpack_opts *opts)\n+static int unpack(struct odb_transaction *transaction, int pack_fd,\n+\t\t  struct strbuf *err_msg, const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2349,7 +2349,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tint err_fd = opts->err_fd;\n \n-\thdr_err = parse_pack_header(&hdr);\n+\thdr_err = parse_pack_header(&hdr, pack_fd);\n \tif (hdr_err) {\n \t\tif (err_fd > 0)\n \t\t\tclose(err_fd);\n@@ -2376,6 +2376,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n \t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.no_stdout = 1;\n+\t\tchild.in = pack_fd;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = run_command(&child);\n@@ -2410,6 +2411,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n \t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.out = -1;\n+\t\tchild.in = pack_fd;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = start_command(&child);\n@@ -2461,7 +2463,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, err_msg, &opts);\n+\t\treturn unpack(transaction, 0, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2471,7 +2473,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, err_msg, &opts);\n+\tret = unpack(transaction, 0, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550852","messageId":"20260819215311.3880274-9-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260819215311.3880274-1-jltobler@gmail.com","subject":"[PATCH v4 8/9] odb: return temporary ODB source when set","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-19T21:53:10Z","receivedAt":"2026-08-19T21:53:21Z","isPatch":true,"body":"When invoked, `odb_set_temporary_primary_source()` installs a temporary\nobject directory as the new primary ODB source. A caller that wants to\noperate on the ODB source of the open transaction must assume that it is\nthe first entry in the ODB source list which is a bit awkward and\nfragile.\n\nInstead, return the newly installed source directly and report the\nprevious primary source via a new `prev_source` out parameter. Propagate\nthe installed source through `tmp_objdir_replace_primary_odb()` and\nstart storing it in the \"files\" ODB transaction so a subsequent commit\ncan easily access it without relying on the ODB source list ordering.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 3 ++-\n odb.c         | 9 +++++++--\n odb.h         | 6 ++++--\n tmp-objdir.c  | 8 +++++---\n tmp-objdir.h  | 6 ++++--\n 5 files changed, 22 insertions(+), 10 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 4d03c167d5..db63587f6d 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -485,6 +485,7 @@ struct odb_transaction_files {\n \tstruct odb_transaction base;\n \n \tstruct tmp_objdir *objdir;\n+\tstruct odb_source *quarantine;\n \tstruct transaction_packfile packfile;\n \tconst char *prefix;\n };\n@@ -507,7 +508,7 @@ int odb_transaction_files_prepare(struct odb_transaction *base)\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+\ttransaction->quarantine = tmp_objdir_replace_primary_odb(transaction->objdir, 0);\n \n \treturn 0;\n }\ndiff --git a/odb.c b/odb.c\nindex caf1d0f542..8afcb6b637 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -226,7 +226,8 @@ struct odb_source *odb_add_to_alternates_memory(struct object_database *odb,\n }\n \n struct odb_source *odb_set_temporary_primary_source(struct object_database *odb,\n-\t\t\t\t\t\t    const char *dir, int will_destroy)\n+\t\t\t\t\t\t    const char *dir, int will_destroy,\n+\t\t\t\t\t\t    struct odb_source **prev_source)\n {\n \tstruct odb_source *source;\n \n@@ -250,7 +251,11 @@ struct odb_source *odb_set_temporary_primary_source(struct object_database *odb,\n \tsource->will_destroy = will_destroy;\n \tsource->next = odb->sources;\n \todb->sources = source;\n-\treturn source->next;\n+\n+\tif (prev_source)\n+\t\t*prev_source = source->next;\n+\n+\treturn source;\n }\n \n void odb_restore_primary_source(struct object_database *odb,\ndiff --git a/odb.h b/odb.h\nindex fca67e8253..bdfcb9509a 100644\n--- a/odb.h\n+++ b/odb.h\n@@ -199,10 +199,12 @@ struct odb_source *odb_find_source_or_die(struct object_database *odb, const cha\n \n /*\n  * Replace the current writable object directory with the specified temporary\n- * object directory; returns the former primary source.\n+ * object directory and return the newly installed primary source. The former\n+ * primary source is reported via `prev_source` when non-NULL.\n  */\n struct odb_source *odb_set_temporary_primary_source(struct object_database *odb,\n-\t\t\t\t\t\t    const char *dir, int will_destroy);\n+\t\t\t\t\t\t    const char *dir, int will_destroy,\n+\t\t\t\t\t\t    struct odb_source **prev_source);\n \n /*\n  * Restore the primary source that was previously replaced by\ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex d199d39e7c..e633d97e0e 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -327,11 +327,13 @@ void tmp_objdir_add_as_alternate(const struct tmp_objdir *t)\n \todb_add_to_alternates_memory(t->repo->objects, t->path.buf);\n }\n \n-void tmp_objdir_replace_primary_odb(struct tmp_objdir *t, int will_destroy)\n+struct odb_source *tmp_objdir_replace_primary_odb(struct tmp_objdir *t,\n+\t\t\t\t\t\t  int will_destroy)\n {\n \tif (t->prev_source)\n \t\tBUG(\"the primary object database is already replaced\");\n-\tt->prev_source = odb_set_temporary_primary_source(t->repo->objects,\n-\t\t\t\t\t\t\t  t->path.buf, will_destroy);\n \tt->will_destroy = will_destroy;\n+\n+\treturn odb_set_temporary_primary_source(t->repo->objects, t->path.buf,\n+\t\t\t\t\t\twill_destroy, &t->prev_source);\n }\ndiff --git a/tmp-objdir.h b/tmp-objdir.h\nindex ccf800faa7..81eb927413 100644\n--- a/tmp-objdir.h\n+++ b/tmp-objdir.h\n@@ -64,8 +64,10 @@ void tmp_objdir_add_as_alternate(const struct tmp_objdir *);\n /*\n  * Replaces the writable object store in the current process with the temporary\n  * object directory and makes the former main object store an alternate.\n- * If will_destroy is nonzero, the object directory may not be migrated.\n+ * If will_destroy is nonzero, the object directory may not be migrated. Returns\n+ * the newly installed primary source.\n  */\n-void tmp_objdir_replace_primary_odb(struct tmp_objdir *, int will_destroy);\n+struct odb_source *tmp_objdir_replace_primary_odb(struct tmp_objdir *,\n+\t\t\t\t\t\t  int will_destroy);\n \n #endif /* TMP_OBJDIR_H */\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550853","messageId":"20260819215311.3880274-10-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260819215311.3880274-1-jltobler@gmail.com","subject":"[PATCH v4 9/9] odb/transaction: add transaction interface to write packfiles","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-19T21:53:11Z","receivedAt":"2026-08-19T21:53:22Z","isPatch":true,"body":"In git-receive-pack(1), the incoming packfile is written to the ODB via\n`unpack()`, which spawns git-index-pack(1) or git-unpack-objects(1)\ndirectly. With pluggable object databases, an alternative backend may\nneed to handle writing packfile data differently though.\n\nIntroduce `odb_transaction_write_pack()` as a generic interface to\nhandle writing a packfile to a transaction and use the logic from\n`unpack()` as the \"files\" backend implementation. Note that when storing\nthe objects as a packfile, git-index-pack(1) also writes a \".keep\"\nlockfile next to it to prevent a concurrent repack from removing the new\npack prior to reference updates being performed. The \"files\" transaction\nbackend is responsible for managing these \".keep\" files and removes them\npost-commit once the transaction is finalized.\n\nCall sites in git-receive-pack(1) are updated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 160 +-----------------------------------\n object-file.c          | 178 +++++++++++++++++++++++++++++++++++++++++\n odb/transaction.c      |   7 ++\n odb/transaction.h      |  62 ++++++++++++++\n 4 files changed, 250 insertions(+), 157 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex b369466783..e6e54ba55f 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -15,7 +15,6 @@\n #include \"gpg-interface.h\"\n #include \"hex.h\"\n #include \"hook.h\"\n-#include \"lockfile.h\"\n #include \"object.h\"\n #include \"object-file.h\"\n #include \"object-name.h\"\n@@ -23,7 +22,6 @@\n #include \"oid-array.h\"\n #include \"oidset.h\"\n #include \"pack.h\"\n-#include \"packfile.h\"\n #include \"parse-options.h\"\n #include \"pkt-line.h\"\n #include \"protocol.h\"\n@@ -2292,162 +2290,11 @@ static void read_push_options(struct packet_reader *reader,\n \t}\n }\n \n-static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n-{\n-\tswitch (read_pack_header(pack_fd, hdr)) {\n-\tcase PH_ERROR_EOF:\n-\t\treturn \"eof before pack header was fully read\";\n-\n-\tcase PH_ERROR_PACK_SIGNATURE:\n-\t\treturn \"protocol error (pack signature mismatch detected)\";\n-\n-\tcase PH_ERROR_PROTOCOL:\n-\t\treturn \"protocol error (pack version unsupported)\";\n-\n-\tdefault:\n-\t\treturn \"unknown error in parse_pack_header\";\n-\n-\tcase 0:\n-\t\treturn NULL;\n-\t}\n-}\n-\n-static struct tempfile *pack_lockfile;\n-\n-static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n-{\n-\tstrvec_pushf(args, \"--pack_header=%\"PRIu32\",%\"PRIu32,\n-\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n-}\n-\n-static unsigned int get_unpack_limit(struct repository *repo)\n-{\n-\tunsigned int limit = 100;\n-\n-\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n-\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n-\n-\treturn limit;\n-}\n-\n-struct unpack_opts {\n-\tconst char *fsck_msg_types;\n-\tconst char *shallow_file;\n-\toff_t max_input_size;\n-\tint fsck_objects;\n-\tint reject_thin;\n-\tint err_fd;\n-\tint quiet;\n-};\n-\n-static int unpack(struct odb_transaction *transaction, int pack_fd,\n-\t\t  struct strbuf *err_msg, const struct unpack_opts *opts)\n-{\n-\tstruct pack_header hdr;\n-\tconst char *hdr_err;\n-\tint status;\n-\tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint err_fd = opts->err_fd;\n-\n-\thdr_err = parse_pack_header(&hdr, pack_fd);\n-\tif (hdr_err) {\n-\t\tif (err_fd > 0)\n-\t\t\tclose(err_fd);\n-\t\tstrbuf_addstr(err_msg, hdr_err);\n-\t\treturn -1;\n-\t}\n-\n-\tif (opts->shallow_file) {\n-\t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, opts->shallow_file);\n-\t}\n-\n-\todb_transaction_env(transaction, &child.env);\n-\n-\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n-\t\tstrvec_push(&child.args, \"unpack-objects\");\n-\t\tpush_header_arg(&child.args, &hdr);\n-\t\tif (opts->quiet)\n-\t\t\tstrvec_push(&child.args, \"-q\");\n-\t\tif (opts->fsck_objects)\n-\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     opts->fsck_msg_types);\n-\t\tif (opts->max_input_size)\n-\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)opts->max_input_size);\n-\t\tchild.no_stdout = 1;\n-\t\tchild.in = pack_fd;\n-\t\tchild.err = err_fd;\n-\t\tchild.git_cmd = 1;\n-\t\tstatus = run_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n-\t\t\treturn -1;\n-\t\t}\n-\t} else {\n-\t\tchar hostname[HOST_NAME_MAX + 1];\n-\t\tchar *lockfile;\n-\n-\t\tstrvec_pushl(&child.args, \"index-pack\", \"--stdin\", NULL);\n-\t\tpush_header_arg(&child.args, &hdr);\n-\n-\t\tif (xgethostname(hostname, sizeof(hostname)))\n-\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n-\t\tstrvec_pushf(&child.args,\n-\t\t\t     \"--keep=receive-pack %\"PRIuMAX\" on %s\",\n-\t\t\t     (uintmax_t)getpid(),\n-\t\t\t     hostname);\n-\n-\t\tif (!opts->quiet && err_fd)\n-\t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n-\t\tif (err_fd)\n-\t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n-\t\tif (opts->fsck_objects)\n-\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     opts->fsck_msg_types);\n-\t\tif (!opts->reject_thin)\n-\t\t\tstrvec_push(&child.args, \"--fix-thin\");\n-\t\tif (opts->max_input_size)\n-\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)opts->max_input_size);\n-\t\tchild.out = -1;\n-\t\tchild.in = pack_fd;\n-\t\tchild.err = err_fd;\n-\t\tchild.git_cmd = 1;\n-\t\tstatus = start_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\t/*\n-\t\t * The lockfile filepath is expected to be the final location of\n-\t\t * the \".keep\" file after being migrated to the main ODB source.\n-\t\t * This ensures the lockfile can be found and removed later\n-\t\t * after the ODB transaction has been committed.\n-\t\t */\n-\t\tlockfile = index_pack_lockfile(transaction->source, child.out, NULL);\n-\t\tif (lockfile) {\n-\t\t\tpack_lockfile = register_tempfile(lockfile);\n-\t\t\tfree(lockfile);\n-\t\t}\n-\t\tclose(child.out);\n-\n-\t\tstatus = finish_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n-\t\t\treturn -1;\n-\t\t}\n-\t\todb_reprepare(the_repository->objects);\n-\t}\n-\treturn 0;\n-}\n-\n static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\t\t\tconst char *shallow_file,\n \t\t\t\tstruct strbuf *err_msg)\n {\n-\tstruct unpack_opts opts = {\n+\tstruct odb_transaction_write_pack_opts opts = {\n \t\t.fsck_objects = (receive_fsck_objects >= 0\n \t\t\t\t ? receive_fsck_objects\n \t\t\t\t : transfer_fsck_objects >= 0\n@@ -2463,7 +2310,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, 0, err_msg, &opts);\n+\t\treturn odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2473,7 +2320,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, 0, err_msg, &opts);\n+\tret = odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2748,7 +2595,6 @@ int cmd_receive_pack(int argc,\n \t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n \t\t\t\t &push_options);\n \t\todb_transaction_finalize(transaction);\n-\t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\n \t\t\treport_v2(commands, &unpack_status);\ndiff --git a/object-file.c b/object-file.c\nindex db63587f6d..265c5f7a3c 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -10,6 +10,7 @@\n #define USE_THE_REPOSITORY_VARIABLE\n \n #include \"git-compat-util.h\"\n+#include \"config.h\"\n #include \"convert.h\"\n #include \"dir.h\"\n #include \"environment.h\"\n@@ -26,6 +27,7 @@\n #include \"packfile.h\"\n #include \"path.h\"\n #include \"read-cache-ll.h\"\n+#include \"run-command.h\"\n #include \"setup.h\"\n #include \"strvec.h\"\n #include \"tempfile.h\"\n@@ -483,11 +485,16 @@ struct transaction_packfile {\n \n struct odb_transaction_files {\n \tstruct odb_transaction base;\n+\tenum odb_transaction_flags flags;\n \n \tstruct tmp_objdir *objdir;\n \tstruct odb_source *quarantine;\n \tstruct transaction_packfile packfile;\n \tconst char *prefix;\n+\n+\tstruct tempfile **pack_lockfiles;\n+\tsize_t pack_lockfiles_nr;\n+\tsize_t pack_lockfiles_alloc;\n };\n \n int odb_transaction_files_prepare(struct odb_transaction *base)\n@@ -1291,6 +1298,174 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n \treturn 0;\n }\n \n+static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n+{\n+\tswitch (read_pack_header(pack_fd, hdr)) {\n+\tcase PH_ERROR_EOF:\n+\t\treturn \"eof before pack header was fully read\";\n+\n+\tcase PH_ERROR_PACK_SIGNATURE:\n+\t\treturn \"protocol error (pack signature mismatch detected)\";\n+\n+\tcase PH_ERROR_PROTOCOL:\n+\t\treturn \"protocol error (pack version unsupported)\";\n+\n+\tdefault:\n+\t\treturn \"unknown error in parse_pack_header\";\n+\n+\tcase 0:\n+\t\treturn NULL;\n+\t}\n+}\n+\n+static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n+{\n+\tstrvec_pushf(args, \"--pack_header=%\"PRIu32\",%\"PRIu32,\n+\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n+}\n+\n+static unsigned int get_unpack_limit(struct repository *repo,\n+\t\t\t\t     enum odb_transaction_flags flags)\n+{\n+\tunsigned int limit = 0;\n+\n+\tif (flags & ODB_TRANSACTION_RECEIVE) {\n+\t\tlimit = 100;\n+\t\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n+\t\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n+\t}\n+\n+\treturn limit;\n+}\n+\n+static int odb_transaction_files_write_pack(struct odb_transaction *base,\n+\t\t\t\t\t    int pack_fd, struct strbuf *err_msg,\n+\t\t\t\t\t    const struct odb_transaction_write_pack_opts *opts)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\tstruct repository *repo = base->source->odb->repo;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tstruct pack_header hdr;\n+\tconst char *hdr_err;\n+\tint err_fd = opts->err_fd;\n+\tint status;\n+\n+\thdr_err = parse_pack_header(&hdr, pack_fd);\n+\tif (hdr_err) {\n+\t\tif (err_fd > 0)\n+\t\t\tclose(err_fd);\n+\t\tstrbuf_addstr(err_msg, hdr_err);\n+\t\treturn -1;\n+\t}\n+\n+\tif (opts->shallow_file) {\n+\t\tstrvec_push(&child.args, \"--shallow-file\");\n+\t\tstrvec_push(&child.args, opts->shallow_file);\n+\t}\n+\n+\todb_transaction_env(base, &child.env);\n+\n+\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(repo, transaction->flags)) {\n+\t\tstrvec_push(&child.args, \"unpack-objects\");\n+\t\tpush_header_arg(&child.args, &hdr);\n+\t\tif (opts->quiet)\n+\t\t\tstrvec_push(&child.args, \"-q\");\n+\t\tif (opts->fsck_objects)\n+\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (opts->max_input_size)\n+\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n+\t\tchild.no_stdout = 1;\n+\t\tchild.in = pack_fd;\n+\t\tchild.err = err_fd;\n+\t\tchild.git_cmd = 1;\n+\t\tstatus = run_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n+\t} else {\n+\t\tchar hostname[HOST_NAME_MAX + 1];\n+\t\tchar *lockfile;\n+\n+\t\tstrvec_pushl(&child.args, \"index-pack\", \"--stdin\", NULL);\n+\t\tpush_header_arg(&child.args, &hdr);\n+\n+\t\tif (xgethostname(hostname, sizeof(hostname)))\n+\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n+\t\tstrvec_pushf(&child.args,\n+\t\t\t     \"--keep=receive-pack %\"PRIuMAX\" on %s\",\n+\t\t\t     (uintmax_t)getpid(),\n+\t\t\t     hostname);\n+\n+\t\tif (!opts->quiet && err_fd)\n+\t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n+\t\tif (err_fd)\n+\t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n+\t\tif (opts->fsck_objects)\n+\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (!opts->reject_thin)\n+\t\t\tstrvec_push(&child.args, \"--fix-thin\");\n+\t\tif (opts->max_input_size)\n+\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n+\t\tchild.out = -1;\n+\t\tchild.in = pack_fd;\n+\t\tchild.err = err_fd;\n+\t\tchild.git_cmd = 1;\n+\t\tstatus = start_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\t/*\n+\t\t * The lockfile filepath is expected to be the final location of\n+\t\t * the \".keep\" file after being migrated to the main ODB source.\n+\t\t * This ensures the lockfile can be found and removed later\n+\t\t * after the ODB transaction has been committed.\n+\t\t */\n+\t\tlockfile = index_pack_lockfile(base->source, child.out, NULL);\n+\t\tif (lockfile) {\n+\t\t\tALLOC_GROW(transaction->pack_lockfiles,\n+\t\t\t\t   transaction->pack_lockfiles_nr + 1,\n+\t\t\t\t   transaction->pack_lockfiles_alloc);\n+\t\t\ttransaction->pack_lockfiles[transaction->pack_lockfiles_nr++] =\n+\t\t\t\tregister_tempfile(lockfile);\n+\t\t\tfree(lockfile);\n+\t\t}\n+\t\tclose(child.out);\n+\n+\t\tstatus = finish_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\todb_source_prepare(transaction->quarantine,\n+\t\t\t\t   ODB_PREPARE_FLUSH_CACHES);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int odb_transaction_files_finalize(struct odb_transaction *base)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\tint ret = 0;\n+\n+\tfor (size_t i = 0; i < transaction->pack_lockfiles_nr; i++)\n+\t\tret |= delete_tempfile(&transaction->pack_lockfiles[i]);\n+\n+\tfree(transaction->pack_lockfiles);\n+\n+\treturn ret;\n+}\n+\n static int odb_transaction_files_env(struct odb_transaction *base,\n \t\t\t\t     struct strvec *env)\n {\n@@ -1314,8 +1489,11 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction = xcalloc(1, sizeof(*transaction));\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n+\ttransaction->base.finalize = odb_transaction_files_finalize;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\ttransaction->base.write_pack = odb_transaction_files_write_pack;\n \ttransaction->base.env = odb_transaction_files_env;\n+\ttransaction->flags = flags;\n \n \ttransaction->prefix = \"bulk-fsync\";\n \tif (flags & ODB_TRANSACTION_RECEIVE) {\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex 9e9a982778..c9144e6cd6 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -59,6 +59,13 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n \treturn transaction->write_object_stream(transaction, stream, len, oid);\n }\n \n+int odb_transaction_write_pack(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t       struct strbuf *err_msg,\n+\t\t\t       const struct odb_transaction_write_pack_opts *opts)\n+{\n+\treturn transaction->write_pack(transaction, pack_fd, err_msg, opts);\n+}\n+\n int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env)\n {\n \tif (!transaction)\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 6ed39b3d0e..8cb06c1191 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -4,6 +4,50 @@\n #include \"gettext.h\"\n #include \"odb.h\"\n \n+/*\n+ * Options controlling how odb_transaction_write_pack() ingests a packfile.\n+ */\n+struct odb_transaction_write_pack_opts {\n+\t/*\n+\t * Optional fsck severity configuration to apply when incoming objects\n+\t * are verified.\n+\t */\n+\tconst char *fsck_msg_types;\n+\n+\t/*\n+\t * Path to an alternative shallow file describing the shallow boundaries\n+\t * to honor while ingesting the pack.\n+\t */\n+\tconst char *shallow_file;\n+\n+\t/*\n+\t * The max size in bytes of the incoming packfile allowed. No limit is\n+\t * enforced when set to 0.\n+\t */\n+\toff_t max_input_size;\n+\n+\t/*\n+\t * Whether the validity of incoming objects should be verified.\n+\t */\n+\tint fsck_objects;\n+\n+\t/*\n+\t * Whether to reject an incoming packfile if it is \"thin\".\n+\t */\n+\tint reject_thin;\n+\n+\t/*\n+\t * Optional file descriptor for reporting progress and errors. Set to 0\n+\t * for none.\n+\t */\n+\tint err_fd;\n+\n+\t/*\n+\t * Suppresses progress reporting.\n+\t */\n+\tint quiet;\n+};\n+\n /*\n  * A transaction may be started for an object database prior to writing new\n  * objects via odb_transaction_begin(). These objects are not committed until\n@@ -40,6 +84,15 @@ 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+\t/*\n+\t * This callback is expected to ingest the packfile readable via\n+\t * `pack_fd` into the transaction. Returns 0 on success, a negative\n+\t * error code otherwise. On failure, a human-readable description is\n+\t * appended to `err_msg`.\n+\t */\n+\tint (*write_pack)(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t  struct strbuf *err_msg,\n+\t\t\t  const struct odb_transaction_write_pack_opts *opts);\n \n \t/*\n \t * This callback is expected to populate the provided strvec with the\n@@ -107,6 +160,15 @@ 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+ * Ingests the packfile readable via `pack_fd` into the transaction. Returns 0\n+ * on success, a negative error code otherwise. On failure, a human-readable\n+ * description is appended to `err_msg`.\n+ */\n+int odb_transaction_write_pack(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t       struct strbuf *err_msg,\n+\t\t\t       const struct odb_transaction_write_pack_opts *opts);\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-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550867","messageId":"aoaixMF1biKYhWN2@pks.im","threadId":"66133","inReplyTo":"20260819215311.3880274-2-jltobler@gmail.com","subject":"Re: [PATCH v4 1/9] builtin/receive-pack: properly clean up keep files","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-20T06:46:28Z","receivedAt":"2026-08-20T06:46:36Z","isPatch":true,"body":"On Wed, Aug 19, 2026 at 04:53:03PM -0500, Justin Tobler wrote:\n> diff --git a/t/t5547-push-quarantine.sh b/t/t5547-push-quarantine.sh\n> index 0798ddab02..3da253cc1a 100755\n> --- a/t/t5547-push-quarantine.sh\n> +++ b/t/t5547-push-quarantine.sh\n> @@ -70,4 +70,26 @@ test_expect_success 'updating a ref from quarantine is forbidden' '\n>  \tgit -C update.git fsck\n>  '\n>  \n> +test_expect_success '.keep file is removed after push' '\n> +\ttest_when_finished rm -rf keep.git &&\n> +\tgit init --bare keep.git &&\n> +\n> +\tgit -C keep.git config set receive.unpackLimit 0 &&\n> +\n> +\t# While incoming objects are still quarantined, validate that the keep\n> +\t# lockfile does indeed exist.\n> +\ttest_hook -C keep.git pre-receive <<-\\EOF &&\n> +\tkeep=\"$(ls \"$GIT_QUARANTINE_PATH\"/pack/pack-*.keep)\" &&\n> +\ttest -f \"$keep\"\n> +\tEOF\n\nGood. So we know that the file exists while the transaction is\nrunning...\n\n> +\ttest_commit foo &&\n> +\tgit push keep.git HEAD &&\n> +\tpack=\"$(ls keep.git/objects/pack/pack-*.pack)\" &&\n> +\tkeep=\"${pack%.pack}.keep\" &&\n> +\n> +\ttest_path_is_file \"$pack\" &&\n> +\ttest_path_is_missing \"$keep\"\n> +'\n\n... and we know that the packfile exists without its \".keep\" file once\nthe transaction has been committed.\n\nWhat we don't verify is that the \".keep\" file is getting migrated to the\ntarget repository and stays intact while we're updating references. So\ndo we maybe want to add the following diff so that we test for the full\nlifecycle of the \".keep\" file?\n\ndiff --git a/t/t5547-push-quarantine.sh b/t/t5547-push-quarantine.sh\nindex 3da253cc1a..a722a01e8d 100755\n--- a/t/t5547-push-quarantine.sh\n+++ b/t/t5547-push-quarantine.sh\n@@ -83,11 +83,19 @@ test_expect_success '.keep file is removed after push' '\n \ttest -f \"$keep\"\n \tEOF\n \n+\t# And when updating references the keep-file should have been migrated\n+\t# to the actual repository.\n+\ttest_hook -C keep.git reference-transaction <<-\\EOF &&\n+\tkeep=\"$(ls objects/pack/pack-*.keep)\" &&\n+\ttest -f \"$keep\"\n+\tEOF\n+\n \ttest_commit foo &&\n \tgit push keep.git HEAD &&\n+\n+\t# Once done, there should be no \".keep\" files anywhere anymore.\n \tpack=\"$(ls keep.git/objects/pack/pack-*.pack)\" &&\n \tkeep=\"${pack%.pack}.keep\" &&\n-\n \ttest_path_is_file \"$pack\" &&\n \ttest_path_is_missing \"$keep\"\n '\n\nPatrick\n"},{"id":"550868","messageId":"aoaiy1wmSXjL30-m@pks.im","threadId":"66133","inReplyTo":"20260819215311.3880274-3-jltobler@gmail.com","subject":"Re: [PATCH v4 2/9] odb/transaction: add transaction finalize interface","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-20T06:46:35Z","receivedAt":"2026-08-20T06:46:40Z","isPatch":true,"body":"On Wed, Aug 19, 2026 at 04:53:04PM -0500, Justin Tobler wrote:\n> When committing an ODB transaction via `odb_transaction_commit()`, the\n> staged objects are made visible and the underlying transaction is freed\n> at the same time. Coupling these two steps does not leave room for any\n> post-commit transaction operations to be introduced though. Such a\n> capability is useful if an ODB transaction backend needs to hold on to\n> lockfiles after transaction commit until references are updated, as is\n> the case with the existing \"files\" backend in git-receive-pack(1).\n> \n> Stop freeing the transaction in `odb_transaction_commit()` and introduce\n> `odb_transaction_finalize()` to explicitly clean up the transaction\n> accordingly. Note that the finalize interface also provides an optional\n> callback for any backend-specific deferred cleanup. In a subsequent\n> commit, the \"files\" transaction backend will use this to remove \".keep\"\n> files generated for packfiles received via git-receive-pack(1) after\n> references have been updated. In preparation for this, the\n> `odb_transaction_finalize()` call site in git-receive-pack(1) is made\n> after the reference updates are finished.\n> \n> All other callers commit a transaction and immediately finalize it with\n> no work in between and cannot meaningfully recover should either fail,\n> so introduce an `odb_transaction_commit_and_finalize_or_die()` helper\n> that performs both and dies on error. Call sites are updated\n> accordingly.\n\nThat paragraph is a bit hard to read. How about:\n\n    All other callers commit a transaction and immediately finalize it\n    without any work happening in between those two operations.\n    Consequently, they cannot meaningfully recover in case either of\n    them would fail, and spelling out these two separate steps with\n    proper error handling would be quite repetitive and pointless.\n    Introduce a helper `odb_transaction_commit_and_finalize_or_die()` to\n    help those call sites and update them accordingly.\n\nPatrick\n"},{"id":"550869","messageId":"aoaiz7M1oGboydY4@pks.im","threadId":"66133","inReplyTo":"20260819215311.3880274-10-jltobler@gmail.com","subject":"Re: [PATCH v4 9/9] odb/transaction: add transaction interface to write packfiles","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-20T06:46:39Z","receivedAt":"2026-08-20T06:46:44Z","isPatch":true,"body":"On Wed, Aug 19, 2026 at 04:53:11PM -0500, Justin Tobler wrote:\n> diff --git a/object-file.c b/object-file.c\n> index db63587f6d..265c5f7a3c 100644\n> --- a/object-file.c\n> +++ b/object-file.c\n[snip]\n> +static unsigned int get_unpack_limit(struct repository *repo,\n> +\t\t\t\t     enum odb_transaction_flags flags)\n> +{\n> +\tunsigned int limit = 0;\n> +\n> +\tif (flags & ODB_TRANSACTION_RECEIVE) {\n> +\t\tlimit = 100;\n> +\t\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n> +\t\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n> +\t}\n> +\n> +\treturn limit;\n> +}\n\nOkay, instead of assuming that we're running in git-receive-pack(1) we\nnow pass this information along via the flags so that we can pick the\ncorrect limit for the given operation. That's somewhat pointless right\nnow as no other operations use this infra yet, but the upside is that it\nmakes it obvious for how to extend the mechanism going forward.\n\nAlso, we no longer cache the value and the logic to derive it has become\na lot simpler. Good.\n\nPatrick\n"},{"id":"550951","messageId":"aodyeILDMaBOX--K@denethor","threadId":"66133","inReplyTo":"aoaixMF1biKYhWN2@pks.im","subject":"Re: [PATCH v4 1/9] builtin/receive-pack: properly clean up keep files","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-20T21:33:26Z","receivedAt":"2026-08-20T21:33:31Z","isPatch":true,"body":"On 26/08/20 08:46AM, Patrick Steinhardt wrote:\n> On Wed, Aug 19, 2026 at 04:53:03PM -0500, Justin Tobler wrote:\n> What we don't verify is that the \".keep\" file is getting migrated to the\n> target repository and stays intact while we're updating references. So\n> do we maybe want to add the following diff so that we test for the full\n> lifecycle of the \".keep\" file?\n> \n> diff --git a/t/t5547-push-quarantine.sh b/t/t5547-push-quarantine.sh\n> index 3da253cc1a..a722a01e8d 100755\n> --- a/t/t5547-push-quarantine.sh\n> +++ b/t/t5547-push-quarantine.sh\n> @@ -83,11 +83,19 @@ test_expect_success '.keep file is removed after push' '\n>  \ttest -f \"$keep\"\n>  \tEOF\n>  \n> +\t# And when updating references the keep-file should have been migrated\n> +\t# to the actual repository.\n> +\ttest_hook -C keep.git reference-transaction <<-\\EOF &&\n> +\tkeep=\"$(ls objects/pack/pack-*.keep)\" &&\n> +\ttest -f \"$keep\"\n> +\tEOF\n> +\n>  \ttest_commit foo &&\n>  \tgit push keep.git HEAD &&\n> +\n> +\t# Once done, there should be no \".keep\" files anywhere anymore.\n>  \tpack=\"$(ls keep.git/objects/pack/pack-*.pack)\" &&\n>  \tkeep=\"${pack%.pack}.keep\" &&\n> -\n>  \ttest_path_is_file \"$pack\" &&\n>  \ttest_path_is_missing \"$keep\"\n>  '\n\nMakes sense, I'll add something similar in the next version. Thanks :)\n\n-Justin\n"},{"id":"550955","messageId":"20260820234940.894624-1-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260819215311.3880274-1-jltobler@gmail.com","subject":"[PATCH v5 0/9] builtin/receive-pack: support pluggable packfile writes","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-20T23:49:31Z","receivedAt":"2026-08-20T23:49:46Z","isPatch":true,"body":"Greetings,\n\nWith bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\ntransactions, 2026-07-10), git-receive-pack(1) started using the ODB\ntransaction interfaces to stage incoming objects. While this brought the\ncommand closer to being ODB backend agnostic, the underlying\ngit-index-pack(1) and git-unpack-objects(1) processes used to actually\nwrite the objects to the transaction are still fundamentally tied to the\n\"files\" backend.\n\nThis series aims to address this by introducing a generic\n`odb_transaction_write_pack()` transaction interface to handle writing\nthe incoming packfile to the transaction. The existing logic in\ngit-receive-pack(1) that spawns the child processes to write the\npackfile becomes the \"files\" backend implementation of this interface.\n\nChanges since V4:\n- Added an additional test assertion in the frist patch to ensure keep\n  files are also migrated to the main ODB prior to being removed when\n  the transaction is finalized.\n- Updated a commit message.\n\nChances since V3:\n- In preparation for future `odb_transaction_write_pack()` users, the\n  unpack limit takes into consideration odb_transaction_flags to augment\n  configutation.\n- Added additional test assertion in first patch to ensure keep file is\n  generated and placed in quarantine directory.\n- Removed an include statement in favor of just forward declaring a\n  struct.\n- Updated some commit messages.\n\nChanges since V2:\n- Added a patch to address a bug causing \".keep\" files from not being\n  removed.\n- Started handling errors at transaction commit and finalize call sites\n  instead of ignoring them. We also make sure\n  `odb_transaction_finalize()` runs after every successful commit\n  callsite to ensure proper cleanup.\n- Updated the code handling lazy loading of unpack limit configuration\n  to not longer cache the value.\n- Added a patch to begin explictly tracking the ODB source used by the\n  \"files\" transaction to avoid relying on the ordering of the ODB source\n  list.\n- Updated some commit messages to improve clarity.\n\nChanges since V1:\n- Changed the \"release\" interface name to \"finalize\" and updated it to\n  return error codes.\n- Marked some function parameters as const.\n- Unpack limit configuration is now resolved in the ODB transaction\n  backend instead of wiring it through the interface.\n- When writing a packfile to the transaction, now only the transaction\n  source is prepared.\n- Updated some commit messages.\n- Updated some code formatting.\n\nThanks for the review,\n-Justin\n\nJustin Tobler (9):\n  builtin/receive-pack: properly clean up keep files\n  odb/transaction: add transaction finalize interface\n  builtin/receive-pack: pass shallow file explicitly\n  builtin/receive-pack: read unpack limit config lazily\n  builtin/receive-pack: lift global state out of unpack()\n  builtin/receive-pack: report unpack errors via strbuf\n  builtin/receive-pack: explicitly pass packfile fd\n  odb: return temporary ODB source when set\n  odb/transaction: add transaction interface to write packfiles\n\n builtin/add.c              |   4 +-\n builtin/receive-pack.c     | 211 ++++++++-----------------------------\n builtin/unpack-objects.c   |   2 +-\n builtin/update-index.c     |   4 +-\n cache-tree.c               |   2 +-\n fetch-pack.c               |   2 +-\n object-file.c              | 183 +++++++++++++++++++++++++++++++-\n odb.c                      |   9 +-\n odb.h                      |   6 +-\n odb/transaction.c          |  21 ++++\n odb/transaction.h          |  85 +++++++++++++++\n pack-write.c               |   7 +-\n pack.h                     |   4 +-\n read-cache.c               |   2 +-\n t/t5547-push-quarantine.sh |  31 ++++++\n tmp-objdir.c               |   8 +-\n tmp-objdir.h               |   6 +-\n 17 files changed, 399 insertions(+), 188 deletions(-)\n\nRange-diff against v4:\n 1:  13a57feea7 !  1:  1bae015e8c builtin/receive-pack: properly clean up keep files\n    @@ t/t5547-push-quarantine.sh: test_expect_success 'updating a ref from quarantine\n     +\n     +\tgit -C keep.git config set receive.unpackLimit 0 &&\n     +\n    -+\t# While incoming objects are still quarantined, validate that the keep\n    -+\t# lockfile does indeed exist.\n    ++\t# While incoming objects are still quarantined, validate that the\n    ++\t# \".keep\" lockfile is present in the quarantine directory.\n     +\ttest_hook -C keep.git pre-receive <<-\\EOF &&\n     +\tkeep=\"$(ls \"$GIT_QUARANTINE_PATH\"/pack/pack-*.keep)\" &&\n     +\ttest -f \"$keep\"\n     +\tEOF\n     +\n    ++\t# After quarantined objects are migrated, validate that the \".keep\"\n    ++\t# lockfile is migrated and present in the main ODB.\n    ++\ttest_hook -C keep.git reference-transaction <<-\\EOF &&\n    ++\tkeep=\"$(ls objects/pack/pack-*.keep)\" &&\n    ++\ttest -f \"$keep\"\n    ++\tEOF\n    ++\n     +\ttest_commit foo &&\n     +\tgit push keep.git HEAD &&\n    ++\n    ++\t# Once the operation is complete, validate that the \".keep\" lockfile has\n    ++\t# been removed.\n     +\tpack=\"$(ls keep.git/objects/pack/pack-*.pack)\" &&\n     +\tkeep=\"${pack%.pack}.keep\" &&\n    -+\n     +\ttest_path_is_file \"$pack\" &&\n     +\ttest_path_is_missing \"$keep\"\n     +'\n 2:  49254af71c !  2:  a2a10966a8 odb/transaction: add transaction finalize interface\n    @@ Commit message\n         `odb_transaction_finalize()` call site in git-receive-pack(1) is made\n         after the reference updates are finished.\n     \n    -    All other callers commit a transaction and immediately finalize it with\n    -    no work in between and cannot meaningfully recover should either fail,\n    -    so introduce an `odb_transaction_commit_and_finalize_or_die()` helper\n    -    that performs both and dies on error. Call sites are updated\n    -    accordingly.\n    +    All other callers commit a transaction and immediately finalize it\n    +    without any work happening in between those two operations.\n    +    Consequently, they cannot meaningfully recover in case either of them\n    +    would fail, and spelling out these two separate steps with proper error\n    +    handling would be quite repetitive and pointless. Introduce a helper\n    +    `odb_transaction_commit_and_finalize_or_die()` for those call sites and\n    +    update them accordingly.\n     \n         Signed-off-by: Justin Tobler <jltobler@gmail.com>\n     \n 3:  882cf06bc3 =  3:  063b1830a1 builtin/receive-pack: pass shallow file explicitly\n 4:  8deec37a09 =  4:  04c42ebefd builtin/receive-pack: read unpack limit config lazily\n 5:  92d56134f0 =  5:  f4a633a212 builtin/receive-pack: lift global state out of unpack()\n 6:  d614b10715 =  6:  9b89af0bd8 builtin/receive-pack: report unpack errors via strbuf\n 7:  bc5839ad8e =  7:  edb54e79f6 builtin/receive-pack: explicitly pass packfile fd\n 8:  13540b91b8 =  8:  452affa42f odb: return temporary ODB source when set\n 9:  62d46d5c07 =  9:  dae4b96bc3 odb/transaction: add transaction interface to write packfiles\n\nbase-commit: 2c78326f810173a4f3aefd8021f1e07575412481\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550956","messageId":"20260820234940.894624-2-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260820234940.894624-1-jltobler@gmail.com","subject":"[PATCH v5 1/9] builtin/receive-pack: properly clean up keep files","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-20T23:49:32Z","receivedAt":"2026-08-20T23:49:47Z","isPatch":true,"body":"When git-receive-pack(1) stores an incoming packfile with\ngit-index-pack(1), a \".keep\" file is written alongside it in the\ntransaction quarantine directory and also gets migrated to the main ODB\nwhen the ODB transaction is committed. This keep lockfile ensures the\npackfile remains in place until the references have been updated and is\nremoved afterwards. The path used to remove it is derived via\n`index_pack_lockfile()` from the repository's primary object directory.\n\nIn bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB\ntransactions, 2026-07-10), git-receive-pack(1) started using the ODB\ntransaction interfaces instead of managing a temporary directory\ndirectly. When starting an ODB transaction, the sources list is\nreordered to insert the newly created transaction source first as the\nprimary to ensure writes are routed to it accordingly.\n\nPrior to using ODB transactions, git-receive-pack(1) would only set the\ntemporary directory as the primary source for the child\ngit-index-pack(1) and git-unpack-objects(1) processes it spawned and the\nparent process would set the temporary directory set as an alternate\nonly. By using ODB transactions, the ODB source list is also reordered\nfor the parent process which results in `index_pack_lockfile()` deriving\nthe \".keep\" path relative to the temporary directory instead of the\nactual main ODB source path. Consequently, this prevents the \".keep\"\nfile from being properly removed after being migrated into the main ODB\nsource post-commit.\n\nUpdate `index_pack_lockfile()` to operate on an ODB source explicitly\nprovided to it and update call sites accordingly to pass the expected\nODB source.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c     |  8 +++++++-\n fetch-pack.c               |  2 +-\n pack-write.c               |  7 ++++---\n pack.h                     |  4 +++-\n t/t5547-push-quarantine.sh | 31 +++++++++++++++++++++++++++++++\n 5 files changed, 46 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 86933d8d7e..d74b787148 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2412,7 +2412,13 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n \t\tif (status)\n \t\t\treturn \"index-pack fork failed\";\n \n-\t\tlockfile = index_pack_lockfile(the_repository, child.out, NULL);\n+\t\t/*\n+\t\t * The lockfile filepath is expected to be the final location of\n+\t\t * the \".keep\" file after being migrated to the main ODB source.\n+\t\t * This ensures the lockfile can be found and removed later\n+\t\t * after the ODB transaction has been committed.\n+\t\t */\n+\t\tlockfile = index_pack_lockfile(transaction->source, child.out, NULL);\n \t\tif (lockfile) {\n \t\t\tpack_lockfile = register_tempfile(lockfile);\n \t\t\tfree(lockfile);\ndiff --git a/fetch-pack.c b/fetch-pack.c\nindex 922a9b2581..6df5813b33 100644\n--- a/fetch-pack.c\n+++ b/fetch-pack.c\n@@ -1075,7 +1075,7 @@ static int get_pack(struct fetch_pack_args *args,\n \t\tdie(_(\"fetch-pack: unable to fork off %s\"), cmd_name);\n \tif (do_keep && (pack_lockfiles || fsck_objects)) {\n \t\tint is_well_formed;\n-\t\tchar *pack_lockfile = index_pack_lockfile(the_repository,\n+\t\tchar *pack_lockfile = index_pack_lockfile(the_repository->objects->sources,\n \t\t\t\t\t\t\t  cmd.out,\n \t\t\t\t\t\t\t  &is_well_formed);\n \ndiff --git a/pack-write.c b/pack-write.c\nindex 24033a9101..85674e4b72 100644\n--- a/pack-write.c\n+++ b/pack-write.c\n@@ -469,10 +469,11 @@ void fixup_pack_header_footer(const struct git_hash_algo *hash_algo,\n \tfsync_component_or_die(FSYNC_COMPONENT_PACK, pack_fd, pack_name);\n }\n \n-char *index_pack_lockfile(struct repository *r, int ip_out, int *is_well_formed)\n+char *index_pack_lockfile(struct odb_source *source, int ip_out,\n+\t\t\t  int *is_well_formed)\n {\n \tchar packname[GIT_MAX_HEXSZ + 6];\n-\tconst int len = r->hash_algo->hexsz + 6;\n+\tconst int len = source->odb->repo->hash_algo->hexsz + 6;\n \n \t/*\n \t * The first thing we expect from index-pack's output\n@@ -489,7 +490,7 @@ char *index_pack_lockfile(struct repository *r, int ip_out, int *is_well_formed)\n \t\tpackname[len-1] = 0;\n \t\tif (skip_prefix(packname, \"keep\\t\", &name))\n \t\t\treturn xstrfmt(\"%s/pack/pack-%s.keep\",\n-\t\t\t\t       repo_get_object_directory(r), name);\n+\t\t\t\t       source->path, name);\n \t\treturn NULL;\n \t}\n \tif (is_well_formed)\ndiff --git a/pack.h b/pack.h\nindex 1cde92082b..ada506b5c5 100644\n--- a/pack.h\n+++ b/pack.h\n@@ -7,6 +7,7 @@\n struct packed_git;\n struct pack_window;\n struct repository;\n+struct odb_source;\n \n /*\n  * Packed object header\n@@ -105,7 +106,8 @@ off_t write_pack_header(struct hashfile *f, uint32_t);\n void fixup_pack_header_footer(const struct git_hash_algo *, int,\n \t\t\t      unsigned char *, const char *, uint32_t,\n \t\t\t      unsigned char *, off_t);\n-char *index_pack_lockfile(struct repository *r, int fd, int *is_well_formed);\n+char *index_pack_lockfile(struct odb_source *source, int fd,\n+\t\t\t  int *is_well_formed);\n \n struct ref;\n \ndiff --git a/t/t5547-push-quarantine.sh b/t/t5547-push-quarantine.sh\nindex 0798ddab02..1b7097179e 100755\n--- a/t/t5547-push-quarantine.sh\n+++ b/t/t5547-push-quarantine.sh\n@@ -70,4 +70,35 @@ test_expect_success 'updating a ref from quarantine is forbidden' '\n \tgit -C update.git fsck\n '\n \n+test_expect_success '.keep file is removed after push' '\n+\ttest_when_finished rm -rf keep.git &&\n+\tgit init --bare keep.git &&\n+\n+\tgit -C keep.git config set receive.unpackLimit 0 &&\n+\n+\t# While incoming objects are still quarantined, validate that the\n+\t# \".keep\" lockfile is present in the quarantine directory.\n+\ttest_hook -C keep.git pre-receive <<-\\EOF &&\n+\tkeep=\"$(ls \"$GIT_QUARANTINE_PATH\"/pack/pack-*.keep)\" &&\n+\ttest -f \"$keep\"\n+\tEOF\n+\n+\t# After quarantined objects are migrated, validate that the \".keep\"\n+\t# lockfile is migrated and present in the main ODB.\n+\ttest_hook -C keep.git reference-transaction <<-\\EOF &&\n+\tkeep=\"$(ls objects/pack/pack-*.keep)\" &&\n+\ttest -f \"$keep\"\n+\tEOF\n+\n+\ttest_commit foo &&\n+\tgit push keep.git HEAD &&\n+\n+\t# Once the operation is complete, validate that the \".keep\" lockfile has\n+\t# been removed.\n+\tpack=\"$(ls keep.git/objects/pack/pack-*.pack)\" &&\n+\tkeep=\"${pack%.pack}.keep\" &&\n+\ttest_path_is_file \"$pack\" &&\n+\ttest_path_is_missing \"$keep\"\n+'\n+\n test_done\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550957","messageId":"20260820234940.894624-3-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260820234940.894624-1-jltobler@gmail.com","subject":"[PATCH v5 2/9] odb/transaction: add transaction finalize interface","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-20T23:49:33Z","receivedAt":"2026-08-20T23:49:48Z","isPatch":true,"body":"When committing an ODB transaction via `odb_transaction_commit()`, the\nstaged objects are made visible and the underlying transaction is freed\nat the same time. Coupling these two steps does not leave room for any\npost-commit transaction operations to be introduced though. Such a\ncapability is useful if an ODB transaction backend needs to hold on to\nlockfiles after transaction commit until references are updated, as is\nthe case with the existing \"files\" backend in git-receive-pack(1).\n\nStop freeing the transaction in `odb_transaction_commit()` and introduce\n`odb_transaction_finalize()` to explicitly clean up the transaction\naccordingly. Note that the finalize interface also provides an optional\ncallback for any backend-specific deferred cleanup. In a subsequent\ncommit, the \"files\" transaction backend will use this to remove \".keep\"\nfiles generated for packfiles received via git-receive-pack(1) after\nreferences have been updated. In preparation for this, the\n`odb_transaction_finalize()` call site in git-receive-pack(1) is made\nafter the reference updates are finished.\n\nAll other callers commit a transaction and immediately finalize it\nwithout any work happening in between those two operations.\nConsequently, they cannot meaningfully recover in case either of them\nwould fail, and spelling out these two separate steps with proper error\nhandling would be quite repetitive and pointless. Introduce a helper\n`odb_transaction_commit_and_finalize_or_die()` for those call sites and\nupdate them accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/add.c            |  4 ++--\n builtin/receive-pack.c   |  1 +\n builtin/unpack-objects.c |  2 +-\n builtin/update-index.c   |  4 ++--\n cache-tree.c             |  2 +-\n object-file.c            |  2 +-\n odb/transaction.c        | 14 ++++++++++++++\n odb/transaction.h        | 23 +++++++++++++++++++++++\n read-cache.c             |  2 +-\n 9 files changed, 46 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 60ffbede2b..ad418a5952 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -393,7 +393,7 @@ int cmd_add(int argc,\n \tchar *seen = NULL;\n \tchar *ps_matched = NULL;\n \tstruct lock_file lock_file = LOCK_INIT;\n-\tstruct odb_transaction *transaction;\n+\tstruct odb_transaction *transaction = NULL;\n \n \trepo_config(repo, add_config, NULL);\n \n@@ -600,7 +600,7 @@ int cmd_add(int argc,\n \n \tif (chmod_arg && pathspec.nr)\n \t\texit_status |= chmod_pathspec(repo, &pathspec, chmod_arg[0], show_only);\n-\todb_transaction_commit(transaction);\n+\todb_transaction_commit_and_finalize_or_die(transaction);\n \n finish:\n \tif (write_locked_index(repo->index, &lock_file,\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex d74b787148..ed1edcbe93 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2720,6 +2720,7 @@ int cmd_receive_pack(int argc,\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n \t\texecute_commands(commands, unpack_status, &si, transaction,\n \t\t\t\t &push_options);\n+\t\todb_transaction_finalize(transaction);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\ndiff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c\nindex 4263edfbec..d6a2d616d9 100644\n--- a/builtin/unpack-objects.c\n+++ b/builtin/unpack-objects.c\n@@ -603,7 +603,7 @@ static void unpack_all(void)\n \t\tunpack_one(i);\n \t\tdisplay_progress(progress, i + 1);\n \t}\n-\todb_transaction_commit(transaction);\n+\todb_transaction_commit_and_finalize_or_die(transaction);\n \tstop_progress(&progress);\n \n \tif (delta_list)\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 241abd4332..b25d4ecb10 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1156,7 +1156,7 @@ int cmd_update_index(int argc,\n \t\t\t * a transaction.\n \t\t\t */\n \t\t\tif (transaction && verbose) {\n-\t\t\t\todb_transaction_commit(transaction);\n+\t\t\t\todb_transaction_commit_and_finalize_or_die(transaction);\n \t\t\t\ttransaction = NULL;\n \t\t\t}\n \n@@ -1224,7 +1224,7 @@ int cmd_update_index(int argc,\n \t/*\n \t * By now we have added all of the new objects\n \t */\n-\todb_transaction_commit(transaction);\n+\todb_transaction_commit_and_finalize_or_die(transaction);\n \n \tif (split_index > 0) {\n \t\tif (repo_config_get_split_index(the_repository) == 0)\ndiff --git a/cache-tree.c b/cache-tree.c\nindex d92f513286..a220372a42 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -538,7 +538,7 @@ int cache_tree_update(struct index_state *istate, int flags)\n \ti = update_one(istate->cache_tree, istate->cache, istate->cache_nr,\n \t\t       \"\", 0, &skip, flags);\n \tif (!inflight)\n-\t\todb_transaction_commit(transaction);\n+\t\todb_transaction_commit_and_finalize_or_die(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 ec35c318bc..4d03c167d5 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -965,7 +965,7 @@ int index_fd(struct index_state *istate, struct object_id *oid,\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\tif (!inflight)\n-\t\t\t\todb_transaction_commit(transaction);\n+\t\t\t\todb_transaction_commit_and_finalize_or_die(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 dab7da6a9a..9e9a982778 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -33,6 +33,20 @@ int odb_transaction_commit(struct odb_transaction *transaction)\n \n \tret = transaction->commit(transaction);\n \ttransaction->source->odb->transaction = NULL;\n+\n+\treturn ret;\n+}\n+\n+int odb_transaction_finalize(struct odb_transaction *transaction)\n+{\n+\tint ret = 0;\n+\n+\tif (!transaction)\n+\t\treturn 0;\n+\n+\tif (transaction->finalize)\n+\t\tret = transaction->finalize(transaction);\n+\n \tfree(transaction);\n \n \treturn ret;\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 4cb2eafcbf..6ed39b3d0e 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -22,6 +22,13 @@ struct odb_transaction {\n \t */\n \tint (*commit)(struct odb_transaction *transaction);\n \n+\t/*\n+\t * Optional ODB source specific callback invoked when the transaction\n+\t * needs to perform any deferred cleanup after objects have been\n+\t * committed. Returns 0 on success, a negative error code otherwise.\n+\t */\n+\tint (*finalize)(struct odb_transaction *transaction);\n+\n \t/*\n \t * This callback is expected to write the given object stream into\n \t * the ODB transaction. Note that for now, only blobs support streaming.\n@@ -75,6 +82,22 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb,\n  */\n int odb_transaction_commit(struct odb_transaction *transaction);\n \n+/*\n+ * Finalizes an ODB transaction, performing any deferred cleanup and freeing it.\n+ * Must be called for every successfully started transaction. Note that, if the\n+ * specified transaction is NULL, the function is a no-op. Returns 0 on success,\n+ * a negative error code otherwise.\n+ */\n+int odb_transaction_finalize(struct odb_transaction *transaction);\n+\n+static inline void odb_transaction_commit_and_finalize_or_die(struct odb_transaction *transaction)\n+{\n+\tif (odb_transaction_commit(transaction))\n+\t\tdie(_(\"failed to commit ODB transaction\"));\n+\tif (odb_transaction_finalize(transaction))\n+\t\tdie(_(\"failed to finalize ODB transaction\"));\n+}\n+\n /*\n  * Writes the object in the provided stream into the transaction. The resulting\n  * object ID is written into the out pointer. Returns 0 on success, a negative\ndiff --git a/read-cache.c b/read-cache.c\nindex 6c449f393d..0cd0ef85ec 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -4049,7 +4049,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix,\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+\t\todb_transaction_commit_and_finalize_or_die(transaction);\n \n \trelease_revisions(&rev);\n \treturn !!data.add_errors;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550958","messageId":"20260820234940.894624-4-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260820234940.894624-1-jltobler@gmail.com","subject":"[PATCH v5 3/9] builtin/receive-pack: pass shallow file explicitly","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-20T23:49:34Z","receivedAt":"2026-08-20T23:49:48Z","isPatch":true,"body":"If shallow information is provided during `unpack()`, a temporary\nshallow file is created and stored in global state. In a subsequent\ncommit, the `unpack()` logic is moved behind a generic ODB transaction\ninterface to handle writing packfiles and thus can no longer rely on\nsuch global state. Lift the setup of the temporary shallow file out of\n`unpack()` and wire it through to its call sites explicitly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 38 ++++++++++++++++++++++----------------\n 1 file changed, 22 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex ed1edcbe93..135105deae 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -86,7 +86,6 @@ static const char *head_name;\n static void *head_name_to_free;\n static int sent_capabilities;\n static int shallow_update;\n-static const char *alt_shallow_file;\n static struct strbuf push_cert = STRBUF_INIT;\n static struct object_id push_cert_oid;\n static struct signature_check sigcheck;\n@@ -2334,8 +2333,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-\t\t\t  struct odb_transaction *transaction)\n+static const char *unpack(struct odb_transaction *transaction,\n+\t\t\t  const char *shallow_file, int err_fd)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2354,10 +2353,9 @@ static const char *unpack(int err_fd, struct shallow_info *si,\n \t\treturn hdr_err;\n \t}\n \n-\tif (si->nr_ours || si->nr_theirs) {\n-\t\talt_shallow_file = setup_temporary_shallow(si->shallow);\n+\tif (shallow_file) {\n \t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, alt_shallow_file);\n+\t\tstrvec_push(&child.args, shallow_file);\n \t}\n \n \todb_transaction_env(transaction, &child.env);\n@@ -2433,14 +2431,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-\t\t\t\t\tstruct odb_transaction *transaction)\n+static const char *unpack_with_sideband(struct odb_transaction *transaction,\n+\t\t\t\t\tconst char *shallow_file)\n {\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(0, si, transaction);\n+\t\treturn unpack(transaction, shallow_file, 0);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2449,13 +2447,14 @@ 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, transaction);\n+\tret = unpack(transaction, shallow_file, muxer.in);\n \n \tfinish_async(&muxer);\n \treturn ret;\n }\n \n-static void prepare_shallow_update(struct shallow_info *si)\n+static void prepare_shallow_update(struct shallow_info *si,\n+\t\t\t\t   const char *shallow_file)\n {\n \tint i, j, k, bitmap_size = DIV_ROUND_UP(si->ref->nr, 32);\n \n@@ -2495,12 +2494,13 @@ static void prepare_shallow_update(struct shallow_info *si)\n \t * command. check_connected() will be done with\n \t * true .git/shallow though.\n \t */\n-\tsetenv(GIT_SHALLOW_FILE_ENVIRONMENT, alt_shallow_file, 1);\n+\tsetenv(GIT_SHALLOW_FILE_ENVIRONMENT, shallow_file, 1);\n }\n \n static void update_shallow_info(struct command *commands,\n \t\t\t\tstruct shallow_info *si,\n-\t\t\t\tstruct oid_array *ref)\n+\t\t\t\tstruct oid_array *ref,\n+\t\t\t\tconst char *shallow_file)\n {\n \tstruct command *cmd;\n \tint *ref_status;\n@@ -2519,7 +2519,7 @@ static void update_shallow_info(struct command *commands,\n \tsi->ref = ref;\n \n \tif (shallow_update) {\n-\t\tprepare_shallow_update(si);\n+\t\tprepare_shallow_update(si, shallow_file);\n \t\treturn;\n \t}\n \n@@ -2711,11 +2711,17 @@ 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\tconst char *alt_shallow_file = NULL;\n+\n+\t\t\tif (si.nr_ours || si.nr_theirs)\n+\t\t\t\talt_shallow_file = setup_temporary_shallow(si.shallow);\n+\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\t\tunpack_status = unpack_with_sideband(transaction, alt_shallow_file);\n+\n+\t\t\tupdate_shallow_info(commands, &si, &ref, alt_shallow_file);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n \t\texecute_commands(commands, unpack_status, &si, transaction,\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550959","messageId":"20260820234940.894624-5-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260820234940.894624-1-jltobler@gmail.com","subject":"[PATCH v5 4/9] builtin/receive-pack: read unpack limit config lazily","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-20T23:49:35Z","receivedAt":"2026-08-20T23:49:49Z","isPatch":true,"body":"In git-receive-pack(1), the `receive.unpackLimit` and\n`transfer.unpackLimit` configuration decides whether an incoming\npackfile should be exploded into loose objects or kept as a packfile\non-disk. In a subsequent commit, the logic to write the incoming\npackfile is made ODB backend agnostic and moved behind a pluggable ODB\ntransaction interface. Consequently, whether to explode a packfile is a\ndetail of how a particular backend stores objects and should not be a\npart of the generic interface itself.\n\nIn preparation for this, instead resolve the unpack limit lazily inside\n`unpack()` by reading the configuration directly. The now-unused unpack\nlimit globals are dropped accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 30 +++++++++++-------------------\n 1 file changed, 11 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 135105deae..971dc3f52e 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -62,12 +62,9 @@ static enum deny_action deny_delete_current = DENY_UNCONFIGURED;\n static int receive_fsck_objects = -1;\n static int transfer_fsck_objects = -1;\n static struct strbuf fsck_msg_types = STRBUF_INIT;\n-static int receive_unpack_limit = -1;\n-static int transfer_unpack_limit = -1;\n static int advertise_atomic_push = 1;\n static int advertise_push_options;\n static int advertise_sid;\n-static int unpack_limit = 100;\n static off_t max_input_size;\n static int report_status;\n static int report_status_v2;\n@@ -157,16 +154,6 @@ static int receive_pack_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n-\tif (strcmp(var, \"receive.unpacklimit\") == 0) {\n-\t\treceive_unpack_limit = git_config_int(var, value, ctx->kvi);\n-\t\treturn 0;\n-\t}\n-\n-\tif (strcmp(var, \"transfer.unpacklimit\") == 0) {\n-\t\ttransfer_unpack_limit = git_config_int(var, value, ctx->kvi);\n-\t\treturn 0;\n-\t}\n-\n \tif (strcmp(var, \"receive.fsck.skiplist\") == 0) {\n \t\tchar *path;\n \n@@ -2333,6 +2320,16 @@ 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 unsigned int get_unpack_limit(struct repository *repo)\n+{\n+\tunsigned int limit = 100;\n+\n+\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n+\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n+\n+\treturn limit;\n+}\n+\n static const char *unpack(struct odb_transaction *transaction,\n \t\t\t  const char *shallow_file, int err_fd)\n {\n@@ -2360,7 +2357,7 @@ static const char *unpack(struct odb_transaction *transaction,\n \n \todb_transaction_env(transaction, &child.env);\n \n-\tif (ntohl(hdr.hdr_entries) < unpack_limit) {\n+\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n \t\tpush_header_arg(&child.args, &hdr);\n \t\tif (quiet)\n@@ -2658,11 +2655,6 @@ int cmd_receive_pack(int argc,\n \tif (cert_nonce_seed)\n \t\tpush_cert_nonce = prepare_push_cert_nonce(service_dir, time(NULL));\n \n-\tif (0 <= receive_unpack_limit)\n-\t\tunpack_limit = receive_unpack_limit;\n-\telse if (0 <= transfer_unpack_limit)\n-\t\tunpack_limit = transfer_unpack_limit;\n-\n \tswitch (determine_protocol_version_server()) {\n \tcase protocol_v2:\n \t\t/*\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550960","messageId":"20260820234940.894624-6-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260820234940.894624-1-jltobler@gmail.com","subject":"[PATCH v5 5/9] builtin/receive-pack: lift global state out of unpack()","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-20T23:49:36Z","receivedAt":"2026-08-20T23:49:50Z","isPatch":true,"body":"In git-receive-pack(1), writing the packfile to the transaction is\nhandled via `unpack()` which relies on global variables to decide how to\ninvoke the underlying git-index-pack(1) or git-unpack-objects(1) child\nprocesses. In a subsequent commit, the `unpack()` logic is moved behind\na generic ODB transaction interface to handle writing packfiles and thus\ncan no longer rely on these globals.\n\nLift the global state out of `unpack()` by instead storing this state in\na `struct unpack_opts` that gets passed to the function explicitly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 63 +++++++++++++++++++++++++++---------------\n 1 file changed, 41 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 971dc3f52e..f062b93b8d 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2330,18 +2330,24 @@ static unsigned int get_unpack_limit(struct repository *repo)\n \treturn limit;\n }\n \n+struct unpack_opts {\n+\tconst char *fsck_msg_types;\n+\tconst char *shallow_file;\n+\toff_t max_input_size;\n+\tint fsck_objects;\n+\tint reject_thin;\n+\tint err_fd;\n+\tint quiet;\n+};\n+\n static const char *unpack(struct odb_transaction *transaction,\n-\t\t\t  const char *shallow_file, int err_fd)\n+\t\t\t  const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n \tint status;\n \tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint fsck_objects = (receive_fsck_objects >= 0\n-\t\t\t    ? receive_fsck_objects\n-\t\t\t    : transfer_fsck_objects >= 0\n-\t\t\t    ? transfer_fsck_objects\n-\t\t\t    : 0);\n+\tint err_fd = opts->err_fd;\n \n \thdr_err = parse_pack_header(&hdr);\n \tif (hdr_err) {\n@@ -2350,9 +2356,9 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\treturn hdr_err;\n \t}\n \n-\tif (shallow_file) {\n+\tif (opts->shallow_file) {\n \t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, shallow_file);\n+\t\tstrvec_push(&child.args, opts->shallow_file);\n \t}\n \n \todb_transaction_env(transaction, &child.env);\n@@ -2360,14 +2366,14 @@ static const char *unpack(struct odb_transaction *transaction,\n \tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n \t\tstrvec_push(&child.args, \"unpack-objects\");\n \t\tpush_header_arg(&child.args, &hdr);\n-\t\tif (quiet)\n+\t\tif (opts->quiet)\n \t\t\tstrvec_push(&child.args, \"-q\");\n-\t\tif (fsck_objects)\n+\t\tif (opts->fsck_objects)\n \t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     fsck_msg_types.buf);\n-\t\tif (max_input_size)\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (opts->max_input_size)\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)max_input_size);\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.no_stdout = 1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n@@ -2388,18 +2394,18 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\t\t     (uintmax_t)getpid(),\n \t\t\t     hostname);\n \n-\t\tif (!quiet && err_fd)\n+\t\tif (!opts->quiet && err_fd)\n \t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n-\t\tif (use_sideband)\n+\t\tif (err_fd)\n \t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n-\t\tif (fsck_objects)\n+\t\tif (opts->fsck_objects)\n \t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     fsck_msg_types.buf);\n-\t\tif (!reject_thin)\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (!opts->reject_thin)\n \t\t\tstrvec_push(&child.args, \"--fix-thin\");\n-\t\tif (max_input_size)\n+\t\tif (opts->max_input_size)\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)max_input_size);\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.out = -1;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n@@ -2431,11 +2437,23 @@ static const char *unpack(struct odb_transaction *transaction,\n static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \t\t\t\t\tconst char *shallow_file)\n {\n+\tstruct unpack_opts opts = {\n+\t\t.fsck_objects = (receive_fsck_objects >= 0\n+\t\t\t\t ? receive_fsck_objects\n+\t\t\t\t : transfer_fsck_objects >= 0\n+\t\t\t\t ? transfer_fsck_objects\n+\t\t\t\t : 0),\n+\t\t.fsck_msg_types = fsck_msg_types.buf,\n+\t\t.max_input_size = max_input_size,\n+\t\t.shallow_file = shallow_file,\n+\t\t.reject_thin = reject_thin,\n+\t\t.quiet = quiet,\n+\t};\n \tstruct async muxer;\n \tconst char *ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, shallow_file, 0);\n+\t\treturn unpack(transaction, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2444,7 +2462,8 @@ static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \tif (start_async(&muxer))\n \t\treturn NULL;\n \n-\tret = unpack(transaction, shallow_file, muxer.in);\n+\topts.err_fd = muxer.in;\n+\tret = unpack(transaction, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550961","messageId":"20260820234940.894624-7-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260820234940.894624-1-jltobler@gmail.com","subject":"[PATCH v5 6/9] builtin/receive-pack: report unpack errors via strbuf","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-20T23:49:37Z","receivedAt":"2026-08-20T23:49:51Z","isPatch":true,"body":"When writing packfiles via `unpack()`, error messages are returned\ndirectly by the function. In preparation for `unpack()` logic being\nmoved behind a generic ODB transaction interface, update the function to\ninstead write any error messages to a caller provided strbuf and return\na negative value on error. Call sites are updated to use the error\nstrbuf accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 63 ++++++++++++++++++++++++------------------\n 1 file changed, 36 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex f062b93b8d..6df872697b 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2015,7 +2015,7 @@ static void execute_commands_atomic(struct command *commands,\n }\n \n static void execute_commands(struct command *commands,\n-\t\t\t     const char *unpacker_error,\n+\t\t\t     int 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@@ -2340,8 +2340,8 @@ struct unpack_opts {\n \tint quiet;\n };\n \n-static const char *unpack(struct odb_transaction *transaction,\n-\t\t\t  const struct unpack_opts *opts)\n+static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n+\t\t  const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2353,7 +2353,8 @@ static const char *unpack(struct odb_transaction *transaction,\n \tif (hdr_err) {\n \t\tif (err_fd > 0)\n \t\t\tclose(err_fd);\n-\t\treturn hdr_err;\n+\t\tstrbuf_addstr(err_msg, hdr_err);\n+\t\treturn -1;\n \t}\n \n \tif (opts->shallow_file) {\n@@ -2378,8 +2379,10 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = run_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"unpack-objects abnormal exit\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n \t} else {\n \t\tchar hostname[HOST_NAME_MAX + 1];\n \t\tchar *lockfile;\n@@ -2410,8 +2413,10 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = start_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"index-pack fork failed\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n+\t\t\treturn -1;\n+\t\t}\n \n \t\t/*\n \t\t * The lockfile filepath is expected to be the final location of\n@@ -2427,15 +2432,18 @@ static const char *unpack(struct odb_transaction *transaction,\n \t\tclose(child.out);\n \n \t\tstatus = finish_command(&child);\n-\t\tif (status)\n-\t\t\treturn \"index-pack abnormal exit\";\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n \t\todb_reprepare(the_repository->objects);\n \t}\n-\treturn NULL;\n+\treturn 0;\n }\n \n-static const char *unpack_with_sideband(struct odb_transaction *transaction,\n-\t\t\t\t\tconst char *shallow_file)\n+static int unpack_with_sideband(struct odb_transaction *transaction,\n+\t\t\t\tconst char *shallow_file,\n+\t\t\t\tstruct strbuf *err_msg)\n {\n \tstruct unpack_opts opts = {\n \t\t.fsck_objects = (receive_fsck_objects >= 0\n@@ -2450,20 +2458,20 @@ static const char *unpack_with_sideband(struct odb_transaction *transaction,\n \t\t.quiet = quiet,\n \t};\n \tstruct async muxer;\n-\tconst char *ret;\n+\tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, &opts);\n+\t\treturn unpack(transaction, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n \tmuxer.proc = copy_to_sideband;\n \tmuxer.in = -1;\n \tif (start_async(&muxer))\n-\t\treturn NULL;\n+\t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, &opts);\n+\tret = unpack(transaction, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2552,13 +2560,13 @@ static void update_shallow_info(struct command *commands,\n \tfree(ref_status);\n }\n \n-static void report(struct command *commands, const char *unpack_status)\n+static void report(struct command *commands, const struct strbuf *unpack_status)\n {\n \tstruct command *cmd;\n \tstruct strbuf buf = STRBUF_INIT;\n \n \tpacket_buf_write(&buf, \"unpack %s\\n\",\n-\t\t\t unpack_status ? unpack_status : \"ok\");\n+\t\t\t unpack_status->len ? unpack_status->buf : \"ok\");\n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (!cmd->error_string)\n \t\t\tpacket_buf_write(&buf, \"ok %s\\n\",\n@@ -2576,14 +2584,14 @@ static void report(struct command *commands, const char *unpack_status)\n \tstrbuf_release(&buf);\n }\n \n-static void report_v2(struct command *commands, const char *unpack_status)\n+static void report_v2(struct command *commands, const struct strbuf *unpack_status)\n {\n \tstruct command *cmd;\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct ref_push_report *report;\n \n \tpacket_buf_write(&buf, \"unpack %s\\n\",\n-\t\t\t unpack_status ? unpack_status : \"ok\");\n+\t\t\t unpack_status->len ? unpack_status->buf : \"ok\");\n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tint count = 0;\n \n@@ -2707,8 +2715,8 @@ int cmd_receive_pack(int argc,\n \t\t\t   PACKET_READ_DIE_ON_ERR_PACKET);\n \n \tif ((commands = read_head_info(&reader, &shallow))) {\n-\t\tconst char *unpack_status = NULL;\n \t\tstruct string_list push_options = STRING_LIST_INIT_DUP;\n+\t\tstruct strbuf unpack_status = STRBUF_INIT;\n \n \t\tif (use_push_options)\n \t\t\tread_push_options(&reader, &push_options);\n@@ -2728,22 +2736,22 @@ int cmd_receive_pack(int argc,\n \t\t\t\talt_shallow_file = setup_temporary_shallow(si.shallow);\n \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\t\tstrbuf_addstr(&unpack_status, \"unable to start object transaction\");\n \t\t\telse\n-\t\t\t\tunpack_status = unpack_with_sideband(transaction, alt_shallow_file);\n+\t\t\t\tunpack_with_sideband(transaction, alt_shallow_file, &unpack_status);\n \n \t\t\tupdate_shallow_info(commands, &si, &ref, alt_shallow_file);\n \t\t}\n \t\tuse_keepalive = KEEPALIVE_ALWAYS;\n-\t\texecute_commands(commands, unpack_status, &si, transaction,\n+\t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n \t\t\t\t &push_options);\n \t\todb_transaction_finalize(transaction);\n \t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\n-\t\t\treport_v2(commands, unpack_status);\n+\t\t\treport_v2(commands, &unpack_status);\n \t\telse if (report_status)\n-\t\t\treport(commands, unpack_status);\n+\t\t\treport(commands, &unpack_status);\n \t\tsigchain_pop(SIGPIPE);\n \t\trun_receive_hook(commands, \"post-receive\", 1, NULL,\n \t\t\t\t &push_options);\n@@ -2768,6 +2776,7 @@ int cmd_receive_pack(int argc,\n \t\tif (auto_update_server_info)\n \t\t\tupdate_server_info(the_repository, 0);\n \t\tclear_shallow_info(&si);\n+\t\tstrbuf_release(&unpack_status);\n \t}\n \tif (use_sideband)\n \t\tpacket_flush(1);\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550962","messageId":"20260820234940.894624-9-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260820234940.894624-1-jltobler@gmail.com","subject":"[PATCH v5 8/9] odb: return temporary ODB source when set","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-20T23:49:39Z","receivedAt":"2026-08-20T23:49:53Z","isPatch":true,"body":"When invoked, `odb_set_temporary_primary_source()` installs a temporary\nobject directory as the new primary ODB source. A caller that wants to\noperate on the ODB source of the open transaction must assume that it is\nthe first entry in the ODB source list which is a bit awkward and\nfragile.\n\nInstead, return the newly installed source directly and report the\nprevious primary source via a new `prev_source` out parameter. Propagate\nthe installed source through `tmp_objdir_replace_primary_odb()` and\nstart storing it in the \"files\" ODB transaction so a subsequent commit\ncan easily access it without relying on the ODB source list ordering.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n object-file.c | 3 ++-\n odb.c         | 9 +++++++--\n odb.h         | 6 ++++--\n tmp-objdir.c  | 8 +++++---\n tmp-objdir.h  | 6 ++++--\n 5 files changed, 22 insertions(+), 10 deletions(-)\n\ndiff --git a/object-file.c b/object-file.c\nindex 4d03c167d5..db63587f6d 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -485,6 +485,7 @@ struct odb_transaction_files {\n \tstruct odb_transaction base;\n \n \tstruct tmp_objdir *objdir;\n+\tstruct odb_source *quarantine;\n \tstruct transaction_packfile packfile;\n \tconst char *prefix;\n };\n@@ -507,7 +508,7 @@ int odb_transaction_files_prepare(struct odb_transaction *base)\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+\ttransaction->quarantine = tmp_objdir_replace_primary_odb(transaction->objdir, 0);\n \n \treturn 0;\n }\ndiff --git a/odb.c b/odb.c\nindex caf1d0f542..8afcb6b637 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -226,7 +226,8 @@ struct odb_source *odb_add_to_alternates_memory(struct object_database *odb,\n }\n \n struct odb_source *odb_set_temporary_primary_source(struct object_database *odb,\n-\t\t\t\t\t\t    const char *dir, int will_destroy)\n+\t\t\t\t\t\t    const char *dir, int will_destroy,\n+\t\t\t\t\t\t    struct odb_source **prev_source)\n {\n \tstruct odb_source *source;\n \n@@ -250,7 +251,11 @@ struct odb_source *odb_set_temporary_primary_source(struct object_database *odb,\n \tsource->will_destroy = will_destroy;\n \tsource->next = odb->sources;\n \todb->sources = source;\n-\treturn source->next;\n+\n+\tif (prev_source)\n+\t\t*prev_source = source->next;\n+\n+\treturn source;\n }\n \n void odb_restore_primary_source(struct object_database *odb,\ndiff --git a/odb.h b/odb.h\nindex fca67e8253..bdfcb9509a 100644\n--- a/odb.h\n+++ b/odb.h\n@@ -199,10 +199,12 @@ struct odb_source *odb_find_source_or_die(struct object_database *odb, const cha\n \n /*\n  * Replace the current writable object directory with the specified temporary\n- * object directory; returns the former primary source.\n+ * object directory and return the newly installed primary source. The former\n+ * primary source is reported via `prev_source` when non-NULL.\n  */\n struct odb_source *odb_set_temporary_primary_source(struct object_database *odb,\n-\t\t\t\t\t\t    const char *dir, int will_destroy);\n+\t\t\t\t\t\t    const char *dir, int will_destroy,\n+\t\t\t\t\t\t    struct odb_source **prev_source);\n \n /*\n  * Restore the primary source that was previously replaced by\ndiff --git a/tmp-objdir.c b/tmp-objdir.c\nindex d199d39e7c..e633d97e0e 100644\n--- a/tmp-objdir.c\n+++ b/tmp-objdir.c\n@@ -327,11 +327,13 @@ void tmp_objdir_add_as_alternate(const struct tmp_objdir *t)\n \todb_add_to_alternates_memory(t->repo->objects, t->path.buf);\n }\n \n-void tmp_objdir_replace_primary_odb(struct tmp_objdir *t, int will_destroy)\n+struct odb_source *tmp_objdir_replace_primary_odb(struct tmp_objdir *t,\n+\t\t\t\t\t\t  int will_destroy)\n {\n \tif (t->prev_source)\n \t\tBUG(\"the primary object database is already replaced\");\n-\tt->prev_source = odb_set_temporary_primary_source(t->repo->objects,\n-\t\t\t\t\t\t\t  t->path.buf, will_destroy);\n \tt->will_destroy = will_destroy;\n+\n+\treturn odb_set_temporary_primary_source(t->repo->objects, t->path.buf,\n+\t\t\t\t\t\twill_destroy, &t->prev_source);\n }\ndiff --git a/tmp-objdir.h b/tmp-objdir.h\nindex ccf800faa7..81eb927413 100644\n--- a/tmp-objdir.h\n+++ b/tmp-objdir.h\n@@ -64,8 +64,10 @@ void tmp_objdir_add_as_alternate(const struct tmp_objdir *);\n /*\n  * Replaces the writable object store in the current process with the temporary\n  * object directory and makes the former main object store an alternate.\n- * If will_destroy is nonzero, the object directory may not be migrated.\n+ * If will_destroy is nonzero, the object directory may not be migrated. Returns\n+ * the newly installed primary source.\n  */\n-void tmp_objdir_replace_primary_odb(struct tmp_objdir *, int will_destroy);\n+struct odb_source *tmp_objdir_replace_primary_odb(struct tmp_objdir *,\n+\t\t\t\t\t\t  int will_destroy);\n \n #endif /* TMP_OBJDIR_H */\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550963","messageId":"20260820234940.894624-10-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260820234940.894624-1-jltobler@gmail.com","subject":"[PATCH v5 9/9] odb/transaction: add transaction interface to write packfiles","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-20T23:49:40Z","receivedAt":"2026-08-20T23:49:54Z","isPatch":true,"body":"In git-receive-pack(1), the incoming packfile is written to the ODB via\n`unpack()`, which spawns git-index-pack(1) or git-unpack-objects(1)\ndirectly. With pluggable object databases, an alternative backend may\nneed to handle writing packfile data differently though.\n\nIntroduce `odb_transaction_write_pack()` as a generic interface to\nhandle writing a packfile to a transaction and use the logic from\n`unpack()` as the \"files\" backend implementation. Note that when storing\nthe objects as a packfile, git-index-pack(1) also writes a \".keep\"\nlockfile next to it to prevent a concurrent repack from removing the new\npack prior to reference updates being performed. The \"files\" transaction\nbackend is responsible for managing these \".keep\" files and removes them\npost-commit once the transaction is finalized.\n\nCall sites in git-receive-pack(1) are updated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 160 +-----------------------------------\n object-file.c          | 178 +++++++++++++++++++++++++++++++++++++++++\n odb/transaction.c      |   7 ++\n odb/transaction.h      |  62 ++++++++++++++\n 4 files changed, 250 insertions(+), 157 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex b369466783..e6e54ba55f 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -15,7 +15,6 @@\n #include \"gpg-interface.h\"\n #include \"hex.h\"\n #include \"hook.h\"\n-#include \"lockfile.h\"\n #include \"object.h\"\n #include \"object-file.h\"\n #include \"object-name.h\"\n@@ -23,7 +22,6 @@\n #include \"oid-array.h\"\n #include \"oidset.h\"\n #include \"pack.h\"\n-#include \"packfile.h\"\n #include \"parse-options.h\"\n #include \"pkt-line.h\"\n #include \"protocol.h\"\n@@ -2292,162 +2290,11 @@ static void read_push_options(struct packet_reader *reader,\n \t}\n }\n \n-static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n-{\n-\tswitch (read_pack_header(pack_fd, hdr)) {\n-\tcase PH_ERROR_EOF:\n-\t\treturn \"eof before pack header was fully read\";\n-\n-\tcase PH_ERROR_PACK_SIGNATURE:\n-\t\treturn \"protocol error (pack signature mismatch detected)\";\n-\n-\tcase PH_ERROR_PROTOCOL:\n-\t\treturn \"protocol error (pack version unsupported)\";\n-\n-\tdefault:\n-\t\treturn \"unknown error in parse_pack_header\";\n-\n-\tcase 0:\n-\t\treturn NULL;\n-\t}\n-}\n-\n-static struct tempfile *pack_lockfile;\n-\n-static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n-{\n-\tstrvec_pushf(args, \"--pack_header=%\"PRIu32\",%\"PRIu32,\n-\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n-}\n-\n-static unsigned int get_unpack_limit(struct repository *repo)\n-{\n-\tunsigned int limit = 100;\n-\n-\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n-\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n-\n-\treturn limit;\n-}\n-\n-struct unpack_opts {\n-\tconst char *fsck_msg_types;\n-\tconst char *shallow_file;\n-\toff_t max_input_size;\n-\tint fsck_objects;\n-\tint reject_thin;\n-\tint err_fd;\n-\tint quiet;\n-};\n-\n-static int unpack(struct odb_transaction *transaction, int pack_fd,\n-\t\t  struct strbuf *err_msg, const struct unpack_opts *opts)\n-{\n-\tstruct pack_header hdr;\n-\tconst char *hdr_err;\n-\tint status;\n-\tstruct child_process child = CHILD_PROCESS_INIT;\n-\tint err_fd = opts->err_fd;\n-\n-\thdr_err = parse_pack_header(&hdr, pack_fd);\n-\tif (hdr_err) {\n-\t\tif (err_fd > 0)\n-\t\t\tclose(err_fd);\n-\t\tstrbuf_addstr(err_msg, hdr_err);\n-\t\treturn -1;\n-\t}\n-\n-\tif (opts->shallow_file) {\n-\t\tstrvec_push(&child.args, \"--shallow-file\");\n-\t\tstrvec_push(&child.args, opts->shallow_file);\n-\t}\n-\n-\todb_transaction_env(transaction, &child.env);\n-\n-\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(the_repository)) {\n-\t\tstrvec_push(&child.args, \"unpack-objects\");\n-\t\tpush_header_arg(&child.args, &hdr);\n-\t\tif (opts->quiet)\n-\t\t\tstrvec_push(&child.args, \"-q\");\n-\t\tif (opts->fsck_objects)\n-\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     opts->fsck_msg_types);\n-\t\tif (opts->max_input_size)\n-\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)opts->max_input_size);\n-\t\tchild.no_stdout = 1;\n-\t\tchild.in = pack_fd;\n-\t\tchild.err = err_fd;\n-\t\tchild.git_cmd = 1;\n-\t\tstatus = run_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n-\t\t\treturn -1;\n-\t\t}\n-\t} else {\n-\t\tchar hostname[HOST_NAME_MAX + 1];\n-\t\tchar *lockfile;\n-\n-\t\tstrvec_pushl(&child.args, \"index-pack\", \"--stdin\", NULL);\n-\t\tpush_header_arg(&child.args, &hdr);\n-\n-\t\tif (xgethostname(hostname, sizeof(hostname)))\n-\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n-\t\tstrvec_pushf(&child.args,\n-\t\t\t     \"--keep=receive-pack %\"PRIuMAX\" on %s\",\n-\t\t\t     (uintmax_t)getpid(),\n-\t\t\t     hostname);\n-\n-\t\tif (!opts->quiet && err_fd)\n-\t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n-\t\tif (err_fd)\n-\t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n-\t\tif (opts->fsck_objects)\n-\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n-\t\t\t\t     opts->fsck_msg_types);\n-\t\tif (!opts->reject_thin)\n-\t\t\tstrvec_push(&child.args, \"--fix-thin\");\n-\t\tif (opts->max_input_size)\n-\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n-\t\t\t\t     (uintmax_t)opts->max_input_size);\n-\t\tchild.out = -1;\n-\t\tchild.in = pack_fd;\n-\t\tchild.err = err_fd;\n-\t\tchild.git_cmd = 1;\n-\t\tstatus = start_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\t/*\n-\t\t * The lockfile filepath is expected to be the final location of\n-\t\t * the \".keep\" file after being migrated to the main ODB source.\n-\t\t * This ensures the lockfile can be found and removed later\n-\t\t * after the ODB transaction has been committed.\n-\t\t */\n-\t\tlockfile = index_pack_lockfile(transaction->source, child.out, NULL);\n-\t\tif (lockfile) {\n-\t\t\tpack_lockfile = register_tempfile(lockfile);\n-\t\t\tfree(lockfile);\n-\t\t}\n-\t\tclose(child.out);\n-\n-\t\tstatus = finish_command(&child);\n-\t\tif (status) {\n-\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n-\t\t\treturn -1;\n-\t\t}\n-\t\todb_reprepare(the_repository->objects);\n-\t}\n-\treturn 0;\n-}\n-\n static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\t\t\tconst char *shallow_file,\n \t\t\t\tstruct strbuf *err_msg)\n {\n-\tstruct unpack_opts opts = {\n+\tstruct odb_transaction_write_pack_opts opts = {\n \t\t.fsck_objects = (receive_fsck_objects >= 0\n \t\t\t\t ? receive_fsck_objects\n \t\t\t\t : transfer_fsck_objects >= 0\n@@ -2463,7 +2310,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, 0, err_msg, &opts);\n+\t\treturn odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2473,7 +2320,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, 0, err_msg, &opts);\n+\tret = odb_transaction_write_pack(transaction, 0, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n@@ -2748,7 +2595,6 @@ int cmd_receive_pack(int argc,\n \t\texecute_commands(commands, !!unpack_status.len, &si, transaction,\n \t\t\t\t &push_options);\n \t\todb_transaction_finalize(transaction);\n-\t\tdelete_tempfile(&pack_lockfile);\n \t\tsigchain_push(SIGPIPE, SIG_IGN);\n \t\tif (report_status_v2)\n \t\t\treport_v2(commands, &unpack_status);\ndiff --git a/object-file.c b/object-file.c\nindex db63587f6d..265c5f7a3c 100644\n--- a/object-file.c\n+++ b/object-file.c\n@@ -10,6 +10,7 @@\n #define USE_THE_REPOSITORY_VARIABLE\n \n #include \"git-compat-util.h\"\n+#include \"config.h\"\n #include \"convert.h\"\n #include \"dir.h\"\n #include \"environment.h\"\n@@ -26,6 +27,7 @@\n #include \"packfile.h\"\n #include \"path.h\"\n #include \"read-cache-ll.h\"\n+#include \"run-command.h\"\n #include \"setup.h\"\n #include \"strvec.h\"\n #include \"tempfile.h\"\n@@ -483,11 +485,16 @@ struct transaction_packfile {\n \n struct odb_transaction_files {\n \tstruct odb_transaction base;\n+\tenum odb_transaction_flags flags;\n \n \tstruct tmp_objdir *objdir;\n \tstruct odb_source *quarantine;\n \tstruct transaction_packfile packfile;\n \tconst char *prefix;\n+\n+\tstruct tempfile **pack_lockfiles;\n+\tsize_t pack_lockfiles_nr;\n+\tsize_t pack_lockfiles_alloc;\n };\n \n int odb_transaction_files_prepare(struct odb_transaction *base)\n@@ -1291,6 +1298,174 @@ static int odb_transaction_files_commit(struct odb_transaction *base)\n \treturn 0;\n }\n \n+static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n+{\n+\tswitch (read_pack_header(pack_fd, hdr)) {\n+\tcase PH_ERROR_EOF:\n+\t\treturn \"eof before pack header was fully read\";\n+\n+\tcase PH_ERROR_PACK_SIGNATURE:\n+\t\treturn \"protocol error (pack signature mismatch detected)\";\n+\n+\tcase PH_ERROR_PROTOCOL:\n+\t\treturn \"protocol error (pack version unsupported)\";\n+\n+\tdefault:\n+\t\treturn \"unknown error in parse_pack_header\";\n+\n+\tcase 0:\n+\t\treturn NULL;\n+\t}\n+}\n+\n+static void push_header_arg(struct strvec *args, struct pack_header *hdr)\n+{\n+\tstrvec_pushf(args, \"--pack_header=%\"PRIu32\",%\"PRIu32,\n+\t\t     ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));\n+}\n+\n+static unsigned int get_unpack_limit(struct repository *repo,\n+\t\t\t\t     enum odb_transaction_flags flags)\n+{\n+\tunsigned int limit = 0;\n+\n+\tif (flags & ODB_TRANSACTION_RECEIVE) {\n+\t\tlimit = 100;\n+\t\trepo_config_get_uint(repo, \"transfer.unpacklimit\", &limit);\n+\t\trepo_config_get_uint(repo, \"receive.unpacklimit\", &limit);\n+\t}\n+\n+\treturn limit;\n+}\n+\n+static int odb_transaction_files_write_pack(struct odb_transaction *base,\n+\t\t\t\t\t    int pack_fd, struct strbuf *err_msg,\n+\t\t\t\t\t    const struct odb_transaction_write_pack_opts *opts)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\tstruct repository *repo = base->source->odb->repo;\n+\tstruct child_process child = CHILD_PROCESS_INIT;\n+\tstruct pack_header hdr;\n+\tconst char *hdr_err;\n+\tint err_fd = opts->err_fd;\n+\tint status;\n+\n+\thdr_err = parse_pack_header(&hdr, pack_fd);\n+\tif (hdr_err) {\n+\t\tif (err_fd > 0)\n+\t\t\tclose(err_fd);\n+\t\tstrbuf_addstr(err_msg, hdr_err);\n+\t\treturn -1;\n+\t}\n+\n+\tif (opts->shallow_file) {\n+\t\tstrvec_push(&child.args, \"--shallow-file\");\n+\t\tstrvec_push(&child.args, opts->shallow_file);\n+\t}\n+\n+\todb_transaction_env(base, &child.env);\n+\n+\tif (ntohl(hdr.hdr_entries) < get_unpack_limit(repo, transaction->flags)) {\n+\t\tstrvec_push(&child.args, \"unpack-objects\");\n+\t\tpush_header_arg(&child.args, &hdr);\n+\t\tif (opts->quiet)\n+\t\t\tstrvec_push(&child.args, \"-q\");\n+\t\tif (opts->fsck_objects)\n+\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (opts->max_input_size)\n+\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n+\t\tchild.no_stdout = 1;\n+\t\tchild.in = pack_fd;\n+\t\tchild.err = err_fd;\n+\t\tchild.git_cmd = 1;\n+\t\tstatus = run_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"unpack-objects abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n+\t} else {\n+\t\tchar hostname[HOST_NAME_MAX + 1];\n+\t\tchar *lockfile;\n+\n+\t\tstrvec_pushl(&child.args, \"index-pack\", \"--stdin\", NULL);\n+\t\tpush_header_arg(&child.args, &hdr);\n+\n+\t\tif (xgethostname(hostname, sizeof(hostname)))\n+\t\t\txsnprintf(hostname, sizeof(hostname), \"localhost\");\n+\t\tstrvec_pushf(&child.args,\n+\t\t\t     \"--keep=receive-pack %\"PRIuMAX\" on %s\",\n+\t\t\t     (uintmax_t)getpid(),\n+\t\t\t     hostname);\n+\n+\t\tif (!opts->quiet && err_fd)\n+\t\t\tstrvec_push(&child.args, \"--show-resolving-progress\");\n+\t\tif (err_fd)\n+\t\t\tstrvec_push(&child.args, \"--report-end-of-input\");\n+\t\tif (opts->fsck_objects)\n+\t\t\tstrvec_pushf(&child.args, \"--strict%s\",\n+\t\t\t\t     opts->fsck_msg_types);\n+\t\tif (!opts->reject_thin)\n+\t\t\tstrvec_push(&child.args, \"--fix-thin\");\n+\t\tif (opts->max_input_size)\n+\t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n+\t\t\t\t     (uintmax_t)opts->max_input_size);\n+\t\tchild.out = -1;\n+\t\tchild.in = pack_fd;\n+\t\tchild.err = err_fd;\n+\t\tchild.git_cmd = 1;\n+\t\tstatus = start_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack fork failed\");\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\t/*\n+\t\t * The lockfile filepath is expected to be the final location of\n+\t\t * the \".keep\" file after being migrated to the main ODB source.\n+\t\t * This ensures the lockfile can be found and removed later\n+\t\t * after the ODB transaction has been committed.\n+\t\t */\n+\t\tlockfile = index_pack_lockfile(base->source, child.out, NULL);\n+\t\tif (lockfile) {\n+\t\t\tALLOC_GROW(transaction->pack_lockfiles,\n+\t\t\t\t   transaction->pack_lockfiles_nr + 1,\n+\t\t\t\t   transaction->pack_lockfiles_alloc);\n+\t\t\ttransaction->pack_lockfiles[transaction->pack_lockfiles_nr++] =\n+\t\t\t\tregister_tempfile(lockfile);\n+\t\t\tfree(lockfile);\n+\t\t}\n+\t\tclose(child.out);\n+\n+\t\tstatus = finish_command(&child);\n+\t\tif (status) {\n+\t\t\tstrbuf_addstr(err_msg, \"index-pack abnormal exit\");\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\todb_source_prepare(transaction->quarantine,\n+\t\t\t\t   ODB_PREPARE_FLUSH_CACHES);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int odb_transaction_files_finalize(struct odb_transaction *base)\n+{\n+\tstruct odb_transaction_files *transaction =\n+\t\tcontainer_of(base, struct odb_transaction_files, base);\n+\tint ret = 0;\n+\n+\tfor (size_t i = 0; i < transaction->pack_lockfiles_nr; i++)\n+\t\tret |= delete_tempfile(&transaction->pack_lockfiles[i]);\n+\n+\tfree(transaction->pack_lockfiles);\n+\n+\treturn ret;\n+}\n+\n static int odb_transaction_files_env(struct odb_transaction *base,\n \t\t\t\t     struct strvec *env)\n {\n@@ -1314,8 +1489,11 @@ int odb_transaction_files_begin(struct odb_source *source,\n \ttransaction = xcalloc(1, sizeof(*transaction));\n \ttransaction->base.source = source;\n \ttransaction->base.commit = odb_transaction_files_commit;\n+\ttransaction->base.finalize = odb_transaction_files_finalize;\n \ttransaction->base.write_object_stream = odb_transaction_files_write_object_stream;\n+\ttransaction->base.write_pack = odb_transaction_files_write_pack;\n \ttransaction->base.env = odb_transaction_files_env;\n+\ttransaction->flags = flags;\n \n \ttransaction->prefix = \"bulk-fsync\";\n \tif (flags & ODB_TRANSACTION_RECEIVE) {\ndiff --git a/odb/transaction.c b/odb/transaction.c\nindex 9e9a982778..c9144e6cd6 100644\n--- a/odb/transaction.c\n+++ b/odb/transaction.c\n@@ -59,6 +59,13 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction,\n \treturn transaction->write_object_stream(transaction, stream, len, oid);\n }\n \n+int odb_transaction_write_pack(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t       struct strbuf *err_msg,\n+\t\t\t       const struct odb_transaction_write_pack_opts *opts)\n+{\n+\treturn transaction->write_pack(transaction, pack_fd, err_msg, opts);\n+}\n+\n int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env)\n {\n \tif (!transaction)\ndiff --git a/odb/transaction.h b/odb/transaction.h\nindex 6ed39b3d0e..8cb06c1191 100644\n--- a/odb/transaction.h\n+++ b/odb/transaction.h\n@@ -4,6 +4,50 @@\n #include \"gettext.h\"\n #include \"odb.h\"\n \n+/*\n+ * Options controlling how odb_transaction_write_pack() ingests a packfile.\n+ */\n+struct odb_transaction_write_pack_opts {\n+\t/*\n+\t * Optional fsck severity configuration to apply when incoming objects\n+\t * are verified.\n+\t */\n+\tconst char *fsck_msg_types;\n+\n+\t/*\n+\t * Path to an alternative shallow file describing the shallow boundaries\n+\t * to honor while ingesting the pack.\n+\t */\n+\tconst char *shallow_file;\n+\n+\t/*\n+\t * The max size in bytes of the incoming packfile allowed. No limit is\n+\t * enforced when set to 0.\n+\t */\n+\toff_t max_input_size;\n+\n+\t/*\n+\t * Whether the validity of incoming objects should be verified.\n+\t */\n+\tint fsck_objects;\n+\n+\t/*\n+\t * Whether to reject an incoming packfile if it is \"thin\".\n+\t */\n+\tint reject_thin;\n+\n+\t/*\n+\t * Optional file descriptor for reporting progress and errors. Set to 0\n+\t * for none.\n+\t */\n+\tint err_fd;\n+\n+\t/*\n+\t * Suppresses progress reporting.\n+\t */\n+\tint quiet;\n+};\n+\n /*\n  * A transaction may be started for an object database prior to writing new\n  * objects via odb_transaction_begin(). These objects are not committed until\n@@ -40,6 +84,15 @@ 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+\t/*\n+\t * This callback is expected to ingest the packfile readable via\n+\t * `pack_fd` into the transaction. Returns 0 on success, a negative\n+\t * error code otherwise. On failure, a human-readable description is\n+\t * appended to `err_msg`.\n+\t */\n+\tint (*write_pack)(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t  struct strbuf *err_msg,\n+\t\t\t  const struct odb_transaction_write_pack_opts *opts);\n \n \t/*\n \t * This callback is expected to populate the provided strvec with the\n@@ -107,6 +160,15 @@ 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+ * Ingests the packfile readable via `pack_fd` into the transaction. Returns 0\n+ * on success, a negative error code otherwise. On failure, a human-readable\n+ * description is appended to `err_msg`.\n+ */\n+int odb_transaction_write_pack(struct odb_transaction *transaction, int pack_fd,\n+\t\t\t       struct strbuf *err_msg,\n+\t\t\t       const struct odb_transaction_write_pack_opts *opts);\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-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"550964","messageId":"20260820234940.894624-8-jltobler@gmail.com","threadId":"66133","inReplyTo":"20260820234940.894624-1-jltobler@gmail.com","subject":"[PATCH v5 7/9] builtin/receive-pack: explicitly pass packfile fd","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-08-20T23:49:38Z","receivedAt":"2026-08-20T23:49:54Z","isPatch":true,"body":"When processing the incoming packfile in git-receive-pack(1), `unpack()`\nassumes it should always read it from stdin. In preparation for\n`unpack()` logic being moved behind a generic ODB transaction interface,\nupdate the function signature to take the an explicit fd provided by\ncallers to read the incoming packfile from instead. Call sites are\nupdated accordingly.\n\nSigned-off-by: Justin Tobler <jltobler@gmail.com>\n---\n builtin/receive-pack.c | 16 +++++++++-------\n 1 file changed, 9 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 6df872697b..b369466783 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -2292,9 +2292,9 @@ static void read_push_options(struct packet_reader *reader,\n \t}\n }\n \n-static const char *parse_pack_header(struct pack_header *hdr)\n+static const char *parse_pack_header(struct pack_header *hdr, int pack_fd)\n {\n-\tswitch (read_pack_header(0, hdr)) {\n+\tswitch (read_pack_header(pack_fd, hdr)) {\n \tcase PH_ERROR_EOF:\n \t\treturn \"eof before pack header was fully read\";\n \n@@ -2340,8 +2340,8 @@ struct unpack_opts {\n \tint quiet;\n };\n \n-static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n-\t\t  const struct unpack_opts *opts)\n+static int unpack(struct odb_transaction *transaction, int pack_fd,\n+\t\t  struct strbuf *err_msg, const struct unpack_opts *opts)\n {\n \tstruct pack_header hdr;\n \tconst char *hdr_err;\n@@ -2349,7 +2349,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \tstruct child_process child = CHILD_PROCESS_INIT;\n \tint err_fd = opts->err_fd;\n \n-\thdr_err = parse_pack_header(&hdr);\n+\thdr_err = parse_pack_header(&hdr, pack_fd);\n \tif (hdr_err) {\n \t\tif (err_fd > 0)\n \t\t\tclose(err_fd);\n@@ -2376,6 +2376,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n \t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.no_stdout = 1;\n+\t\tchild.in = pack_fd;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = run_command(&child);\n@@ -2410,6 +2411,7 @@ static int unpack(struct odb_transaction *transaction, struct strbuf *err_msg,\n \t\t\tstrvec_pushf(&child.args, \"--max-input-size=%\"PRIuMAX,\n \t\t\t\t     (uintmax_t)opts->max_input_size);\n \t\tchild.out = -1;\n+\t\tchild.in = pack_fd;\n \t\tchild.err = err_fd;\n \t\tchild.git_cmd = 1;\n \t\tstatus = start_command(&child);\n@@ -2461,7 +2463,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \tint ret;\n \n \tif (!use_sideband)\n-\t\treturn unpack(transaction, err_msg, &opts);\n+\t\treturn unpack(transaction, 0, err_msg, &opts);\n \n \tuse_keepalive = KEEPALIVE_AFTER_NUL;\n \tmemset(&muxer, 0, sizeof(muxer));\n@@ -2471,7 +2473,7 @@ static int unpack_with_sideband(struct odb_transaction *transaction,\n \t\treturn 0;\n \n \topts.err_fd = muxer.in;\n-\tret = unpack(transaction, err_msg, &opts);\n+\tret = unpack(transaction, 0, err_msg, &opts);\n \n \tfinish_async(&muxer);\n \treturn ret;\n-- \n2.55.0.424.g13c7afec21\n\n"},{"id":"551006","messageId":"aohD54ZQEyybw008@pks.im","threadId":"66133","inReplyTo":"20260820234940.894624-1-jltobler@gmail.com","subject":"Re: [PATCH v5 0/9] builtin/receive-pack: support pluggable packfile writes","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-08-21T12:26:15Z","receivedAt":"2026-08-21T12:26:21Z","isPatch":true,"body":"On Thu, Aug 20, 2026 at 06:49:31PM -0500, Justin Tobler wrote:\n> Changes since V4:\n> - Added an additional test assertion in the frist patch to ensure keep\n>   files are also migrated to the main ODB prior to being removed when\n>   the transaction is finalized.\n> - Updated a commit message.\n\nThanks, I'm happy with this version.\n\nPatrick\n"},{"id":"551027","messageId":"xmqqo6evqzsu.fsf@gitster.g","threadId":"66133","inReplyTo":"20260820234940.894624-10-jltobler@gmail.com","subject":"Re: [PATCH v5 9/9] odb/transaction: add transaction interface to write packfiles","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-21T15:05:37Z","receivedAt":"2026-08-21T15:05:40Z","isPatch":true,"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> In git-receive-pack(1), the incoming packfile is written to the ODB via\n> `unpack()`, which spawns git-index-pack(1) or git-unpack-objects(1)\n> directly. With pluggable object databases, an alternative backend may\n> need to handle writing packfile data differently though.\n>\n> Introduce `odb_transaction_write_pack()` as a generic interface to\n> handle writing a packfile to a transaction and use the logic from\n> `unpack()` as the \"files\" backend implementation. Note that when storing\n> the objects as a packfile, git-index-pack(1) also writes a \".keep\"\n> lockfile next to it to prevent a concurrent repack from removing the new\n> pack prior to reference updates being performed. The \"files\" transaction\n> backend is responsible for managing these \".keep\" files and removes them\n> post-commit once the transaction is finalized.\n>\n> Call sites in git-receive-pack(1) are updated accordingly.\n>\n> Signed-off-by: Justin Tobler <jltobler@gmail.com>\n> ---\n>  builtin/receive-pack.c | 160 +-----------------------------------\n>  object-file.c          | 178 +++++++++++++++++++++++++++++++++++++++++\n>  odb/transaction.c      |   7 ++\n>  odb/transaction.h      |  62 ++++++++++++++\n>  4 files changed, 250 insertions(+), 157 deletions(-)\n\nReading receive.unpackLimit and transfer.unpackLimit in generic\nobject-layer code feels like a layering violation, as these settings\nbelong to the transfer layer.  However, deciding whether to unpack\nor index is inherently up to the file-backend, which is what the\n'*.unpacklimit' settings control.  Future ODB backends might not\ndistinguish loose from packed objects, and even if they do, their\nperformance characteristics will differ.\n\nWe can attribute these '*.unpackLimit' names to historical wart; we\nlacked non-file ODB backends when they were named.  Had we named\nthem today, something like 'odb-file.unpackLimit' would have been\nmore accurate.  If we had other bulk-import mechanisms that use pack\nstreams, they would use the same '*.unpacklimit' to optimize the\nobject layout for file-backed ODB stores.\n\nThanks.\n"}]}