Git development
 help / color / mirror / Atom feed
* [PATCH v2 2/2] commit: allow -c/-C for all kinds of --fixup
From: erik @ 2026-05-26 10:47 UTC (permalink / raw)
  To: git; +Cc: gitster, phillip.wood123, Erik Cervin-Edin
In-Reply-To: <cover.1779792311.git.erik@cervined.in>

From: Erik Cervin-Edin <erik@cervined.in>

The previous commit allowed -m and -F for all --fixup variations.  The
-c/-C flags were blocked by the same higher-layer incompatibility check
that previously caught -F, namely die_for_incompatible_opt4() grouping
them with --fixup.

Drop --fixup from that check and route the resolved commit through
prepare_amend_commit() in the fixup path, mirroring the no-message-source
behaviour of --fixup=amend.  With this in place, -m/-F/-c/-C all behave
consistently across the plain, amend, and reword --fixup forms.

Signed-off-by: Erik Cervin-Edin <erik@cervined.in>
---
 Documentation/git-commit.adoc             |  9 ++--
 builtin/commit.c                          | 13 +++--
 t/t7500-commit-template-squash-signoff.sh | 60 +++++++++++++++++++++--
 3 files changed, 71 insertions(+), 11 deletions(-)

diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
index 61efd29e66..98c50a3be5 100644
--- a/Documentation/git-commit.adoc
+++ b/Documentation/git-commit.adoc
@@ -102,8 +102,8 @@ include::diff-context-options.adoc[]
 +
 The commit created by plain `--fixup=<commit>` has a title
 composed of "fixup!" followed by the title of _<commit>_,
-and is recognized specially by `git rebase --autosquash`. The `-m`
-or `-F` option may be used to supplement the log message
+and is recognized specially by `git rebase --autosquash`. The `-m`,
+`-F`, `-C`, or `-c` option may be used to supplement the log message
 of the created commit, but the additional commentary will be thrown
 away once the "fixup!" commit is squashed into _<commit>_ by
 `git rebase --autosquash`.
@@ -112,8 +112,9 @@ The commit created by `--fixup=amend:<commit>` is similar but its
 title is instead prefixed with "amend!". The log message of
 _<commit>_ is copied into the log message of the "amend!" commit and
 opened in an editor so it can be refined. The replacement message may
-also be supplied directly using `-m` or `-F`, bypassing the
-need to open an editor. When `git rebase
+also be supplied directly using `-m`, `-F`, or `-C`, bypassing the
+need to open an editor, or using `-c` to open the editor pre-populated
+with the referenced commit's message. When `git rebase
 --autosquash` squashes the "amend!" commit into _<commit>_, the log
 message of _<commit>_ is replaced by the refined log message from the
 "amend!" commit. It is an error for the "amend!" commit's log message
diff --git a/builtin/commit.c b/builtin/commit.c
index 3f1fca2919..fcf148eb21 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -837,9 +837,9 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 		hook_arg1 = "message";
 
 		/*
-		 * Only `-m` and `-F` are handled here. `-c`/`-C` are
-		 * incompatible with --fixup and have already errored out
-		 * during option parsing.
+		 * `-m`, `-F`, `-C`, and `-c` provide the message body.
+		 * If none was given and this is an amend, use the target
+		 * commit's body instead.
 		 */
 		if (have_option_m) {
 			strbuf_addbuf(&sb, &message);
@@ -851,6 +851,11 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 		} else if (logfile) {
 			if (strbuf_read_file(&sb, logfile, 0) < 0)
 				die_errno(_("could not read log file '%s'"), logfile);
+		} else if (use_message) {
+			struct commit *c = lookup_commit_reference_by_name(use_message);
+			if (!c)
+				die(_("could not lookup commit '%s'"), use_message);
+			prepare_amend_commit(c, &sb, &ctx);
 		} else if (!strcmp(fixup_prefix, "amend")) {
 			prepare_amend_commit(commit, &sb, &ctx);
 		}
@@ -1341,7 +1346,7 @@ static int parse_and_validate_options(int argc, const char *argv[],
 		die(_("options '%s' and '%s' cannot be used together"), "--squash", "--fixup");
 	die_for_incompatible_opt3(!!use_message, "-C",
 				  !!edit_message, "-c",
-				  !!fixup_message, "--fixup");
+				  !!logfile, "-F");
 	die_for_incompatible_opt4(have_option_m, "-m",
 				  !!edit_message, "-c",
 				  !!use_message, "-C",
diff --git a/t/t7500-commit-template-squash-signoff.sh b/t/t7500-commit-template-squash-signoff.sh
index 01c7400136..48e1247d9e 100755
--- a/t/t7500-commit-template-squash-signoff.sh
+++ b/t/t7500-commit-template-squash-signoff.sh
@@ -492,6 +492,62 @@ test_expect_success 'commit --fixup works with -F' '
 	EOF
 '
 
+test_expect_success 'commit --fixup works with -C' '
+	commit_for_rebase_autosquash_setup &&
+	git commit --fixup HEAD~ -C HEAD &&
+	test_commit_message HEAD <<-EOF
+	fixup! $(git log -1 --format=%s HEAD~2)
+
+	$(get_commit_msg HEAD~)
+	EOF
+'
+
+test_expect_success 'commit --fixup=amend: works with -c' '
+	commit_for_rebase_autosquash_setup &&
+	test_set_editor : &&
+	git commit --fixup=amend:HEAD -c HEAD~ &&
+	test_commit_message HEAD <<-EOF
+	amend! intermediate commit
+
+	target message subject line
+
+	target message body line 1
+	target message body line 2
+	EOF
+'
+
+test_expect_success 'commit --fixup=amend:HEAD with -C HEAD and without have the same message' '
+	commit_for_rebase_autosquash_setup &&
+	start=$(git rev-parse HEAD) &&
+
+	git commit --fixup=amend:HEAD -C HEAD &&
+	git commit --fixup=amend:HEAD -C HEAD &&
+	git log -1 --pretty=%B >with-c &&
+
+	git reset --hard "$start" &&
+	test_set_editor : &&
+	git commit --fixup=amend:HEAD &&
+	git commit --fixup=amend:HEAD &&
+	git log -1 --pretty=%B >without-c &&
+
+	test_cmp with-c without-c
+'
+
+test_expect_success 'commit --fixup=amend: with -C copies full subject + body of squash commit' '
+	commit_for_rebase_autosquash_setup &&
+	git commit --squash HEAD~ -m "inner body" &&
+	echo "extra" >>foo &&
+	git add foo &&
+	git commit --fixup=amend:HEAD -C HEAD &&
+	test_commit_message HEAD <<-EOF
+	amend! squash! $(git log -1 --format=%s HEAD~3)
+
+	squash! $(git log -1 --format=%s HEAD~3)
+
+	inner body
+	EOF
+'
+
 test_expect_success 'commit --fixup=reword: works with -F' '
 	commit_for_rebase_autosquash_setup &&
 	echo "message from file" >msgfile &&
@@ -553,9 +609,7 @@ test_expect_success 'invalid message options when using --fixup' '
 	echo changes >>foo &&
 	echo "message" >log &&
 	git add foo &&
-	test_must_fail git commit --fixup HEAD~1 --squash HEAD~2 &&
-	test_must_fail git commit --fixup HEAD~1 -C HEAD~2 &&
-	test_must_fail git commit --fixup HEAD~1 -c HEAD~2
+	test_must_fail git commit --fixup HEAD~1 --squash HEAD~2
 '
 
 cat >expected-template <<EOF
-- 
2.54.0.1014.g842965a2d5


^ permalink raw reply related

* [PATCH v2 1/2] commit: allow -m/-F for all kinds of --fixup
From: erik @ 2026-05-26 10:47 UTC (permalink / raw)
  To: git; +Cc: gitster, phillip.wood123, Erik Cervin-Edin
In-Reply-To: <cover.1779792311.git.erik@cervined.in>

From: Erik Cervin-Edin <erik@cervined.in>

The ability to provide a commit message for git commit --fixup and its
variations is limited:

  * Plain --fixup only allows using the -m flag

  * The amend/reword --fixup variants only allow supplying the message
    using an editor

For amend/reword, the -m and -F flags are rejected: -m is caught by a
die() in prepare_to_commit(), and -F is caught by
die_for_incompatible_opt4() which groups -F with --fixup as mutually
exclusive.  This makes these modes poorly suited for non-interactive
workflows -- notably when using AI coding agents.

When support to use the -m option was introduced in [1] it was noted
that there could be support for other options but at the time the use
case was deemed too niche.  Later, when the amend suboption was
introduced in [2] -m support for amend fixups was discussed but not
pursued, and -F was already caught by the higher-layer incompatibility
check grouping it with --fixup.

The rejections of these options hark back to when --fixup was
introduced in [3] and as noted in [1] -- there's nothing inherently
preventing support for them.  The current patchwork of which flags
work with which --fixup variants has no strong logic to it, and
allowing all of them simplifies both the code and the interface.

Allow -m and -F to supply the message body for all --fixup variations,
mirroring the flow of a regular commit.  -c and -C, which are blocked
by the same incompatibility check, are handled in the next commit.

1. 30884c9afc (commit: add support for --fixup <commit> -m"<extra
   message>", 2017-12-22)

2. 494d314a05 (commit: add amend suboption to --fixup to create amend!
   commit, 2021-03-15)

3. d71b8ba7c9 (commit: --fixup option for use with rebase --autosquash,
   2010-11-02)

Helped-by: Junio C Hamano <gitster@pobox.com>
Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Erik Cervin-Edin <erik@cervined.in>
---
 Documentation/git-commit.adoc             | 19 ++++----
 builtin/commit.c                          | 34 +++++++-------
 t/t7500-commit-template-squash-signoff.sh | 56 +++++++++++++++++------
 3 files changed, 69 insertions(+), 40 deletions(-)

diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
index 8329c1034b..61efd29e66 100644
--- a/Documentation/git-commit.adoc
+++ b/Documentation/git-commit.adoc
@@ -103,20 +103,21 @@ include::diff-context-options.adoc[]
 The commit created by plain `--fixup=<commit>` has a title
 composed of "fixup!" followed by the title of _<commit>_,
 and is recognized specially by `git rebase --autosquash`. The `-m`
-option may be used to supplement the log message of the created
-commit, but the additional commentary will be thrown away once the
-"fixup!" commit is squashed into _<commit>_ by
+or `-F` option may be used to supplement the log message
+of the created commit, but the additional commentary will be thrown
+away once the "fixup!" commit is squashed into _<commit>_ by
 `git rebase --autosquash`.
 +
 The commit created by `--fixup=amend:<commit>` is similar but its
 title is instead prefixed with "amend!". The log message of
 _<commit>_ is copied into the log message of the "amend!" commit and
-opened in an editor so it can be refined. When `git rebase
---autosquash` squashes the "amend!" commit into _<commit>_, the
-log message of _<commit>_ is replaced by the refined log message
-from the "amend!" commit. It is an error for the "amend!" commit's
-log message to be empty unless `--allow-empty-message` is
-specified.
+opened in an editor so it can be refined. The replacement message may
+also be supplied directly using `-m` or `-F`, bypassing the
+need to open an editor. When `git rebase
+--autosquash` squashes the "amend!" commit into _<commit>_, the log
+message of _<commit>_ is replaced by the refined log message from the
+"amend!" commit. It is an error for the "amend!" commit's log message
+to be empty unless `--allow-empty-message` is specified.
 +
 `--fixup=reword:<commit>` is shorthand for `--fixup=amend:<commit>
  --only`. It creates an "amend!" commit with only a log message
diff --git a/builtin/commit.c b/builtin/commit.c
index 28f6174503..3f1fca2919 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -804,18 +804,18 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 	if (have_option_m && !fixup_message) {
 		strbuf_addbuf(&sb, &message);
 		hook_arg1 = "message";
-	} else if (logfile && !strcmp(logfile, "-")) {
+	} else if (logfile && !fixup_message && !strcmp(logfile, "-")) {
 		if (isatty(0))
 			fprintf(stderr, _("(reading log message from standard input)\n"));
 		if (strbuf_read(&sb, 0, 0) < 0)
 			die_errno(_("could not read log from standard input"));
 		hook_arg1 = "message";
-	} else if (logfile) {
+	} else if (logfile && !fixup_message) {
 		if (strbuf_read_file(&sb, logfile, 0) < 0)
 			die_errno(_("could not read log file '%s'"),
 				  logfile);
 		hook_arg1 = "message";
-	} else if (use_message) {
+	} else if (use_message && !fixup_message) {
 		const char *buffer;
 		buffer = strstr(use_message_buffer, "\n\n");
 		if (buffer)
@@ -837,20 +837,21 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 		hook_arg1 = "message";
 
 		/*
-		 * Only `-m` commit message option is checked here, as
-		 * it supports `--fixup` to append the commit message.
-		 *
-		 * The other commit message options `-c`/`-C`/`-F` are
-		 * incompatible with all the forms of `--fixup` and
-		 * have already errored out while parsing the `git commit`
-		 * options.
+		 * Only `-m` and `-F` are handled here. `-c`/`-C` are
+		 * incompatible with --fixup and have already errored out
+		 * during option parsing.
 		 */
-		if (have_option_m && !strcmp(fixup_prefix, "fixup"))
+		if (have_option_m) {
 			strbuf_addbuf(&sb, &message);
-
-		if (!strcmp(fixup_prefix, "amend")) {
-			if (have_option_m)
-				die(_("options '%s' and '%s:%s' cannot be used together"), "-m", "--fixup", fixup_message);
+		} else if (logfile && !strcmp(logfile, "-")) {
+			if (isatty(0))
+				fprintf(stderr, _("(reading log message from standard input)\n"));
+			if (strbuf_read(&sb, 0, 0) < 0)
+				die_errno(_("could not read log from standard input"));
+		} else if (logfile) {
+			if (strbuf_read_file(&sb, logfile, 0) < 0)
+				die_errno(_("could not read log file '%s'"), logfile);
+		} else if (!strcmp(fixup_prefix, "amend")) {
 			prepare_amend_commit(commit, &sb, &ctx);
 		}
 	} else if (!stat(git_path_merge_msg(the_repository), &statbuf)) {
@@ -1338,9 +1339,8 @@ static int parse_and_validate_options(int argc, const char *argv[],
 	}
 	if (fixup_message && squash_message)
 		die(_("options '%s' and '%s' cannot be used together"), "--squash", "--fixup");
-	die_for_incompatible_opt4(!!use_message, "-C",
+	die_for_incompatible_opt3(!!use_message, "-C",
 				  !!edit_message, "-c",
-				  !!logfile, "-F",
 				  !!fixup_message, "--fixup");
 	die_for_incompatible_opt4(have_option_m, "-m",
 				  !!edit_message, "-c",
diff --git a/t/t7500-commit-template-squash-signoff.sh b/t/t7500-commit-template-squash-signoff.sh
index 66aff8e097..01c7400136 100755
--- a/t/t7500-commit-template-squash-signoff.sh
+++ b/t/t7500-commit-template-squash-signoff.sh
@@ -384,18 +384,24 @@ test_expect_success '--fixup=reword: ignores staged changes' '
 	test_cmp foo actual
 '
 
-test_expect_success '--fixup=reword: error out with -m option' '
+test_expect_success 'commit --fixup=reword: works with -m' '
 	commit_for_rebase_autosquash_setup &&
-	echo "fatal: options '\''-m'\'' and '\''--fixup:reword'\'' cannot be used together" >expect &&
-	test_must_fail git commit --fixup=reword:HEAD~ -m "reword commit message" 2>actual &&
-	test_cmp expect actual
+	git commit --fixup=reword:HEAD~ -m "reword commit message" &&
+	test_commit_message HEAD <<-EOF
+	amend! $(git log -1 --format=%s HEAD~2)
+
+	reword commit message
+	EOF
 '
 
-test_expect_success '--fixup=amend: error out with -m option' '
+test_expect_success 'commit --fixup=amend: works with -m' '
 	commit_for_rebase_autosquash_setup &&
-	echo "fatal: options '\''-m'\'' and '\''--fixup:amend'\'' cannot be used together" >expect &&
-	test_must_fail git commit --fixup=amend:HEAD~ -m "amend commit message" 2>actual &&
-	test_cmp expect actual
+	git commit --fixup=amend:HEAD~ -m "amend commit message" &&
+	test_commit_message HEAD <<-EOF
+	amend! $(git log -1 --format=%s HEAD~2)
+
+	amend commit message
+	EOF
 '
 
 test_expect_success 'consecutive amend! commits remove amend! line from commit msg body' '
@@ -432,6 +438,13 @@ test_expect_success 'deny to create amend! commit if its commit msg body is empt
 	test_cmp expected actual
 '
 
+test_expect_success 'deny to create amend! commit if -m is empty' '
+	commit_for_rebase_autosquash_setup &&
+	echo "Aborting commit due to empty commit message body." >expect &&
+	test_must_fail git commit --fixup=amend:HEAD~ -m "" 2>actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'amend! commit allows empty commit msg body with --allow-empty-message' '
 	commit_for_rebase_autosquash_setup &&
 	cat >expected <<-EOF &&
@@ -468,10 +481,26 @@ test_expect_success '--fixup=reword: give error with pathsec' '
 	test_cmp expect actual
 '
 
-test_expect_success '--fixup=reword: -F give error message' '
-	echo "fatal: options '\''-F'\'' and '\''--fixup'\'' cannot be used together" >expect &&
-	test_must_fail git commit --fixup=reword:HEAD~ -F msg  2>actual &&
-	test_cmp expect actual
+test_expect_success 'commit --fixup works with -F' '
+	commit_for_rebase_autosquash_setup &&
+	echo "message" >msgfile &&
+	git commit --fixup HEAD~ -F msgfile &&
+	test_commit_message HEAD <<-EOF
+	fixup! $(git log -1 --format=%s HEAD~2)
+
+	message
+	EOF
+'
+
+test_expect_success 'commit --fixup=reword: works with -F' '
+	commit_for_rebase_autosquash_setup &&
+	echo "message from file" >msgfile &&
+	git commit --fixup=reword:HEAD~ -F msgfile &&
+	test_commit_message HEAD <<-EOF
+	amend! $(git log -1 --format=%s HEAD~2)
+
+	$(cat msgfile)
+	EOF
 '
 
 test_expect_success 'commit --squash works with -F' '
@@ -526,8 +555,7 @@ test_expect_success 'invalid message options when using --fixup' '
 	git add foo &&
 	test_must_fail git commit --fixup HEAD~1 --squash HEAD~2 &&
 	test_must_fail git commit --fixup HEAD~1 -C HEAD~2 &&
-	test_must_fail git commit --fixup HEAD~1 -c HEAD~2 &&
-	test_must_fail git commit --fixup HEAD~1 -F log
+	test_must_fail git commit --fixup HEAD~1 -c HEAD~2
 '
 
 cat >expected-template <<EOF
-- 
2.54.0.1014.g842965a2d5


^ permalink raw reply related

* [PATCH v2 0/2] commit: allow -m/-F/-c/-C for all --fixup variations
From: erik @ 2026-05-26 10:47 UTC (permalink / raw)
  To: git; +Cc: gitster, phillip.wood123, Erik Cervin-Edin
In-Reply-To: <20260518112225.73172-2-erik@cervined.in>

From: Erik Cervin-Edin <erik@cervined.in>

V1 was a single patch and only addressed -m/-F.  V2 makes two
substantive changes.

First, the refactor.  V1 added a special-cased file slurp in
cmd_commit() that pretended -F had been spelled -m.  Junio noted
this loses the "this came from a file" origin if anything goes
wrong later in the control flow [1], and Phillip suggested
refactoring prepare_to_commit() [2].  V2 does that: prepare_to_commit()
now consults fixup_message at each message-source branch and routes the
message origin flags through the same code path for all --fixup
variations.

Second, scope extended to -c/-C.  In the V1 thread I noted [3]
that -F made sense across all --fixup variants for consistency,
but that -c/-C felt "probably less justifiable" for plain --fixup.
Looking at it more while refactoring, the existing patchwork of
which flag works with which variant looked less and less like a
design and more like an accident -- once -F was threaded through
prepare_to_commit(), -c/-C fell out of the same path naturally
(blocked by the same die_for_incompatible_opt4() grouping that
caught -F).  This lives in its own patch (2/2) -- I won't object
if reviewers prefer to drop it or rolling it separately.

There is one wrinkle worth flagging on 2/2.  When -c/-C names a
source commit whose message starts with "amend! ",
prepare_amend_commit() strips that line -- the same stripping that
happens for a no-source --fixup=amend:<commit>.  This is independent
of which --fixup variant is being produced (the target); it depends
only on whether the -c/-C source is itself an amend!/reword! commit.
The upside is that

    git commit --fixup=amend:foo -C foo

and

    GIT_EDITOR=: git commit --fixup=amend:foo

produce the same commit.  If reviewers would rather -c/-C take the
source message verbatim, that's a small change and I'm open to it.

Smaller fixes from V1 review:

  * "unusable" softened to "poorly suited" in the rationale [1].

  * Dropped the incorrect claim that plain --fixup -m is
    "practically a no-op"

  * Adopted Phillip's suggestion to use test_commit_message in the
    new --fixup=amend: -m test, which also resolves the
    expected/expect golden-file naming Junio called out [1][2].

[1] https://lore.kernel.org/git/xmqqik8kc2nj.fsf@gitster.g/
[2] https://lore.kernel.org/git/ac6aaaca-2b7c-4892-ba93-0dc3e3c18ff7@gmail.com/
[3] https://lore.kernel.org/git/aguM7UIbAo19Zojv@mbp/

Erik Cervin-Edin (2):
  commit: allow -m/-F for all kinds of --fixup
  commit: allow -c/-C for all kinds of --fixup

 Documentation/git-commit.adoc             |  22 +++--
 builtin/commit.c                          |  41 ++++----
 t/t7500-commit-template-squash-signoff.sh | 114 +++++++++++++++++++---
 3 files changed, 133 insertions(+), 44 deletions(-)

Range-diff against v1:
1:  49e202f04b ! 1:  e9f07d49ee commit: allow -m/-F with --fixup=amend: or reword:
    @@ Metadata
     Author: Erik Cervin-Edin <erik@cervined.in>
     
      ## Commit message ##
    -    commit: allow -m/-F with --fixup=amend: or reword:
    +    commit: allow -m/-F for all kinds of --fixup
     
    -    commit: allow -m/-F with --fixup=amend: or reword:
    +    The ability to provide a commit message for git commit --fixup and its
    +    variations is limited:
     
    -    --fixup=amend: and --fixup=reword: require an editor to supply the
    -    replacement commit message. The -m and -F flags are rejected: -m is
    -    caught by a die() in prepare_to_commit(), and -F is caught by
    +      * Plain --fixup only allows using the -m flag
    +
    +      * The amend/reword --fixup variants only allow supplying the message
    +        using an editor
    +
    +    For amend/reword, the -m and -F flags are rejected: -m is caught by a
    +    die() in prepare_to_commit(), and -F is caught by
         die_for_incompatible_opt4() which groups -F with --fixup as mutually
    -    exclusive. This makes these modes unusable in non-interactive
    -    workflows -- notably AI coding agents.
    +    exclusive.  This makes these modes poorly suited for non-interactive
    +    workflows -- notably when using AI coding agents.
    +
    +    When support to use the -m option was introduced in [1] it was noted
    +    that there could be support for other options but at the time the use
    +    case was deemed too niche.  Later, when the amend suboption was
    +    introduced in [2] -m support for amend fixups was discussed but not
    +    pursued, and -F was already caught by the higher-layer incompatibility
    +    check grouping it with --fixup.
    +
    +    The rejections of these options hark back to when --fixup was
    +    introduced in [3] and as noted in [1] -- there's nothing inherently
    +    preventing support for them.  The current patchwork of which flags
    +    work with which --fixup variants has no strong logic to it, and
    +    allowing all of them simplifies both the code and the interface.
     
    -    When the amend suboption was introduced in 494d314a05 (commit: add
    -    amend suboption to --fixup to create amend! commit, 2021-03-15),
    -    -m support for amend fixups was discussed but not pursued, and -F
    -    was already caught by the higher-layer incompatibility check grouping
    -    it with --fixup.
    +    Allow -m and -F to supply the message body for all --fixup variations,
    +    mirroring the flow of a regular commit.  -c and -C, which are blocked
    +    by the same incompatibility check, are handled in the next commit.
     
    -    Allow -m and -F to supply the replacement message body for amend and
    -    reword fixups. When provided, bypass the editor and directly use the
    -    user's message as the body, replacing the original commit's message. For
    -    -F, the file contents are read into the message strbuf and then handled
    -    identically to -m.
    +    1. 30884c9afc (commit: add support for --fixup <commit> -m"<extra
    +       message>", 2017-12-22)
     
    -    Plain --fixup (without amend: or reword:) continues to reject -F but
    -    still accepts -m (even though it's practically a no-op).
    +    2. 494d314a05 (commit: add amend suboption to --fixup to create amend!
    +       commit, 2021-03-15)
     
    -    Signed-off-by: Erik Cervin Edin <erik@cervined.in>
    +    3. d71b8ba7c9 (commit: --fixup option for use with rebase --autosquash,
    +       2010-11-02)
    +
    +    Helped-by: Junio C Hamano <gitster@pobox.com>
    +    Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
    +    Signed-off-by: Erik Cervin-Edin <erik@cervined.in>
     
      ## Documentation/git-commit.adoc ##
    -@@ Documentation/git-commit.adoc: commit, but the additional commentary will be thrown away once the
    +@@ Documentation/git-commit.adoc: include::diff-context-options.adoc[]
    + The commit created by plain `--fixup=<commit>` has a title
    + composed of "fixup!" followed by the title of _<commit>_,
    + and is recognized specially by `git rebase --autosquash`. The `-m`
    +-option may be used to supplement the log message of the created
    +-commit, but the additional commentary will be thrown away once the
    +-"fixup!" commit is squashed into _<commit>_ by
    ++or `-F` option may be used to supplement the log message
    ++of the created commit, but the additional commentary will be thrown
    ++away once the "fixup!" commit is squashed into _<commit>_ by
    + `git rebase --autosquash`.
    + +
      The commit created by `--fixup=amend:<commit>` is similar but its
      title is instead prefixed with "amend!". The log message of
      _<commit>_ is copied into the log message of the "amend!" commit and
    @@ Documentation/git-commit.adoc: commit, but the additional commentary will be thr
     -log message to be empty unless `--allow-empty-message` is
     -specified.
     +opened in an editor so it can be refined. The replacement message may
    -+also be supplied directly using `-m` or `-F`, bypassing the need to open
    -+an editor. When `git rebase --autosquash` squashes the "amend!" commit
    -+into _<commit>_, the log message of _<commit>_ is replaced by the
    -+refined log message from the "amend!" commit. It is an error for the
    -+"amend!" commit's log message to be empty unless `--allow-empty-message`
    -+is specified.
    ++also be supplied directly using `-m` or `-F`, bypassing the
    ++need to open an editor. When `git rebase
    ++--autosquash` squashes the "amend!" commit into _<commit>_, the log
    ++message of _<commit>_ is replaced by the refined log message from the
    ++"amend!" commit. It is an error for the "amend!" commit's log message
    ++to be empty unless `--allow-empty-message` is specified.
      +
      `--fixup=reword:<commit>` is shorthand for `--fixup=amend:<commit>
       --only`. It creates an "amend!" commit with only a log message
     
      ## builtin/commit.c ##
    +@@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const char *prefix,
    + 	if (have_option_m && !fixup_message) {
    + 		strbuf_addbuf(&sb, &message);
    + 		hook_arg1 = "message";
    +-	} else if (logfile && !strcmp(logfile, "-")) {
    ++	} else if (logfile && !fixup_message && !strcmp(logfile, "-")) {
    + 		if (isatty(0))
    + 			fprintf(stderr, _("(reading log message from standard input)\n"));
    + 		if (strbuf_read(&sb, 0, 0) < 0)
    + 			die_errno(_("could not read log from standard input"));
    + 		hook_arg1 = "message";
    +-	} else if (logfile) {
    ++	} else if (logfile && !fixup_message) {
    + 		if (strbuf_read_file(&sb, logfile, 0) < 0)
    + 			die_errno(_("could not read log file '%s'"),
    + 				  logfile);
    + 		hook_arg1 = "message";
    +-	} else if (use_message) {
    ++	} else if (use_message && !fixup_message) {
    + 		const char *buffer;
    + 		buffer = strstr(use_message_buffer, "\n\n");
    + 		if (buffer)
     @@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const char *prefix,
      		hook_arg1 = "message";
      
    @@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const cha
     -		 * incompatible with all the forms of `--fixup` and
     -		 * have already errored out while parsing the `git commit`
     -		 * options.
    -+		 * `-m` (and `-F`, converted to `-m` earlier for
    -+		 * amend/reword) appends the message body here.
    -+		 * `-c`/`-C` are still incompatible with all forms
    -+		 * of `--fixup`.
    ++		 * Only `-m` and `-F` are handled here. `-c`/`-C` are
    ++		 * incompatible with --fixup and have already errored out
    ++		 * during option parsing.
      		 */
    - 		if (have_option_m && !strcmp(fixup_prefix, "fixup"))
    +-		if (have_option_m && !strcmp(fixup_prefix, "fixup"))
    ++		if (have_option_m) {
      			strbuf_addbuf(&sb, &message);
    - 
    - 		if (!strcmp(fixup_prefix, "amend")) {
    - 			if (have_option_m)
    +-
    +-		if (!strcmp(fixup_prefix, "amend")) {
    +-			if (have_option_m)
     -				die(_("options '%s' and '%s:%s' cannot be used together"), "-m", "--fixup", fixup_message);
    --			prepare_amend_commit(commit, &sb, &ctx);
    -+				strbuf_addbuf(&sb, &message);
    -+			else
    -+				prepare_amend_commit(commit, &sb, &ctx);
    ++		} else if (logfile && !strcmp(logfile, "-")) {
    ++			if (isatty(0))
    ++				fprintf(stderr, _("(reading log message from standard input)\n"));
    ++			if (strbuf_read(&sb, 0, 0) < 0)
    ++				die_errno(_("could not read log from standard input"));
    ++		} else if (logfile) {
    ++			if (strbuf_read_file(&sb, logfile, 0) < 0)
    ++				die_errno(_("could not read log file '%s'"), logfile);
    ++		} else if (!strcmp(fixup_prefix, "amend")) {
    + 			prepare_amend_commit(commit, &sb, &ctx);
      		}
      	} else if (!stat(git_path_merge_msg(the_repository), &statbuf)) {
    - 		size_t merge_msg_start;
     @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *argv[],
      	}
      	if (fixup_message && squash_message)
    @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *ar
      				  !!edit_message, "-c",
     -				  !!logfile, "-F",
      				  !!fixup_message, "--fixup");
    -+	die_for_incompatible_opt3(!!use_message, "-C",
    -+				  !!edit_message, "-c",
    -+				  !!logfile, "-F");
      	die_for_incompatible_opt4(have_option_m, "-m",
      				  !!edit_message, "-c",
    - 				  !!use_message, "-C",
    -@@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *argv[],
    - 		}
    - 	}
    - 
    -+	if (logfile && fixup_message && !strcmp(fixup_prefix, "fixup"))
    -+		die(_("options '%s' and '%s' cannot be used together"), "-F", "--fixup");
    -+
    - 	if (0 <= edit_flag)
    - 		use_editor = edit_flag;
    - 
    -@@ builtin/commit.c: int cmd_commit(int argc,
    - 	argc = parse_and_validate_options(argc, argv, builtin_commit_options,
    - 					  builtin_commit_usage,
    - 					  prefix, current_head, &s);
    -+
    -+	if (logfile && fixup_message && !strcmp(fixup_prefix, "amend")) {
    -+		if (!strcmp(logfile, "-")) {
    -+			if (isatty(0))
    -+				fprintf(stderr, _("(reading log message from standard input)\n"));
    -+			if (strbuf_read(&message, 0, 0) < 0)
    -+				die_errno(_("could not read log from standard input"));
    -+		} else {
    -+			if (strbuf_read_file(&message, logfile, 0) < 0)
    -+				die_errno(_("could not read log file '%s'"), logfile);
    -+		}
    -+		strbuf_complete_line(&message);
    -+		have_option_m = 1;
    -+		FREE_AND_NULL(logfile);
    -+	}
    -+
    - 	if (trailer_args.nr)
    - 		trailer_config_init();
    - 
     
      ## t/t7500-commit-template-squash-signoff.sh ##
     @@ t/t7500-commit-template-squash-signoff.sh: test_expect_success '--fixup=reword: ignores staged changes' '
    @@ t/t7500-commit-template-squash-signoff.sh: test_expect_success '--fixup=reword:
      '
      
     -test_expect_success '--fixup=reword: error out with -m option' '
    -+test_expect_success '--fixup=amend: with -m option' '
    ++test_expect_success 'commit --fixup=reword: works with -m' '
      	commit_for_rebase_autosquash_setup &&
     -	echo "fatal: options '\''-m'\'' and '\''--fixup:reword'\'' cannot be used together" >expect &&
     -	test_must_fail git commit --fixup=reword:HEAD~ -m "reword commit message" 2>actual &&
    -+	cat >expect <<-EOF &&
    -+	amend! $(git log -1 --format=%s HEAD~)
    +-	test_cmp expect actual
    ++	git commit --fixup=reword:HEAD~ -m "reword commit message" &&
    ++	test_commit_message HEAD <<-EOF
    ++	amend! $(git log -1 --format=%s HEAD~2)
     +
    -+	amend commit message
    ++	reword commit message
     +	EOF
    -+	git commit --fixup=amend:HEAD~ -m "amend commit message" &&
    -+	get_commit_msg HEAD >actual &&
    - 	test_cmp expect actual
      '
      
     -test_expect_success '--fixup=amend: error out with -m option' '
    -+test_expect_success '--fixup=reword: with -m option' '
    ++test_expect_success 'commit --fixup=amend: works with -m' '
      	commit_for_rebase_autosquash_setup &&
     -	echo "fatal: options '\''-m'\'' and '\''--fixup:amend'\'' cannot be used together" >expect &&
     -	test_must_fail git commit --fixup=amend:HEAD~ -m "amend commit message" 2>actual &&
    -+	cat >expect <<-EOF &&
    -+	amend! $(git log -1 --format=%s HEAD~)
    +-	test_cmp expect actual
    ++	git commit --fixup=amend:HEAD~ -m "amend commit message" &&
    ++	test_commit_message HEAD <<-EOF
    ++	amend! $(git log -1 --format=%s HEAD~2)
     +
    -+	reword commit message
    ++	amend commit message
     +	EOF
    -+	git commit --fixup=reword:HEAD~ -m "reword commit message" &&
    -+	get_commit_msg HEAD >actual &&
    - 	test_cmp expect actual
      '
      
    + test_expect_success 'consecutive amend! commits remove amend! line from commit msg body' '
     @@ t/t7500-commit-template-squash-signoff.sh: test_expect_success 'deny to create amend! commit if its commit msg body is empt
      	test_cmp expected actual
      '
      
    -+test_expect_success '--fixup=amend: -m with empty message aborts' '
    ++test_expect_success 'deny to create amend! commit if -m is empty' '
     +	commit_for_rebase_autosquash_setup &&
    -+	test_must_fail git commit --fixup=amend:HEAD~ -m "" 2>err &&
    -+	test_grep "empty commit message body" err
    ++	echo "Aborting commit due to empty commit message body." >expect &&
    ++	test_must_fail git commit --fixup=amend:HEAD~ -m "" 2>actual &&
    ++	test_cmp expect actual
     +'
     +
      test_expect_success 'amend! commit allows empty commit msg body with --allow-empty-message' '
    @@ t/t7500-commit-template-squash-signoff.sh: test_expect_success '--fixup=reword:
     -test_expect_success '--fixup=reword: -F give error message' '
     -	echo "fatal: options '\''-F'\'' and '\''--fixup'\'' cannot be used together" >expect &&
     -	test_must_fail git commit --fixup=reword:HEAD~ -F msg  2>actual &&
    -+test_expect_success '--fixup=reword: with -F option' '
    -+	commit_for_rebase_autosquash_setup &&
    -+	echo "message from file" >msgfile &&
    -+	cat >expect <<-EOF &&
    -+	amend! $(git log -1 --format=%s HEAD~)
    -+
    -+	message from file
    -+	EOF
    -+	git commit --fixup=reword:HEAD~ -F msgfile &&
    -+	get_commit_msg HEAD >actual &&
    - 	test_cmp expect actual
    - '
    - 
    -+test_expect_success '--fixup=amend: with -F option' '
    +-	test_cmp expect actual
    ++test_expect_success 'commit --fixup works with -F' '
     +	commit_for_rebase_autosquash_setup &&
    -+	echo "amend message from file" >msgfile &&
    -+	cat >expect <<-EOF &&
    -+	amend! $(git log -1 --format=%s HEAD~)
    ++	echo "message" >msgfile &&
    ++	git commit --fixup HEAD~ -F msgfile &&
    ++	test_commit_message HEAD <<-EOF
    ++	fixup! $(git log -1 --format=%s HEAD~2)
     +
    -+	amend message from file
    ++	message
     +	EOF
    -+	git commit --fixup=amend:HEAD~ -F msgfile &&
    -+	get_commit_msg HEAD >actual &&
    -+	test_cmp expect actual
     +'
     +
    -+test_expect_success '-F with plain --fixup still errors' '
    ++test_expect_success 'commit --fixup=reword: works with -F' '
     +	commit_for_rebase_autosquash_setup &&
    -+	echo "message" >msgfile &&
    -+	test_must_fail git commit --fixup HEAD~ -F msgfile 2>err &&
    -+	test_grep "cannot be used together" err
    -+'
    ++	echo "message from file" >msgfile &&
    ++	git commit --fixup=reword:HEAD~ -F msgfile &&
    ++	test_commit_message HEAD <<-EOF
    ++	amend! $(git log -1 --format=%s HEAD~2)
     +
    ++	$(cat msgfile)
    ++	EOF
    + '
    + 
      test_expect_success 'commit --squash works with -F' '
    - 	commit_for_rebase_autosquash_setup &&
    - 	echo "log message from file" >msgfile &&
    +@@ t/t7500-commit-template-squash-signoff.sh: test_expect_success 'invalid message options when using --fixup' '
    + 	git add foo &&
    + 	test_must_fail git commit --fixup HEAD~1 --squash HEAD~2 &&
    + 	test_must_fail git commit --fixup HEAD~1 -C HEAD~2 &&
    +-	test_must_fail git commit --fixup HEAD~1 -c HEAD~2 &&
    +-	test_must_fail git commit --fixup HEAD~1 -F log
    ++	test_must_fail git commit --fixup HEAD~1 -c HEAD~2
    + '
    + 
    + cat >expected-template <<EOF
-:  ---------- > 2:  b3fc743abf commit: allow -c/-C for all kinds of --fixup

base-commit: 208068f2d8ae29d7edaa245d9975b1b22ec65738
-- 
2.54.0.1014.g842965a2d5


^ permalink raw reply

* Thought Your IT Background Might Be a Good Fit
From: Victor @ 2026-05-26  9:58 UTC (permalink / raw)
  To: git

Hi Noômen Ben Hassin,

I saw your profile on GitHub and liked your background in IT. I’m reaching out because I’m working with a flexible remote opportunity that may align with your technical experience.

Would you be open to a quick overview? If not, no worries at all.

Best,
Victor

^ permalink raw reply

* [PATCH 2/2] update-index: add --refresh-stat-only
From: George Giorgidze via GitGitGadget @ 2026-05-26  8:03 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, George Giorgidze, George Giorgidze
In-Reply-To: <pull.2125.git.1779782591.gitgitgadget@gmail.com>

From: George Giorgidze <giorgidze@meta.com>

When a working tree is copied from another machine, or restored from
a tarball, container image, or CI cache on the same machine, the
files may be byte-for-byte identical while cached stat data in the
index no longer matches. Backup and sync tools can preserve mtimes,
but fields like inode and device numbers are filesystem-local, so
large repositories can still end up paying for expensive refresh
checks on every "git status".

Git already has runtime configuration for reducing which stat fields
are checked, such as core.checkStat=<minimal|default>. That affects
how future checks interpret cached stat data, but it does not provide
a one-shot way to update the index's cached stat data to match the
current filesystem without also rehashing file contents. Setting
core.checkStat=minimal is "sticky": it weakens every subsequent
operation in the repository for the duration of the configuration,
rather than performing a single, bounded correction at a well-defined
point.

A similar idea was discussed on the list in January 2017 under the
name "--assume-content-unchanged"; see the thread starting at
<20170105112359.GN8116@chrystal.oracle.com>. The concern raised there
was that exposing a way to update cached stat data without content
comparison opens the index to abuse: an interactive user could skip a
slow refresh, lie to Git about the worktree, then file a bug after a
later merge corrupts a file. That concern is taken seriously here,
and this proposal is deliberately narrower than the 2017 one:

  * It is a one-shot action, not a sticky configuration or per-entry
    bit. The name --refresh-stat-only reflects that: it describes
    what the command does in a single invocation, not a trust state
    attached to entries (contrast with --assume-unchanged).

  * The trust assertion is intended for closed-loop callers (CI cache
    restore, container provisioning, backup/restore tooling) where
    the worktree and the index were produced or verified together by
    the same process. It is not a knob for interactive users to reach
    for when "git status" feels slow.

  * The failure mode is named directly in the documentation: if the
    worktree does not in fact match the index, affected entries will
    appear clean while the recorded object ID remains stale. The user
    must type the flag, having read the warning. This is a narrower
    contract than core.checkStat=minimal, which silently affects
    every subsequent operation.

Container-based CI has become the dominant deployment model in the
years since that 2017 discussion. The current workaround -- setting
core.checkStat=minimal in every job step, or accepting the cost of
full content rehashing -- is operationally fragile: it requires every
step in every pipeline to set and preserve the configuration, and it
permanently weakens stat semantics for every command those steps
run. A single explicit invocation at restore time is a tighter, more
local fix.

Teach git update-index --refresh-stat-only to refresh only cached
stat information. It follows the existing refresh machinery, but
skips ie_modified() and treats racy entries as dirty by stat instead
of resolving them by content. Like --really-refresh, it ignores the
"assume unchanged" setting, so stale stat data on those entries is
still updated; that behaviour is documented alongside the flag.

The preload pass is extended to recognise REFRESH_STAT_ONLY (on top
of REFRESH_REALLY, which was wired up in the preceding commit) so
that assume-unchanged entries are not marked uptodate before the main
refresh path can update them.

Add tests covering object ID preservation, missing-file handling with
and without --ignore-missing, assume-unchanged override, and quiet
output.

Signed-off-by: George Giorgidze <giorgidze@meta.com>
---
 Documentation/git-update-index.adoc       | 19 ++++++++
 builtin/update-index.c                    | 12 +++++
 preload-index.c                           |  2 +-
 read-cache-ll.h                           |  3 ++
 read-cache.c                              | 24 +++++----
 t/meson.build                             |  1 +
 t/t2109-update-index-refresh-stat-only.sh | 59 +++++++++++++++++++++++
 7 files changed, 110 insertions(+), 10 deletions(-)
 create mode 100755 t/t2109-update-index-refresh-stat-only.sh

diff --git a/Documentation/git-update-index.adoc b/Documentation/git-update-index.adoc
index 9bea9fab9a..2840a52ae4 100644
--- a/Documentation/git-update-index.adoc
+++ b/Documentation/git-update-index.adoc
@@ -109,6 +109,25 @@ you will need to handle the situation manually.
 	Like `--refresh`, but checks stat information unconditionally,
 	without regard to the "assume unchanged" setting.
 
+--refresh-stat-only::
+	Like `--refresh`, but updates only the stat information
+	in the index, without rehashing the file contents. This is
+	useful for large repositories after a working tree has been
+	produced or restored by means other than a normal checkout --
+	for example, a CI cache restore, container provisioning, or
+	copying a working tree from another machine -- when the file
+	contents are known to be correct but the cached stat
+	information no longer matches. Some backup and syncing tools
+	preserve mtimes, but inode numbers, device identifiers, and
+	other filesystem-specific stat fields generally cannot be
+	preserved across machines or even across mounts on the same
+	machine. Like `--really-refresh`, this option disregards the
+	"assume unchanged" setting so that stale stat data on those
+	entries is still updated. Use with care: if the worktree
+	content does not actually match what the index records, the
+	affected entries will appear clean while the recorded object
+	ID remains stale.
+
 --skip-worktree::
 --no-skip-worktree::
 	When one of these flags is specified, the object names recorded
diff --git a/builtin/update-index.c b/builtin/update-index.c
index 8a5907767b..5e5d2e77c9 100644
--- a/builtin/update-index.c
+++ b/builtin/update-index.c
@@ -766,6 +766,14 @@ static int really_refresh_callback(const struct option *opt,
 	return refresh(opt->value, REFRESH_REALLY);
 }
 
+static int refresh_stat_only_callback(const struct option *opt,
+				const char *arg, int unset)
+{
+	BUG_ON_OPT_NEG(unset);
+	BUG_ON_OPT_ARG(arg);
+	return refresh(opt->value, REFRESH_STAT_ONLY);
+}
+
 static int chmod_callback(const struct option *opt,
 				const char *arg, int unset)
 {
@@ -957,6 +965,10 @@ int cmd_update_index(int argc,
 			N_("like --refresh, but ignore assume-unchanged setting"),
 			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
 			really_refresh_callback),
+		OPT_CALLBACK_F(0, "refresh-stat-only", &refresh_args, NULL,
+			N_("refresh stat information without checking content"),
+			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
+			refresh_stat_only_callback),
 		{
 			.type = OPTION_LOWLEVEL_CALLBACK,
 			.long_name = "cacheinfo",
diff --git a/preload-index.c b/preload-index.c
index 88bb486329..9ecc6e4af0 100644
--- a/preload-index.c
+++ b/preload-index.c
@@ -89,7 +89,7 @@ static void *preload_thread(void *_data)
 		p->t2_nr_lstat++;
 		if (lstat(ce->name, &st))
 			continue;
-		if (p->refresh_flags & REFRESH_REALLY)
+		if (p->refresh_flags & (REFRESH_REALLY | REFRESH_STAT_ONLY))
 			ce_option |= CE_MATCH_IGNORE_VALID;
 		if (ie_match_stat(index, ce, &st, ce_option))
 			continue;
diff --git a/read-cache-ll.h b/read-cache-ll.h
index 2c8b4b21b1..7e4b555a31 100644
--- a/read-cache-ll.h
+++ b/read-cache-ll.h
@@ -425,6 +425,8 @@ void *read_blob_data_from_index(struct index_state *, const char *, unsigned lon
 #define CE_MATCH_REFRESH		0x10
 /* don't refresh_fsmonitor state or do stat comparison even if CE_FSMONITOR_VALID is true */
 #define CE_MATCH_IGNORE_FSMONITOR 0X20
+/* update stat info without checking content */
+#define CE_MATCH_STAT_ONLY		0x40
 int is_racy_timestamp(const struct index_state *istate,
 		      const struct cache_entry *ce);
 int has_racy_timestamp(struct index_state *istate);
@@ -452,6 +454,7 @@ int fake_lstat(const struct cache_entry *ce, struct stat *st);
 #define REFRESH_IN_PORCELAIN             (1 << 5) /* user friendly output, not "needs update" */
 #define REFRESH_PROGRESS                 (1 << 6) /* show progress bar if stderr is tty */
 #define REFRESH_IGNORE_SKIP_WORKTREE     (1 << 7) /* ignore skip_worktree entries */
+#define REFRESH_STAT_ONLY                (1 << 8) /* update stat info without checking content */
 int refresh_index(struct index_state *, unsigned int flags, const struct pathspec *pathspec, char *seen, const char *header_msg);
 /*
  * Refresh the index and write it to disk.
diff --git a/read-cache.c b/read-cache.c
index f5023b9a8b..ec9419cbe2 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1350,6 +1350,7 @@ static struct cache_entry *refresh_cache_ent(struct index_state *istate,
 	int ignore_skip_worktree = options & CE_MATCH_IGNORE_SKIP_WORKTREE;
 	int ignore_missing = options & CE_MATCH_IGNORE_MISSING;
 	int ignore_fsmonitor = options & CE_MATCH_IGNORE_FSMONITOR;
+	int stat_only = options & CE_MATCH_STAT_ONLY;
 
 	if (!refresh || ce_uptodate(ce))
 		return ce;
@@ -1420,12 +1421,14 @@ static struct cache_entry *refresh_cache_ent(struct index_state *istate,
 		}
 	}
 
-	if (t2_did_scan)
-		*t2_did_scan = 1;
-	if (ie_modified(istate, ce, &st, options)) {
-		if (err)
-			*err = EINVAL;
-		return NULL;
+	if (!stat_only) {
+		if (t2_did_scan)
+			*t2_did_scan = 1;
+		if (ie_modified(istate, ce, &st, options)) {
+			if (err)
+				*err = EINVAL;
+			return NULL;
+		}
 	}
 
 	updated = make_empty_cache_entry(istate, ce_namelen(ce));
@@ -1490,11 +1493,14 @@ int refresh_index(struct index_state *istate, unsigned int flags,
 	int not_new = (flags & REFRESH_IGNORE_MISSING) != 0;
 	int ignore_submodules = (flags & REFRESH_IGNORE_SUBMODULES) != 0;
 	int ignore_skip_worktree = (flags & REFRESH_IGNORE_SKIP_WORKTREE) != 0;
+	int stat_only = (flags & REFRESH_STAT_ONLY) != 0;
 	int first = 1;
 	int in_porcelain = (flags & REFRESH_IN_PORCELAIN);
 	unsigned int options = (CE_MATCH_REFRESH |
-				(really ? CE_MATCH_IGNORE_VALID : 0) |
-				(not_new ? CE_MATCH_IGNORE_MISSING : 0));
+				((really || stat_only) ? CE_MATCH_IGNORE_VALID : 0) |
+				(not_new ? CE_MATCH_IGNORE_MISSING : 0) |
+				(stat_only ? (CE_MATCH_STAT_ONLY |
+					      CE_MATCH_RACY_IS_DIRTY) : 0));
 	const char *modified_fmt;
 	const char *deleted_fmt;
 	const char *typechange_fmt;
@@ -1520,7 +1526,7 @@ int refresh_index(struct index_state *istate, unsigned int flags,
 	 * cache entries quickly then in the single threaded loop below,
 	 * we only have to do the special cases that are left.
 	 */
-	preload_index(istate, pathspec, flags & REFRESH_REALLY);
+	preload_index(istate, pathspec, flags & (REFRESH_REALLY | REFRESH_STAT_ONLY));
 	trace2_region_enter("index", "refresh", NULL);
 
 	for (i = 0; i < istate->cache_nr; i++) {
diff --git a/t/meson.build b/t/meson.build
index fd955f44ef..e1e68921b4 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -291,6 +291,7 @@ integration_tests = [
   't2106-update-index-assume-unchanged.sh',
   't2107-update-index-basic.sh',
   't2108-update-index-refresh-racy.sh',
+  't2109-update-index-refresh-stat-only.sh',
   't2200-add-update.sh',
   't2201-add-update-typechange.sh',
   't2202-add-addremove.sh',
diff --git a/t/t2109-update-index-refresh-stat-only.sh b/t/t2109-update-index-refresh-stat-only.sh
new file mode 100755
index 0000000000..404d2a6fb2
--- /dev/null
+++ b/t/t2109-update-index-refresh-stat-only.sh
@@ -0,0 +1,59 @@
+#!/bin/sh
+
+test_description='git update-index --refresh-stat-only'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	test_commit initial base-file base
+'
+
+test_expect_success '--refresh-stat-only updates stat info without rehashing' '
+	test_commit refresh-stat refresh-stat original &&
+	git ls-files --stage -- refresh-stat >expect &&
+	git ls-files --debug refresh-stat | grep mtime >before &&
+	printf "modified\n" >refresh-stat &&
+	test-tool chmtime -100000 refresh-stat &&
+	test_must_fail git diff-files --quiet -- refresh-stat &&
+	git update-index --refresh-stat-only &&
+	git ls-files --debug refresh-stat | grep mtime >after &&
+	! test_cmp before after &&
+	git ls-files --stage -- refresh-stat >actual &&
+	test_cmp expect actual &&
+	git diff-files --quiet -- refresh-stat
+'
+
+test_expect_success '--refresh-stat-only ignores assume-unchanged' '
+	test_commit assume-unchanged assume-unchanged old &&
+	git update-index --assume-unchanged assume-unchanged &&
+	printf "new\n" >assume-unchanged &&
+	test-tool chmtime -100000 assume-unchanged &&
+	GIT_TEST_PRELOAD_INDEX=1 git update-index --refresh-stat-only &&
+	git update-index --no-assume-unchanged assume-unchanged &&
+	git diff-files --quiet -- assume-unchanged
+'
+
+test_expect_success '--refresh-stat-only with missing file and --ignore-missing' '
+	test_commit missing-ignore missing-ignore content &&
+	rm missing-ignore &&
+	git update-index --ignore-missing --refresh-stat-only &&
+	git checkout -- missing-ignore
+'
+
+test_expect_success '--refresh-stat-only reports error on missing file without --ignore-missing' '
+	test_commit missing-error missing-error content &&
+	rm missing-error &&
+	test_must_fail git update-index --refresh-stat-only >out 2>err &&
+	test_grep "needs update" out &&
+	git checkout -- missing-error
+'
+
+test_expect_success '--refresh-stat-only with -q is quiet' '
+	test_commit missing-quiet missing-quiet content &&
+	rm missing-quiet &&
+	git update-index -q --ignore-missing --refresh-stat-only >out 2>err &&
+	test_must_be_empty out &&
+	test_must_be_empty err
+'
+
+test_done
-- 
gitgitgadget

^ permalink raw reply related

* [PATCH 1/2] preload-index: respect --really-refresh override of assume-unchanged
From: George Giorgidze via GitGitGadget @ 2026-05-26  8:03 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, George Giorgidze, George Giorgidze
In-Reply-To: <pull.2125.git.1779782591.gitgitgadget@gmail.com>

From: George Giorgidze <giorgidze@meta.com>

When refresh_index() is invoked with REFRESH_REALLY (e.g. via
"git update-index --really-refresh"), the documented behaviour is that
the "assume unchanged" bit on cache entries is disregarded so that
stale stat data on those entries is still refreshed.

The preload pass runs before the single-threaded refresh loop and is
intended to mark up-to-date entries quickly so the slow path only has
to deal with the leftovers. However, preload_thread() unconditionally
called ie_match_stat() with CE_MATCH_RACY_IS_DIRTY|CE_MATCH_IGNORE_FSMONITOR
and never with CE_MATCH_IGNORE_VALID, so it honoured the "assume
unchanged" bit. When a modified file's entry was marked
assume-unchanged, preload would conclude the entry was clean and call
ce_mark_uptodate(); the subsequent --really-refresh loop would then
skip the entry (because ce_uptodate(ce) is true) and never report it
as needing an update.

This only manifests when preload is active, so it has been latent in
default configurations. It is observable today via GIT_TEST_PRELOAD_INDEX=1.

Plumb the refresh flags through to the preload threads via a new
refresh_flags field on struct thread_data, and have preload_thread()
add CE_MATCH_IGNORE_VALID to its match options when REFRESH_REALLY is
in effect. Update refresh_index() to pass "flags & REFRESH_REALLY" to
preload_index() instead of a bare 0.

Add a regression test under t2106 that forces preload on and confirms
that "update-index --really-refresh" reports a modified
assume-unchanged entry as needing update.

Signed-off-by: George Giorgidze <giorgidze@meta.com>
---
 preload-index.c                          |  7 ++++++-
 read-cache.c                             |  2 +-
 t/t2106-update-index-assume-unchanged.sh | 11 +++++++++++
 3 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/preload-index.c b/preload-index.c
index b222821b44..88bb486329 100644
--- a/preload-index.c
+++ b/preload-index.c
@@ -42,6 +42,7 @@ struct thread_data {
 	struct progress_data *progress;
 	int offset, nr;
 	int t2_nr_lstat;
+	unsigned int refresh_flags;
 };
 
 static void *preload_thread(void *_data)
@@ -60,6 +61,7 @@ static void *preload_thread(void *_data)
 	do {
 		struct cache_entry *ce = *cep++;
 		struct stat st;
+		unsigned int ce_option = CE_MATCH_RACY_IS_DIRTY | CE_MATCH_IGNORE_FSMONITOR;
 
 		if (ce_stage(ce))
 			continue;
@@ -87,7 +89,9 @@ static void *preload_thread(void *_data)
 		p->t2_nr_lstat++;
 		if (lstat(ce->name, &st))
 			continue;
-		if (ie_match_stat(index, ce, &st, CE_MATCH_RACY_IS_DIRTY|CE_MATCH_IGNORE_FSMONITOR))
+		if (p->refresh_flags & REFRESH_REALLY)
+			ce_option |= CE_MATCH_IGNORE_VALID;
+		if (ie_match_stat(index, ce, &st, ce_option))
 			continue;
 		ce_mark_uptodate(ce);
 		mark_fsmonitor_valid(index, ce);
@@ -150,6 +154,7 @@ void preload_index(struct index_state *index,
 			copy_pathspec(&p->pathspec, pathspec);
 		p->offset = offset;
 		p->nr = work;
+		p->refresh_flags = refresh_flags;
 		if (pd.progress)
 			p->progress = &pd;
 		offset += work;
diff --git a/read-cache.c b/read-cache.c
index 38a04b8de3..f5023b9a8b 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1520,7 +1520,7 @@ int refresh_index(struct index_state *istate, unsigned int flags,
 	 * cache entries quickly then in the single threaded loop below,
 	 * we only have to do the special cases that are left.
 	 */
-	preload_index(istate, pathspec, 0);
+	preload_index(istate, pathspec, flags & REFRESH_REALLY);
 	trace2_region_enter("index", "refresh", NULL);
 
 	for (i = 0; i < istate->cache_nr; i++) {
diff --git a/t/t2106-update-index-assume-unchanged.sh b/t/t2106-update-index-assume-unchanged.sh
index 6b2ccc21a9..266c88d45d 100755
--- a/t/t2106-update-index-assume-unchanged.sh
+++ b/t/t2106-update-index-assume-unchanged.sh
@@ -24,4 +24,15 @@ test_expect_success 'do not switch branches with dirty file' '
 	test_grep overwritten err
 '
 
+test_expect_success '--really-refresh overrides assume-unchanged under preload' '
+	git reset --hard &&
+	test_commit really-refresh really-refresh original &&
+	git update-index --assume-unchanged really-refresh &&
+	printf "modified\n" >really-refresh &&
+	test-tool chmtime -100000 really-refresh &&
+	test_must_fail env GIT_TEST_PRELOAD_INDEX=1 \
+		git update-index --really-refresh >out 2>err &&
+	test_grep "needs update" out
+'
+
 test_done
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH 0/2] update-index: add --refresh-stat-only
From: George Giorgidze via GitGitGadget @ 2026-05-26  8:03 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, George Giorgidze

This two-patch series adds "git update-index --refresh-stat-only", a
one-shot way to update the index's cached stat data to match the current
filesystem without rehashing file contents.

When a working tree is produced or restored by means other than a normal
checkout -- a CI cache restore, container provisioning, a tarball
extraction, or a copy from another machine -- the files may be byte-for-byte
identical while filesystem-local stat fields like inode and device numbers
no longer match. Today the available workarounds are to (a) pay for full
content rehashing on the next "git status", or (b) set
core.checkStat=minimal, which is sticky and weakens every subsequent
operation. Neither composes well with modern container-based CI, where every
job step would otherwise need to set and preserve the configuration.

A similar idea ("--assume-content-unchanged") was discussed in January 2017;
see the thread starting at 20170105112359.GN8116@chrystal.oracle.com. The
concern raised there was that exposing a way to update cached stat data
without content comparison opens the index to abuse. The flag in this series
is deliberately narrower than the 2017 proposal:

 * one-shot action, not a sticky config or per-entry bit;
 * the name describes what the invocation does, not a trust state attached
   to entries (contrast --assume-unchanged);
 * intended for closed-loop callers (CI cache restore, container
   provisioning, backup/restore tooling) that produced or verified the
   worktree atomically;
 * the failure mode -- stale object IDs becoming invisible until the next
   content check -- is named directly in the docs, and the flag must be
   typed explicitly.

The series is organised so the bug fix is reviewable on its own:

1/2 preload-index: respect --really-refresh override of assume-unchanged

   A latent bug observable today via GIT_TEST_PRELOAD_INDEX=1:
   preload_thread() never sets CE_MATCH_IGNORE_VALID, so it
   honours the "assume unchanged" bit and marks modified
   assume-unchanged entries as uptodate before the
   --really-refresh loop sees them. Plumb refresh flags
   through to preload threads and add a regression test under
   t2106.


2/2 update-index: add --refresh-stat-only

   Add the new flag, extend the preload mask to also recognise
   REFRESH_STAT_ONLY, document the assume-unchanged override
   behaviour alongside the flag, and add coverage for object
   ID preservation, missing-file handling (with and without
   --ignore-missing), assume-unchanged override, and quiet
   output under t2109.


George Giorgidze (2):
  preload-index: respect --really-refresh override of assume-unchanged
  update-index: add --refresh-stat-only

 Documentation/git-update-index.adoc       | 19 ++++++++
 builtin/update-index.c                    | 12 +++++
 preload-index.c                           |  7 ++-
 read-cache-ll.h                           |  3 ++
 read-cache.c                              | 24 +++++----
 t/meson.build                             |  1 +
 t/t2106-update-index-assume-unchanged.sh  | 11 +++++
 t/t2109-update-index-refresh-stat-only.sh | 59 +++++++++++++++++++++++
 8 files changed, 126 insertions(+), 10 deletions(-)
 create mode 100755 t/t2109-update-index-refresh-stat-only.sh


base-commit: 56a4f3c3a221adf1df9b39da69b8a6890f803157
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2125%2Fgiorgidze%2Fmaster-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2125/giorgidze/master-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2125
-- 
gitgitgadget

^ permalink raw reply

* Re: [PATCH v2 2/3] commit-reach: deduplicate queue entries in paint_down_to_common
From: Kristofer Karlsson @ 2026-05-26  6:57 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Kristofer Karlsson via GitGitGadget, git, Derrick Stolee,
	Jeff King
In-Reply-To: <xmqqzf1ncded.fsf@gitster.g>

On Tue, 26 May 2026 at 00:50, Junio C Hamano <gitster@pobox.com> wrote:
> OK.  I guess an obvious alternative design would be to have an
> associated hashtable for deduping, or tweak prio_queue_get() so
> that it notices duplicated entry just before it returns (i.e.,
> peek and discard until queue->array[0].data is different from
> what you are going to return).  Both would not beat the cheap cost
> of using a single bit per object, I guess ;-)

Yes, I think a hashtable or hashset would work here too. I realize that I
have done a lot of local experimentation with alternative approaches but I
forgot to mention the ones I discarded for various reasons - but that
would be useful information for you to have too. Let me rectify that here.

oidset instead of enqueued flag: Works fine, but is ~15-20% slower end-to-end.
Both are O(1) but the overhead is quite significant compared to a flag.

Peek and discard: the problem here is that the commits are not necessarily
ordered. We can have a sequence of A,B,A if we are unlucky. What I did try
however was an alternative to this - just change the fast-exit heuristic to
overshoot until comparison returns > 0 - i.e. consume some
extra commits in the queue. This works and in my example data we typically
would only need to walk ~16 extra commits with this heuristic, so it's not
bad at all. But the extra comparisons we need to run on each iteration make
it ~15-20% slower.

Another thing I tried was simply tracking the minimum generation seen and
terminate as soon as we have gone past it. This is fast and simple and does
not require deduping, but it only works if we have a commit graph and
generation numbers.

The advantage of the approach with deduping via the ENQUEUED flag and then
just tracking the most recently enqueued commit is that it works independently
of ordering guarantees. All it needs to work is the fact that we can prove
that we have reached a point where queue no longer has any non-stale commits
at all.

Summary:
  Approach        Dedup         Works w/o commit-graph?  Speed
  ENQUEUED flag   yes (1 bit)   yes                      fastest
  Hashtable       yes           yes                      15-20% slower
  Peek-discard    -             -                        broken
  Cmp overshoot   no            yes                      15-20% slower
  Gen overshoot   no            no                       same as ENQUEUED

^ permalink raw reply

* [PATCH v2 8/8] setup: construct object database in `apply_repository_format()`
From: Patrick Steinhardt @ 2026-05-26  5:57 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Junio C Hamano
In-Reply-To: <20260526-b4-pks-setup-centralize-odb-creation-v2-0-2fa5b385c13e@pks.im>

With the preceding changes we now always construct the repository's
object database before applying the repository format. Remove this
duplication by constructing it in `apply_repository_format()` instead.

Note that we create the object database _after_ having set up the
repository's hash algorithm, but _before_ setting the compat hash
algorithm. This is intentional:

  - Constructing the object database may require knowledge of its
    intended object format.

  - Setting up the compatibility hash requires the object database to be
    initialized already, because we immediately read the loose object
    map.

The first point is sensible, the second maybe a little less so. Ideally,
it should be the responsibility of the object database itself to
initialize any data structures required for the compatibility hash. But
this would require further changes, so this is kept as-is for now.

Further note that this requires us to move handling of the environment
variables GIT_OBJECT_DIRECTORY and GIT_ALTERNATE_OBJECT_DIRECTORIES into
the repository format, as well. This allows the caller more flexibility
around whether or not those environment variables are being honored, as
we want to respect them in "setup.c", but not in "repository.c".

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 repository.c |  4 +---
 setup.c      | 45 +++++++++++++++++++++------------------------
 setup.h      | 10 ++++++++++
 3 files changed, 32 insertions(+), 27 deletions(-)

diff --git a/repository.c b/repository.c
index 61dfbb8be6..187dd471c4 100644
--- a/repository.c
+++ b/repository.c
@@ -291,13 +291,11 @@ int repo_init(struct repository *repo,
 	if (read_repository_format_from_commondir(&format, repo->commondir))
 		goto error;
 
-	if (apply_repository_format(repo, &format, &err) < 0) {
+	if (apply_repository_format(repo, &format, 0, &err) < 0) {
 		warning("%s", err.buf);
 		goto error;
 	}
 
-	repo->objects = odb_new(repo, NULL, NULL);
-
 	if (worktree)
 		repo_set_worktree(repo, worktree);
 
diff --git a/setup.c b/setup.c
index 4a8d6230b1..513fc88749 100644
--- a/setup.c
+++ b/setup.c
@@ -1752,12 +1752,22 @@ enum discovery_result discover_git_directory_reason(struct strbuf *commondir,
 
 int apply_repository_format(struct repository *repo,
 			    const struct repository_format *format,
+			    enum apply_repository_format_flags flags,
 			    struct strbuf *err)
 {
+	char *object_directory = NULL, *alternate_object_directories = NULL;
+
 	if (verify_repository_format(format, err) < 0)
 		return -1;
 
+	if (flags & APPLY_REPOSITORY_FORMAT_HONOR_ENV) {
+		object_directory = xstrdup_or_null(getenv(DB_ENVIRONMENT));
+		alternate_object_directories = xstrdup_or_null(getenv(ALTERNATE_DB_ENVIRONMENT));
+	}
+
 	repo_set_hash_algo(repo, format->hash_algo);
+	repo->objects = odb_new(repo, object_directory,
+				alternate_object_directories);
 	repo_set_compat_hash_algo(repo, format->compat_hash_algo);
 	repo_set_ref_storage_format(repo,
 				    format->ref_storage_format,
@@ -1773,6 +1783,8 @@ int apply_repository_format(struct repository *repo,
 	repo->repository_format_precious_objects =
 		format->precious_objects;
 
+	free(alternate_object_directories);
+	free(object_directory);
 	return 0;
 }
 
@@ -1785,7 +1797,8 @@ int apply_repository_format(struct repository *repo,
  * If successful and fmt is not NULL, fill fmt with data.
  */
 static void check_and_apply_repository_format(struct repository *repo,
-					      struct repository_format *fmt)
+					      struct repository_format *fmt,
+					      enum apply_repository_format_flags flags)
 {
 	struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
 	struct strbuf err = STRBUF_INIT;
@@ -1794,7 +1807,7 @@ static void check_and_apply_repository_format(struct repository *repo,
 		fmt = &repo_fmt;
 
 	check_repository_format_gently(repo_get_git_dir(repo), fmt, NULL);
-	if (apply_repository_format(repo, fmt, &err) < 0)
+	if (apply_repository_format(repo, fmt, flags, &err) < 0)
 		die("%s", err.buf);
 	startup_info->have_repository = 1;
 
@@ -1874,15 +1887,9 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags
 	}
 
 	if (is_git_directory(".")) {
-		struct strvec to_free = STRVEC_INIT;
-
 		set_git_dir(repo, ".", 0);
-		repo->objects = odb_new(repo,
-					getenv_safe(&to_free, DB_ENVIRONMENT),
-					getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT));
-		check_and_apply_repository_format(repo, NULL);
-
-		strvec_clear(&to_free);
+		check_and_apply_repository_format(repo, NULL,
+						  APPLY_REPOSITORY_FORMAT_HONOR_ENV);
 		return path;
 	}
 
@@ -2034,8 +2041,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 	    startup_info->have_repository ||
 	    /* GIT_DIR_EXPLICIT */
 	    getenv(GIT_DIR_ENVIRONMENT)) {
-		struct strvec to_free = STRVEC_INIT;
-
 		if (!repo->gitdir) {
 			const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);
 			if (!gitdir)
@@ -2046,17 +2051,13 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 		if (startup_info->have_repository) {
 			struct strbuf err = STRBUF_INIT;
 
-			repo->objects = odb_new(repo,
-						getenv_safe(&to_free, DB_ENVIRONMENT),
-						getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT));
-			if (apply_repository_format(repo, &repo_fmt, &err) < 0)
+			if (apply_repository_format(repo, &repo_fmt,
+						    APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0)
 				die("%s", err.buf);
 
 			clear_repository_format(&repo_fmt);
 			strbuf_release(&err);
 		}
-
-		strvec_clear(&to_free);
 	}
 	/*
 	 * Since precompose_string_if_needed() needs to look at
@@ -2805,7 +2806,6 @@ int init_db(struct repository *repo,
 	int exist_ok = flags & INIT_DB_EXIST_OK;
 	char *original_git_dir = real_pathdup(git_dir, 1);
 	struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
-	struct strvec to_free = STRVEC_INIT;
 
 	if (real_git_dir) {
 		struct stat st;
@@ -2826,16 +2826,14 @@ int init_db(struct repository *repo,
 	}
 	startup_info->have_repository = 1;
 
-	repo->objects = odb_new(repo, getenv_safe(&to_free, DB_ENVIRONMENT),
-				getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT));
-
 	/*
 	 * Check to see if the repository version is right.
 	 * Note that a newly created repository does not have
 	 * config file, so this will not fail.  What we are catching
 	 * is an attempt to reinitialize new repository with an old tool.
 	 */
-	check_and_apply_repository_format(repo, &repo_fmt);
+	check_and_apply_repository_format(repo, &repo_fmt,
+					  APPLY_REPOSITORY_FORMAT_HONOR_ENV);
 
 	repository_format_configure(repo, &repo_fmt, hash, ref_storage_format);
 
@@ -2892,7 +2890,6 @@ int init_db(struct repository *repo,
 	}
 
 	clear_repository_format(&repo_fmt);
-	strvec_clear(&to_free);
 	free(original_git_dir);
 	return 0;
 }
diff --git a/setup.h b/setup.h
index 5ed92f53fa..821b55aca0 100644
--- a/setup.h
+++ b/setup.h
@@ -221,6 +221,15 @@ void clear_repository_format(struct repository_format *format);
 int verify_repository_format(const struct repository_format *format,
 			     struct strbuf *err);
 
+enum apply_repository_format_flags {
+	/*
+	 * Honor environment variables when applying the repository format to
+	 * the repository. For now, this only covers environment variables that
+	 * relate to the object database.
+	 */
+	APPLY_REPOSITORY_FORMAT_HONOR_ENV = (1 << 0),
+};
+
 /*
  * Apply the given repository format to the repo. This initializes extensions
  * and basic data structures required for normal operation. Returns 0 on
@@ -228,6 +237,7 @@ int verify_repository_format(const struct repository_format *format,
  */
 int apply_repository_format(struct repository *repo,
 			    const struct repository_format *format,
+			    enum apply_repository_format_flags flags,
 			    struct strbuf *err);
 
 const char *get_template_dir(const char *option_template);

-- 
2.54.0.926.g75ba10bac6.dirty


^ permalink raw reply related

* [PATCH v2 7/8] repository: stop reading loose object map twice on repo init
From: Patrick Steinhardt @ 2026-05-26  5:57 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Junio C Hamano
In-Reply-To: <20260526-b4-pks-setup-centralize-odb-creation-v2-0-2fa5b385c13e@pks.im>

When initializing a repository via `repo_init()` we end up reading the
loose object map twice:

  - `apply_repository_format()` calls `repo_set_compat_hash_algo()`,
    which in turn calls `repo_read_loose_object_map()` if we have a
    compatibility hash configured.

  - `repo_init()` calls `repo_read_loose_object_map()` directly a second
    time.

Drop the second read of the loose object map in `repo_init()`.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 repository.c | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/repository.c b/repository.c
index 2c2395105f..61dfbb8be6 100644
--- a/repository.c
+++ b/repository.c
@@ -301,9 +301,6 @@ int repo_init(struct repository *repo,
 	if (worktree)
 		repo_set_worktree(repo, worktree);
 
-	if (repo->compat_hash_algo)
-		repo_read_loose_object_map(repo);
-
 	clear_repository_format(&format);
 	strbuf_release(&err);
 	return 0;

-- 
2.54.0.926.g75ba10bac6.dirty


^ permalink raw reply related

* [PATCH v2 6/8] setup: stop initializing object database without repository
From: Patrick Steinhardt @ 2026-05-26  5:57 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Junio C Hamano
In-Reply-To: <20260526-b4-pks-setup-centralize-odb-creation-v2-0-2fa5b385c13e@pks.im>

The function `setup_git_directory_gently()` is responsible for
discovering and setting up a Git repository based on various environment
variables and the current working directory. The result is thus a fully
usable Git repository.

One oddity of this function is that we may set up the object database
even in the case where we don't have a repository, namely in the case
where the `GIT_DIR_EXPLICIT` environment variable is set but points to a
non-existent repository. If so, we call `setup_git_env_internal()` with
the value of the environment variable so that the repository's Git
directory is configured, even if it points to a non-existent directory.

Historically though, this function didn't only configure the repository,
but also initialized the object database. We retained this behaviour
from a preceding commit, even though it really doesn't make much sense
in the first place -- there is no repository, so we don't have an object
database either. There seemingly isn't much of a reason to construct the
object database, as we typically won't try to read objects when we don't
have an object database.

There's one exception though: git-index-pack(1) may run outside of a
repository, which can be used to perform consistency checks for a
packfile. The code path is _almost_ working: we already know to call
`parse_object_buffer()`, which can read objects without an object
database being available. And that works for all object types except for
commits, because `parse_commit_buffer()` calls `parse_commit_graph()`,
and that function doesn't handle the case where we don't have an object
database.

Fix this instance to check for the object database instead of checking
for the Git directory having been initialized. With this fixed, we can
now stop constructing an object database completely.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 commit-graph.c | 4 ++--
 setup.c        | 7 +++----
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/commit-graph.c b/commit-graph.c
index 9abe62bd5a..0820cf5fb8 100644
--- a/commit-graph.c
+++ b/commit-graph.c
@@ -740,13 +740,13 @@ static struct commit_graph *prepare_commit_graph(struct repository *r)
 	struct odb_source *source;
 
 	/*
-	 * Early return if there is no git dir or if the commit graph is
+	 * Early return if there is no object database or if the commit graph is
 	 * disabled.
 	 *
 	 * This must come before the "already attempted?" check below, because
 	 * we want to disable even an already-loaded graph file.
 	 */
-	if (!r->gitdir || r->commit_graph_disabled)
+	if (!r->objects || r->commit_graph_disabled)
 		return NULL;
 
 	if (r->objects->commit_graph_attempted)
diff --git a/setup.c b/setup.c
index 0dc9fe4565..4a8d6230b1 100644
--- a/setup.c
+++ b/setup.c
@@ -2043,13 +2043,12 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 			setup_git_env_internal(repo, gitdir);
 		}
 
-		repo->objects = odb_new(repo,
-					getenv_safe(&to_free, DB_ENVIRONMENT),
-					getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT));
-
 		if (startup_info->have_repository) {
 			struct strbuf err = STRBUF_INIT;
 
+			repo->objects = odb_new(repo,
+						getenv_safe(&to_free, DB_ENVIRONMENT),
+						getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT));
 			if (apply_repository_format(repo, &repo_fmt, &err) < 0)
 				die("%s", err.buf);
 

-- 
2.54.0.926.g75ba10bac6.dirty


^ permalink raw reply related

* [PATCH v2 4/8] repository: stop initializing the object database in `repo_set_gitdir()`
From: Patrick Steinhardt @ 2026-05-26  5:56 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Junio C Hamano
In-Reply-To: <20260526-b4-pks-setup-centralize-odb-creation-v2-0-2fa5b385c13e@pks.im>

The function `repo_set_gitdir()` obviously sets the Git directory for a
given repository. Less obviously though, the function also configures a
couple of auxiliary settings.

One such thing is that we create the object database in this function.
This logic only happens conditionally though, as `set_git_dir()` may be
called multiple times during repository setup, and we don't want to
create the object database multiple times. This is somewhat tangled and
hard to follow.

Remove the logic from `repo_set_gitdir()` and instead initialize the
object database outside of it. This leads to some duplication right now,
but that duplication will be removed in a subsequent step where we will
start initializing the object database as part of applying the repo's
format.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 repository.c | 8 ++------
 repository.h | 3 ---
 setup.c      | 7 ++++---
 3 files changed, 6 insertions(+), 12 deletions(-)

diff --git a/repository.c b/repository.c
index 58a13f7c4f..2c2395105f 100644
--- a/repository.c
+++ b/repository.c
@@ -181,12 +181,6 @@ void repo_set_gitdir(struct repository *repo,
 	free(old_gitdir);
 
 	repo_set_commondir(repo, o->commondir);
-
-	if (!repo->objects)
-		repo->objects = odb_new(repo, o->object_dir, o->alternate_db);
-	else if (!o->skip_initializing_odb)
-		BUG("cannot reinitialize an already-initialized object directory");
-
 	repo->disable_ref_updates = o->disable_ref_updates;
 
 	expand_base_dir(&repo->graft_file, o->graft_file,
@@ -302,6 +296,8 @@ int repo_init(struct repository *repo,
 		goto error;
 	}
 
+	repo->objects = odb_new(repo, NULL, NULL);
+
 	if (worktree)
 		repo_set_worktree(repo, worktree);
 
diff --git a/repository.h b/repository.h
index c3ec0f4b79..36e2db2633 100644
--- a/repository.h
+++ b/repository.h
@@ -221,12 +221,9 @@ const char *repo_get_work_tree(struct repository *repo);
  */
 struct set_gitdir_args {
 	const char *commondir;
-	const char *object_dir;
 	const char *graft_file;
 	const char *index_file;
-	const char *alternate_db;
 	bool disable_ref_updates;
-	bool skip_initializing_odb;
 };
 
 void repo_set_gitdir(struct repository *repo, const char *root,
diff --git a/setup.c b/setup.c
index c5015923f1..3bd3f6c592 100644
--- a/setup.c
+++ b/setup.c
@@ -1045,17 +1045,18 @@ static void setup_git_env_internal(struct repository *repo,
 	struct strvec to_free = STRVEC_INIT;
 
 	args.commondir = getenv_safe(&to_free, GIT_COMMON_DIR_ENVIRONMENT);
-	args.object_dir = getenv_safe(&to_free, DB_ENVIRONMENT);
 	args.graft_file = getenv_safe(&to_free, GRAFT_ENVIRONMENT);
 	args.index_file = getenv_safe(&to_free, INDEX_ENVIRONMENT);
-	args.alternate_db = getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT);
 	if (getenv(GIT_QUARANTINE_ENVIRONMENT))
 		args.disable_ref_updates = true;
-	args.skip_initializing_odb = skip_initializing_odb;
 
 	repo_set_gitdir(repo, git_dir, &args);
 	strvec_clear(&to_free);
 
+	if (!skip_initializing_odb)
+		repo->objects = odb_new(repo, getenv_safe(&to_free, DB_ENVIRONMENT),
+					getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT));
+
 	if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))
 		disable_replace_refs();
 	replace_ref_base = getenv(GIT_REPLACE_REF_BASE_ENVIRONMENT);

-- 
2.54.0.926.g75ba10bac6.dirty


^ permalink raw reply related

* [PATCH v2 5/8] setup: stop creating the object database in `setup_git_env()`
From: Patrick Steinhardt @ 2026-05-26  5:57 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Junio C Hamano
In-Reply-To: <20260526-b4-pks-setup-centralize-odb-creation-v2-0-2fa5b385c13e@pks.im>

In the preceding commit we have stopped creating the object database in
`repo_set_gitdir()`. But the logic is still somewhat confusing as we
still end up creating it conditionally in `setup_git_dir()`, which is
called multiple times.

Drop the conditional logic and instead create the object database in all
places where we have discovered and configured a repository.

This leads to even more duplication than we already had in the preceding
commit, but an alert reader may notice that we now (almost) always call
`odb_new()` directly before having called `apply_repository_format()`.
The only exception to this is `setup_git_directory_gently()`, where we
also call the function when _not_ applying the repository format. This
will be fixed in the next commit, and once that's done we can then unify
creation of the object database into `apply_repository_format()`.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 setup.c | 37 ++++++++++++++++++++++++++-----------
 1 file changed, 26 insertions(+), 11 deletions(-)

diff --git a/setup.c b/setup.c
index 3bd3f6c592..0dc9fe4565 100644
--- a/setup.c
+++ b/setup.c
@@ -1035,8 +1035,7 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)
 }
 
 static void setup_git_env_internal(struct repository *repo,
-				   const char *git_dir,
-				   bool skip_initializing_odb)
+				   const char *git_dir)
 {
 	char *git_replace_ref_base;
 	const char *shallow_file;
@@ -1053,10 +1052,6 @@ static void setup_git_env_internal(struct repository *repo,
 	repo_set_gitdir(repo, git_dir, &args);
 	strvec_clear(&to_free);
 
-	if (!skip_initializing_odb)
-		repo->objects = odb_new(repo, getenv_safe(&to_free, DB_ENVIRONMENT),
-					getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT));
-
 	if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))
 		disable_replace_refs();
 	replace_ref_base = getenv(GIT_REPLACE_REF_BASE_ENVIRONMENT);
@@ -1072,10 +1067,10 @@ static void setup_git_env_internal(struct repository *repo,
 		fetch_if_missing = 0;
 }
 
-static void set_git_dir_1(struct repository *repo, const char *path, bool skip_initializing_odb)
+static void set_git_dir_1(struct repository *repo, const char *path)
 {
 	xsetenv(GIT_DIR_ENVIRONMENT, path, 1);
-	setup_git_env_internal(repo, path, skip_initializing_odb);
+	setup_git_env_internal(repo, path);
 }
 
 static void update_relative_gitdir(const char *name UNUSED,
@@ -1089,7 +1084,7 @@ static void update_relative_gitdir(const char *name UNUSED,
 	trace_printf_key(&trace_setup_key,
 			 "setup: move $GIT_DIR to '%s'",
 			 path);
-	set_git_dir_1(repo, path, true);
+	set_git_dir_1(repo, path);
 	free(path);
 }
 
@@ -1102,7 +1097,7 @@ static void set_git_dir(struct repository *repo, const char *path, int make_real
 		path = realpath.buf;
 	}
 
-	set_git_dir_1(repo, path, false);
+	set_git_dir_1(repo, path);
 	if (!is_absolute_path(path))
 		chdir_notify_register(NULL, update_relative_gitdir, repo);
 
@@ -1879,8 +1874,15 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags
 	}
 
 	if (is_git_directory(".")) {
+		struct strvec to_free = STRVEC_INIT;
+
 		set_git_dir(repo, ".", 0);
+		repo->objects = odb_new(repo,
+					getenv_safe(&to_free, DB_ENVIRONMENT),
+					getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT));
 		check_and_apply_repository_format(repo, NULL);
+
+		strvec_clear(&to_free);
 		return path;
 	}
 
@@ -2032,13 +2034,19 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 	    startup_info->have_repository ||
 	    /* GIT_DIR_EXPLICIT */
 	    getenv(GIT_DIR_ENVIRONMENT)) {
+		struct strvec to_free = STRVEC_INIT;
+
 		if (!repo->gitdir) {
 			const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);
 			if (!gitdir)
 				gitdir = DEFAULT_GIT_DIR_ENVIRONMENT;
-			setup_git_env_internal(repo, gitdir, false);
+			setup_git_env_internal(repo, gitdir);
 		}
 
+		repo->objects = odb_new(repo,
+					getenv_safe(&to_free, DB_ENVIRONMENT),
+					getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT));
+
 		if (startup_info->have_repository) {
 			struct strbuf err = STRBUF_INIT;
 
@@ -2048,6 +2056,8 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 			clear_repository_format(&repo_fmt);
 			strbuf_release(&err);
 		}
+
+		strvec_clear(&to_free);
 	}
 	/*
 	 * Since precompose_string_if_needed() needs to look at
@@ -2796,6 +2806,7 @@ int init_db(struct repository *repo,
 	int exist_ok = flags & INIT_DB_EXIST_OK;
 	char *original_git_dir = real_pathdup(git_dir, 1);
 	struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
+	struct strvec to_free = STRVEC_INIT;
 
 	if (real_git_dir) {
 		struct stat st;
@@ -2816,6 +2827,9 @@ int init_db(struct repository *repo,
 	}
 	startup_info->have_repository = 1;
 
+	repo->objects = odb_new(repo, getenv_safe(&to_free, DB_ENVIRONMENT),
+				getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT));
+
 	/*
 	 * Check to see if the repository version is right.
 	 * Note that a newly created repository does not have
@@ -2879,6 +2893,7 @@ int init_db(struct repository *repo,
 	}
 
 	clear_repository_format(&repo_fmt);
+	strvec_clear(&to_free);
 	free(original_git_dir);
 	return 0;
 }

-- 
2.54.0.926.g75ba10bac6.dirty


^ permalink raw reply related

* [PATCH v2 3/8] setup: deduplicate logic to apply repository format
From: Patrick Steinhardt @ 2026-05-26  5:56 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Junio C Hamano
In-Reply-To: <20260526-b4-pks-setup-centralize-odb-creation-v2-0-2fa5b385c13e@pks.im>

After having discovered the repository format we then apply it to the
repository so that it knows to use the proper repository extensions. The
logic to apply the format is duplicated across three callsites, which
makes it rather painfull to add new extensions.

Introduce a new function `apply_repository_format()` that takes a repo
and applies a given format to it and adapt all callsites to use it.
While at it, rename `check_repository_format()` to clarify that it
doesn't only _check_ the format, but that it also applies it.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 repository.c | 31 +++++++-------------
 setup.c      | 93 ++++++++++++++++++++++++++++++++----------------------------
 setup.h      |  9 ++++++
 3 files changed, 70 insertions(+), 63 deletions(-)

diff --git a/repository.c b/repository.c
index db57b8308b..58a13f7c4f 100644
--- a/repository.c
+++ b/repository.c
@@ -262,8 +262,8 @@ void repo_set_worktree(struct repository *repo, const char *path)
 	trace2_def_repo(repo);
 }
 
-static int read_and_verify_repository_format(struct repository_format *format,
-					     const char *commondir)
+static int read_repository_format_from_commondir(struct repository_format *format,
+						 const char *commondir)
 {
 	int ret = 0;
 	struct strbuf sb = STRBUF_INIT;
@@ -272,11 +272,6 @@ static int read_and_verify_repository_format(struct repository_format *format,
 	read_repository_format(format, sb.buf);
 	strbuf_reset(&sb);
 
-	if (verify_repository_format(format, &sb) < 0) {
-		warning("%s", sb.buf);
-		ret = -1;
-	}
-
 	strbuf_release(&sb);
 	return ret;
 }
@@ -290,6 +285,8 @@ int repo_init(struct repository *repo,
 	      const char *worktree)
 {
 	struct repository_format format = REPOSITORY_FORMAT_INIT;
+	struct strbuf err = STRBUF_INIT;
+
 	memset(repo, 0, sizeof(*repo));
 
 	initialize_repository(repo);
@@ -297,21 +294,13 @@ int repo_init(struct repository *repo,
 	if (repo_init_gitdir(repo, gitdir))
 		goto error;
 
-	if (read_and_verify_repository_format(&format, repo->commondir))
+	if (read_repository_format_from_commondir(&format, repo->commondir))
 		goto error;
 
-	repo_set_hash_algo(repo, format.hash_algo);
-	repo_set_compat_hash_algo(repo, format.compat_hash_algo);
-	repo_set_ref_storage_format(repo, format.ref_storage_format,
-				    format.ref_storage_payload);
-	repo->repository_format_worktree_config = format.worktree_config;
-	repo->repository_format_relative_worktrees = format.relative_worktrees;
-	repo->repository_format_precious_objects = format.precious_objects;
-	repo->repository_format_submodule_path_cfg = format.submodule_path_cfg;
-
-	/* take ownership of format.partial_clone */
-	repo->repository_format_partial_clone = format.partial_clone;
-	format.partial_clone = NULL;
+	if (apply_repository_format(repo, &format, &err) < 0) {
+		warning("%s", err.buf);
+		goto error;
+	}
 
 	if (worktree)
 		repo_set_worktree(repo, worktree);
@@ -320,10 +309,12 @@ int repo_init(struct repository *repo,
 		repo_read_loose_object_map(repo);
 
 	clear_repository_format(&format);
+	strbuf_release(&err);
 	return 0;
 
 error:
 	clear_repository_format(&format);
+	strbuf_release(&err);
 	repo_clear(repo);
 	return -1;
 }
diff --git a/setup.c b/setup.c
index 252b443117..c5015923f1 100644
--- a/setup.c
+++ b/setup.c
@@ -750,8 +750,7 @@ static int check_repo_format(const char *var, const char *value,
 	return read_worktree_config(var, value, ctx, vdata);
 }
 
-static int check_repository_format_gently(struct repository *repo,
-					  const char *gitdir,
+static int check_repository_format_gently(const char *gitdir,
 					  struct repository_format *candidate,
 					  int *nongit_ok)
 {
@@ -765,7 +764,7 @@ static int check_repository_format_gently(struct repository *repo,
 	strbuf_release(&sb);
 
 	/*
-	 * For historical use of check_repository_format() in git-init,
+	 * For historical use of check_and_apply_repository_format() in git-init,
 	 * we treat a missing config as a silent "ok", even when nongit_ok
 	 * is unset.
 	 */
@@ -782,8 +781,6 @@ static int check_repository_format_gently(struct repository *repo,
 		die("%s", err.buf);
 	}
 
-	repo->repository_format_precious_objects = candidate->precious_objects;
-
 	string_list_clear(&candidate->unknown_extensions, 0);
 	string_list_clear(&candidate->v1_only_extensions, 0);
 
@@ -1140,7 +1137,7 @@ static const char *setup_explicit_git_dir(struct repository *repo,
 		die(_("not a git repository: '%s'"), gitdirenv);
 	}
 
-	if (check_repository_format_gently(repo, gitdirenv, repo_fmt, nongit_ok)) {
+	if (check_repository_format_gently(gitdirenv, repo_fmt, nongit_ok)) {
 		free(gitfile);
 		return NULL;
 	}
@@ -1217,7 +1214,7 @@ static const char *setup_discovered_git_dir(struct repository *repo,
 					    struct repository_format *repo_fmt,
 					    int *nongit_ok)
 {
-	if (check_repository_format_gently(repo, gitdir, repo_fmt, nongit_ok))
+	if (check_repository_format_gently(gitdir, repo_fmt, nongit_ok))
 		return NULL;
 
 	/* --work-tree is set without --git-dir; use discovered one */
@@ -1265,7 +1262,7 @@ static const char *setup_bare_git_dir(struct repository *repo,
 {
 	int root_len;
 
-	if (check_repository_format_gently(repo, ".", repo_fmt, nongit_ok))
+	if (check_repository_format_gently(".", repo_fmt, nongit_ok))
 		return NULL;
 
 	setenv(GIT_IMPLICIT_WORK_TREE_ENVIRONMENT, "0", 1);
@@ -1757,6 +1754,32 @@ enum discovery_result discover_git_directory_reason(struct strbuf *commondir,
 	return result;
 }
 
+int apply_repository_format(struct repository *repo,
+			    const struct repository_format *format,
+			    struct strbuf *err)
+{
+	if (verify_repository_format(format, err) < 0)
+		return -1;
+
+	repo_set_hash_algo(repo, format->hash_algo);
+	repo_set_compat_hash_algo(repo, format->compat_hash_algo);
+	repo_set_ref_storage_format(repo,
+				    format->ref_storage_format,
+				    format->ref_storage_payload);
+	repo->repository_format_worktree_config =
+		format->worktree_config;
+	repo->repository_format_submodule_path_cfg =
+		format->submodule_path_cfg;
+	repo->repository_format_relative_worktrees =
+		format->relative_worktrees;
+	repo->repository_format_partial_clone =
+		xstrdup_or_null(format->partial_clone);
+	repo->repository_format_precious_objects =
+		format->precious_objects;
+
+	return 0;
+}
+
 /*
  * Check the repository format version in the path found in repo_get_git_dir(repo),
  * and die if it is a version we don't understand. Generally one would
@@ -1765,26 +1788,20 @@ enum discovery_result discover_git_directory_reason(struct strbuf *commondir,
  *
  * If successful and fmt is not NULL, fill fmt with data.
  */
-static void check_repository_format(struct repository *repo, struct repository_format *fmt)
+static void check_and_apply_repository_format(struct repository *repo,
+					      struct repository_format *fmt)
 {
 	struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
+	struct strbuf err = STRBUF_INIT;
+
 	if (!fmt)
 		fmt = &repo_fmt;
-	check_repository_format_gently(repo, repo_get_git_dir(repo), fmt, NULL);
+
+	check_repository_format_gently(repo_get_git_dir(repo), fmt, NULL);
+	if (apply_repository_format(repo, fmt, &err) < 0)
+		die("%s", err.buf);
 	startup_info->have_repository = 1;
-	repo_set_hash_algo(repo, fmt->hash_algo);
-	repo_set_compat_hash_algo(repo, fmt->compat_hash_algo);
-	repo_set_ref_storage_format(repo,
-				    fmt->ref_storage_format,
-				    fmt->ref_storage_payload);
-	repo->repository_format_worktree_config =
-		fmt->worktree_config;
-	repo->repository_format_submodule_path_cfg =
-		fmt->submodule_path_cfg;
-	repo->repository_format_relative_worktrees =
-		fmt->relative_worktrees;
-	repo->repository_format_partial_clone =
-		xstrdup_or_null(fmt->partial_clone);
+
 	clear_repository_format(&repo_fmt);
 }
 
@@ -1862,7 +1879,7 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags
 
 	if (is_git_directory(".")) {
 		set_git_dir(repo, ".", 0);
-		check_repository_format(repo, NULL);
+		check_and_apply_repository_format(repo, NULL);
 		return path;
 	}
 
@@ -2020,25 +2037,15 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 				gitdir = DEFAULT_GIT_DIR_ENVIRONMENT;
 			setup_git_env_internal(repo, gitdir, false);
 		}
+
 		if (startup_info->have_repository) {
-			repo_set_hash_algo(repo, repo_fmt.hash_algo);
-			repo_set_compat_hash_algo(repo,
-						  repo_fmt.compat_hash_algo);
-			repo_set_ref_storage_format(repo,
-						    repo_fmt.ref_storage_format,
-						    repo_fmt.ref_storage_payload);
-			repo->repository_format_worktree_config =
-				repo_fmt.worktree_config;
-			repo->repository_format_relative_worktrees =
-				repo_fmt.relative_worktrees;
-			repo->repository_format_submodule_path_cfg =
-				repo_fmt.submodule_path_cfg;
-			/* take ownership of repo_fmt.partial_clone */
-			repo->repository_format_partial_clone =
-				repo_fmt.partial_clone;
-			repo_fmt.partial_clone = NULL;
-			repo->repository_format_precious_objects =
-				repo_fmt.precious_objects;
+			struct strbuf err = STRBUF_INIT;
+
+			if (apply_repository_format(repo, &repo_fmt, &err) < 0)
+				die("%s", err.buf);
+
+			clear_repository_format(&repo_fmt);
+			strbuf_release(&err);
 		}
 	}
 	/*
@@ -2814,7 +2821,7 @@ int init_db(struct repository *repo,
 	 * config file, so this will not fail.  What we are catching
 	 * is an attempt to reinitialize new repository with an old tool.
 	 */
-	check_repository_format(repo, &repo_fmt);
+	check_and_apply_repository_format(repo, &repo_fmt);
 
 	repository_format_configure(repo, &repo_fmt, hash, ref_storage_format);
 
diff --git a/setup.h b/setup.h
index 9409326fe4..5ed92f53fa 100644
--- a/setup.h
+++ b/setup.h
@@ -221,6 +221,15 @@ void clear_repository_format(struct repository_format *format);
 int verify_repository_format(const struct repository_format *format,
 			     struct strbuf *err);
 
+/*
+ * Apply the given repository format to the repo. This initializes extensions
+ * and basic data structures required for normal operation. Returns 0 on
+ * success, a negative error code otherwise.
+ */
+int apply_repository_format(struct repository *repo,
+			    const struct repository_format *format,
+			    struct strbuf *err);
+
 const char *get_template_dir(const char *option_template);
 
 #define INIT_DB_QUIET      (1 << 0)

-- 
2.54.0.926.g75ba10bac6.dirty


^ permalink raw reply related

* [PATCH v2 2/8] setup: drop `setup_git_env()`
From: Patrick Steinhardt @ 2026-05-26  5:56 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Junio C Hamano
In-Reply-To: <20260526-b4-pks-setup-centralize-odb-creation-v2-0-2fa5b385c13e@pks.im>

The `setup_git_env()` function is a trivial wrapper around
`setup_git_env_internal()` and has a single call site only. Drop the
function.

While at it, drop stale documentation in "environment.h" that points to
this function, even though it hasn't been exposed to callers outside of
"setup.c" since 43ad1047a9 (setup: stop using `the_repository` in
`setup_git_env()`, 2026-03-27) anymore.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 environment.h | 8 +-------
 refs.c        | 3 ++-
 setup.c       | 7 +------
 3 files changed, 4 insertions(+), 14 deletions(-)

diff --git a/environment.h b/environment.h
index 9eb97b3869..ccfcf37bfb 100644
--- a/environment.h
+++ b/environment.h
@@ -130,13 +130,6 @@ void repo_config_values_init(struct repo_config_values *cfg);
  * `the_repository`. We should eventually get rid of these and make the
  * dependency on a repository explicit:
  *
- *   - `setup_git_env()` ideally shouldn't exist as it modifies global state,
- *     namely the environment. The current process shouldn't ever access that
- *     state via envvars though, but should instead consult a `struct
- *     repository`. When spawning new processes, we would ideally also pass a
- *     `struct repository` and then set up the environment variables for the
- *     child process, only.
- *
  *   - `have_git_dir()` should not have to exist at all. Instead, we should
  *     decide on whether or not we have a `struct repository`.
  *
@@ -147,6 +140,7 @@ void repo_config_values_init(struct repo_config_values *cfg);
  * Please do not add new global config variables here.
  */
 # ifdef USE_THE_REPOSITORY_VARIABLE
+
 /*
  * Returns true iff we have a configured git repository (either via
  * setup_git_directory, or in the environment via $GIT_DIR).
diff --git a/refs.c b/refs.c
index 0f3355d2ee..e7070eb743 100644
--- a/refs.c
+++ b/refs.c
@@ -126,7 +126,8 @@ struct ref_namespace_info ref_namespace[] = {
 		 * points to the content of another. Unlike the other
 		 * ref namespaces, this one can be changed by the
 		 * GIT_REPLACE_REF_BASE environment variable. This
-		 * .namespace value will be overwritten in setup_git_env().
+		 * .namespace value will be overwritten during repository
+		 * setup.
 		 */
 		.ref = "refs/replace/",
 		.decoration = DECORATION_GRAFTED,
diff --git a/setup.c b/setup.c
index d723306dfe..252b443117 100644
--- a/setup.c
+++ b/setup.c
@@ -1074,11 +1074,6 @@ static void setup_git_env_internal(struct repository *repo,
 		fetch_if_missing = 0;
 }
 
-static void setup_git_env(struct repository *repo, const char *git_dir)
-{
-	setup_git_env_internal(repo, git_dir, false);
-}
-
 static void set_git_dir_1(struct repository *repo, const char *path, bool skip_initializing_odb)
 {
 	xsetenv(GIT_DIR_ENVIRONMENT, path, 1);
@@ -2023,7 +2018,7 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok)
 			const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);
 			if (!gitdir)
 				gitdir = DEFAULT_GIT_DIR_ENVIRONMENT;
-			setup_git_env(repo, gitdir);
+			setup_git_env_internal(repo, gitdir, false);
 		}
 		if (startup_info->have_repository) {
 			repo_set_hash_algo(repo, repo_fmt.hash_algo);

-- 
2.54.0.926.g75ba10bac6.dirty


^ permalink raw reply related

* [PATCH v2 0/8] setup: centralize object database creation
From: Patrick Steinhardt @ 2026-05-26  5:56 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Junio C Hamano
In-Reply-To: <20260521-b4-pks-setup-centralize-odb-creation-v1-0-f130d2a7e8ae@pks.im>

Hi,

this small patch series refactors the logic for how we discover and
configure repositories. Most importantly, this involves the following
two steps:

  1. We unify the logic to apply the repository format, which is
     currently open-coded across multiple sites. These sites have
     already diverged, where some repository extensions are not
     consistently applied.

  2. We then centralize creation of the object database to happen at the
     same time we apply the repository format.

The end result is that we apply the repository format exactly once, and
that's also the point in time where we can finalize the setup of the
repo's data structures as we know about all details of the repo at that
time. Ultimately, this makes it trivial to introduce the "objectStorage"
extension, even though that's not part of this patch series.

The series is built on top of aec3f58750 (Sync with 'maint', 2026-05-21)
with ps/setup-wo-the-repository at df69f40c34 (setup: stop using
`the_repository` in `init_db()`, 2026-05-19) merged into it.

Changes in v2:
  - Commit message improvements.
  - Link to v1: https://patch.msgid.link/20260521-b4-pks-setup-centralize-odb-creation-v1-0-f130d2a7e8ae@pks.im

Thanks!

Patrick

---
Patrick Steinhardt (8):
      t0001: plug test gaps for git-init(1) with GIT_OBJECT_DIRECTORY
      setup: drop `setup_git_env()`
      setup: deduplicate logic to apply repository format
      repository: stop initializing the object database in `repo_set_gitdir()`
      setup: stop creating the object database in `setup_git_env()`
      setup: stop initializing object database without repository
      repository: stop reading loose object map twice on repo init
      setup: construct object database in `apply_repository_format()`

 commit-graph.c  |   4 +-
 environment.h   |   8 +---
 refs.c          |   3 +-
 repository.c    |  40 +++++------------
 repository.h    |   3 --
 setup.c         | 130 +++++++++++++++++++++++++++++++-------------------------
 setup.h         |  19 +++++++++
 t/t0001-init.sh |  10 +++++
 8 files changed, 117 insertions(+), 100 deletions(-)

Range-diff versus v1:

1:  dd1fcc7096 ! 1:  14521d16e6 t0001: plug test gaps for git-init(1) with GIT_OBJECT_DIRECTORY
    @@ Commit message
         t0001: plug test gaps for git-init(1) with GIT_OBJECT_DIRECTORY
     
         In subsequent commits we'll rework how we set up the repository. This is
    -    a somewhat intricate and thus fragile sequence, there's many things that
    +    a somewhat intricate and thus fragile sequence; there's many things that
         can go subtly wrong, and there are lots of interesting interactions that
         one can discover.
     
         One such discovered edge case was the interaction between git-init(1)
    -    and the "GIT_OBJECT_DIRECTORY" enviroment variable. When set, the
    +    and the "GIT_OBJECT_DIRECTORY" environment variable. When set, the
         behaviour is that the object directory should be created at the path
         that the variable points to. This behaviour is documented as such in
         its man page:
    @@ Commit message
           directory is used.
     
         Curiously enough though we don't seem to have any tests that exercise
    -    this directly, and thus a subsequent commit inadvertently broke this
    -    expectation.
    +    this directly, and thus a subsequent commit inadvertently would have
    +    broken this expectation.
     
         Plug this test gap.
     
2:  b150ecc19f = 2:  9c099e511b setup: drop `setup_git_env()`
3:  9a638f22e8 = 3:  3bbddc1021 setup: deduplicate logic to apply repository format
4:  60f4647bc2 = 4:  5f531305b2 repository: stop initializing the object database in `repo_set_gitdir()`
5:  6d4013ac62 = 5:  57e1de3a36 setup: stop creating the object database in `setup_git_env()`
6:  f4465744a1 = 6:  944dbaeaf8 setup: stop initializing object database without repository
7:  4b6c61be63 = 7:  2eb5646afc repository: stop reading loose object map twice on repo init
8:  54e3a338e5 ! 8:  5322ce123e setup: construct object database in `apply_repository_format()`
    @@ Commit message
         variables GIT_OBJECT_DIRECTORY and GIT_ALTERNATE_OBJECT_DIRECTORIES into
         the repository format, as well. This allows the caller more flexibility
         around whether or not those environment variables are being honored, as
    -    we do do want to respect them in "setup.c", but not in "repository.c".
    +    we want to respect them in "setup.c", but not in "repository.c".
     
         Signed-off-by: Patrick Steinhardt <ps@pks.im>
     

---
base-commit: 3398daa441965513c48744305d33bd36404547d6
change-id: 20260519-b4-pks-setup-centralize-odb-creation-3479c53fb11d


^ permalink raw reply

* [PATCH v2 1/8] t0001: plug test gaps for git-init(1) with GIT_OBJECT_DIRECTORY
From: Patrick Steinhardt @ 2026-05-26  5:56 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Junio C Hamano
In-Reply-To: <20260526-b4-pks-setup-centralize-odb-creation-v2-0-2fa5b385c13e@pks.im>

In subsequent commits we'll rework how we set up the repository. This is
a somewhat intricate and thus fragile sequence; there's many things that
can go subtly wrong, and there are lots of interesting interactions that
one can discover.

One such discovered edge case was the interaction between git-init(1)
and the "GIT_OBJECT_DIRECTORY" environment variable. When set, the
behaviour is that the object directory should be created at the path
that the variable points to. This behaviour is documented as such in
its man page:

  If the object storage directory is specified via the
  GIT_OBJECT_DIRECTORY environment variable then the sha1 directories
  are created underneath; otherwise, the default $GIT_DIR/objects
  directory is used.

Curiously enough though we don't seem to have any tests that exercise
this directly, and thus a subsequent commit inadvertently would have
broken this expectation.

Plug this test gap.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/t0001-init.sh | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/t/t0001-init.sh b/t/t0001-init.sh
index e4d32bb4d2..e89feca544 100755
--- a/t/t0001-init.sh
+++ b/t/t0001-init.sh
@@ -980,4 +980,14 @@ test_expect_success 're-init reads matching includeIf.onbranch' '
 	test_cmp expect err
 '
 
+test_expect_success 'init honors GIT_OBJECT_DIRECTORY' '
+	test_when_finished "rm -rf init-objdir custom-odb" &&
+	mkdir custom-odb &&
+	env GIT_OBJECT_DIRECTORY="$(pwd)/custom-odb" \
+		git init init-objdir &&
+	test_path_is_missing init-objdir/.git/objects/pack &&
+	test_path_is_dir custom-odb/pack &&
+	test_path_is_dir custom-odb/info
+'
+
 test_done

-- 
2.54.0.926.g75ba10bac6.dirty


^ permalink raw reply related

* [PATCH v2] doc: clarify push.default=simple behavior
From: Ivan Baluta via GitGitGadget @ 2026-05-26  3:58 UTC (permalink / raw)
  To: git; +Cc: Ivan Baluta, Ivan Baluta
In-Reply-To: <pull.2115.git.1779433093971.gitgitgadget@gmail.com>

From: Ivan Baluta <ivanbaluta.dev@gmail.com>

The documentation for the 'simple' push mode currently singles out
the centralized workflow, which can cause confusion about its
behavior in other scenarios, such as triangular workflows.

Clarify that 'simple' always pushes the current branch to a branch
of the same name, but only enforces the strict upstream tracking
requirement when pushing back to the same remote being pulled from.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Ivan Baluta <ivanbaluta.dev@gmail.com>
---
    doc: clarify push.default=simple in triangular workflows

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2115%2Fivanbaluta%2Fdoc-push-simple-triangular-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2115/ivanbaluta/doc-push-simple-triangular-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2115

Range-diff vs v1:

 1:  37ff842622 ! 1:  3deb7f5b0c doc: clarify push.default=simple in triangular workflows
     @@
       ## Metadata ##
     -Author: ivanbaluta <ivanbaluta.dev@gmail.com>
     +Author: Ivan Baluta <ivanbaluta.dev@gmail.com>
      
       ## Commit message ##
     -    doc: clarify push.default=simple in triangular workflows
     +    doc: clarify push.default=simple behavior
      
     -    The documentation for 'simple' push mode currently focuses on the
     -    centralized workflow. However, the implementation in builtin/push.c
     -    falls back to 'current' behavior when pushing to a remote different
     -    from the upstream (a triangular workflow).
     +    The documentation for the 'simple' push mode currently singles out
     +    the centralized workflow, which can cause confusion about its
     +    behavior in other scenarios, such as triangular workflows.
      
     -    Clarify this in the manual to align the documentation with the
     -    long-standing implementation and prevent user confusion.
     +    Clarify that 'simple' always pushes the current branch to a branch
     +    of the same name, but only enforces the strict upstream tracking
     +    requirement when pushing back to the same remote being pulled from.
      
     -    Signed-off-by: ivanbaluta <ivanbaluta.dev@gmail.com>
     +    Suggested-by: Junio C Hamano <gitster@pobox.com>
     +    Signed-off-by: Ivan Baluta <ivanbaluta.dev@gmail.com>
      
       ## Documentation/config/push.adoc ##
     -@@ Documentation/config/push.adoc: If you are working on a centralized workflow (pushing to the same repository you
     - pull from, which is typically `origin`), then you need to configure an upstream
     - branch with the same name.
     +@@ Documentation/config/push.adoc: this is a deprecated synonym for `upstream`.
     + `simple`;;
     + push the current branch with the same name on the remote.
     + +
     +-If you are working on a centralized workflow (pushing to the same repository you
     +-pull from, which is typically `origin`), then you need to configure an upstream
     +-branch with the same name.
     ++This mode requires that the remote repository to be pushed to is
     ++known.  When pushing back to the same remote you pull from, the
     ++current branch must also have an upstream tracking branch with the
     ++same name.
       +
     -+In a triangular workflow (pushing to a remote different from the upstream),
     -+`simple` behaves like `current`.
     -++
       This mode is the default since Git 2.0, and is the safest option suited for
       beginners.
     - 


 Documentation/config/push.adoc | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/Documentation/config/push.adoc b/Documentation/config/push.adoc
index d9112b2260..28132eedfe 100644
--- a/Documentation/config/push.adoc
+++ b/Documentation/config/push.adoc
@@ -41,9 +41,10 @@ this is a deprecated synonym for `upstream`.
 `simple`;;
 push the current branch with the same name on the remote.
 +
-If you are working on a centralized workflow (pushing to the same repository you
-pull from, which is typically `origin`), then you need to configure an upstream
-branch with the same name.
+This mode requires that the remote repository to be pushed to is
+known.  When pushing back to the same remote you pull from, the
+current branch must also have an upstream tracking branch with the
+same name.
 +
 This mode is the default since Git 2.0, and is the safest option suited for
 beginners.

base-commit: 59ff4886a579f4bc91e976fe18590b9ae02c7a08
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH 2/2] restore: avoid sparse index expansion
From: Derrick Stolee @ 2026-05-26  2:54 UTC (permalink / raw)
  To: Junio C Hamano, Derrick Stolee via GitGitGadget; +Cc: git
In-Reply-To: <xmqqtsrwh0hx.fsf@gitster.g>

On 5/24/26 7:05 PM, Junio C Hamano wrote:
> "Derrick Stolee via GitGitGadget" <gitgitgadget@gmail.com> writes:


>> -	if (S_ISDIR(mode))
>> +	if (S_ISDIR(mode)) {
>> +		/*
>> +		 * If this directory exists as a sparse directory entry in
>> +		 * the index, we can handle it at the tree level without
>> +		 * descending into individual files.
>> +		 */
>> +		if (the_repository->index->sparse_index) {
> 
> I wonder if this deep nesting is a sign that the newly added code
> from here to ...
> 
>> +			struct strbuf dirpath = STRBUF_INIT;
>> +
>> +			strbuf_addbuf(&dirpath, base);
>> +			strbuf_addstr(&dirpath, pathname);
>> +			strbuf_addch(&dirpath, '/');
>> +
>> +			pos = index_name_pos_sparse(the_repository->index,
>> +						    dirpath.buf, dirpath.len);
>> +			if (pos >= 0) {
>> +				struct cache_entry *old =
>> +					the_repository->index->cache[pos];
>> +				if (S_ISSPARSEDIR(old->ce_mode)) {
>> +					if (oideq(oid, &old->oid)) {
>> +						strbuf_release(&dirpath);
>> +						return 0;
>> +					}
>> +					if (!overlay_mode) {
>> +						/*
>> +						 * In non-overlay mode (e.g.,
>> +						 * restore --staged), we can
>> +						 * replace the sparse dir OID
>> +						 * directly since files not in
>> +						 * the source tree should be
>> +						 * removed anyway.
>> +						 */
>> +						oidcpy(&old->oid, oid);
>> +						old->ce_flags |= CE_UPDATE;
>> +						strbuf_release(&dirpath);
>> +						return 0;
>> +					}
>> +				}
>> +			}
>> +			strbuf_release(&dirpath);
>> +		}
> 
> ... here may become easier to understand if it is made into a small
> helper function with a descriptive name.

Good idea. I'll try that and send a v2.

Thanks,
-Stolee


^ permalink raw reply

* Re: [PATCH v2 3/4] diff: add long-running diff process via diff.<driver>.process
From: Junio C Hamano @ 2026-05-26  2:26 UTC (permalink / raw)
  To: Michael Montalbo via GitGitGadget; +Cc: git, Michael Montalbo
In-Reply-To: <c25647c6e571e293fc994e0620ca37709f680f8a.1779733799.git.gitgitgadget@gmail.com>

"Michael Montalbo via GitGitGadget" <gitgitgadget@gmail.com> writes:

> Zero hunks with status=success means the tool considers the
> files equivalent.  Git skips diff output for that file.

Is "zero hunk" a common word or some random string you invented?  If
the latter, which is I am assuming it to be, you should define what
it means at/before the first use.  Here in the proposed log message,
and ...

>
> Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
> ---
>  Documentation/config/diff.adoc   |   8 +
>  Documentation/gitattributes.adoc |  40 ++++
>  Makefile                         |   1 +
>  diff-process.c                   | 206 +++++++++++++++++++
>  diff-process.h                   |  28 +++
>  diff.c                           |  23 +++
>  t/.gitattributes                 |   1 +
>  t/t4080-diff-process.sh          | 338 +++++++++++++++++++++++++++++++
>  8 files changed, 645 insertions(+)
>  create mode 100644 diff-process.c
>  create mode 100644 diff-process.h
>  create mode 100755 t/t4080-diff-process.sh
>
> diff --git a/Documentation/config/diff.adoc b/Documentation/config/diff.adoc
> index 1135a62a0a..4ab5f60df6 100644
> --- a/Documentation/config/diff.adoc
> +++ b/Documentation/config/diff.adoc
> @@ -218,6 +218,14 @@ endif::git-diff[]
>  	Set this option to `true` to make the diff driver cache the text
>  	conversion outputs.  See linkgit:gitattributes[5] for details.
>  
> +`diff.<driver>.process`::
> +	The command to run as a long-running diff process.
> +	The tool communicates via the pkt-line protocol and returns
> +	hunks that are fed into Git's diff and blame pipelines.
> +	If the tool returns zero hunks, the file is treated as
> +	unchanged for both diff output and blame attribution.
> +	See linkgit:gitattributes[5] for details.

... also here.

I do not know if you mean "the tool returns no hunks" (there is no
"hunk <old_start> <old_count> <new_start> <new_count>" line passed
from the tool over the protocol) or "the tool returns zero-hunk"
(there is a special "zero-hunk" message to signal this particular
condition sent over the protocol), and this description does not
quite help disambiguating between the two.

If the former, then avoid "zero hunks" as it sounds like a noun with
special meaning.  Yes, we can say "tool returns one hunk", "tool
returns 31 hunks", etc., so "tool returns zero hunks" may logically
be correct, but "when the tool returns no hunks with status=success"
is much less confusing, I think.

^ permalink raw reply

* Re: [PATCH v2 3/4] diff: add long-running diff process via diff.<driver>.process
From: Junio C Hamano @ 2026-05-26  1:56 UTC (permalink / raw)
  To: Michael Montalbo via GitGitGadget; +Cc: git, Michael Montalbo
In-Reply-To: <c25647c6e571e293fc994e0620ca37709f680f8a.1779733799.git.gitgitgadget@gmail.com>

"Michael Montalbo via GitGitGadget" <gitgitgadget@gmail.com> writes:

> +struct diff_subprocess {
> +	struct subprocess_entry subprocess;
> +	unsigned int supported_capabilities;
> +};
> +
> +static int subprocess_map_initialized;
> +static struct hashmap subprocess_map;

Can we avoid introducing new global variables like these?  Would
"struct userdiff_driver" or "struct diff_options" be a good place to
hang this hashmap, perhaps?

> +static int send_file_content(int fd, const char *buf, long size)
> +{
> +	int ret;
> +
> +	if (size > 0)
> +		ret = write_packetized_from_buf_no_flush(buf, size, fd);
> +	else
> +		ret = 0;

Shouldn't "size == -24" be flagged as an invalid input?

> +	if (ret)
> +		return ret;
> +	return packet_flush_gently(fd);
> +}

> +static int parse_hunk_line(const char *line, struct xdl_hunk *hunk)
> +{
> +...
> +}

This gives a silent error diagnosis, which is good for a lower level
helper.

> +int diff_process_get_hunks(struct userdiff_driver *drv,
> +			   const char *path,
> +			   const char *old_buf, long old_size,
> +			   const char *new_buf, long new_size,
> +			   struct xdl_hunk **hunks_out,
> +			   size_t *nr_hunks_out)
> +{
> +	struct diff_subprocess *backend;
> +	struct child_process *process;
> +	int fd_in, fd_out;
> +	struct strbuf status = STRBUF_INIT;
> +	struct xdl_hunk *hunks = NULL;
> +	struct xdl_hunk hunk;
> +	size_t nr_hunks = 0, alloc_hunks = 0;
> +	int len;
> +	char *line;
> +
> +	if (!drv || !drv->process)
> +		return -1;

A driver that does not define process is not an error; it is
perfectly normal in the current world order where nobody has such an
external process and even fi this patch lands, external processes
are optional.  So here "return -1" does not mean an error, and
silent return is perfectly fine.

> +	backend = find_or_start_process(drv->process);
> +	if (!backend)
> +		return -1;

This is probably an error; the user specified drv->process, we
either tried to find or start the process and failed.  Isn't it an
event that deserves to be reported in an error message?

> +	if (!(backend->supported_capabilities & CAP_HUNKS))
> +		return -1;

Backend started, but the "hunks" feature is not supported.  Perhaps
in a year or two, this external process protocol may have become so
popular that it gained more capabilities, possibly making get_hunks
obsolete.  We may be looking at such an external process that uses
other capabilities but not this one.  This is not an error, so
silent return is perfectly fine.

> +	process = subprocess_get_child_process(&backend->subprocess);
> +	fd_in = process->in;
> +	fd_out = process->out;
> +
> +	/* Send request */
> +	if (packet_write_fmt_gently(fd_in, "command=hunks\n") ||
> +	    packet_write_fmt_gently(fd_in, "pathname=%s\n", path) ||
> +	    packet_flush_gently(fd_in))
> +		goto error;
> +
> +	/* Send old file content */
> +	if (send_file_content(fd_in, old_buf, old_size))
> +		goto error;
> +
> +	/* Send new file content */
> +	if (send_file_content(fd_in, new_buf, new_size))
> +		goto error;
> +
> +	/* Read hunks until flush packet */
> +	while ((len = packet_read_line_gently(fd_out, NULL, &line)) >= 0 &&
> +	       line) {
> +		if (parse_hunk_line(line, &hunk) < 0)
> +			goto error;
> +		ALLOC_GROW(hunks, nr_hunks + 1, alloc_hunks);
> +		hunks[nr_hunks++] = hunk;
> +	}
> +	if (len < 0)
> +		goto error;
> +
> +	/* Read status */
> +	if (subprocess_read_status(fd_out, &status))
> +		goto error;
> +
> +	if (strcmp(status.buf, "success")) {
> +		if (!strcmp(status.buf, "abort"))
> +			backend->supported_capabilities &= ~CAP_HUNKS;
> +		goto error;
> +	}
> +
> +	*hunks_out = hunks;
> +	*nr_hunks_out = nr_hunks;
> +	strbuf_release(&status);
> +	return 0;
> +
> +error:

All exceptions that lead here look like events that should be
reported to the end-user.

> +	free(hunks);
> +	strbuf_release(&status);
> +	return -1;
> +}

> +/*
> + * Query a diff process for hunks describing the changes
> + * between old_buf and new_buf.
> + *
> + * The backend is a long-running subprocess configured via
> + * diff.<driver>.process.  It receives file content via
> + * pkt-line and returns hunks with 1-based line numbers.
> + *
> + * On success, sets *hunks_out and *nr_hunks_out to a newly allocated
> + * array (caller must free) and returns 0.
> + *
> + * On failure, returns -1.  The caller should fall back to the
> + * builtin diff algorithm.
> + */

I do not agree with this.  If it is a failure, the user should fix
the external process (or disable).  It shouldn't be hidden behind a
fallback.  As I left comments, in this round of implementation,
there are conditions that returns -1 for soemthing that is not an
error (i.e., not configured, or process not supporting the
particular capability) *and* in those cases the caller should fall
back as if nothing happened.  But some error cases, the caller
should't hide them.

^ permalink raw reply

* Re: Expected test suite behavior
From: Junio C Hamano @ 2026-05-26  1:11 UTC (permalink / raw)
  To: brian m. carlson; +Cc: Amogh Dambal, Jeff King, Michael Montalbo, git
In-Reply-To: <ahTsTDhVPkHTEbB_@fruit.crustytoothpaste.net>

"brian m. carlson" <sandals@crustytoothpaste.net> writes:

> I think I know what the problem is: you're running as root.  I suspect
> `test -x` in the test says that you have permission to execute it
> because you're root and root always ignores permissions.  My guess is
> that most of the tests you're failing have to do with permissions of
> some sort that are being ignored because you're privileged.

I know !SANITY defeats a-rw and lets the tester read or write to the
path, but this is the first time I heard that !SANITY defeats a-x
and lets the tester _execute_ it.

I do not think we drop POSIXPERM prereq when !SANITY automatically,
and I do not think we should, but we should probably have an
automated check to drop POSIXPERM?

    (
	# is an unexecutable look to the user as executable?
	umask 0; >testfile; chmod a-w testfile; test -x testfile;
	status=$?;
	rm -f testfile; exit $status
    )

> I'll just note that if you just want to do Git development, macOS is a
> fully supported platform on which to do that.  I will admit most of the
> major contributors (with the notable exception of the Git for Windows
> maintainer) do use Linux and of course I like and endorse Debian, but
> macOS should build and run just fine if you prefer that.

Hear hear.

We want to encourage developers to do more _native_ development on
their own system, and this is not limited to macOS.  As long as
users on a particular platform rely on working Git there, we are
better off if we have more people actively using that platform to
build, test, develop on, and debug Git for their own use.

Thanks.


^ permalink raw reply

* Re: Expected test suite behavior
From: brian m. carlson @ 2026-05-26  0:41 UTC (permalink / raw)
  To: Amogh Dambal; +Cc: Jeff King, Michael Montalbo, git
In-Reply-To: <4649049a-ded5-4cc6-bc2b-d5f543e6df99@gmail.com>

[-- Attachment #1: Type: text/plain, Size: 2679 bytes --]

On 2026-05-25 at 22:25:11, Amogh Dambal wrote:
> > What are the OS and file system on the host?  We tend to see
> > executable bits set when NTFS, FAT, or other Windows-adjacent file
> > systems are used on Linux and you're mounting `$(PWD)` into the
> > container as a volume.
> 
> Ah, this is a smoking gun. I'm not on a Windows-adjacent file system; I'm
> running macOS Sequoia 15.5 on the host. Specifically:
> 
> $ uname -msprsv
> Darwin 24.5.0 Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:26 PDT 2025;
> root:xnu-11417.121.6~2/RELEASE_ARM64_T8112 arm64 arm
> 
> But I am mounting $(PWD) into the container as a volume.

I wouldn't expect that to be a problem, then.  macOS uses Unix-style
permissions and I've never seen odd permissions behaviour mounting a
macOS APFS file system into a container.  I will, however, note that I
am using a case-sensitive APFS volume, but I cannot imagine how this
would occur with _any_ macOS APFS volume mounted into a Linux container.

> > Git doesn't use `/tmp` for most files in the tests.  Those are stored
> > under `t/`, so you'd want to create your test directory there.
> 
> ACK, good to know, thanks! I am still seeing the same behavior with a
> `debug` directory under `t/`:
> 
> root@ec94ab1b260e:~/git/t/debug# /root/git/git init plain
> root@ec94ab1b260e:~/git/t/debug# ls -alhrt
> /root/git/t/debug/plain/.git/config
> -rw-r--r-- 1 root root 111 May 25 22:24 /root/git/t/debug/plain/.git/config

I think I know what the problem is: you're running as root.  I suspect
`test -x` in the test says that you have permission to execute it
because you're root and root always ignores permissions.  My guess is
that most of the tests you're failing have to do with permissions of
some sort that are being ignored because you're privileged.

In general, you would not want to run this as root.  Use `adduser` to
create yourself a regular user and then use the `USER` directive in the
Dockerfile to change users.  I don't run the tests as root and I'm sure
none of the other regular contributors do, either.  Running unprivileged
in a container is a best practice anyway.

I'll just note that if you just want to do Git development, macOS is a
fully supported platform on which to do that.  I will admit most of the
major contributors (with the notable exception of the Git for Windows
maintainer) do use Linux and of course I like and endorse Debian, but
macOS should build and run just fine if you prefer that.

But if you do want to use a container, I'd try unprivileged and see if
substantially more of the tests pass.
-- 
brian m. carlson (they/them)
Toronto, Ontario, CA

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]

^ permalink raw reply

* Re: How does git track history overwrites?
From: Jens Tröger @ 2026-05-25 23:20 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Chris Torek, git
In-Reply-To: <xmqqfr3fnl2n.fsf@gitster.g>

Hello Junio,

> Sorry, I may have been unclear.  I specifically meant the "grafted,
> " part in the message.  I know how "git log" output looks like ;-)

The “grafted” too was part of the git log output; here is the complete
cmd line output:

    /tmp/bla > git log
    commit fda77690955e9b63c6687d8806bafd56a526e45f (grafted, HEAD)
    Author: Adam Johnson <me@adamj.eu>
    Date:   Mon Sep 8 16:31:35 2025 +0100

        Version 1.20.0

On *why* the “grafted” is there in the first place, I suspect that’s got
to do with the fetch --depth=1 and that previous history isn’t available
in the shallow repo clone.

Cheers,
Jens

^ permalink raw reply

* Re: How does git track history overwrites?
From: Junio C Hamano @ 2026-05-25 23:09 UTC (permalink / raw)
  To: Jens Tröger; +Cc: Chris Torek, git
In-Reply-To: <074E783A-027D-4C5B-BC44-CC38C53735D7@light-speed.de>

Jens Tröger <jens.troeger@light-speed.de> writes:

> Thank you Chris and Junio!
>
>
>> [Junio] Where does this line in your discussion page at GitHub (which is
>> omitted from the post to this list) come from?
>> 
>>    commit fda77690955e9b63c6687d8806bafd56a526e45f (grafted, HEAD)
>> 
>> Are you doing anything funky with .git/info/grafts by any chance?
>
> That line is the result of a `git log` after the `git fetch` I mentioned in my initial email.

Sorry, I may have been unclear.  I specifically meant the "grafted,
" part in the message.  I know how "git log" output looks like ;-)


^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox