From ff1da37f58e754a29dff0df03d58b1042a4d5654 Mon Sep 17 00:00:00 2001 From: Dominique Martinet Date: Wed, 8 Jul 2026 03:33:42 +0000 Subject: [PATCH 01/32] submodule--helper: accept '-i' shorthand for update --init commit 3ad0ba722744 ("git-submodule.sh: improve variables readability") made `git submodules update -i` pass `-i` as is to submodule--helper, but it fails with `error: unknown switch `i'` because the helper does not accept the short option. All other short options supported by git-submodule.sh are properly handle in the helper, so also add the alias for --init Fixes: 3ad0ba722744 ("git-submodule.sh: improve variables readability") Signed-off-by: Dominique Martinet Signed-off-by: Junio C Hamano --- builtin/submodule--helper.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 1cc82a134db22e..3ec8bf50532ea5 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -2990,7 +2990,7 @@ static int module_update(int argc, const char **argv, const char *prefix, struct option module_update_options[] = { OPT__SUPER_PREFIX(&opt.super_prefix), OPT__FORCE(&opt.force, N_("force checkout updates"), 0), - OPT_BOOL(0, "init", &opt.init, + OPT_BOOL('i', "init", &opt.init, N_("initialize uninitialized submodules before update")), OPT_BOOL(0, "remote", &opt.remote, N_("use SHA-1 of submodule's remote tracking branch")), From 52bd2ff2741e376ab8b92c354ba64bdf9d1c2c0a Mon Sep 17 00:00:00 2001 From: Chen Linxuan Date: Fri, 10 Jul 2026 14:43:29 +0800 Subject: [PATCH 02/32] config: refactor include_by_gitdir() into include_by_path() The include_by_gitdir() function matches the realpath of a given path against a glob pattern, but its interface is tightly coupled to the gitdir condition: it takes a struct config_options *opts and extracts opts->git_dir internally. Refactor it into a more generic include_by_path() helper that takes a const char *path parameter directly, and update the gitdir and gitdir/i callers to pass opts->git_dir explicitly. No behavior change, just preparing for the addition of a new worktree condition that will reuse the same path-matching logic with a different path. Signed-off-by: Chen Linxuan Signed-off-by: Junio C Hamano --- config.c | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/config.c b/config.c index 6a0de86e3ae958..00eeeea370c962 100644 --- a/config.c +++ b/config.c @@ -235,23 +235,20 @@ static int prepare_include_condition_pattern(const struct key_value_info *kvi, return 0; } -static int include_by_gitdir(const struct key_value_info *kvi, - const struct config_options *opts, - const char *cond, size_t cond_len, int icase) +static int include_by_path(const struct key_value_info *kvi, + const char *path, + const char *cond, size_t cond_len, int icase) { struct strbuf text = STRBUF_INIT; struct strbuf pattern = STRBUF_INIT; size_t prefix; int ret = 0; - const char *git_dir; int already_tried_absolute = 0; - if (opts->git_dir) - git_dir = opts->git_dir; - else + if (!path) goto done; - strbuf_realpath(&text, git_dir, 1); + strbuf_realpath(&text, path, 1); strbuf_add(&pattern, cond, cond_len); ret = prepare_include_condition_pattern(kvi, &pattern, &prefix); if (ret < 0) @@ -284,7 +281,7 @@ static int include_by_gitdir(const struct key_value_info *kvi, * which'll do the right thing */ strbuf_reset(&text); - strbuf_add_absolute_path(&text, git_dir); + strbuf_add_absolute_path(&text, path); already_tried_absolute = 1; goto again; } @@ -400,9 +397,9 @@ static int include_condition_is_true(const struct key_value_info *kvi, const struct config_options *opts = inc->opts; if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len)) - return include_by_gitdir(kvi, opts, cond, cond_len, 0); + return include_by_path(kvi, opts->git_dir, cond, cond_len, 0); else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len)) - return include_by_gitdir(kvi, opts, cond, cond_len, 1); + return include_by_path(kvi, opts->git_dir, cond, cond_len, 1); else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len)) return include_by_branch(inc, cond, cond_len); else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond, From 5004829756596498a20305b3fce8387629396835 Mon Sep 17 00:00:00 2001 From: Chen Linxuan Date: Fri, 10 Jul 2026 14:43:30 +0800 Subject: [PATCH 03/32] config: add "worktree" and "worktree/i" includeIf conditions The includeIf mechanism already supports matching on the .git directory path (gitdir) and the currently checked out branch (onbranch). But in multi-worktree setups the .git directory of a linked worktree points into the main repository's .git/worktrees/ area, which makes gitdir patterns cumbersome when one wants to include config based on the working tree's checkout path instead. Introduce two new condition keywords: - worktree: matches the realpath of the current worktree's working directory (i.e. repo_get_work_tree()) against a glob pattern. This is the path returned by git rev-parse --show-toplevel. - worktree/i: is the case-insensitive variant. The implementation reuses the include_by_path() helper introduced in the previous commit, passing the worktree path in place of the gitdir. The condition never matches in bare repositories (where there is no worktree) or during early config reading (where no repository is available). Add documentation describing the new conditions, including a comparison with extensions.worktreeConfig and a note that worktree matching currently uses the realpath-resolved worktree location. Add tests covering bare repositories, multiple worktrees, realpath-resolved symlinked worktree paths, case-sensitive and case-insensitive matching, early config reading, and non-repository scenarios. Signed-off-by: Chen Linxuan Signed-off-by: Junio C Hamano --- Documentation/config.adoc | 53 ++++++++++++++++ config.c | 6 ++ t/t1305-config-include.sh | 128 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) diff --git a/Documentation/config.adoc b/Documentation/config.adoc index 15b1a4d5934758..1ef72de62f2ba6 100644 --- a/Documentation/config.adoc +++ b/Documentation/config.adoc @@ -146,6 +146,51 @@ refer to linkgit:gitignore[5] for details. For convenience: This is the same as `gitdir` except that matching is done case-insensitively (e.g. on case-insensitive file systems) +`worktree`:: + The data that follows the keyword `worktree` and a colon is used as a + glob pattern. If the working directory of the current worktree matches + the pattern, the include condition is met. ++ +The worktree location is the path where files are checked out (as returned +by `git rev-parse --show-toplevel`). This is different from `gitdir`, which +matches the `.git` directory path. In a linked worktree, the worktree path +is the directory where that worktree's files are located, not the main +repository's `.git` directory. ++ +The pattern uses the same glob syntax as `gitdir` (including `~/`, `./`, +`**/`, and trailing-`/` prefix matching). This condition will never match +in a bare repository (which has no worktree). ++ +Unlike `gitdir`, the `worktree` condition currently matches only the +realpath-resolved worktree location. If the working tree was entered via a +symbolic link, a pattern that uses the symbolic-link spelling may not match; +use the real path instead. ++ +This is useful when you want to apply configuration based on where the +working tree is located on the filesystem. For example, a contributor who +works on the same project both personally and as an employee can use +different `user.name` and `user.email` values depending on which directory +the worktree is checked out under: ++ +---- +[includeIf "worktree:/home/user/work/"] + path = ~/.config/git/work.inc +[includeIf "worktree:/home/user/personal/"] + path = ~/.config/git/personal.inc +---- ++ +While `extensions.worktreeConfig` (see linkgit:git-worktree[1]) also supports +per-worktree configuration, it stores the config inside each repository's +`.git/config.worktree` file and requires running `git config --worktree` +inside each worktree individually. In contrast, `includeIf "worktree:..."` +can be set once in a global or system-level configuration file (e.g. +`~/.config/git/config`) and applies to all repositories at once based on +their worktree location. + +`worktree/i`:: + This is the same as `worktree` except that matching is done + case-insensitively (e.g. on case-insensitive file systems) + `onbranch`:: The data that follows the keyword `onbranch` and a colon is taken to be a pattern with standard globbing wildcards and two additional @@ -244,6 +289,14 @@ Example [includeIf "gitdir:~/to/group/"] path = /path/to/foo.inc +; include if the worktree is at /path/to/project-build +[includeIf "worktree:/path/to/project-build"] + path = build-config.inc + +; include for all worktrees inside /path/to/group +[includeIf "worktree:/path/to/group/"] + path = group-config.inc + ; relative paths are always relative to the including ; file (if the condition is true); their location is not ; affected by the condition diff --git a/config.c b/config.c index 00eeeea370c962..9d6d7872d76c10 100644 --- a/config.c +++ b/config.c @@ -400,6 +400,12 @@ static int include_condition_is_true(const struct key_value_info *kvi, return include_by_path(kvi, opts->git_dir, cond, cond_len, 0); else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len)) return include_by_path(kvi, opts->git_dir, cond, cond_len, 1); + else if (skip_prefix_mem(cond, cond_len, "worktree:", &cond, &cond_len)) + return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL, + cond, cond_len, 0); + else if (skip_prefix_mem(cond, cond_len, "worktree/i:", &cond, &cond_len)) + return include_by_path(kvi, inc->repo ? repo_get_work_tree(inc->repo) : NULL, + cond, cond_len, 1); else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len)) return include_by_branch(inc, cond, cond_len); else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond, diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh index f3892578e4ff86..4e840dfdb35be4 100755 --- a/t/t1305-config-include.sh +++ b/t/t1305-config-include.sh @@ -396,4 +396,132 @@ test_expect_success 'onbranch without repository but explicit nonexistent Git di test_must_fail nongit git --git-dir=nonexistent config get foo.bar ' +# worktree: conditional include tests + +test_expect_success 'conditional include, worktree bare repo' ' + git init --bare wt-bare && + ( + cd wt-bare && + echo "[includeIf \"worktree:/\"]path=bar-bare" >>config && + echo "[test]wtbare=1" >bar-bare && + test_must_fail git config test.wtbare + ) +' + +test_expect_success 'conditional include, worktree multiple worktrees' ' + git init wt-multi && + ( + cd wt-multi && + test_commit initial && + git worktree add -b linked-branch ../wt-linked HEAD && + git worktree add -b prefix-branch ../wt-prefix/linked HEAD + ) && + wt_main="$(cd wt-multi && pwd)" && + wt_linked="$(cd wt-linked && pwd)" && + wt_prefix_parent="$(cd wt-prefix && pwd)" && + cat >>wt-multi/.git/config <<-EOF && + [includeIf "worktree:$wt_main"] + path = main-config + [includeIf "worktree:$wt_linked"] + path = linked-config + [includeIf "worktree:$wt_prefix_parent/"] + path = prefix-config + EOF + echo "[test]mainvar=main" >wt-multi/.git/main-config && + echo "[test]linkedvar=linked" >wt-multi/.git/linked-config && + echo "[test]prefixvar=prefix" >wt-multi/.git/prefix-config && + echo main >expect && + git -C wt-multi config test.mainvar >actual && + test_cmp expect actual && + test_must_fail git -C wt-multi config test.linkedvar && + test_must_fail git -C wt-multi config test.prefixvar && + echo linked >expect && + git -C wt-linked config test.linkedvar >actual && + test_cmp expect actual && + test_must_fail git -C wt-linked config test.mainvar && + test_must_fail git -C wt-linked config test.prefixvar && + echo prefix >expect && + git -C wt-prefix/linked config test.prefixvar >actual && + test_cmp expect actual && + test_must_fail git -C wt-prefix/linked config test.mainvar && + test_must_fail git -C wt-prefix/linked config test.linkedvar +' + +test_expect_success SYMLINKS 'conditional include, worktree resolves symlinks' ' + mkdir real-wt && + ln -s real-wt link-wt && + git init link-wt/repo && + ( + cd link-wt/repo && + # repo->worktree resolves symlinks, so use real path in pattern + echo "[includeIf \"worktree:**/real-wt/repo\"]path=bar-link" >>.git/config && + echo "[test]wtlink=2" >.git/bar-link && + echo 2 >expect && + git config test.wtlink >actual && + test_cmp expect actual + ) +' + +test_expect_success !CASE_INSENSITIVE_FS 'conditional include, worktree, case sensitive' ' + git init wt-case && + ( + cd wt-case && + test_commit initial && + wt_path="$(pwd)" && + wt_upper=$(echo "$wt_path" | tr a-z A-Z) && + echo "[includeIf \"worktree:$wt_upper\"]path=case-inc" >>.git/config && + echo "[test]wtcase=1" >.git/case-inc && + test_must_fail git config test.wtcase + ) +' + +test_expect_success 'conditional include, worktree, icase' ' + git init wt-icase && + ( + cd wt-icase && + test_commit initial && + wt_path="$(pwd)" && + wt_upper=$(echo "$wt_path" | tr a-z A-Z) && + echo "[includeIf \"worktree/i:$wt_upper\"]path=icase-inc" >>.git/config && + echo "[test]wticase=1" >.git/icase-inc && + echo 1 >expect && + git config test.wticase >actual && + test_cmp expect actual + ) +' + +# The "worktree" condition cannot match during early config reading +# because the repository object is not yet fully initialized and +# repo_get_work_tree() returns NULL. +test_expect_success 'conditional include, worktree does not match in early config' ' + git init wt-early && + ( + cd wt-early && + test_commit initial && + wt_path="$(pwd)" && + echo "[includeIf \"worktree:$wt_path\"]path=early-inc" >>.git/config && + echo "[test]wtearly=1" >.git/early-inc && + test-tool config read_early_config test.wtearly >actual && + test_must_be_empty actual + ) +' + +# Use a loose pattern so the "present in non-worktree cases" check works +# for Unix-style absolute paths and Windows paths like D:/a/git/... +test_expect_success 'conditional include, worktree without repository' ' + test_when_finished "rm -f .gitconfig config.inc" && + git config set -f .gitconfig "includeIf.worktree:**.path" config.inc && + git config set -f config.inc foo.bar baz && + git config get foo.bar && + test_must_fail nongit git config get foo.bar +' + +test_expect_success 'conditional include, worktree without repository but explicit nonexistent Git directory' ' + test_when_finished "rm -f .gitconfig config.inc" && + git config set -f .gitconfig "includeIf.worktree:**.path" config.inc && + git config set -f config.inc foo.bar baz && + git config get foo.bar && + test_must_fail nongit git --git-dir=nonexistent config get foo.bar +' + test_done From f065d5985c30d41d8d43ca86c299a311533691bc Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Fri, 10 Jul 2026 11:37:12 -0500 Subject: [PATCH 04/32] object-file: rename files transaction prepare function The "files" ODB transaction backend lazily creates a temporary object directory when the first loose object is written to the transaction via `prepare_loose_object_transaction()`. In a subsequent commit, the temporary directory is used to also write packfiles to. Rename the function to `odb_transaction_files_prepare()` accordingly. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- object-file.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/object-file.c b/object-file.c index bce941874eb994..9f5e51eda4be4d 100644 --- a/object-file.c +++ b/object-file.c @@ -499,7 +499,7 @@ struct odb_transaction_files { struct transaction_packfile packfile; }; -static void prepare_loose_object_transaction(struct odb_transaction *base) +static void odb_transaction_files_prepare(struct odb_transaction *base) { struct odb_transaction_files *transaction = container_of_or_null(base, struct odb_transaction_files, base); @@ -760,7 +760,7 @@ int write_loose_object(struct odb_source_loose *loose, static struct strbuf filename = STRBUF_INIT; if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) - prepare_loose_object_transaction(loose->base.odb->transaction); + odb_transaction_files_prepare(loose->base.odb->transaction); odb_loose_path(loose, &filename, oid); @@ -824,7 +824,7 @@ int odb_source_loose_write_stream(struct odb_source_loose *loose, int hdrlen; if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) - prepare_loose_object_transaction(loose->base.odb->transaction); + odb_transaction_files_prepare(loose->base.odb->transaction); /* Since oid is not determined, save tmp file to odb path. */ strbuf_addf(&filename, "%s/", loose->base.path); From 9c182bb59bf0eb7fc3deecec65b4d5efe47df28c Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Fri, 10 Jul 2026 11:37:13 -0500 Subject: [PATCH 05/32] object-file: rename files transaction fsync function When writing an object to a "files" ODB transaction, a full hardware flush is not initially performed during the fsync in `fsync_loose_object_transaction()` and instead delayed until the transaction is later committed. To be more consistent with other "files" ODB transaction helpers, rename the function to `odb_transaction_files_fsync()` accordingly. The conditional in the helper is also slightly restructured to improve clarity to readers. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- object-file.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/object-file.c b/object-file.c index 9f5e51eda4be4d..469e0f7e61bf65 100644 --- a/object-file.c +++ b/object-file.c @@ -518,12 +518,17 @@ static void odb_transaction_files_prepare(struct odb_transaction *base) tmp_objdir_replace_primary_odb(transaction->objdir, 0); } -static void fsync_loose_object_transaction(struct odb_transaction *base, - int fd, const char *filename) +static void odb_transaction_files_fsync(struct odb_transaction *base, + int fd, const char *filename) { struct odb_transaction_files *transaction = container_of_or_null(base, struct odb_transaction_files, base); + if (!transaction || !transaction->objdir) { + fsync_or_die(fd, filename); + return; + } + /* * If we have an active ODB transaction, we issue a call that * cleans the filesystem page cache but avoids a hardware flush @@ -531,8 +536,7 @@ static void fsync_loose_object_transaction(struct odb_transaction *base, * before renaming the objects to their final names as part of * flush_batch_fsync. */ - if (!transaction || !transaction->objdir || - git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) { + if (git_fsync(fd, FSYNC_WRITEOUT_ONLY) < 0) { if (errno == ENOSYS) warning(_("core.fsyncMethod = batch is unsupported on this platform")); fsync_or_die(fd, filename); @@ -553,7 +557,7 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac /* * Issue a full hardware flush against a temporary file to ensure * that all objects are durable before any renames occur. The code in - * fsync_loose_object_transaction has already issued a writeout + * odb_transaction_files_fsync has already issued a writeout * request, but it has not flushed any writeback cache in the storage * hardware or any filesystem logs. This fsync call acts as a barrier * to ensure that the data in each new object file is durable before @@ -582,7 +586,7 @@ static void close_loose_object(struct odb_source_loose *loose, goto out; if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) - fsync_loose_object_transaction(loose->base.odb->transaction, fd, filename); + odb_transaction_files_fsync(loose->base.odb->transaction, fd, filename); else if (fsync_object_files > 0) fsync_or_die(fd, filename); else From ee1785a49080943dcde9303c3771de04c873e93c Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Fri, 10 Jul 2026 11:37:14 -0500 Subject: [PATCH 06/32] object-file: embed transaction flush logic in commit function When a "files" transaction is committed, `flush_loose_object_transaction()` is invoked to handle performing a hardware flush along with migrating the temporary object directory into the primary and configuring the repository ODB source accordingly. The function name here is a bit misleading because the helper is doing a bit more than just "flushing" the transaction contents. Also, in a subsequent commit, the transaction temporary directory is used to stage packfiles and not just loose objects anymore. Lift the helper function logic into `odb_transaction_files_commit()` to more accurately signal to readers the operation being performed. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- object-file.c | 64 ++++++++++++++++++++++----------------------------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/object-file.c b/object-file.c index 469e0f7e61bf65..160ae093cc7227 100644 --- a/object-file.c +++ b/object-file.c @@ -543,41 +543,6 @@ static void odb_transaction_files_fsync(struct odb_transaction *base, } } -/* - * Cleanup after batch-mode fsync_object_files. - */ -static void flush_loose_object_transaction(struct odb_transaction_files *transaction) -{ - struct strbuf temp_path = STRBUF_INIT; - struct tempfile *temp; - - if (!transaction->objdir) - return; - - /* - * Issue a full hardware flush against a temporary file to ensure - * that all objects are durable before any renames occur. The code in - * odb_transaction_files_fsync has already issued a writeout - * request, but it has not flushed any writeback cache in the storage - * hardware or any filesystem logs. This fsync call acts as a barrier - * to ensure that the data in each new object file is durable before - * the final name is visible. - */ - strbuf_addf(&temp_path, "%s/bulk_fsync_XXXXXX", - repo_get_object_directory(transaction->base.source->odb->repo)); - temp = xmks_tempfile(temp_path.buf); - fsync_or_die(get_tempfile_fd(temp), get_tempfile_path(temp)); - delete_tempfile(&temp); - strbuf_release(&temp_path); - - /* - * Make the object files visible in the primary ODB after their data is - * fully durable. - */ - tmp_objdir_migrate(transaction->objdir); - transaction->objdir = NULL; -} - /* Finalize a file on disk, and close it. */ static void close_loose_object(struct odb_source_loose *loose, int fd, const char *filename) @@ -1677,7 +1642,34 @@ static void odb_transaction_files_commit(struct odb_transaction *base) struct odb_transaction_files *transaction = container_of(base, struct odb_transaction_files, base); - flush_loose_object_transaction(transaction); + if (transaction->objdir) { + struct strbuf temp_path = STRBUF_INIT; + struct tempfile *temp; + + /* + * Issue a full hardware flush against a temporary file to ensure + * that all objects are durable before any renames occur. The code in + * odb_transaction_files_fsync has already issued a writeout + * request, but it has not flushed any writeback cache in the storage + * hardware or any filesystem logs. This fsync call acts as a barrier + * to ensure that the data in each new object file is durable before + * the final name is visible. + */ + strbuf_addf(&temp_path, "%s/bulk_fsync_XXXXXX", + repo_get_object_directory(transaction->base.source->odb->repo)); + temp = xmks_tempfile(temp_path.buf); + fsync_or_die(get_tempfile_fd(temp), get_tempfile_path(temp)); + delete_tempfile(&temp); + strbuf_release(&temp_path); + + /* + * Make the object files visible in the primary ODB after their data is + * fully durable. + */ + tmp_objdir_migrate(transaction->objdir); + transaction->objdir = NULL; + } + flush_packfile_transaction(transaction); } From 926618e78cb345e69106f9cd3fb2651d99518919 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Fri, 10 Jul 2026 11:37:15 -0500 Subject: [PATCH 07/32] object-file: drop check for inflight transactions ODB transactions are started via `odb_transaction_begin()` and contain validation to avoid starting multiple transactions at the same time. The "files" backend also has the same logic, but is redundant due to the generic layer already handling it. Drop this validation from the "files" backend accordingly. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- object-file.c | 4 ---- object-file.h | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/object-file.c b/object-file.c index 160ae093cc7227..c16b2c2ac1c225 100644 --- a/object-file.c +++ b/object-file.c @@ -1676,10 +1676,6 @@ static void odb_transaction_files_commit(struct odb_transaction *base) struct odb_transaction *odb_transaction_files_begin(struct odb_source *source) { struct odb_transaction_files *transaction; - struct object_database *odb = source->odb; - - if (odb->transaction) - return NULL; transaction = xcalloc(1, sizeof(*transaction)); transaction->base.source = source; diff --git a/object-file.h b/object-file.h index 528c4e6e697f87..ea43d818f096cb 100644 --- a/object-file.h +++ b/object-file.h @@ -194,8 +194,7 @@ struct odb_transaction; /* * Tell the object database to optimize for adding * multiple objects. odb_transaction_files_commit must be called - * to make new objects visible. If a transaction is already - * pending, NULL is returned. + * to make new objects visible. */ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source); From 6d017185e3b4052354fca3a40e8453bd85624896 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Fri, 10 Jul 2026 11:37:16 -0500 Subject: [PATCH 08/32] object-file: propagate files transaction errors The "files" transaction backend may encounter errors related to managing the temporary directory used to stage objects, but silently ignores these errors. Instead return errors encountered in the `odb_transaction_files_{prepare,begin,commit}()` interfaces to allow callers to handle them as needed. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- object-file.c | 26 ++++++++++++++++++-------- object-file.h | 3 ++- odb/source-files.c | 6 +----- odb/transaction.h | 7 +++++-- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/object-file.c b/object-file.c index c16b2c2ac1c225..3d76b890b8d5e9 100644 --- a/object-file.c +++ b/object-file.c @@ -499,7 +499,7 @@ struct odb_transaction_files { struct transaction_packfile packfile; }; -static void odb_transaction_files_prepare(struct odb_transaction *base) +static int odb_transaction_files_prepare(struct odb_transaction *base) { struct odb_transaction_files *transaction = container_of_or_null(base, struct odb_transaction_files, base); @@ -511,11 +511,15 @@ static void odb_transaction_files_prepare(struct odb_transaction *base) * added at the time they call odb_transaction_files_begin. */ if (!transaction || transaction->objdir) - return; + return 0; transaction->objdir = tmp_objdir_create(base->source->odb->repo, "bulk-fsync"); - if (transaction->objdir) - tmp_objdir_replace_primary_odb(transaction->objdir, 0); + if (!transaction->objdir) + return error(_("unable to create temporary object directory")); + + tmp_objdir_replace_primary_odb(transaction->objdir, 0); + + return 0; } static void odb_transaction_files_fsync(struct odb_transaction *base, @@ -1637,7 +1641,7 @@ int read_loose_object(struct repository *repo, return ret; } -static void odb_transaction_files_commit(struct odb_transaction *base) +static int odb_transaction_files_commit(struct odb_transaction *base) { struct odb_transaction_files *transaction = container_of(base, struct odb_transaction_files, base); @@ -1666,14 +1670,19 @@ static void odb_transaction_files_commit(struct odb_transaction *base) * Make the object files visible in the primary ODB after their data is * fully durable. */ - tmp_objdir_migrate(transaction->objdir); + if (tmp_objdir_migrate(transaction->objdir)) + return error(_("unable to migrate temporary objects")); + transaction->objdir = NULL; } flush_packfile_transaction(transaction); + + return 0; } -struct odb_transaction *odb_transaction_files_begin(struct odb_source *source) +int odb_transaction_files_begin(struct odb_source *source, + struct odb_transaction **out) { struct odb_transaction_files *transaction; @@ -1681,6 +1690,7 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source) transaction->base.source = source; transaction->base.commit = odb_transaction_files_commit; transaction->base.write_object_stream = odb_transaction_files_write_object_stream; + *out = &transaction->base; - return &transaction->base; + return 0; } diff --git a/object-file.h b/object-file.h index ea43d818f096cb..1a023226ac7cd3 100644 --- a/object-file.h +++ b/object-file.h @@ -196,6 +196,7 @@ struct odb_transaction; * multiple objects. odb_transaction_files_commit must be called * to make new objects visible. */ -struct odb_transaction *odb_transaction_files_begin(struct odb_source *source); +int odb_transaction_files_begin(struct odb_source *source, + struct odb_transaction **out); #endif /* OBJECT_FILE_H */ diff --git a/odb/source-files.c b/odb/source-files.c index 5bdd0429225397..2545bd81d448b0 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -182,11 +182,7 @@ static int odb_source_files_write_object_stream(struct odb_source *source, static int odb_source_files_begin_transaction(struct odb_source *source, struct odb_transaction **out) { - struct odb_transaction *tx = odb_transaction_files_begin(source); - if (!tx) - return -1; - *out = tx; - return 0; + return odb_transaction_files_begin(source, out); } static int odb_source_files_read_alternates(struct odb_source *source, diff --git a/odb/transaction.h b/odb/transaction.h index 854fda06f576e4..d52f0533ce3ee8 100644 --- a/odb/transaction.h +++ b/odb/transaction.h @@ -16,8 +16,11 @@ struct odb_transaction { /* The ODB source the transaction is opened against. */ struct odb_source *source; - /* The ODB source specific callback invoked to commit a transaction. */ - void (*commit)(struct odb_transaction *transaction); + /* + * The ODB source specific callback invoked to commit a transaction. + * Returns 0 on success, a negative error code otherwise. + */ + int (*commit)(struct odb_transaction *transaction); /* * This callback is expected to write the given object stream into From 4576d6c5225fec402fa8d4190abb63e6f938b98d Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Fri, 10 Jul 2026 11:37:17 -0500 Subject: [PATCH 09/32] odb/transaction: propagate begin errors When `odb_transaction_begin()` is invoked, the function returns the transaction pointer directly. There is no way for the backend to signal that it failed to set up its state, such as when creating the temporary object directory backing the transaction. In a subsequent commit, git-receive-pack(1) starts using ODB transactions and needs to be able to report such failures rather than silently ignore them. Refactor `odb_transaction_begin()` to return an int error code and write the resulting transaction into an out parameter. Also introduce `odb_transaction_begin_or_die()` as a convenience for callsites that do not need to handle errors explicitly. Note that `odb_transaction_begin()` now returns an error when the ODB already has an inflight transaction pending. ODB transaction call sites that may encounter an inflight transaction are updated to explicitly handle this case. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- builtin/add.c | 2 +- builtin/unpack-objects.c | 2 +- builtin/update-index.c | 2 +- cache-tree.c | 7 +++++-- object-file.c | 10 +++++++--- odb/transaction.c | 14 ++++++++++---- odb/transaction.h | 19 +++++++++++++++---- read-cache.c | 7 +++++-- 8 files changed, 45 insertions(+), 18 deletions(-) diff --git a/builtin/add.c b/builtin/add.c index c859f665199efa..3d5d9cfdb9e30c 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -581,7 +581,7 @@ int cmd_add(int argc, string_list_clear(&only_match_skip_worktree, 0); } - transaction = odb_transaction_begin(repo->objects); + odb_transaction_begin_or_die(repo->objects, &transaction); ps_matched = xcalloc(pathspec.nr, 1); if (add_renormalize) diff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c index 59e9b8711e3c2b..1a195bf0451c1a 100644 --- a/builtin/unpack-objects.c +++ b/builtin/unpack-objects.c @@ -596,7 +596,7 @@ static void unpack_all(void) progress = start_progress(the_repository, _("Unpacking objects"), nr_objects); CALLOC_ARRAY(obj_list, nr_objects); - transaction = odb_transaction_begin(the_repository->objects); + odb_transaction_begin_or_die(the_repository->objects, &transaction); for (i = 0; i < nr_objects; i++) { unpack_one(i); display_progress(progress, i + 1); diff --git a/builtin/update-index.c b/builtin/update-index.c index 3d6646c318b98e..17f3ea284cbb2c 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -1124,7 +1124,7 @@ int cmd_update_index(int argc, * Allow the object layer to optimize adding multiple objects in * a batch. */ - transaction = odb_transaction_begin(the_repository->objects); + odb_transaction_begin_or_die(the_repository->objects, &transaction); while (ctx.argc) { if (parseopt_state != PARSE_OPT_DONE) parseopt_state = parse_options_step(&ctx, options, diff --git a/cache-tree.c b/cache-tree.c index 184f7e2635b9f4..8eec1d4d5247e7 100644 --- a/cache-tree.c +++ b/cache-tree.c @@ -474,6 +474,7 @@ static int update_one(struct cache_tree *it, int cache_tree_update(struct index_state *istate, int flags) { + int inflight = !!the_repository->objects->transaction; struct odb_transaction *transaction; int skip, i; @@ -490,10 +491,12 @@ int cache_tree_update(struct index_state *istate, int flags) trace_performance_enter(); trace2_region_enter("cache_tree", "update", istate->repo); - transaction = odb_transaction_begin(the_repository->objects); + if (!inflight) + odb_transaction_begin_or_die(the_repository->objects, &transaction); i = update_one(istate->cache_tree, istate->cache, istate->cache_nr, "", 0, &skip, flags); - odb_transaction_commit(transaction); + if (!inflight) + odb_transaction_commit(transaction); trace2_region_leave("cache_tree", "update", istate->repo); trace_performance_leave("cache_tree_update"); if (i < 0) diff --git a/object-file.c b/object-file.c index 3d76b890b8d5e9..f1ec9f6a8bbf92 100644 --- a/object-file.c +++ b/object-file.c @@ -1352,13 +1352,17 @@ int index_fd(struct index_state *istate, struct object_id *oid, if (flags & INDEX_WRITE_OBJECT) { struct object_database *odb = the_repository->objects; - struct odb_transaction *transaction = odb_transaction_begin(odb); + struct odb_transaction *transaction = odb->transaction; + int inflight = !!transaction; - ret = odb_transaction_write_object_stream(odb->transaction, + if (!inflight) + odb_transaction_begin_or_die(odb, &transaction); + ret = odb_transaction_write_object_stream(transaction, &stream, xsize_t(st->st_size), oid); - odb_transaction_commit(transaction); + if (!inflight) + odb_transaction_commit(transaction); } else { ret = hash_blob_stream(&stream, the_repository->hash_algo, oid, diff --git a/odb/transaction.c b/odb/transaction.c index b16e07aebfc5ac..b6da4a3942f888 100644 --- a/odb/transaction.c +++ b/odb/transaction.c @@ -1,15 +1,21 @@ #include "git-compat-util.h" +#include "gettext.h" #include "odb/source.h" #include "odb/transaction.h" -struct odb_transaction *odb_transaction_begin(struct object_database *odb) +int odb_transaction_begin(struct object_database *odb, + struct odb_transaction **out) { + int ret; + if (odb->transaction) - return NULL; + return error(_("object database transaction already pending")); - odb_source_begin_transaction(odb->sources, &odb->transaction); + ret = odb_source_begin_transaction(odb->sources, out); + if (!ret) + odb->transaction = *out; - return odb->transaction; + return ret; } void odb_transaction_commit(struct odb_transaction *transaction) diff --git a/odb/transaction.h b/odb/transaction.h index d52f0533ce3ee8..f5c43187c94dd3 100644 --- a/odb/transaction.h +++ b/odb/transaction.h @@ -1,6 +1,7 @@ #ifndef ODB_TRANSACTION_H #define ODB_TRANSACTION_H +#include "gettext.h" #include "odb.h" #include "odb/source.h" @@ -36,11 +37,21 @@ struct odb_transaction { }; /* - * Starts an ODB transaction. Subsequent objects are written to the transaction - * and not committed until odb_transaction_commit() is invoked on the - * transaction. If the ODB already has a pending transaction, NULL is returned. + * Starts an ODB transaction and returns it via `out`. Subsequent objects are + * written to the transaction and not committed until odb_transaction_commit() + * is invoked on the transaction. Returns 0 on success and a negative value on + * error. Note that it is considered an error to start a new transaction if the + * ODB already has an inflight transaction pending. */ -struct odb_transaction *odb_transaction_begin(struct object_database *odb); +int odb_transaction_begin(struct object_database *odb, + struct odb_transaction **out); + +static inline void odb_transaction_begin_or_die(struct object_database *odb, + struct odb_transaction **out) +{ + if (odb_transaction_begin(odb, out)) + die(_("failed to start ODB transaction")); +} /* * Commits an ODB transaction making the written objects visible. If the diff --git a/read-cache.c b/read-cache.c index 21829102ae275e..bf25989956b5f4 100644 --- a/read-cache.c +++ b/read-cache.c @@ -4012,6 +4012,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix, const struct pathspec *pathspec, char *ps_matched, int include_sparse, int flags, int ignored_too ) { + int inflight = !!repo->objects->transaction; struct odb_transaction *transaction; struct update_callback_data data; struct rev_info rev; @@ -4042,9 +4043,11 @@ int add_files_to_cache(struct repository *repo, const char *prefix, * This function is invoked from commands other than 'add', which * may not have their own transaction active. */ - transaction = odb_transaction_begin(repo->objects); + if (!inflight) + odb_transaction_begin_or_die(repo->objects, &transaction); run_diff_files(&rev, DIFF_RACY_IS_MODIFIED); - odb_transaction_commit(transaction); + if (!inflight) + odb_transaction_commit(transaction); release_revisions(&rev); return !!data.add_errors; From dbb3c87ee40d55a1224275de118f0ec19b60ca32 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Fri, 10 Jul 2026 11:37:18 -0500 Subject: [PATCH 10/32] odb/transaction: propagate commit errors When `odb_transaction_commit()` is invoked, the return value of the backend commit callback is silently discarded. A backend has no way to signal that committing failed, such as when the "files" backend cannot migrate its temporary object directory into the permanent ODB. In a subsequent commit, git-receive-pack(1) starts using ODB transaction to stage objects and consequently cares about such failures so it can handle the error appropriately. Change the commit callback signature to return an int error code and have `odb_transaction_commit()` forward it accordingly. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- odb/transaction.c | 10 +++++++--- odb/transaction.h | 7 ++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/odb/transaction.c b/odb/transaction.c index b6da4a3942f888..249ef4d9b7adde 100644 --- a/odb/transaction.c +++ b/odb/transaction.c @@ -18,19 +18,23 @@ int odb_transaction_begin(struct object_database *odb, return ret; } -void odb_transaction_commit(struct odb_transaction *transaction) +int odb_transaction_commit(struct odb_transaction *transaction) { + int ret; + if (!transaction) - return; + return 0; /* * Ensure the transaction ending matches the pending transaction. */ ASSERT(transaction == transaction->source->odb->transaction); - transaction->commit(transaction); + ret = transaction->commit(transaction); transaction->source->odb->transaction = NULL; free(transaction); + + return ret; } int odb_transaction_write_object_stream(struct odb_transaction *transaction, diff --git a/odb/transaction.h b/odb/transaction.h index f5c43187c94dd3..3b0a5a78e5927d 100644 --- a/odb/transaction.h +++ b/odb/transaction.h @@ -54,10 +54,11 @@ static inline void odb_transaction_begin_or_die(struct object_database *odb, } /* - * Commits an ODB transaction making the written objects visible. If the - * specified transaction is NULL, the function is a no-op. + * Commits an ODB transaction making the written objects visible. Returns 0 on + * success, a negative error code otherwise. Note that, if the specified + * transaction is NULL, the function is a no-op and no error is returned. */ -void odb_transaction_commit(struct odb_transaction *transaction); +int odb_transaction_commit(struct odb_transaction *transaction); /* * Writes the object in the provided stream into the transaction. The resulting From 28fbc0676936e2c024f3973066bec6b2bbfed610 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Fri, 10 Jul 2026 11:37:19 -0500 Subject: [PATCH 11/32] odb/transaction: add transaction env interface The ODB transaction backend is responsible for creating/managing its own staging area for writing objects. Other child processes spawned by Git may need access to uncommitted objects or write new objects in the staging area though. Introduce `odb_transaction_env()` which is expected to provide the set of environment variables needed by a child process to access the transaction's staging area. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- object-file.c | 16 ++++++++++++++++ odb/transaction.c | 8 ++++++++ odb/transaction.h | 17 +++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/object-file.c b/object-file.c index f1ec9f6a8bbf92..7426a0c891a22c 100644 --- a/object-file.c +++ b/object-file.c @@ -27,6 +27,7 @@ #include "path.h" #include "read-cache-ll.h" #include "setup.h" +#include "strvec.h" #include "tempfile.h" #include "tmp-objdir.h" @@ -1685,6 +1686,20 @@ static int odb_transaction_files_commit(struct odb_transaction *base) return 0; } +static int odb_transaction_files_env(struct odb_transaction *base, + struct strvec *env) +{ + struct odb_transaction_files *transaction = + container_of(base, struct odb_transaction_files, base); + int ret; + + ret = odb_transaction_files_prepare(&transaction->base); + if (!ret) + strvec_pushv(env, tmp_objdir_env(transaction->objdir)); + + return ret; +} + int odb_transaction_files_begin(struct odb_source *source, struct odb_transaction **out) { @@ -1694,6 +1709,7 @@ int odb_transaction_files_begin(struct odb_source *source, transaction->base.source = source; transaction->base.commit = odb_transaction_files_commit; transaction->base.write_object_stream = odb_transaction_files_write_object_stream; + transaction->base.env = odb_transaction_files_env; *out = &transaction->base; return 0; diff --git a/odb/transaction.c b/odb/transaction.c index 249ef4d9b7adde..92ec8786a12461 100644 --- a/odb/transaction.c +++ b/odb/transaction.c @@ -43,3 +43,11 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction, { return transaction->write_object_stream(transaction, stream, len, oid); } + +int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env) +{ + if (!transaction) + return 0; + + return transaction->env(transaction, env); +} diff --git a/odb/transaction.h b/odb/transaction.h index 3b0a5a78e5927d..5e51ce5ca44b83 100644 --- a/odb/transaction.h +++ b/odb/transaction.h @@ -34,6 +34,14 @@ struct odb_transaction { int (*write_object_stream)(struct odb_transaction *transaction, struct odb_write_stream *stream, size_t len, struct object_id *oid); + + /* + * This callback is expected to populate the provided strvec with the + * environment variables that a child process should inherit so that its + * object writes participate in the transaction. Returns 0 on success, a + * negative error code otherwise. + */ + int (*env)(struct odb_transaction *transaction, struct strvec *env); }; /* @@ -69,4 +77,13 @@ int odb_transaction_write_object_stream(struct odb_transaction *transaction, struct odb_write_stream *stream, size_t len, struct object_id *oid); +/* + * Populates the provided strvec with the environment variables that a child + * process should inherit so that its object writes participate in the + * transaction, suitable for using via child_process.env. Returns 0 on success, + * a negative error code otherwise. Note that, if the specified transaction is + * NULL, the function is a no-op and no error is returned. + */ +int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env); + #endif From 48d730a16a6f02ca952f653cf70d8c0471137112 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Fri, 10 Jul 2026 11:37:20 -0500 Subject: [PATCH 12/32] odb/transaction: introduce ODB transaction flags The temporary directory used by git-receive-pack(1) to write objects is managed slightly differently than how it is done via ODB transactions: - The temporary directory is eagerly created upfront, instead of waiting for the first object write. - The prefix name of the temporary directory is "incoming" instead of "bulk-fsync". In a subsequent commit, git-receive-pack(1) will use ODB transactions instead of `tmp_objdir` directly. To provide a means to configure the same transaction behavior, introduce `enum odb_transaction_flags` and the ODB_TRANSACTION_RECEIVE flag intended as a signal for ODB transactions using the "files" backend to be set up for git-receive-pack(1). Transaction call sites are updated accordingly to provide the required flag parameter. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- builtin/add.c | 2 +- builtin/unpack-objects.c | 2 +- builtin/update-index.c | 2 +- cache-tree.c | 2 +- object-file.c | 29 ++++++++++++++++++++++++++--- object-file.h | 4 +++- odb/source-files.c | 5 +++-- odb/source-inmemory.c | 3 ++- odb/source-loose.c | 3 ++- odb/source.h | 9 ++++++--- odb/transaction.c | 5 +++-- odb/transaction.h | 15 +++++++++++---- read-cache.c | 2 +- 13 files changed, 61 insertions(+), 22 deletions(-) diff --git a/builtin/add.c b/builtin/add.c index 3d5d9cfdb9e30c..60ffbede2be58a 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -581,7 +581,7 @@ int cmd_add(int argc, string_list_clear(&only_match_skip_worktree, 0); } - odb_transaction_begin_or_die(repo->objects, &transaction); + odb_transaction_begin_or_die(repo->objects, &transaction, 0); ps_matched = xcalloc(pathspec.nr, 1); if (add_renormalize) diff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c index 1a195bf0451c1a..39c44282e5d1ee 100644 --- a/builtin/unpack-objects.c +++ b/builtin/unpack-objects.c @@ -596,7 +596,7 @@ static void unpack_all(void) progress = start_progress(the_repository, _("Unpacking objects"), nr_objects); CALLOC_ARRAY(obj_list, nr_objects); - odb_transaction_begin_or_die(the_repository->objects, &transaction); + odb_transaction_begin_or_die(the_repository->objects, &transaction, 0); for (i = 0; i < nr_objects; i++) { unpack_one(i); display_progress(progress, i + 1); diff --git a/builtin/update-index.c b/builtin/update-index.c index 17f3ea284cbb2c..bf6ea60ef44aaf 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -1124,7 +1124,7 @@ int cmd_update_index(int argc, * Allow the object layer to optimize adding multiple objects in * a batch. */ - odb_transaction_begin_or_die(the_repository->objects, &transaction); + odb_transaction_begin_or_die(the_repository->objects, &transaction, 0); while (ctx.argc) { if (parseopt_state != PARSE_OPT_DONE) parseopt_state = parse_options_step(&ctx, options, diff --git a/cache-tree.c b/cache-tree.c index 8eec1d4d5247e7..99c6a0a7d0efce 100644 --- a/cache-tree.c +++ b/cache-tree.c @@ -492,7 +492,7 @@ int cache_tree_update(struct index_state *istate, int flags) trace_performance_enter(); trace2_region_enter("cache_tree", "update", istate->repo); if (!inflight) - odb_transaction_begin_or_die(the_repository->objects, &transaction); + 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) diff --git a/object-file.c b/object-file.c index 7426a0c891a22c..bf263f546d313e 100644 --- a/object-file.c +++ b/object-file.c @@ -498,6 +498,7 @@ struct odb_transaction_files { struct tmp_objdir *objdir; struct transaction_packfile packfile; + const char *prefix; }; static int odb_transaction_files_prepare(struct odb_transaction *base) @@ -514,7 +515,7 @@ static int odb_transaction_files_prepare(struct odb_transaction *base) if (!transaction || transaction->objdir) return 0; - transaction->objdir = tmp_objdir_create(base->source->odb->repo, "bulk-fsync"); + transaction->objdir = tmp_objdir_create(base->source->odb->repo, transaction->prefix); if (!transaction->objdir) return error(_("unable to create temporary object directory")); @@ -1357,7 +1358,7 @@ int index_fd(struct index_state *istate, struct object_id *oid, int inflight = !!transaction; if (!inflight) - odb_transaction_begin_or_die(odb, &transaction); + odb_transaction_begin_or_die(odb, &transaction, 0); ret = odb_transaction_write_object_stream(transaction, &stream, xsize_t(st->st_size), @@ -1701,7 +1702,8 @@ static int odb_transaction_files_env(struct odb_transaction *base, } int odb_transaction_files_begin(struct odb_source *source, - struct odb_transaction **out) + struct odb_transaction **out, + enum odb_transaction_flags flags) { struct odb_transaction_files *transaction; @@ -1710,6 +1712,27 @@ int odb_transaction_files_begin(struct odb_source *source, transaction->base.commit = odb_transaction_files_commit; transaction->base.write_object_stream = odb_transaction_files_write_object_stream; transaction->base.env = odb_transaction_files_env; + + transaction->prefix = "bulk-fsync"; + if (flags & ODB_TRANSACTION_RECEIVE) { + /* + * ODB transactions for git-receive-pack(1) eagerly create a + * temporary directory and use a different temporary directory + * prefix. + * + * NEEDSWORK: This transaction flag is only used by the "files" + * backend to special case temporary directory set up and + * handling. Ideally transaction users should not have to care + * though. To avoid this, we could eagerly create the temporary + * directory and use the same prefix name for all transactions. + */ + transaction->prefix = "incoming"; + if (odb_transaction_files_prepare(&transaction->base)) { + free(transaction); + return -1; + } + } + *out = &transaction->base; return 0; diff --git a/object-file.h b/object-file.h index 1a023226ac7cd3..bdd2d67a2e6047 100644 --- a/object-file.h +++ b/object-file.h @@ -5,6 +5,7 @@ #include "object.h" #include "odb.h" #include "odb/source-loose.h" +#include "odb/transaction.h" /* The maximum size for an object header. */ #define MAX_HEADER_LEN 32 @@ -197,6 +198,7 @@ struct odb_transaction; * to make new objects visible. */ int odb_transaction_files_begin(struct odb_source *source, - struct odb_transaction **out); + struct odb_transaction **out, + enum odb_transaction_flags flags); #endif /* OBJECT_FILE_H */ diff --git a/odb/source-files.c b/odb/source-files.c index 2545bd81d448b0..534f48aad9554e 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -180,9 +180,10 @@ static int odb_source_files_write_object_stream(struct odb_source *source, } static int odb_source_files_begin_transaction(struct odb_source *source, - struct odb_transaction **out) + struct odb_transaction **out, + enum odb_transaction_flags flags) { - return odb_transaction_files_begin(source, out); + return odb_transaction_files_begin(source, out, flags); } static int odb_source_files_read_alternates(struct odb_source *source, diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c index e004566d768b01..9644d9d474b59a 100644 --- a/odb/source-inmemory.c +++ b/odb/source-inmemory.c @@ -304,7 +304,8 @@ static int odb_source_inmemory_freshen_object(struct odb_source *source, } static int odb_source_inmemory_begin_transaction(struct odb_source *source UNUSED, - struct odb_transaction **out UNUSED) + struct odb_transaction **out UNUSED, + enum odb_transaction_flags flags UNUSED) { return error("in-memory source does not support transactions"); } diff --git a/odb/source-loose.c b/odb/source-loose.c index 7d7ea2fb842537..f4eac801e04b2f 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -646,7 +646,8 @@ static int odb_source_loose_write_object_stream(struct odb_source *source, } static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED, - struct odb_transaction **out UNUSED) + struct odb_transaction **out UNUSED, + enum odb_transaction_flags flags UNUSED) { /* TODO: this is a known omission that we'll want to address eventually. */ return error("loose source does not support transactions"); diff --git a/odb/source.h b/odb/source.h index 8bcb67787ebafd..c013762d33fc8d 100644 --- a/odb/source.h +++ b/odb/source.h @@ -3,6 +3,7 @@ #include "object.h" #include "odb.h" +#include "odb/transaction.h" enum odb_source_type { /* @@ -228,7 +229,8 @@ struct odb_source { * negative error code otherwise. */ int (*begin_transaction)(struct odb_source *source, - struct odb_transaction **out); + struct odb_transaction **out, + enum odb_transaction_flags flags); /* * This callback is expected to read the list of alternate object @@ -467,9 +469,10 @@ static inline int odb_source_write_alternate(struct odb_source *source, * Returns 0 on success, a negative error code otherwise. */ static inline int odb_source_begin_transaction(struct odb_source *source, - struct odb_transaction **out) + struct odb_transaction **out, + enum odb_transaction_flags flags) { - return source->begin_transaction(source, out); + return source->begin_transaction(source, out, flags); } #endif diff --git a/odb/transaction.c b/odb/transaction.c index 92ec8786a12461..dab7da6a9a4f55 100644 --- a/odb/transaction.c +++ b/odb/transaction.c @@ -4,14 +4,15 @@ #include "odb/transaction.h" int odb_transaction_begin(struct object_database *odb, - struct odb_transaction **out) + struct odb_transaction **out, + enum odb_transaction_flags flags) { int ret; if (odb->transaction) return error(_("object database transaction already pending")); - ret = odb_source_begin_transaction(odb->sources, out); + ret = odb_source_begin_transaction(odb->sources, out, flags); if (!ret) odb->transaction = *out; diff --git a/odb/transaction.h b/odb/transaction.h index 5e51ce5ca44b83..4cb2eafcbf08f5 100644 --- a/odb/transaction.h +++ b/odb/transaction.h @@ -3,7 +3,6 @@ #include "gettext.h" #include "odb.h" -#include "odb/source.h" /* * A transaction may be started for an object database prior to writing new @@ -44,6 +43,12 @@ struct odb_transaction { int (*env)(struct odb_transaction *transaction, struct strvec *env); }; +/* Flags used to configure an ODB transaction. */ +enum odb_transaction_flags { + /* Configures the transaction for use with git-receive-pack(1). */ + ODB_TRANSACTION_RECEIVE = (1 << 0), +}; + /* * Starts an ODB transaction and returns it via `out`. Subsequent objects are * written to the transaction and not committed until odb_transaction_commit() @@ -52,12 +57,14 @@ struct odb_transaction { * ODB already has an inflight transaction pending. */ int odb_transaction_begin(struct object_database *odb, - struct odb_transaction **out); + struct odb_transaction **out, + enum odb_transaction_flags flags); static inline void odb_transaction_begin_or_die(struct object_database *odb, - struct odb_transaction **out) + struct odb_transaction **out, + enum odb_transaction_flags flags) { - if (odb_transaction_begin(odb, out)) + if (odb_transaction_begin(odb, out, flags)) die(_("failed to start ODB transaction")); } diff --git a/read-cache.c b/read-cache.c index bf25989956b5f4..94d7416f584992 100644 --- a/read-cache.c +++ b/read-cache.c @@ -4044,7 +4044,7 @@ int add_files_to_cache(struct repository *repo, const char *prefix, * may not have their own transaction active. */ if (!inflight) - odb_transaction_begin_or_die(repo->objects, &transaction); + odb_transaction_begin_or_die(repo->objects, &transaction, 0); run_diff_files(&rev, DIFF_RACY_IS_MODIFIED); if (!inflight) odb_transaction_commit(transaction); From 6c242569c190ec5c53b015d5284180b37f5b1542 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Fri, 10 Jul 2026 11:37:21 -0500 Subject: [PATCH 13/32] builtin/receive-pack: drop redundant tmpdir env When performing the connectivity checks for a shallow ref in `update_shallow_ref()`, the child process environment variables are populated via `tmp_objdir_env()`. This is unnecessary though as `update_shallow_ref()` is only reached after `tmp_objdir_migrate()` has been performed which means there is no longer a temporary directory that needs to be shared with child processes. Drop the call to `tmp_objdir_env()` accordingly. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 1 - 1 file changed, 1 deletion(-) diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 19eb6a1b61c3a7..50bc05c70c24e8 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -1363,7 +1363,6 @@ static int update_shallow_ref(struct command *cmd, struct shallow_info *si) !delayed_reachability_test(si, i)) oid_array_append(&extra, &si->shallow->oid[i]); - opt.env = tmp_objdir_env(tmp_objdir); setup_alternate_shallow(&shallow_lock, &opt.shallow_file, &extra); if (check_connected(command_singleton_iterator, cmd, &opt)) { rollback_shallow_file(the_repository, &shallow_lock); From bdee7b30135f51f406d71c565cf29bb7db64f1cd Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Fri, 10 Jul 2026 11:37:22 -0500 Subject: [PATCH 14/32] builtin/receive-pack: stage incoming objects via ODB transactions Objects received by git-receive-pack(1) are quarantined in a temporary "incoming" directory and migrated into the object database prior to the reference updates. The quarantine is currently managed through `tmp_objdir` directly. In a pluggable ODB future, how exactly an object gets written to a transaction may vary for a given ODB source. Refactor git-receive-pack(1) to use the ODB transaction interfaces to manage the object staging area in a more agnostic manner accordingly. Note that the ODB transaction is now responsible for managing the primary and alternate ODBs for the repository. One small change as a result is that the temporary directory is now applied as the primary ODB in the main process instead of an alternate. This does not change anything for git-receive-pack(1) though because it only needs access to the newly written objects and doesn't care how exactly it is set up. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- builtin/receive-pack.c | 68 ++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 50bc05c70c24e8..8b8c20dc1aed76 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -37,7 +37,6 @@ #include "sigchain.h" #include "string-list.h" #include "strvec.h" -#include "tmp-objdir.h" #include "trace.h" #include "trace2.h" #include "version.h" @@ -112,8 +111,6 @@ static enum { } use_keepalive; static int keepalive_in_sec = 5; -static struct tmp_objdir *tmp_objdir; - static struct proc_receive_ref { unsigned int want_add:1, want_delete:1, @@ -926,6 +923,7 @@ static void receive_hook_feed_state_free(void *data) static int run_receive_hook(struct command *commands, const char *hook_name, int skip_broken, + struct odb_transaction *transaction, const struct string_list *push_options) { struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; @@ -959,8 +957,8 @@ static int run_receive_hook(struct command *commands, strvec_push(&opt.env, "GIT_PUSH_OPTION_COUNT"); } - if (tmp_objdir) - strvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir)); + if (transaction) + odb_transaction_env(transaction, &opt.env); prepare_push_cert_sha1(&opt); @@ -1789,24 +1787,30 @@ static const struct object_id *command_singleton_iterator(void *cb_data) } static void set_connectivity_errors(struct command *commands, - struct shallow_info *si) + struct shallow_info *si, + struct odb_transaction *transaction) { struct command *cmd; for (cmd = commands; cmd; cmd = cmd->next) { struct command *singleton = cmd; struct check_connected_options opt = CHECK_CONNECTED_INIT; + struct strvec env = STRVEC_INIT; if (shallow_update && si->shallow_ref[cmd->index]) /* to be checked in update_shallow_ref() */ continue; - opt.env = tmp_objdir_env(tmp_objdir); + odb_transaction_env(transaction, &env); + opt.env = env.v; + if (!check_connected(command_singleton_iterator, &singleton, &opt)) continue; cmd->error_string = "missing necessary objects"; + + strvec_clear(&env); } } @@ -2027,6 +2031,7 @@ static void execute_commands_atomic(struct command *commands, static void execute_commands(struct command *commands, const char *unpacker_error, struct shallow_info *si, + struct odb_transaction *transaction, const struct string_list *push_options) { struct check_connected_options opt = CHECK_CONNECTED_INIT; @@ -2043,6 +2048,8 @@ static void execute_commands(struct command *commands, } if (!skip_connectivity_check) { + struct strvec env = STRVEC_INIT; + if (use_sideband) { memset(&muxer, 0, sizeof(muxer)); muxer.proc = copy_to_sideband; @@ -2056,14 +2063,17 @@ static void execute_commands(struct command *commands, data.si = si; opt.err_fd = err_fd; opt.progress = err_fd && !quiet; - opt.env = tmp_objdir_env(tmp_objdir); + odb_transaction_env(transaction, &env); + opt.env = env.v; opt.exclude_hidden_refs_section = "receive"; if (check_connected(iterate_receive_command_list, &data, &opt)) - set_connectivity_errors(commands, si); + set_connectivity_errors(commands, si, transaction); if (use_sideband) finish_async(&muxer); + + strvec_clear(&env); } reject_updates_to_hidden(commands); @@ -2084,7 +2094,7 @@ static void execute_commands(struct command *commands, } } - if (run_receive_hook(commands, "pre-receive", 0, push_options)) { + if (run_receive_hook(commands, "pre-receive", 0, transaction, push_options)) { for (cmd = commands; cmd; cmd = cmd->next) { if (!cmd->error_string) cmd->error_string = "pre-receive hook declined"; @@ -2105,14 +2115,13 @@ static void execute_commands(struct command *commands, * Now we'll start writing out refs, which means the objects need * to be in their final positions so that other processes can see them. */ - if (tmp_objdir_migrate(tmp_objdir) < 0) { + if (odb_transaction_commit(transaction)) { for (cmd = commands; cmd; cmd = cmd->next) { if (!cmd->error_string) cmd->error_string = "unable to migrate objects to permanent storage"; } return; } - tmp_objdir = NULL; check_aliased_updates(commands); @@ -2325,7 +2334,8 @@ static void push_header_arg(struct strvec *args, struct pack_header *hdr) ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries)); } -static const char *unpack(int err_fd, struct shallow_info *si) +static const char *unpack(int err_fd, struct shallow_info *si, + struct odb_transaction *transaction) { struct pack_header hdr; const char *hdr_err; @@ -2350,20 +2360,7 @@ static const char *unpack(int err_fd, struct shallow_info *si) strvec_push(&child.args, alt_shallow_file); } - tmp_objdir = tmp_objdir_create(the_repository, "incoming"); - if (!tmp_objdir) { - if (err_fd > 0) - close(err_fd); - return "unable to create temporary object directory"; - } - strvec_pushv(&child.env, tmp_objdir_env(tmp_objdir)); - - /* - * Normally we just pass the tmp_objdir environment to the child - * processes that do the heavy lifting, but we may need to see these - * objects ourselves to set up shallow information. - */ - tmp_objdir_add_as_alternate(tmp_objdir); + odb_transaction_env(transaction, &child.env); if (ntohl(hdr.hdr_entries) < unpack_limit) { strvec_push(&child.args, "unpack-objects"); @@ -2430,13 +2427,14 @@ static const char *unpack(int err_fd, struct shallow_info *si) return NULL; } -static const char *unpack_with_sideband(struct shallow_info *si) +static const char *unpack_with_sideband(struct shallow_info *si, + struct odb_transaction *transaction) { struct async muxer; const char *ret; if (!use_sideband) - return unpack(0, si); + return unpack(0, si, transaction); use_keepalive = KEEPALIVE_AFTER_NUL; memset(&muxer, 0, sizeof(muxer)); @@ -2445,7 +2443,7 @@ static const char *unpack_with_sideband(struct shallow_info *si) if (start_async(&muxer)) return NULL; - ret = unpack(muxer.in, si); + ret = unpack(muxer.in, si, transaction); finish_async(&muxer); return ret; @@ -2622,6 +2620,7 @@ int cmd_receive_pack(int argc, struct oid_array ref = OID_ARRAY_INIT; struct shallow_info si; struct packet_reader reader; + struct odb_transaction *transaction = NULL; struct option options[] = { OPT__QUIET(&quiet, N_("quiet")), @@ -2706,11 +2705,14 @@ int cmd_receive_pack(int argc, if (!si.nr_ours && !si.nr_theirs) shallow_update = 0; if (!delete_only(commands)) { - unpack_status = unpack_with_sideband(&si); + if (odb_transaction_begin(the_repository->objects, &transaction, ODB_TRANSACTION_RECEIVE)) + unpack_status = "unable to start object transaction"; + else + unpack_status = unpack_with_sideband(&si, transaction); update_shallow_info(commands, &si, &ref); } use_keepalive = KEEPALIVE_ALWAYS; - execute_commands(commands, unpack_status, &si, + execute_commands(commands, unpack_status, &si, transaction, &push_options); delete_tempfile(&pack_lockfile); sigchain_push(SIGPIPE, SIG_IGN); @@ -2719,7 +2721,7 @@ int cmd_receive_pack(int argc, else if (report_status) report(commands, unpack_status); sigchain_pop(SIGPIPE); - run_receive_hook(commands, "post-receive", 1, + run_receive_hook(commands, "post-receive", 1, NULL, &push_options); run_update_post_hook(commands); free_commands(commands); From dbe8efd6632d250b1d2638013b70f08bbc587ad6 Mon Sep 17 00:00:00 2001 From: Marcelo Machado Lage Date: Sat, 11 Jul 2026 13:04:46 -0300 Subject: [PATCH 15/32] t9811: break long && chains into multiple lines Rewrite single-line && chains by breaking them into multiple lines. Co-authored-by: Vinicius Lira de Freitas Signed-off-by: Vinicius Lira de Freitas Signed-off-by: Marcelo Machado Lage Signed-off-by: Junio C Hamano --- t/t9811-git-p4-label-import.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/t/t9811-git-p4-label-import.sh b/t/t9811-git-p4-label-import.sh index 7614dfbd953efe..9c161036f2dc24 100755 --- a/t/t9811-git-p4-label-import.sh +++ b/t/t9811-git-p4-label-import.sh @@ -64,7 +64,9 @@ test_expect_success 'basic p4 labels' ' git checkout TAG_F1_ONLY && ! test -f f2 && git checkout TAG_WITH\$_SHELL_CHAR && - test -f f1 && test -f f2 && test -f file_with_\$metachar && + test -f f1 && + test -f f2 && + test -f file_with_\$metachar && git show TAG_LONG_LABEL | grep -q "A Label second line" ) @@ -231,17 +233,25 @@ test_expect_success 'importing labels with missing revisions' ' P4CLIENT=missing-revision && client_view "//depot/missing-revision/... //missing-revision/..." && cd "$cli" && - >f1 && p4 add f1 && p4 submit -d "start" && + >f1 && + p4 add f1 && + p4 submit -d "start" && p4 tag -l TAG_S0 ... && - >f2 && p4 add f2 && p4 submit -d "second" && + >f2 && + p4 add f2 && + p4 submit -d "second" && startrev=$(p4_head_revision //depot/missing-revision/...) && - >f3 && p4 add f3 && p4 submit -d "third" && + >f3 && + p4 add f3 && + p4 submit -d "third" && - p4 edit f2 && date >f2 && p4 submit -d "change" f2 && + p4 edit f2 && + date >f2 && + p4 submit -d "change" f2 && endrev=$(p4_head_revision //depot/missing-revision/...) && From 156991928c3def5df519d42efc46735b0a090079 Mon Sep 17 00:00:00 2001 From: Marcelo Machado Lage Date: Sat, 11 Jul 2026 13:04:47 -0300 Subject: [PATCH 16/32] t9811: replace 'test -f' and '! test -f' with 'test_path_*' Replace the basic shell commands 'test -f', with more modern test helpers 'test_path_is_file' and 'test_path_is_missing'. These modern helpers emit useful information when the corresponding tests fail, unlike 'test -f' and '! test -f'. The occurrences of '! test -f filename' were replaced by 'file_path_is_missing filename', a stronger guarantee equivalent to '! test -e filename'. Co-authored-by: Vinicius Lira de Freitas Signed-off-by: Vinicius Lira de Freitas Signed-off-by: Marcelo Machado Lage Signed-off-by: Junio C Hamano --- t/t9811-git-p4-label-import.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/t/t9811-git-p4-label-import.sh b/t/t9811-git-p4-label-import.sh index 9c161036f2dc24..ee257011b3c97f 100755 --- a/t/t9811-git-p4-label-import.sh +++ b/t/t9811-git-p4-label-import.sh @@ -62,11 +62,11 @@ test_expect_success 'basic p4 labels' ' cd main && git checkout TAG_F1_ONLY && - ! test -f f2 && + test_path_is_missing f2 && git checkout TAG_WITH\$_SHELL_CHAR && - test -f f1 && - test -f f2 && - test -f file_with_\$metachar && + test_path_is_file f1 && + test_path_is_file f2 && + test_path_is_file file_with_\$metachar && git show TAG_LONG_LABEL | grep -q "A Label second line" ) @@ -104,11 +104,11 @@ test_expect_success 'two labels on the same changelist' ' git checkout TAG_F1_1 && ls && - test -f f1 && + test_path_is_file f1 && git checkout TAG_F1_2 && ls && - test -f f1 + test_path_is_file f1 ) ' @@ -137,9 +137,9 @@ test_expect_success 'export git tags to p4' ' p4 labels ... | grep LIGHTWEIGHT_TAG && p4 label -o GIT_TAG_1 | grep "tag created in git:xyzzy" && p4 sync ...@GIT_TAG_1 && - ! test -f main/f10 && + test_path_is_missing main/f10 && p4 sync ...@GIT_TAG_2 && - test -f main/f10 + test_path_is_file main/f10 ) ' @@ -170,9 +170,9 @@ test_expect_success 'export git tags to p4 with deletion' ' cd "$cli" && p4 sync ... && p4 sync ...@GIT_TAG_ON_DELETED && - test -f main/deleted_file && + test_path_is_file main/deleted_file && p4 sync ...@GIT_TAG_AFTER_DELETION && - ! test -f main/deleted_file && + test_path_is_missing main/deleted_file && echo "checking label contents" && p4 label -o GIT_TAG_ON_DELETED | grep "tag on deleted file" ) From b9a7080755d54e9fc2397201cde7a6f5028e09ca Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Mon, 13 Jul 2026 14:41:53 +0200 Subject: [PATCH 17/32] fast-export: standardize usage string and SYNOPSIS The output of `git fast-export -h` currently starts with: usage: git fast-export [] while the SYNOPSIS section in this command's documentation shows: 'git fast-export' [] | 'git fast-import' Let's make both of these consistent with each other and with other Git commands by describing the arguments with: [] [] [[--] ...] This takes into account the following: - `git fast-export` accepts both rev-list arguments and a number of genuine options of its own (--[no-]progress, --[no-]signed-tags, --[no-]signed-commits, etc). - `git fast-export` was the only command using `[]` while many other commands describe their revision arguments as `[] [[--] ...]`. - In the DESCRIPTION section of the documentation, it's already mentioned several times that the output should eventually be fed to `git fast-import`. This also enables us to remove fast-export from "t/t0450/adoc-help-mismatches". Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- Documentation/git-fast-export.adoc | 2 +- builtin/fast-export.c | 2 +- t/t0450/adoc-help-mismatches | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Documentation/git-fast-export.adoc b/Documentation/git-fast-export.adoc index 297b57bb2efdc2..719aeca244d534 100644 --- a/Documentation/git-fast-export.adoc +++ b/Documentation/git-fast-export.adoc @@ -9,7 +9,7 @@ git-fast-export - Git data exporter SYNOPSIS -------- [verse] -'git fast-export' [] | 'git fast-import' +'git fast-export' [] [] [[--] ...] DESCRIPTION ----------- diff --git a/builtin/fast-export.c b/builtin/fast-export.c index 0be43104dc0d23..629d7c591a9d70 100644 --- a/builtin/fast-export.c +++ b/builtin/fast-export.c @@ -33,7 +33,7 @@ #include "gpg-interface.h" static const char *const fast_export_usage[] = { - N_("git fast-export []"), + N_("git fast-export [] [] [[--] ...]"), NULL }; diff --git a/t/t0450/adoc-help-mismatches b/t/t0450/adoc-help-mismatches index e8d6c13ccd0333..c4a55ff4e35a4f 100644 --- a/t/t0450/adoc-help-mismatches +++ b/t/t0450/adoc-help-mismatches @@ -12,7 +12,6 @@ column credential credential-cache credential-store -fast-export fast-import fetch-pack fmt-merge-msg From 678f3bb60dabece745b95f3a268813a7b17d6955 Mon Sep 17 00:00:00 2001 From: Shlok Kulshreshtha Date: Tue, 14 Jul 2026 17:50:32 +0530 Subject: [PATCH 18/32] t1100: modernize test style The tests in this script use the old style in which the test title and body are passed as separate backslash-continued arguments, with bodies indented using spaces: test_expect_success \ 'title' \ 'body' Convert them to the modern style in which the body is a single-quoted block on its own lines, indented with a tab: test_expect_success 'title' ' body ' While at it, remove an extraneous blank line between two tests. This is a style-only change; no test logic is modified. Signed-off-by: Shlok Kulshreshtha Signed-off-by: Junio C Hamano --- t/t1100-commit-tree-options.sh | 43 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/t/t1100-commit-tree-options.sh b/t/t1100-commit-tree-options.sh index ae66ba5babf347..9a639f946c3ec7 100755 --- a/t/t1100-commit-tree-options.sh +++ b/t/t1100-commit-tree-options.sh @@ -22,29 +22,28 @@ committer Committer Name 1117150200 +0000 comment text EOF -test_expect_success \ - 'test preparation: write empty tree' \ - 'git write-tree >treeid' - -test_expect_success \ - 'construct commit' \ - 'echo comment text | - GIT_AUTHOR_NAME="Author Name" \ - GIT_AUTHOR_EMAIL="author@email" \ - GIT_AUTHOR_DATE="2005-05-26 23:00" \ - GIT_COMMITTER_NAME="Committer Name" \ - GIT_COMMITTER_EMAIL="committer@email" \ - GIT_COMMITTER_DATE="2005-05-26 23:30" \ - TZ=GMT git commit-tree $(cat treeid) >commitid 2>/dev/null' - -test_expect_success \ - 'read commit' \ - 'git cat-file commit $(cat commitid) >commit' - -test_expect_success \ - 'compare commit' \ - 'test_cmp expected commit' +test_expect_success 'test preparation: write empty tree' ' + git write-tree >treeid +' + +test_expect_success 'construct commit' ' + echo comment text | + GIT_AUTHOR_NAME="Author Name" \ + GIT_AUTHOR_EMAIL="author@email" \ + GIT_AUTHOR_DATE="2005-05-26 23:00" \ + GIT_COMMITTER_NAME="Committer Name" \ + GIT_COMMITTER_EMAIL="committer@email" \ + GIT_COMMITTER_DATE="2005-05-26 23:30" \ + TZ=GMT git commit-tree $(cat treeid) >commitid 2>/dev/null +' +test_expect_success 'read commit' ' + git cat-file commit $(cat commitid) >commit +' + +test_expect_success 'compare commit' ' + test_cmp expected commit +' test_expect_success 'flags and then non flags' ' test_tick && From 9719c290ee5926487caacc1ff059c043323ed183 Mon Sep 17 00:00:00 2001 From: Shlok Kulshreshtha Date: Tue, 14 Jul 2026 17:50:33 +0530 Subject: [PATCH 19/32] t1100: move creation of expected output into setup test The "expected" file is created at the top-level of the script, outside of any test. Code that runs outside of a test is not protected by the test harness: a failure there is not reported as a test failure and is easy to miss. Move the here-doc that creates "expected" into the existing setup test ("test preparation: write empty tree"), using a "<<-" here-doc so its body can be indented along with the rest of the test. Signed-off-by: Shlok Kulshreshtha Signed-off-by: Junio C Hamano --- t/t1100-commit-tree-options.sh | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/t/t1100-commit-tree-options.sh b/t/t1100-commit-tree-options.sh index 9a639f946c3ec7..3e888909f58d30 100755 --- a/t/t1100-commit-tree-options.sh +++ b/t/t1100-commit-tree-options.sh @@ -14,15 +14,14 @@ Also make sure that command line parser understands the normal . ./test-lib.sh -cat >expected < 1117148400 +0000 -committer Committer Name 1117150200 +0000 - -comment text -EOF - test_expect_success 'test preparation: write empty tree' ' + cat >expected <<-EOF && + tree $EMPTY_TREE + author Author Name 1117148400 +0000 + committer Committer Name 1117150200 +0000 + + comment text + EOF git write-tree >treeid ' From 7b0203b15c1a15c77436160c58b45bef9b087c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Tue, 14 Jul 2026 10:45:59 +0200 Subject: [PATCH 20/32] strbuf: avoid redundant reset in strbuf_getwholeline() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HAVE_GETDELIM variant of strbuf_getwholeline() calls strbuf_reset() on the strbuf before handing it over to getdelim(3). This is unnecessary: - getdelim(3) doesn't care whether the old buffer contents is NUL-terminated and has no access to ->len, - on success getdelim(3) NUL-terminates the buffer and we set ->len, - on error we either call strbuf_init() or strbuf_reset(). Remove the superfluous preparatory call. Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- strbuf.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/strbuf.c b/strbuf.c index 764b629927f0ce..44955669e8c504 100644 --- a/strbuf.c +++ b/strbuf.c @@ -646,8 +646,6 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term) if (feof(fp)) return EOF; - strbuf_reset(sb); - /* Translate slopbuf to NULL, as we cannot call realloc on it */ if (!sb->alloc) sb->buf = NULL; From 27bed41ebb11ffdfcb79cf74fd4122ac41420f26 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 15 Jul 2026 08:22:31 +0200 Subject: [PATCH 21/32] odb/source-packed: improve lookup when enumerating objects When iterating through objects of a packed source that have a specific prefix we do so via two different methods: - When a multi-pack index is available we use that one to efficiently loop through all objects. - We then loop through all packfiles that aren't covered by a multi-pack index. Regardless of which mechanism we use, we then iterate through all the objects indexed by the respective data structure. Curiously though, while we use the indices for enumerating the objects, we completely ignore it for the actual object lookup. Instead, we call into the generic `odb_source_read_object_info()` function, which will itself consult the indices to figure out where the object in question even lives. This has two consequences: - It's inefficient, as we basically have to figure out the position of the object a second time. - It's subtly wrong, as it may now happen that a specific object will be looked up via a different pack in case it exists multiple times. This is unlikely to have any real-world consequences, but it's still the wrong thing to do. Fix the issue by using `packed_object_info()` directly. While at it, rename the `store` variable to `source`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-packed.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/odb/source-packed.c b/odb/source-packed.c index 0edea5356d8210..9cfa02b7a2e53e 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -143,7 +143,7 @@ static bool should_exclude_pack(struct packed_git *p, enum odb_for_each_object_f } static int for_each_prefixed_object_in_midx( - struct odb_source_packed *store, + struct odb_source_packed *source, struct multi_pack_index *m, const struct odb_for_each_object_options *opts, struct odb_source_packed_for_each_object_wrapper_data *data) @@ -170,6 +170,7 @@ static int for_each_prefixed_object_in_midx( */ for (i = first; i < num; i++) { const struct object_id *current = NULL; + struct packed_git *pack; struct object_id oid; current = nth_midxed_object_oid(&oid, m, i); @@ -177,9 +178,8 @@ static int for_each_prefixed_object_in_midx( if (!match_hash(len, opts->prefix->hash, current->hash)) break; - if (opts->flags) { + if (opts->flags || data->request) { uint32_t pack_id = nth_midxed_pack_int_id(m, i); - struct packed_git *pack; if (prepare_midx_pack(m, pack_id)) { pack_errors = true; @@ -193,9 +193,9 @@ static int for_each_prefixed_object_in_midx( if (data->request) { struct object_info oi = *data->request; + off_t offset = nth_midxed_offset(m, i); - ret = odb_source_read_object_info(&store->base, current, - &oi, 0); + ret = packed_object_info(source, pack, offset, &oi); if (ret) goto out; @@ -219,7 +219,7 @@ static int for_each_prefixed_object_in_midx( } static int for_each_prefixed_object_in_pack( - struct odb_source_packed *store, + struct odb_source_packed *source, struct packed_git *p, const struct odb_for_each_object_options *opts, struct odb_source_packed_for_each_object_wrapper_data *data) @@ -246,8 +246,9 @@ static int for_each_prefixed_object_in_pack( if (data->request) { struct object_info oi = *data->request; + off_t offset = nth_packed_object_offset(p, i); - ret = odb_source_read_object_info(&store->base, &oid, &oi, 0); + ret = packed_object_info(source, p, offset, &oi); if (ret) goto out; From 03aaa4f8985ce4813033c1afa36ebec7d7e2a9a1 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 15 Jul 2026 08:22:32 +0200 Subject: [PATCH 22/32] pack-bitmap: mark object filter as `const` The function `for_each_bitmapped_object()` accepts an optional object filter. This filter is never modified by the function, but is not declared as `const`. Fix this. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- pack-bitmap.c | 6 +++--- pack-bitmap.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pack-bitmap.c b/pack-bitmap.c index 35774b6f0c0da7..a47c2316324054 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -1976,7 +1976,7 @@ static void filter_bitmap_object_type(struct bitmap_index *bitmap_git, static int filter_bitmap(struct bitmap_index *bitmap_git, struct object_list *tip_objects, struct bitmap *to_filter, - struct list_objects_filter_options *filter) + const struct list_objects_filter_options *filter) { if (!filter || filter->choice == LOFC_DISABLED) return 0; @@ -2027,7 +2027,7 @@ static int filter_bitmap(struct bitmap_index *bitmap_git, return -1; } -static int can_filter_bitmap(struct list_objects_filter_options *filter) +static int can_filter_bitmap(const struct list_objects_filter_options *filter) { return !filter_bitmap(NULL, NULL, NULL, filter); } @@ -2058,7 +2058,7 @@ static void filter_packed_objects_from_bitmap(struct bitmap_index *bitmap_git, } int for_each_bitmapped_object(struct bitmap_index *bitmap_git, - struct list_objects_filter_options *filter, + const struct list_objects_filter_options *filter, show_reachable_fn show_reach, void *payload) { diff --git a/pack-bitmap.h b/pack-bitmap.h index 19a86554579f7c..47935eb24e29dd 100644 --- a/pack-bitmap.h +++ b/pack-bitmap.h @@ -96,7 +96,7 @@ struct list_objects_filter_options; * not supported, `0` otherwise. */ int for_each_bitmapped_object(struct bitmap_index *bitmap_git, - struct list_objects_filter_options *filter, + const struct list_objects_filter_options *filter, show_reachable_fn show_reach, void *payload); From 6f48b8ce56171419f768902b300365c1b6708c96 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Wed, 15 Jul 2026 08:22:33 +0200 Subject: [PATCH 23/32] pack-objects: drop unused return value from add_object_entry() This function returns 0/1 to its caller to tell them whether we actually added a new entry (or if we considered it redundant). But nobody has relied on that behavior since 5379a5c5ee (Thin pack generation: optimization., 2006-04-05). The extra return does not hurt much, but it is a bit confusing. We have a sister function, add_object_entry_from_bitmap(), which has the same return value semantics. That function is about to change to always return 0 (not void, because it must conform to a callback function interface). So with that change, we'd have two related functions which both return an "int" but with different semantics. Let's drop the unused "int" return from add_object_entry() entirely, which makes it more clear that the two functions have diverged. Signed-off-by: Jeff King [ps: slightly massaged the commit message] Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index ea5eab4cf841bf..188c4f6d4bd756 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -1867,8 +1867,8 @@ static const char no_closure_warning[] = N_( "disabling bitmap writing, as some objects are not being packed" ); -static int add_object_entry(const struct object_id *oid, enum object_type type, - const char *name, int exclude) +static void add_object_entry(const struct object_id *oid, enum object_type type, + const char *name, int exclude) { struct packed_git *found_pack = NULL; off_t found_offset = 0; @@ -1876,7 +1876,7 @@ static int add_object_entry(const struct object_id *oid, enum object_type type, display_progress(progress_state, ++nr_seen); if (have_duplicate_entry(oid, exclude)) - return 0; + return; if (!want_object_in_pack(oid, exclude, &found_pack, &found_offset)) { /* The pack is missing an object, so it will not have closure */ @@ -1885,13 +1885,12 @@ static int add_object_entry(const struct object_id *oid, enum object_type type, warning(_(no_closure_warning)); write_bitmap_index = 0; } - return 0; + return; } create_object_entry(oid, type, pack_name_hash_fn(name), exclude, name && no_try_delta(name), found_pack, found_offset); - return 1; } static int add_object_entry_from_bitmap(const struct object_id *oid, From 1ca65ca7b8bce87268900e39315888fd10fc350c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 15 Jul 2026 08:22:34 +0200 Subject: [PATCH 24/32] pack-bitmap: allow aborting iteration of bitmapped objects In a subsequent commit we'll lift iteration of bitmapped objects into the "packed" backend and make it accessible via `odb_for_each_object()`. The calling convention for that function is that the callback may return a non-zero exit code, and if so we'll abort iteration. This is currently impossible to realize though, as `for_each_bitmapped_object()` will ignore any return value and just churn through all objects completely. This doesn't matter to the callers of `for_each_bitmapped_object()`, as there's only one of them in git-cat-file(1), and the callbacks we pass always return zero. But once we move the logic into the generic infrastructure it becomes a latent bug waiting to happen. Refactor the code so that the return value of the `show_reach` callback is not ignored anymore. Instead, returning a non-zero value will cause us to abort iteration in both `show_objects_for_type()` and in `for_each_bitmapped_object()`. Note though that there's a second user of `show_objects_for_type()` with `traverse_bitmap_commit_list()`, and that function does indeed invoke callbacks that may return non-zero. This non-zero return value never had any effect at all though, and the callbacks that return non-zero values are only ever invoked via `traverse_bitmap_commit_list()`. Consequently, we adapt them to always return 0. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 2 +- builtin/rev-list.c | 2 +- pack-bitmap.c | 31 +++++++++++++++++++++---------- pack-bitmap.h | 3 ++- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 188c4f6d4bd756..3673b14b89b275 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -1908,7 +1908,7 @@ static int add_object_entry_from_bitmap(const struct object_id *oid, return 0; create_object_entry(oid, type, name_hash, 0, 0, pack, offset); - return 1; + return 0; } struct pbase_tree_cache { diff --git a/builtin/rev-list.c b/builtin/rev-list.c index 8f63003709242e..02818b81c63fd7 100644 --- a/builtin/rev-list.c +++ b/builtin/rev-list.c @@ -486,7 +486,7 @@ static int show_object_fast( void *payload UNUSED) { fprintf(stdout, "%s\n", oid_to_hex(oid)); - return 1; + return 0; } static void print_disk_usage(off_t size) diff --git a/pack-bitmap.c b/pack-bitmap.c index a47c2316324054..eda38a54337395 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -1695,7 +1695,7 @@ static void init_type_iterator(struct ewah_or_iterator *it, } } -static void show_objects_for_type( +static int show_objects_for_type( struct bitmap_index *bitmap_git, struct bitmap *objects, enum object_type object_type, @@ -1704,6 +1704,7 @@ static void show_objects_for_type( { size_t i = 0; uint32_t offset; + int ret; struct ewah_or_iterator it; eword_t filter; @@ -1749,11 +1750,17 @@ static void show_objects_for_type( hash = bitmap_name_hash(bitmap_git, index_pos); - show_reach(&oid, object_type, 0, hash, pack, ofs, payload); + ret = show_reach(&oid, object_type, 0, hash, pack, ofs, payload); + if (ret) + goto out; } } + ret = 0; + +out: ewah_or_iterator_release(&it); + return ret; } static int in_bitmapped_pack(struct bitmap_index *bitmap_git, @@ -2062,6 +2069,12 @@ int for_each_bitmapped_object(struct bitmap_index *bitmap_git, show_reachable_fn show_reach, void *payload) { + const enum object_type types[] = { + OBJ_COMMIT, + OBJ_TREE, + OBJ_BLOB, + OBJ_TAG, + }; struct bitmap *filtered_bitmap = NULL; uint32_t objects_nr; size_t full_word_count; @@ -2086,14 +2099,12 @@ int for_each_bitmapped_object(struct bitmap_index *bitmap_git, goto out; } - show_objects_for_type(bitmap_git, filtered_bitmap, - OBJ_COMMIT, show_reach, payload); - show_objects_for_type(bitmap_git, filtered_bitmap, - OBJ_TREE, show_reach, payload); - show_objects_for_type(bitmap_git, filtered_bitmap, - OBJ_BLOB, show_reach, payload); - show_objects_for_type(bitmap_git, filtered_bitmap, - OBJ_TAG, show_reach, payload); + for (size_t i = 0; i < ARRAY_SIZE(types); i++) { + ret = show_objects_for_type(bitmap_git, filtered_bitmap, + types[i], show_reach, payload); + if (ret) + goto out; + } ret = 0; out: diff --git a/pack-bitmap.h b/pack-bitmap.h index 47935eb24e29dd..ae8dc491acbbbf 100644 --- a/pack-bitmap.h +++ b/pack-bitmap.h @@ -93,7 +93,8 @@ struct list_objects_filter_options; /* * Filter bitmapped objects and iterate through all resulting objects, * executing `show_reach` for each of them. Returns `-1` in case the filter is - * not supported, `0` otherwise. + * not supported, `0` otherwise. Aborts iteration and bubbles up the return + * value in case `show_reach()` returns non-zero. */ int for_each_bitmapped_object(struct bitmap_index *bitmap_git, const struct list_objects_filter_options *filter, From eaa9807c970254e503cdb2d719d873521de4ed05 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 15 Jul 2026 08:22:35 +0200 Subject: [PATCH 25/32] pack-bitmap: iterate object sources when opening bitmaps When opening a bitmap for a repository we perform two steps: - We first look for a multi-pack index bitmap in any of the object sources connected to the repository. - We then look for a packfile bitmap in any of the packfiles of any of the object sources. Both of these steps thus iterate through object sources themselves, one via `odb_prepare_alternates()` and one via `repo_for_each_pack()`. This layout makes it hard to introduce a way to open the bitmap of one specific object source, which is functionality that we'll require in a subsequent commit. Reverse the loop so that we instead loop through all sources in the outer loop, and then for each source we try to load its bitmap via either the multi-pack index or via a packfile. Note that this changes the precedence of bitmaps in one specific edge case: when an earlier object source only has a packfile bitmap, but a later source has a multi-pack index bitmap, we now pick the packfile bitmap of the earlier source. Previously, a multi-pack index bitmap from any source would have taken precedence over all packfile bitmaps. Given that object sources are ordered such that the local source comes first, this arguably is an improvement, as we now prefer local bitmaps over bitmaps in alternates. Furthermore, we already warn about repositories that have multiple bitmaps, so this setup is broken and thus arguably not worth worrying about too much. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- pack-bitmap.c | 69 +++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/pack-bitmap.c b/pack-bitmap.c index eda38a54337395..e32795a595b8a7 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -680,60 +680,53 @@ static int load_bitmap(struct repository *r, struct bitmap_index *bitmap_git, return 0; } -static int open_pack_bitmap(struct repository *r, - struct bitmap_index *bitmap_git) +static int open_bitmap_for_source(struct odb_source_packed *source, + struct bitmap_index *bitmap_git) { - struct packed_git *p; - int ret = -1; + struct multi_pack_index *midx = get_multi_pack_index(source); + struct packfile_list_entry *e; + bool found = false; - repo_for_each_pack(r, p) { - if (open_pack_bitmap_1(bitmap_git, p) == 0) { - ret = 0; - /* - * The only reason to keep looking is to report - * duplicates. - */ - if (!trace2_is_enabled()) - break; - } + if (midx && !open_midx_bitmap_1(bitmap_git, midx)) + found = true; + + for (e = packfile_store_get_packs(source); e; e = e->next) { + /* + * When tracing is enabled we want to keep looking to report + * duplicates even if we have already found a bitmap. + */ + if (found && !trace2_is_enabled()) + break; + + if (!open_pack_bitmap_1(bitmap_git, e->pack)) + found = true; } - return ret; + return found ? 0 : -1; } -static int open_midx_bitmap(struct repository *r, - struct bitmap_index *bitmap_git) +static int open_bitmap(struct repository *r, + struct bitmap_index *bitmap_git) { struct odb_source *source; - int ret = -1; + bool found = false; assert(!bitmap_git->map); odb_prepare_alternates(r->objects); for (source = r->objects->sources; source; source = source->next) { struct odb_source_files *files = odb_source_files_downcast(source); - struct multi_pack_index *midx = get_multi_pack_index(files->packed); - if (midx && !open_midx_bitmap_1(bitmap_git, midx)) - ret = 0; - } - return ret; -} - -static int open_bitmap(struct repository *r, - struct bitmap_index *bitmap_git) -{ - int found; - assert(!bitmap_git->map); + if (!open_bitmap_for_source(files->packed, bitmap_git)) + found = true; - found = !open_midx_bitmap(r, bitmap_git); - - /* - * these will all be skipped if we opened a midx bitmap; but run it - * anyway if tracing is enabled to report the duplicates - */ - if (!found || trace2_is_enabled()) - found |= !open_pack_bitmap(r, bitmap_git); + /* + * The only reason to keep looking after having found a bitmap + * is to report duplicates. + */ + if (found && !trace2_is_enabled()) + break; + } return found ? 0 : -1; } From db95bfc121aa9725e8128d7b1dd73330c0beb7f9 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 15 Jul 2026 08:22:36 +0200 Subject: [PATCH 26/32] pack-bitmap: drop `_1` suffix from functions that open bitmaps In the preceding commit we've refactored how we open bitmaps. As part of the refactoring we have consolidated `open_pack_bitmap()` as well as `open_midx_bitmap()` into `open_bitmap_for_source()`. Consequently, we only have their `open_pack_bitmap_1()` and `open_midx_bitmap_1()` variants left over, where the `_1` suffix doesn't really make much sense anymore. Drop the suffix. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- pack-bitmap.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pack-bitmap.c b/pack-bitmap.c index e32795a595b8a7..72c8ae3228036a 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -460,8 +460,8 @@ char *pack_bitmap_filename(struct packed_git *p) return xstrfmt("%.*s.bitmap", (int)len, p->pack_name); } -static int open_midx_bitmap_1(struct bitmap_index *bitmap_git, - struct multi_pack_index *midx) +static int open_midx_bitmap(struct bitmap_index *bitmap_git, + struct multi_pack_index *midx) { struct stat st; char *bitmap_name = midx_bitmap_filename(midx); @@ -539,7 +539,7 @@ static int open_midx_bitmap_1(struct bitmap_index *bitmap_git, return -1; } -static int open_pack_bitmap_1(struct bitmap_index *bitmap_git, struct packed_git *packfile) +static int open_pack_bitmap(struct bitmap_index *bitmap_git, struct packed_git *packfile) { int fd; struct stat st; @@ -603,7 +603,7 @@ static int load_reverse_index(struct repository *r, struct bitmap_index *bitmap_ /* * The multi-pack-index's .rev file is already loaded via - * open_pack_bitmap_1(). + * open_pack_bitmap(). * * But we still need to open the individual pack .rev files, * since we will need to make use of them in pack-objects. @@ -687,7 +687,7 @@ static int open_bitmap_for_source(struct odb_source_packed *source, struct packfile_list_entry *e; bool found = false; - if (midx && !open_midx_bitmap_1(bitmap_git, midx)) + if (midx && !open_midx_bitmap(bitmap_git, midx)) found = true; for (e = packfile_store_get_packs(source); e; e = e->next) { @@ -698,7 +698,7 @@ static int open_bitmap_for_source(struct odb_source_packed *source, if (found && !trace2_is_enabled()) break; - if (!open_pack_bitmap_1(bitmap_git, e->pack)) + if (!open_pack_bitmap(bitmap_git, e->pack)) found = true; } @@ -746,7 +746,7 @@ struct bitmap_index *prepare_midx_bitmap_git(struct multi_pack_index *midx) { struct bitmap_index *bitmap_git = xcalloc(1, sizeof(*bitmap_git)); - if (!open_midx_bitmap_1(bitmap_git, midx)) + if (!open_midx_bitmap(bitmap_git, midx)) return bitmap_git; free_bitmap_index(bitmap_git); From 8cd7ee7b0d12fce9449ea08fe394e18dc97fe059 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 15 Jul 2026 08:22:37 +0200 Subject: [PATCH 27/32] pack-bitmap: introduce function to open bitmap for a single source The function `prepare_bitmap_git()` opens the first bitmap it can find in any of the object sources connected to the repository. In a subsequent commit, the "packed" object database backend will learn to use bitmaps to answer object filters when enumerating objects. That backend operates on a single object source though, so using a bitmap that potentially belongs to a different source would be wrong: - The source would yield objects that are not part of the source itself. - The object source info would be attributed to the wrong source. - With multiple sources, each source would enumerate the same bitmap another time. Introduce a new function `prepare_bitmap_git_for_source()` that only opens bitmaps belonging to the given object source. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- pack-bitmap.c | 12 ++++++++++++ pack-bitmap.h | 2 ++ 2 files changed, 14 insertions(+) diff --git a/pack-bitmap.c b/pack-bitmap.c index 72c8ae3228036a..09ba15d26be88a 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -753,6 +753,18 @@ struct bitmap_index *prepare_midx_bitmap_git(struct multi_pack_index *midx) return NULL; } +struct bitmap_index *prepare_bitmap_git_for_source(struct odb_source_packed *source) +{ + struct bitmap_index *bitmap_git = xcalloc(1, sizeof(*bitmap_git)); + + if (!open_bitmap_for_source(source, bitmap_git) && + !load_bitmap(source->base.odb->repo, bitmap_git, 0)) + return bitmap_git; + + free_bitmap_index(bitmap_git); + return NULL; +} + int bitmap_index_contains_pack(struct bitmap_index *bitmap, struct packed_git *pack) { for (; bitmap; bitmap = bitmap->base) { diff --git a/pack-bitmap.h b/pack-bitmap.h index ae8dc491acbbbf..9f20fb6e563091 100644 --- a/pack-bitmap.h +++ b/pack-bitmap.h @@ -9,6 +9,7 @@ #include "string-list.h" struct commit; +struct odb_source_packed; struct repository; struct rev_info; @@ -68,6 +69,7 @@ struct bitmapped_pack { struct bitmap_index *prepare_bitmap_git(struct repository *r); struct bitmap_index *prepare_midx_bitmap_git(struct multi_pack_index *midx); +struct bitmap_index *prepare_bitmap_git_for_source(struct odb_source_packed *source); /* * Given a bitmap index, determine whether it contains the pack either directly From 204daf5e5c6f931352ddde16236e9705075d15bf Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 15 Jul 2026 08:22:38 +0200 Subject: [PATCH 28/32] odb: introduce object filters to `odb_for_each_object()` The function `for_each_bitmapped_object()` can be used to iterate through all objects covered by a bitmap. The benefit of this function is that it allows the caller to efficiently handle some object filters. For example, this can be used to filter out objects of a specific type with some simple bitmap operations. But callers are currently required to manually wire up the use of bitmaps though, and to do so they have to reach into internals of a given object database source. Introduce a new `struct odb_for_each_object_options::filter` field so that the interface becomes generic. When set, then a backend may optionally use the filter to skip some objects that it would have otherwise yielded. Note that the respective backends are free to ignore this field if they cannot meaningfully optimize for a given filter, and consequently callers need to verify whether they actually want the returned objects. While annoying, we cannot easily lift this restriction anyway as the object filter infrastructure supports some filters that cannot be answered by the object database alone. An alternative might be to limit the filters to only those that _can_ be answered by backends. But ultimately, the filters that can be answered efficiently by the "packed" backend are completely disjunct from those that can be answered by the "loose" backend, and consequently the set of filters supported by all backends would be empty. Furthermore, it would require us to make assumptions about capabilities of future backends, which may be able to efficiently handle more filters than current ones. So in the end, this alternative would only limit us artificially. Implement the logic for the "packed" source. Note that we use the new function `prepare_bitmap_git_for_source()` to open the bitmap: as the backend operates on a single object source, we must only use bitmaps that belong to that specific source. Otherwise we might yield objects that are not part of the source at all, and with multiple sources we would enumerate the same bitmap once per source. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb.h | 12 +++++++++ odb/source-packed.c | 62 +++++++++++++++++++++++++++++++++++++++++++++ pack-bitmap.c | 3 +-- pack-bitmap.h | 3 +++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/odb.h b/odb.h index a1e222f605d5f8..67d0b349426f0c 100644 --- a/odb.h +++ b/odb.h @@ -8,6 +8,7 @@ #include "thread-utils.h" struct cached_object_entry; +struct list_objects_filter_options; struct odb_source_inmemory; struct packed_git; struct repository; @@ -490,6 +491,17 @@ struct odb_for_each_object_options { */ const struct object_id *prefix; size_t prefix_hex_len; + + /* + * Optional object filter that allows backends to skip yielding + * objects that are excluded by the filter as an optimization. The + * filter is a best-effort hint: backends may use it to skip + * excluded objects (e.g. by consulting a reachability bitmap), but + * are also free to ignore it entirely and yield every object. As a + * consequence, callers must re-apply the filter on yielded objects + * if they require strict filtering semantics. + */ + const struct list_objects_filter_options *filter; }; /* diff --git a/odb/source-packed.c b/odb/source-packed.c index 9cfa02b7a2e53e..47773950530781 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -3,11 +3,13 @@ #include "chdir-notify.h" #include "dir.h" #include "git-zlib.h" +#include "list-objects-filter-options.h" #include "mergesort.h" #include "midx.h" #include "odb/source-packed.h" #include "odb/streaming.h" #include "packfile.h" +#include "pack-bitmap.h" static int find_pack_entry(struct odb_source_packed *store, const struct object_id *oid, @@ -315,6 +317,37 @@ static int odb_source_packed_for_each_prefixed_object( return ret; } +struct bitmapped_for_each_object_data { + struct odb_source_packed *packed; + const struct object_info *request; + const struct odb_for_each_object_options *opts; + odb_for_each_object_cb cb; + void *cb_data; +}; + +static int bitmapped_for_each_object(const struct object_id *oid, + enum object_type type UNUSED, + int flags UNUSED, + uint32_t hash UNUSED, + struct packed_git *pack, + off_t offset, + void *cb_data) +{ + struct bitmapped_for_each_object_data *data = cb_data; + + if (should_exclude_pack(pack, data->opts->flags)) + return 0; + + if (data->request) { + struct object_info oi = *data->request; + if (packed_object_info(data->packed, pack, offset, &oi) < 0) + return -1; + return data->cb(oid, &oi, data->cb_data); + } + + return data->cb(oid, NULL, data->cb_data); +} + static int odb_source_packed_for_each_object(struct odb_source *source, const struct object_info *request, odb_for_each_object_cb cb, @@ -328,12 +361,33 @@ static int odb_source_packed_for_each_object(struct odb_source *source, .cb = cb, .cb_data = cb_data, }; + struct bitmap_index *bitmap = NULL; struct packfile_list_entry *e; int pack_errors = 0, ret; if (opts->prefix) return odb_source_packed_for_each_prefixed_object(packed, opts, &data); + if (opts->filter && + opts->filter->choice != LOFC_DISABLED && + can_filter_bitmap(opts->filter)) + bitmap = prepare_bitmap_git_for_source(packed); + if (bitmap) { + struct bitmapped_for_each_object_data bitmap_data = { + .packed = packed, + .request = request, + .opts = opts, + .cb = cb, + .cb_data = cb_data, + }; + + ret = for_each_bitmapped_object(bitmap, opts->filter, + bitmapped_for_each_object, + &bitmap_data); + if (ret) + goto out; + } + packed->skip_mru_updates = true; for (e = packfile_store_get_packs(packed); e; e = e->next) { @@ -342,6 +396,13 @@ static int odb_source_packed_for_each_object(struct odb_source *source, if (should_exclude_pack(p, opts->flags)) continue; + /* + * Objects covered by the bitmap have already been yielded + * above; skip them here to avoid duplicates. + */ + if (bitmap && bitmap_index_contains_pack(bitmap, p)) + continue; + if (open_pack_index(p)) { pack_errors = 1; continue; @@ -357,6 +418,7 @@ static int odb_source_packed_for_each_object(struct odb_source *source, out: packed->skip_mru_updates = false; + free_bitmap_index(bitmap); if (!ret && pack_errors) ret = -1; diff --git a/pack-bitmap.c b/pack-bitmap.c index 09ba15d26be88a..f55a0859eaf528 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -2039,12 +2039,11 @@ static int filter_bitmap(struct bitmap_index *bitmap_git, return -1; } -static int can_filter_bitmap(const struct list_objects_filter_options *filter) +bool can_filter_bitmap(const struct list_objects_filter_options *filter) { return !filter_bitmap(NULL, NULL, NULL, filter); } - static void filter_packed_objects_from_bitmap(struct bitmap_index *bitmap_git, struct bitmap *result) { diff --git a/pack-bitmap.h b/pack-bitmap.h index 9f20fb6e563091..1385027c1ff5fa 100644 --- a/pack-bitmap.h +++ b/pack-bitmap.h @@ -92,6 +92,9 @@ int test_bitmap_pseudo_merge_objects(struct repository *r, uint32_t n); struct list_objects_filter_options; +/* Check whether the filter can be computed via the bitmap. */ +bool can_filter_bitmap(const struct list_objects_filter_options *filter); + /* * Filter bitmapped objects and iterate through all resulting objects, * executing `show_reach` for each of them. Returns `-1` in case the filter is From bfa86e6a3a80b910f0875350d747f7a831694b18 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 15 Jul 2026 08:22:39 +0200 Subject: [PATCH 29/32] builtin/cat-file: filter objects via object database When batching all objects, git-cat-file(1) reaches into the internals of the object database and manually manages bitmaps to apply object filters. This creates coupling between the command and the internals of the respective backend. Refactor git-cat-file(1) to use the new object filter option when batching all objects. This significantly simplifies the logic and ensures that we don't have to reach into internals of the "files" source anymore. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/cat-file.c | 76 +++++----------------------------------------- 1 file changed, 7 insertions(+), 69 deletions(-) diff --git a/builtin/cat-file.c b/builtin/cat-file.c index b4b99a73da9696..1458dd76d683e0 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -20,7 +20,6 @@ #include "userdiff.h" #include "oid-array.h" #include "packfile.h" -#include "pack-bitmap.h" #include "object-file.h" #include "object-name.h" #include "odb.h" @@ -844,28 +843,6 @@ static int batch_one_object_oi(const struct object_id *oid, return payload->callback(oid, NULL, 0, payload->payload); } -static int batch_one_object_packed(const struct object_id *oid, - struct packed_git *pack, - uint32_t pos, - void *_payload) -{ - struct for_each_object_payload *payload = _payload; - return payload->callback(oid, pack, nth_packed_object_offset(pack, pos), - payload->payload); -} - -static int batch_one_object_bitmapped(const struct object_id *oid, - enum object_type type UNUSED, - int flags UNUSED, - uint32_t hash UNUSED, - struct packed_git *pack, - off_t offset, - void *_payload) -{ - struct for_each_object_payload *payload = _payload; - return payload->callback(oid, pack, offset, payload->payload); -} - static void batch_each_object(struct batch_options *opt, for_each_object_fn callback, unsigned flags, @@ -875,56 +852,17 @@ static void batch_each_object(struct batch_options *opt, .callback = callback, .payload = _payload, }; + struct odb_source_info source_info; + struct object_info oi = { + .source_infop = &source_info, + }; struct odb_for_each_object_options opts = { .flags = flags, + .filter = &opt->objects_filter, }; - struct bitmap_index *bitmap = NULL; - struct odb_source *source; - - /* - * TODO: we still need to tap into implementation details of the object - * database sources. Ideally, we should extend `odb_for_each_object()` - * to handle object filters itself so that we can move the filtering - * logic into the individual sources. - */ - odb_prepare_alternates(the_repository->objects); - for (source = the_repository->objects->sources; source; source = source->next) { - struct odb_source_files *files = odb_source_files_downcast(source); - int ret = odb_source_for_each_object(&files->loose->base, NULL, batch_one_object_oi, - &payload, &opts); - if (ret) - break; - } - - if (opt->objects_filter.choice != LOFC_DISABLED && - (bitmap = prepare_bitmap_git(the_repository)) && - !for_each_bitmapped_object(bitmap, &opt->objects_filter, - batch_one_object_bitmapped, &payload)) { - struct packed_git *pack; - - repo_for_each_pack(the_repository, pack) { - if (bitmap_index_contains_pack(bitmap, pack) || - open_pack_index(pack)) - continue; - for_each_object_in_pack(pack, batch_one_object_packed, - &payload, flags); - } - } else { - struct odb_source_info source_info; - struct object_info oi = { - .source_infop = &source_info, - }; - - for (source = the_repository->objects->sources; source; source = source->next) { - struct odb_source_files *files = odb_source_files_downcast(source); - int ret = odb_source_for_each_object(&files->packed->base, &oi, - batch_one_object_oi, &payload, &opts); - if (ret) - break; - } - } - free_bitmap_index(bitmap); + odb_for_each_object_ext(the_repository->objects, &oi, + batch_one_object_oi, &payload, &opts); } static int batch_objects(struct batch_options *opt) From b6b276974e727f4ac92fbaef972f2cd59b3071dd Mon Sep 17 00:00:00 2001 From: Shlok Kulshreshtha Date: Wed, 15 Jul 2026 17:03:44 +0530 Subject: [PATCH 30/32] t7614: avoid hiding git's exit code in a pipe The exit code of the upstream command in a pipe is ignored, so in git cat-file commit HEAD | sed -e "1,/^\$/d" >actual a crash of "git cat-file" would go unnoticed: the exit code of the pipeline is that of "sed", which happily succeeds on empty input. The test would thus pass even though "git cat-file" failed. Write the output of "git cat-file" to a file first and run "sed" on that file, so that the exit codes of both commands are checked by the &&-chain. Signed-off-by: Shlok Kulshreshtha Signed-off-by: Junio C Hamano --- t/t7614-merge-signoff.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/t/t7614-merge-signoff.sh b/t/t7614-merge-signoff.sh index fee258d4f0c2c0..e58bf07b7a4540 100755 --- a/t/t7614-merge-signoff.sh +++ b/t/t7614-merge-signoff.sh @@ -45,7 +45,8 @@ test_expect_success 'git merge --signoff adds a sign-off line' ' test_commit main-branch-2 file2 2 && git checkout other-branch && git merge main --signoff --no-edit && - git cat-file commit HEAD | sed -e "1,/^\$/d" >actual && + git cat-file commit HEAD >commit && + sed -e "1,/^\$/d" commit >actual && test_cmp expected-signed actual ' @@ -55,7 +56,8 @@ test_expect_success 'git merge does not add a sign-off line' ' test_commit main-branch-3 file3 3 && git checkout other-branch && git merge main --no-edit && - git cat-file commit HEAD | sed -e "1,/^\$/d" >actual && + git cat-file commit HEAD >commit && + sed -e "1,/^\$/d" commit >actual && test_cmp expected-unsigned actual ' @@ -65,7 +67,8 @@ test_expect_success 'git merge --no-signoff flag cancels --signoff flag' ' test_commit main-branch-4 file4 4 && git checkout other-branch && git merge main --no-edit --signoff --no-signoff && - git cat-file commit HEAD | sed -e "1,/^\$/d" >actual && + git cat-file commit HEAD >commit && + sed -e "1,/^\$/d" commit >actual && test_cmp expected-unsigned actual ' From 32c4ed70e28f299bec9097db0609a1d954ffac54 Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Thu, 16 Jul 2026 10:47:58 +0000 Subject: [PATCH 31/32] revision: fix --no-walk path filtering regression Since dd4bc01c0a (revision: use priority queue for non-limited streaming walks, 2026-05-27), "git rev-list --no-walk -- " ignores the path arguments and outputs all commits regardless of whether they touch the given paths. That commit introduced a REV_WALK_NO_WALK enum value to separate --no-walk from the streaming walk in get_revision_1(). The new case skips process_parents(), which is correct for not enqueuing parents, but also skips try_to_simplify_commit() which process_parents() calls to evaluate whether each commit touches the given paths. Add a call to try_to_simplify_commit() for the REV_WALK_NO_WALK case, folding it into the existing REV_WALK_REFLOG case which already does the same. Add tests for --no-walk path filtering to t6017. The "single commit, match" test is defensive and passes without the fix, while the other two fail without it. Reported-by: Peter Colberg Signed-off-by: Kristofer Karlsson Signed-off-by: Junio C Hamano --- revision.c | 2 +- t/t6017-rev-list-stdin.sh | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/revision.c b/revision.c index 4bb3b16e43acb9..57e0e23e7e421f 100644 --- a/revision.c +++ b/revision.c @@ -4387,6 +4387,7 @@ static struct commit *get_revision_1(struct rev_info *revs) switch (mode) { case REV_WALK_REFLOG: + case REV_WALK_NO_WALK: try_to_simplify_commit(revs, commit); break; case REV_WALK_TOPO: @@ -4400,7 +4401,6 @@ static struct commit *get_revision_1(struct rev_info *revs) oid_to_hex(&commit->object.oid)); } break; - case REV_WALK_NO_WALK: case REV_WALK_LIMITED: break; } diff --git a/t/t6017-rev-list-stdin.sh b/t/t6017-rev-list-stdin.sh index 4821b90e7479ad..32284f183176a7 100755 --- a/t/t6017-rev-list-stdin.sh +++ b/t/t6017-rev-list-stdin.sh @@ -148,4 +148,22 @@ test_expect_success '--not via stdin does not influence revisions from command l test_cmp expect actual ' +test_expect_success '--no-walk filters by path (single commit, match)' ' + git rev-parse side-1 >expect && + git rev-list --no-walk side-1 -- file-1 >actual && + test_cmp expect actual +' + +test_expect_success '--no-walk filters by path (single commit, no match)' ' + git rev-list --no-walk side-2 -- file-1 >actual && + test_must_be_empty actual +' + +test_expect_success '--no-walk with pathspec exclusion' ' + git rev-parse side-3 side-2 >expect && + git rev-parse side-1 side-2 side-3 >input && + git rev-list --stdin --no-walk -- ":!file-1" actual && + test_cmp expect actual +' + test_done From 9a0c4701dcd5725c4184599322b52933ff5005ca Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 22 Jul 2026 10:30:43 -0700 Subject: [PATCH 32/32] The 7th batch Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.56.0.adoc | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index 9e3bf38f5e5a74..811f74bc7dbcb6 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -48,6 +48,14 @@ UI, Workflows & Features help option ('-h' or '--help') is requested directly by the user, aligning with standard Unix convention. + * The '[includeIf "condition"]' conditional inclusion facility for + configuration files has been taught to use the location of the + worktree in its condition. + + * The usage string and SYNOPSIS for 'git fast-export' have been + standardized to make them consistent with each other and with other + commands. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -167,6 +175,37 @@ Performance, Internal Implementation, Development Support etc. 'change-id' trailer, ensuring that sent tags generated by 'b4' contain the required tracking information for subsequent runs. + * 'git receive-pack' has been refactored to use ODB transaction + interfaces instead of directly managing 'tmp_objdir' for staging + incoming objects, bringing it closer to being ODB backend agnostic. + + * The test script 't/t9811-git-p4-label-import.sh' has been + modernized to use 'test_path_is_file' and 'test_path_is_missing' + instead of raw 'test -f' and '! test -f' calls. + + * A redundant strbuf_reset() call in the 'HAVE_GETDELIM' path of + strbuf_getwholeline() has been removed, as getdelim() overwrites the + buffer and the length is updated afterward. + + * The object database enumeration interface odb_for_each_object() has + been taught to accept object filters, allowing the underlying backends + to optimize the traversal by using reachability bitmaps when + available. 'git cat-file --batch-all-objects' has been updated to use + this generic interface, simplifying its code and avoiding direct + access to ODB backend internals. + + * The test script 't/t1100-commit-tree-options.sh' has been modernized + by converting test cases to the modern style (using single quotes and + tab indentation) and moving the creation of the expected file inside + the setup test so it runs under the protection of the test harness. + + * The test script 't/t7614-merge-signoff.sh' has been updated to avoid + suppressing the exit code of 'git' commands in a pipe. + + * The 'git rev-list --no-walk' command has been corrected to restore + pathspec filtering, which was lost when the streaming walk was + refactored. + Fixes since v2.55 ----------------- @@ -306,3 +345,8 @@ Fixes since v2.55 * The stream-based object signature verification path has been corrected to avoid double-closing the stream on read errors. (merge cfd52a74a0 ps/odb-stream-double-close-fix later to maint). + + * The '-i' shorthand for the '--init' option, which was accepted by the + 'git submodule update' command until it was broken in a modernization + of the option-parsing code, has been restored. + (merge ff1da37f58 dm/submodule-update-i-shorthand later to maint).