From: Justin Tobler <jltobler@gmail.com>
To: git@vger.kernel.org
Cc: ps@pks.im, gitster@pobox.com, Justin Tobler <jltobler@gmail.com>
Subject: [PATCH v3 0/9] builtin/receive-pack: support pluggable packfile writes
Date: Tue, 11 Aug 2026 12:54:06 -0500 [thread overview]
Message-ID: <20260811175415.2044235-1-jltobler@gmail.com> (raw)
In-Reply-To: <20260809190106.1565882-1-jltobler@gmail.com>
Greetings,
With bdee7b3013 (builtin/receive-pack: stage incoming objects via ODB
transactions, 2026-07-10), git-receive-pack(1) started using the ODB
transaction interfaces to stage incoming objects. While this brought the
command closer to being ODB backend agnostic, the underlying
git-index-pack(1) and git-unpack-objects(1) processes used to actually
write the objects to the transaction are still fundamentally tied to the
"files" backend.
This series aims to address this by introducing a generic
`odb_transaction_write_pack()` transaction interface to handle writing
the incoming packfile to the transaction. The existing logic in
git-receive-pack(1) that spawns the child processes to write the
packfile becomes the "files" backend implementation of this interface.
Changes since V2:
- Added a patch to address a bug causing ".keep" files from not being
removed.
- Started handling errors at transaction commit and finalize call sites
instead of ignoring them. We also make sure
`odb_transaction_finalize()` runs after every successful commit
callsite to ensure proper cleanup.
- Updated the code handling lazy loading of unpack limit configuration
to not longer cache the value.
- Added a patch to begin explictly tracking the ODB source used by the
"files" transaction to avoid relying on the ordering of the ODB source
list.
- Updated some commit messages to improve clarity.
Changes since V1:
- Changed the "release" interface name to "finalize" and updated it to
return error codes.
- Marked some function parameters as const.
- Unpack limit configuration is now resolved in the ODB transaction
backend instead of wiring it through the interface.
- When writing a packfile to the transaction, now only the transaction
source is prepared.
- Updated some commit messages.
- Updated some code formatting.
Thanks for the review,
-Justin
Justin Tobler (9):
builtin/receive-pack: properly clean up keep files
odb/transaction: add transaction finalize interface
builtin/receive-pack: pass shallow file explicitly
builtin/receive-pack: read unpack limit config lazily
builtin/receive-pack: lift global state out of unpack()
builtin/receive-pack: report unpack errors via strbuf
builtin/receive-pack: explicitly pass packfile fd
odb: return temporary ODB source when set
odb/transaction: add transaction interface to write packfiles
builtin/add.c | 4 +-
builtin/receive-pack.c | 211 ++++++++-----------------------------
builtin/unpack-objects.c | 2 +-
builtin/update-index.c | 4 +-
cache-tree.c | 2 +-
fetch-pack.c | 2 +-
object-file.c | 177 ++++++++++++++++++++++++++++++-
odb.c | 9 +-
odb.h | 6 +-
odb/transaction.c | 21 ++++
odb/transaction.h | 85 +++++++++++++++
pack-write.c | 7 +-
pack.h | 4 +-
read-cache.c | 2 +-
t/t5547-push-quarantine.sh | 14 +++
tmp-objdir.c | 8 +-
tmp-objdir.h | 6 +-
17 files changed, 376 insertions(+), 188 deletions(-)
Range-diff against v2:
-: ---------- > 1: 58569303f9 builtin/receive-pack: properly clean up keep files
1: 10efcc22e4 ! 2: dba9696866 odb/transaction: add transaction finalize interface
@@ Commit message
`odb_transaction_finalize()` call site in git-receive-pack(1) is made
after the reference updates are finished.
+ All other callers commit a transaction and immediately finalize it with
+ no work in between and cannot meaningfully recover should either step
+ fail, so introduce an `odb_transaction_commit_and_finalize_or_die()`
+ helper that performs both and dies on error. Call sites are updated
+ accordingly.
+
Signed-off-by: Justin Tobler <jltobler@gmail.com>
## builtin/add.c ##
@@ builtin/add.c: int cmd_add(int argc,
repo_config(repo, add_config, NULL);
@@ builtin/add.c: int cmd_add(int argc,
- free(ps_matched);
- dir_clear(&dir);
- clear_pathspec(&pathspec);
-+ odb_transaction_finalize(transaction);
- return exit_status;
- }
+
+ if (chmod_arg && pathspec.nr)
+ exit_status |= chmod_pathspec(repo, &pathspec, chmod_arg[0], show_only);
+- odb_transaction_commit(transaction);
++ odb_transaction_commit_and_finalize_or_die(transaction);
+
+ finish:
+ if (write_locked_index(repo->index, &lock_file,
## builtin/receive-pack.c ##
@@ builtin/receive-pack.c: int cmd_receive_pack(int argc,
@@ builtin/receive-pack.c: int cmd_receive_pack(int argc,
## builtin/unpack-objects.c ##
@@ builtin/unpack-objects.c: static void unpack_all(void)
+ unpack_one(i);
display_progress(progress, i + 1);
}
- odb_transaction_commit(transaction);
-+ odb_transaction_finalize(transaction);
+- odb_transaction_commit(transaction);
++ odb_transaction_commit_and_finalize_or_die(transaction);
stop_progress(&progress);
if (delta_list)
## builtin/update-index.c ##
@@ builtin/update-index.c: int cmd_update_index(int argc,
+ * a transaction.
*/
if (transaction && verbose) {
- odb_transaction_commit(transaction);
-+ odb_transaction_finalize(transaction);
+- odb_transaction_commit(transaction);
++ odb_transaction_commit_and_finalize_or_die(transaction);
transaction = NULL;
}
@@ builtin/update-index.c: int cmd_update_index(int argc,
+ /*
* By now we have added all of the new objects
*/
- odb_transaction_commit(transaction);
-+ odb_transaction_finalize(transaction);
+- odb_transaction_commit(transaction);
++ odb_transaction_commit_and_finalize_or_die(transaction);
if (split_index > 0) {
if (repo_config_get_split_index(the_repository) == 0)
## cache-tree.c ##
@@ cache-tree.c: int cache_tree_update(struct index_state *istate, int flags)
- odb_transaction_begin_or_die(the_repository->objects, &transaction, 0);
i = update_one(istate->cache_tree, istate->cache, istate->cache_nr,
"", 0, &skip, flags);
-- if (!inflight)
-+ if (!inflight) {
- odb_transaction_commit(transaction);
-+ odb_transaction_finalize(transaction);
-+ }
+ if (!inflight)
+- odb_transaction_commit(transaction);
++ odb_transaction_commit_and_finalize_or_die(transaction);
trace2_region_leave("cache_tree", "update", istate->repo);
trace_performance_leave("cache_tree_update");
if (i < 0)
## object-file.c ##
@@ object-file.c: int index_fd(struct index_state *istate, struct object_id *oid,
- &stream,
xsize_t(st->st_size),
oid);
-- if (!inflight)
-+ if (!inflight) {
- odb_transaction_commit(transaction);
-+ odb_transaction_finalize(transaction);
-+ }
+ if (!inflight)
+- odb_transaction_commit(transaction);
++ odb_transaction_commit_and_finalize_or_die(transaction);
} else {
ret = hash_blob_stream(&stream,
the_repository->hash_algo, oid,
@@ odb/transaction.h: static inline void odb_transaction_begin_or_die(struct object
+ * a negative error code otherwise.
+ */
+int odb_transaction_finalize(struct odb_transaction *transaction);
++
++static inline void odb_transaction_commit_and_finalize_or_die(struct odb_transaction *transaction)
++{
++ if (odb_transaction_commit(transaction))
++ die(_("failed to commit ODB transaction"));
++ if (odb_transaction_finalize(transaction))
++ die(_("failed to finalize ODB transaction"));
++}
+
/*
* Writes the object in the provided stream into the transaction. The resulting
@@ odb/transaction.h: static inline void odb_transaction_begin_or_die(struct object
## read-cache.c ##
@@ read-cache.c: int add_files_to_cache(struct repository *repo, const char *prefix,
- if (!inflight)
odb_transaction_begin_or_die(repo->objects, &transaction, 0);
run_diff_files(&rev, DIFF_RACY_IS_MODIFIED);
-- if (!inflight)
-+ if (!inflight) {
- odb_transaction_commit(transaction);
-+ odb_transaction_finalize(transaction);
-+ }
+ if (!inflight)
+- odb_transaction_commit(transaction);
++ odb_transaction_commit_and_finalize_or_die(transaction);
release_revisions(&rev);
return !!data.add_errors;
2: e1903ac32f = 3: 09bc00a070 builtin/receive-pack: pass shallow file explicitly
3: e4950c0abe ! 4: 2586ea4041 builtin/receive-pack: read unpack limit config lazily
@@ builtin/receive-pack.c: static void push_header_arg(struct strvec *args, struct
ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));
}
-+static int get_unpack_limit(struct repository *repo)
++static unsigned int get_unpack_limit(struct repository *repo)
+{
-+ static int limit = -1;
++ unsigned int limit = 100;
+
-+ if (limit < 0) {
-+ int receive_limit = -1;
-+ int transfer_limit = -1;
-+
-+ repo_config_get_int(repo, "receive.unpacklimit",
-+ &receive_limit);
-+ repo_config_get_int(repo, "transfer.unpacklimit",
-+ &transfer_limit);
-+
-+ if (receive_limit >= 0)
-+ limit = receive_limit;
-+ else if (transfer_limit >= 0)
-+ limit = transfer_limit;
-+ else
-+ limit = 100;
-+ }
++ repo_config_get_uint(repo, "transfer.unpacklimit", &limit);
++ repo_config_get_uint(repo, "receive.unpacklimit", &limit);
+
+ return limit;
+}
4: c9b4ff73ba ! 5: adf325095e builtin/receive-pack: lift global state out of unpack()
@@ Commit message
Signed-off-by: Justin Tobler <jltobler@gmail.com>
## builtin/receive-pack.c ##
-@@ builtin/receive-pack.c: static int get_unpack_limit(struct repository *repo)
+@@ builtin/receive-pack.c: static unsigned int get_unpack_limit(struct repository *repo)
return limit;
}
5: 7be990c2c2 ! 6: 29f407bf36 builtin/receive-pack: report unpack errors via strbuf
@@ builtin/receive-pack.c: static const char *unpack(struct odb_transaction *transa
+ return -1;
+ }
- lockfile = index_pack_lockfile(the_repository, child.out, NULL);
- if (lockfile) {
+ /*
+ * The lockfile filepath is expected to be the final location of
@@ builtin/receive-pack.c: static const char *unpack(struct odb_transaction *transaction,
close(child.out);
6: 742c724943 = 7: b85f5e868c builtin/receive-pack: explicitly pass packfile fd
-: ---------- > 8: 620eafe035 odb: return temporary ODB source when set
7: 7743cf242a ! 9: 2e75a8bd6c odb/transaction: add transaction interface to write packfiles
@@ Commit message
Introduce `odb_transaction_write_pack()` as a generic interface to
handle writing a packfile to a transaction and use the logic from
- `unpack()` as the "files" backend implementation. Note that a packfile
- written via git-index-pack(1) is kept in place by a ".keep" lockfile
- that must be retained until references are updated. To faciliate this in
- an ODB backend agnostic manner, the "files" transaction backend takes
- ownership of these lockfiles and removes them post-commit through its
- release callback.
+ `unpack()` as the "files" backend implementation. Note that when storing
+ the objects as a packfile, git-index-pack(1) also writes a ".keep"
+ lockfile next to it to prevent a concurrent repack from removing the new
+ pack prior to reference updates being performed. The "files" transaction
+ backend is responsible for managing these ".keep" files and removes them
+ post-commit once the transaction is finalized.
Call sites in git-receive-pack(1) are updated accordingly.
@@ builtin/receive-pack.c: static void read_push_options(struct packet_reader *read
- ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));
-}
-
--static int get_unpack_limit(struct repository *repo)
+-static unsigned int get_unpack_limit(struct repository *repo)
-{
-- static int limit = -1;
+- unsigned int limit = 100;
-
-- if (limit < 0) {
-- int receive_limit = -1;
-- int transfer_limit = -1;
--
-- repo_config_get_int(repo, "receive.unpacklimit",
-- &receive_limit);
-- repo_config_get_int(repo, "transfer.unpacklimit",
-- &transfer_limit);
--
-- if (receive_limit >= 0)
-- limit = receive_limit;
-- else if (transfer_limit >= 0)
-- limit = transfer_limit;
-- else
-- limit = 100;
-- }
+- repo_config_get_uint(repo, "transfer.unpacklimit", &limit);
+- repo_config_get_uint(repo, "receive.unpacklimit", &limit);
-
- return limit;
-}
@@ builtin/receive-pack.c: static void read_push_options(struct packet_reader *read
- return -1;
- }
-
-- lockfile = index_pack_lockfile(the_repository, child.out, NULL);
+- /*
+- * The lockfile filepath is expected to be the final location of
+- * the ".keep" file after being migrated to the main ODB source.
+- * This ensures the lockfile can be found and removed later
+- * after the ODB transaction has been committed.
+- */
+- lockfile = index_pack_lockfile(transaction->source, child.out, NULL);
- if (lockfile) {
- pack_lockfile = register_tempfile(lockfile);
- free(lockfile);
@@ object-file.c
#include "strvec.h"
#include "tempfile.h"
@@ object-file.c: struct odb_transaction_files {
- struct tmp_objdir *objdir;
+ struct odb_source *quarantine;
struct transaction_packfile packfile;
const char *prefix;
+
@@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b
+ ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries));
+}
+
-+static int get_unpack_limit(struct repository *repo)
++static unsigned int get_unpack_limit(struct repository *repo)
+{
-+ static int limit = -1;
-+
-+ if (limit < 0) {
-+ int receive_limit = -1;
-+ int transfer_limit = -1;
-+
-+ repo_config_get_int(repo, "receive.unpacklimit",
-+ &receive_limit);
-+ repo_config_get_int(repo, "transfer.unpacklimit",
-+ &transfer_limit);
-+
-+ if (receive_limit >= 0)
-+ limit = receive_limit;
-+ else if (transfer_limit >= 0)
-+ limit = transfer_limit;
-+ else
-+ limit = 100;
-+ }
++ unsigned int limit = 100;
++
++ repo_config_get_uint(repo, "transfer.unpacklimit", &limit);
++ repo_config_get_uint(repo, "receive.unpacklimit", &limit);
+
+ return limit;
+}
@@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b
+
+ odb_transaction_env(base, &child.env);
+
-+ if (ntohl(hdr.hdr_entries) < (unsigned int)get_unpack_limit(repo)) {
++ if (ntohl(hdr.hdr_entries) < get_unpack_limit(repo)) {
+ strvec_push(&child.args, "unpack-objects");
+ push_header_arg(&child.args, &hdr);
+ if (opts->quiet)
@@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b
+ return -1;
+ }
+
-+ lockfile = index_pack_lockfile(repo, child.out, NULL);
++ /*
++ * The lockfile filepath is expected to be the final location of
++ * the ".keep" file after being migrated to the main ODB source.
++ * This ensures the lockfile can be found and removed later
++ * after the ODB transaction has been committed.
++ */
++ lockfile = index_pack_lockfile(base->source, child.out, NULL);
+ if (lockfile) {
+ ALLOC_GROW(transaction->pack_lockfiles,
+ transaction->pack_lockfiles_nr + 1,
@@ object-file.c: static int odb_transaction_files_commit(struct odb_transaction *b
+ return -1;
+ }
+
-+ odb_source_prepare(repo->objects->sources,
++ odb_source_prepare(transaction->quarantine,
+ ODB_PREPARE_FLUSH_CACHES);
+ }
+
@@ odb/transaction.h
+ * The max size in bytes of the incoming packfile allowed. No limit is
+ * enforced when set to 0.
+ */
-+
+ off_t max_input_size;
+
+ /*
base-commit: 2c78326f810173a4f3aefd8021f1e07575412481
--
2.55.0.424.g13c7afec21
next prev parent reply other threads:[~2026-08-11 17:54 UTC|newest]
Thread overview: 46+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-06 21:38 [PATCH 0/6] builtin/receive-pack: support pluggable packfile writes Justin Tobler
2026-08-06 21:38 ` [PATCH 1/6] odb/transaction: add transaction release interface Justin Tobler
2026-08-07 7:03 ` Patrick Steinhardt
2026-08-07 15:11 ` Justin Tobler
2026-08-06 21:38 ` [PATCH 2/6] builtin/receive-pack: pass shallow file explicitly Justin Tobler
2026-08-07 7:03 ` Patrick Steinhardt
2026-08-06 21:38 ` [PATCH 3/6] builtin/receive-pack: lift global state out of unpack() Justin Tobler
2026-08-07 7:03 ` Patrick Steinhardt
2026-08-07 15:33 ` Justin Tobler
2026-08-06 21:38 ` [PATCH 4/6] builtin/receive-pack: report unpack errors via strbuf Justin Tobler
2026-08-07 7:03 ` Patrick Steinhardt
2026-08-07 15:36 ` Justin Tobler
2026-08-09 19:00 ` Justin Tobler
2026-08-10 5:15 ` Patrick Steinhardt
2026-08-06 21:38 ` [PATCH 5/6] builtin/receive-pack: explicitly pass packfile fd Justin Tobler
2026-08-06 21:38 ` [PATCH 6/6] odb/transaction: add transaction interface to write packfiles Justin Tobler
2026-08-07 7:03 ` Patrick Steinhardt
2026-08-07 16:01 ` Justin Tobler
2026-08-09 19:00 ` [PATCH v2 0/7] builtin/receive-pack: support pluggable packfile writes Justin Tobler
2026-08-09 19:01 ` [PATCH v2 1/7] odb/transaction: add transaction finalize interface Justin Tobler
2026-08-10 3:38 ` Junio C Hamano
2026-08-10 19:10 ` Justin Tobler
2026-08-09 19:01 ` [PATCH v2 2/7] builtin/receive-pack: pass shallow file explicitly Justin Tobler
2026-08-09 19:01 ` [PATCH v2 3/7] builtin/receive-pack: read unpack limit config lazily Justin Tobler
2026-08-10 5:15 ` Patrick Steinhardt
2026-08-10 15:42 ` Justin Tobler
2026-08-10 17:54 ` Junio C Hamano
2026-08-10 19:16 ` Justin Tobler
2026-08-09 19:01 ` [PATCH v2 4/7] builtin/receive-pack: lift global state out of unpack() Justin Tobler
2026-08-09 19:01 ` [PATCH v2 5/7] builtin/receive-pack: report unpack errors via strbuf Justin Tobler
2026-08-09 19:01 ` [PATCH v2 6/7] builtin/receive-pack: explicitly pass packfile fd Justin Tobler
2026-08-09 19:01 ` [PATCH v2 7/7] odb/transaction: add transaction interface to write packfiles Justin Tobler
2026-08-10 1:54 ` Junio C Hamano
2026-08-10 19:29 ` Justin Tobler
2026-08-10 4:02 ` Junio C Hamano
2026-08-10 19:54 ` Justin Tobler
2026-08-11 17:54 ` Justin Tobler [this message]
2026-08-11 17:54 ` [PATCH v3 1/9] builtin/receive-pack: properly clean up keep files Justin Tobler
2026-08-11 17:54 ` [PATCH v3 2/9] odb/transaction: add transaction finalize interface Justin Tobler
2026-08-11 17:54 ` [PATCH v3 3/9] builtin/receive-pack: pass shallow file explicitly Justin Tobler
2026-08-11 17:54 ` [PATCH v3 4/9] builtin/receive-pack: read unpack limit config lazily Justin Tobler
2026-08-11 17:54 ` [PATCH v3 5/9] builtin/receive-pack: lift global state out of unpack() Justin Tobler
2026-08-11 17:54 ` [PATCH v3 6/9] builtin/receive-pack: report unpack errors via strbuf Justin Tobler
2026-08-11 17:54 ` [PATCH v3 7/9] builtin/receive-pack: explicitly pass packfile fd Justin Tobler
2026-08-11 17:54 ` [PATCH v3 8/9] odb: return temporary ODB source when set Justin Tobler
2026-08-11 17:54 ` [PATCH v3 9/9] odb/transaction: add transaction interface to write packfiles Justin Tobler
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260811175415.2044235-1-jltobler@gmail.com \
--to=jltobler@gmail.com \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=ps@pks.im \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox