Git development
 help / color / mirror / Atom feed
* Re: [PATCH/RFC 1/5] replay: support replaying 2-parent merges
From: Kristoffer Haugsbakk @ 2026-05-26 21:15 UTC (permalink / raw)
  To: Koji Nakamaru, git; +Cc: Elijah Newren, Patrick Steinhardt, Johannes Schindelin
In-Reply-To: <034ab0f83822e6db67baa423d9fcb753b12b5ac8.1778107405.git.gitgitgadget@gmail.com>

On Thu, May 7, 2026, at 00:43, Johannes Schindelin via GitGitGadget wrote:
> From: Johannes Schindelin <johannes.schindelin@gmx.de>
>[snip]
> diff --git a/replay.c b/replay.c
>[snip]
> +out:
> +	free(ancestor_name);
> +	free_commit_list(parent_bases);
> +	free_commit_list(replayed_bases);

`free_commit_list` is deprecated in favor of `commit_list_free` since
52882024 (Merge branch 'ps/commit-list-functions-renamed', 2026-02-13).

> +	merge_finalize(&remerge_opt, &remerge_res);
> +	merge_finalize(&new_merge_opt, &new_merge_res);
> +	return picked;
>  }
>[snip]

^ permalink raw reply

* [PATCH v2 2/2] restore: avoid sparse index expansion
From: Derrick Stolee via GitGitGadget @ 2026-05-26 20:26 UTC (permalink / raw)
  To: git; +Cc: gitster, Derrick Stolee, Derrick Stolee
In-Reply-To: <pull.2121.v2.git.1779827195.gitgitgadget@gmail.com>

From: Derrick Stolee <stolee@gmail.com>

Teach update_some() to handle sparse directory entries at the tree
level rather than expanding the entire sparse index. When iterating a
source tree during checkout/restore operations:

 - If a directory matches a sparse directory entry with the same OID,
   skip it entirely (no change needed).

 - If the OID differs and we are in non-overlay mode (e.g., restore
   --staged), update the sparse directory entry's OID in place. This
   is semantically correct because non-overlay mode removes paths not
   in the source tree anyway.

 - In overlay mode (e.g., checkout <tree> -- .), fall through to
   recursive descent so individual file entries are preserved
   correctly.

Also switch from index_name_pos() to index_name_pos_sparse() for
individual file lookups to avoid triggering ensure_full_index() when
the file is already individually tracked in the index.

Update the test expectation in t1092 to assert that 'restore --staged'
no longer expands the sparse index.

Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
 builtin/checkout.c                       | 65 +++++++++++++++++++++---
 t/t1092-sparse-checkout-compatibility.sh |  8 +--
 2 files changed, 63 insertions(+), 10 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 1345e8574a..86e23a07b1 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -31,6 +31,7 @@
 #include "revision.h"
 #include "sequencer.h"
 #include "setup.h"
+#include "sparse-index.h"
 #include "strvec.h"
 #include "submodule.h"
 #include "symlinks.h"
@@ -141,15 +142,65 @@ static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm
 	return run_hooks_opt(the_repository, "post-checkout", &opt);
 }
 
+/*
+ * Handle a tree object and determine if we need to recurse into the
+ * tree (READ_TREE_RECURSIVE) or skip it (0).
+ */
+static int try_update_sparse_directory(const struct object_id *oid,
+				       struct strbuf *base,
+				       const char *pathname,
+				       int overlay_mode)
+{
+	struct strbuf dirpath = STRBUF_INIT;
+	struct cache_entry *old;
+	int pos, result = READ_TREE_RECURSIVE;
+
+	if (!the_repository->index->sparse_index)
+		return result;
+
+	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)
+		goto cleanup;
+
+	old = the_repository->index->cache[pos];
+	if (!S_ISSPARSEDIR(old->ce_mode))
+		goto cleanup;
+
+	if (oideq(oid, &old->oid)) {
+		/* Tree content already matches; no need to descend. */
+		result = 0;
+	} else if (!overlay_mode) {
+		/*
+		 * In non-overlay mode (e.g., restore --staged), replace the
+		 * sparse directory OID directly since files not present in
+		 * the source tree should be removed anyway.
+		 */
+		oidcpy(&old->oid, oid);
+		old->ce_flags |= CE_UPDATE;
+		result = 0;
+	}
+
+cleanup:
+	strbuf_release(&dirpath);
+	return result;
+}
+
 static int update_some(const struct object_id *oid, struct strbuf *base,
-		       const char *pathname, unsigned mode, void *context UNUSED)
+		       const char *pathname, unsigned mode, void *context)
 {
 	int len;
 	struct cache_entry *ce;
 	int pos;
+	int overlay_mode = context ? *((int *)context) : 1;
 
 	if (S_ISDIR(mode))
-		return READ_TREE_RECURSIVE;
+		return try_update_sparse_directory(oid, base, pathname,
+						   overlay_mode);
 
 	len = base->len + strlen(pathname);
 	ce = make_empty_cache_entry(the_repository->index, len);
@@ -165,7 +216,7 @@ static int update_some(const struct object_id *oid, struct strbuf *base,
 	 * entry in place. Whether it is UPTODATE or not, checkout_entry will
 	 * do the right thing.
 	 */
-	pos = index_name_pos(the_repository->index, ce->name, ce->ce_namelen);
+	pos = index_name_pos_sparse(the_repository->index, ce->name, ce->ce_namelen);
 	if (pos >= 0) {
 		struct cache_entry *old = the_repository->index->cache[pos];
 		if (ce->ce_mode == old->ce_mode &&
@@ -182,10 +233,11 @@ static int update_some(const struct object_id *oid, struct strbuf *base,
 	return 0;
 }
 
-static int read_tree_some(struct tree *tree, const struct pathspec *pathspec)
+static int read_tree_some(struct tree *tree, const struct pathspec *pathspec,
+			  int overlay_mode)
 {
 	read_tree(the_repository, tree,
-		  pathspec, update_some, NULL);
+		  pathspec, update_some, &overlay_mode);
 
 	/* update the index with the given tree's info
 	 * for all args, expanding wildcards, and exit
@@ -580,7 +632,8 @@ static int checkout_paths(const struct checkout_opts *opts,
 		return error(_("index file corrupt"));
 
 	if (opts->source_tree)
-		read_tree_some(opts->source_tree, &opts->pathspec);
+		read_tree_some(opts->source_tree, &opts->pathspec,
+			       opts->overlay_mode);
 	if (opts->merge)
 		unmerge_index(the_repository->index, &opts->pathspec, CE_MATCHED);
 
diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh
index d69434e7ab..8186da5c88 100755
--- a/t/t1092-sparse-checkout-compatibility.sh
+++ b/t/t1092-sparse-checkout-compatibility.sh
@@ -2608,19 +2608,19 @@ test_expect_success 'restore --staged with wildcards' '
 	test_all_match git diff --cached
 '
 
-test_expect_success 'sparse-index is expanded: restore --staged' '
+test_expect_success 'sparse-index is not expanded: restore --staged' '
 	init_repos &&
 
 	git -C sparse-index checkout -b restore-staged-exp base &&
 	git -C sparse-index reset --soft update-folder1 &&
-	ensure_expanded restore --staged .
+	ensure_not_expanded restore --staged .
 '
 
-test_expect_success 'sparse-index is expanded: restore --source --staged' '
+test_expect_success 'sparse-index is not expanded: restore --source --staged' '
 	init_repos &&
 
 	git -C sparse-index checkout -b restore-source-staged base &&
-	ensure_expanded restore --source update-folder1 --staged .
+	ensure_not_expanded restore --source update-folder1 --staged .
 '
 
 test_done
-- 
gitgitgadget

^ permalink raw reply related

* [PATCH v2 1/2] t1092: test 'git restore' with sparse index
From: Derrick Stolee via GitGitGadget @ 2026-05-26 20:26 UTC (permalink / raw)
  To: git; +Cc: gitster, Derrick Stolee, Derrick Stolee
In-Reply-To: <pull.2121.v2.git.1779827195.gitgitgadget@gmail.com>

From: Derrick Stolee <stolee@gmail.com>

A user reported that 'git restore --staged .' causes the sparse index to
expand. This is somewhat natural because the '.' pathspec means 'check
every path'. However, the restore will not update paths marked with the
SKIP_WORKTREE bit, so we shouldn't need to process such entries.

For now, establish the current behavior, including the sparse index
expansion, in the t1092 test case as a baseline.

Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
 t/t1092-sparse-checkout-compatibility.sh | 50 ++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh
index d98cb4ac11..d69434e7ab 100755
--- a/t/t1092-sparse-checkout-compatibility.sh
+++ b/t/t1092-sparse-checkout-compatibility.sh
@@ -2573,4 +2573,54 @@ test_expect_success 'sparse-index is not expanded: merge-ours' '
 	ensure_not_expanded merge -s ours merge-right
 '
 
+test_expect_success 'restore --staged with sparse definition' '
+	init_repos &&
+
+	# Stage changes within the sparse definition
+	test_all_match git checkout -b restore-staged-1 base &&
+	test_all_match git reset --soft update-deep &&
+	test_all_match git restore --staged . &&
+	test_all_match git status --porcelain=v2 &&
+	test_all_match git diff --cached
+'
+
+test_expect_success 'restore --staged with outside sparse definition' '
+	init_repos &&
+
+	# Stage changes that include paths outside the sparse definition.
+	# Although the working tree differs between full and sparse checkouts
+	# after restore, the state of the index should be the same.
+	test_all_match git checkout -b restore-staged-2 base &&
+	test_all_match git reset --soft update-folder1 &&
+	test_sparse_match git restore --staged . &&
+	git -C full-checkout restore --staged . &&
+	test_all_match git ls-files -s -- folder1 &&
+	test_all_match git diff --cached -- folder1
+'
+
+test_expect_success 'restore --staged with wildcards' '
+	init_repos &&
+
+	test_all_match git checkout -b restore-staged-3 base &&
+	test_all_match git reset --soft update-deep &&
+	test_all_match git restore --staged "deep/*" &&
+	test_all_match git status --porcelain=v2 &&
+	test_all_match git diff --cached
+'
+
+test_expect_success 'sparse-index is expanded: restore --staged' '
+	init_repos &&
+
+	git -C sparse-index checkout -b restore-staged-exp base &&
+	git -C sparse-index reset --soft update-folder1 &&
+	ensure_expanded restore --staged .
+'
+
+test_expect_success 'sparse-index is expanded: restore --source --staged' '
+	init_repos &&
+
+	git -C sparse-index checkout -b restore-source-staged base &&
+	ensure_expanded restore --source update-folder1 --staged .
+'
+
 test_done
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v2 0/2] restore: better integrate with sparse index
From: Derrick Stolee via GitGitGadget @ 2026-05-26 20:26 UTC (permalink / raw)
  To: git; +Cc: gitster, Derrick Stolee
In-Reply-To: <pull.2121.git.1779644412.gitgitgadget@gmail.com>

There's still a long tail of situations where Git expands a sparse index
in-memory in order to operate on blob path entries instead of intelligently
handling trees. I was recently alerted to one such case with git restore
--staged -- ..

The basic idea here is that the pathspec . signals that all paths matter,
but what we want to do across those pathspecs will ignore the expanded blob
paths with the SKIP_WORKTREE bit, so we should avoid expanding the tree when
we can.

This series has two patches: first a test to demonstrate the baseline
behavior of git restore across different sparsity cases as well as
demonstrate that the index is currently expanded. The second patch includes
the fix and maintains the same end-to-end behavior with the only change
being the performance improvement from not expanding the sparse index.


Update in v2
============

The logic around handling a tree entry is extracted to a helper method,
making the diff easier to read.

Thanks, -Stolee

Derrick Stolee (2):
  t1092: test 'git restore' with sparse index
  restore: avoid sparse index expansion

 builtin/checkout.c                       | 65 +++++++++++++++++++++---
 t/t1092-sparse-checkout-compatibility.sh | 50 ++++++++++++++++++
 2 files changed, 109 insertions(+), 6 deletions(-)


base-commit: aec3f587505a472db67e9462d0702e7d463a449d
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2121%2Fderrickstolee%2Frestore-sparse-index-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2121/derrickstolee/restore-sparse-index-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2121

Range-diff vs v1:

 1:  7c56d03830 = 1:  7c56d03830 t1092: test 'git restore' with sparse index
 2:  47542cbd42 ! 2:  88f5d26a33 restore: avoid sparse index expansion
     @@ builtin/checkout.c
       #include "submodule.h"
       #include "symlinks.h"
      @@ builtin/checkout.c: static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm
     + 	return run_hooks_opt(the_repository, "post-checkout", &opt);
       }
       
     ++/*
     ++ * Handle a tree object and determine if we need to recurse into the
     ++ * tree (READ_TREE_RECURSIVE) or skip it (0).
     ++ */
     ++static int try_update_sparse_directory(const struct object_id *oid,
     ++				       struct strbuf *base,
     ++				       const char *pathname,
     ++				       int overlay_mode)
     ++{
     ++	struct strbuf dirpath = STRBUF_INIT;
     ++	struct cache_entry *old;
     ++	int pos, result = READ_TREE_RECURSIVE;
     ++
     ++	if (!the_repository->index->sparse_index)
     ++		return result;
     ++
     ++	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)
     ++		goto cleanup;
     ++
     ++	old = the_repository->index->cache[pos];
     ++	if (!S_ISSPARSEDIR(old->ce_mode))
     ++		goto cleanup;
     ++
     ++	if (oideq(oid, &old->oid)) {
     ++		/* Tree content already matches; no need to descend. */
     ++		result = 0;
     ++	} else if (!overlay_mode) {
     ++		/*
     ++		 * In non-overlay mode (e.g., restore --staged), replace the
     ++		 * sparse directory OID directly since files not present in
     ++		 * the source tree should be removed anyway.
     ++		 */
     ++		oidcpy(&old->oid, oid);
     ++		old->ce_flags |= CE_UPDATE;
     ++		result = 0;
     ++	}
     ++
     ++cleanup:
     ++	strbuf_release(&dirpath);
     ++	return result;
     ++}
     ++
       static int update_some(const struct object_id *oid, struct strbuf *base,
      -		       const char *pathname, unsigned mode, void *context UNUSED)
      +		       const char *pathname, unsigned mode, void *context)
     @@ builtin/checkout.c: static int post_checkout_hook(struct commit *old_commit, str
       	int pos;
      +	int overlay_mode = context ? *((int *)context) : 1;
       
     --	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) {
     -+			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);
     -+		}
     - 		return READ_TREE_RECURSIVE;
     -+	}
     + 	if (S_ISDIR(mode))
     +-		return READ_TREE_RECURSIVE;
     ++		return try_update_sparse_directory(oid, base, pathname,
     ++						   overlay_mode);
       
       	len = base->len + strlen(pathname);
       	ce = make_empty_cache_entry(the_repository->index, len);

-- 
gitgitgadget

^ permalink raw reply

* Re: [PATCH v4] config: improve diagnostic for "set" with missing value
From: Harald Nordgren @ 2026-05-26 19:24 UTC (permalink / raw)
  To: Harald Nordgren via GitGitGadget; +Cc: git, Kristoffer Haugsbakk
In-Reply-To: <pull.2302.v4.git.git.1779823288005.gitgitgadget@gmail.com>

I forgot to update the PR description on GitHub, it should have read:

- Diagnose the 1-arg set form (explicit and implicit) directly: report
the missing value, and suggest the split form only when the prefix
before `=` is a valid key.
- Did not act on Junio's secondary suggestion to reword the 2-arg
`error: invalid key: <key>`, fix seemed to become too big.


Harald

On Tue, May 26, 2026 at 9:21 PM Harald Nordgren via GitGitGadget
<gitgitgadget@gmail.com> wrote:
>
> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> "git config set pull.rebase=false" currently fails with "wrong
> number of arguments", and the implicit form "git config
> pull.rebase=false" fails with "invalid key". Neither points at
> the real problem: the value is missing.
>
> Report that directly, and when the argument has the shape
> "<valid-key>=<value>", also suggest the split form:
>
>     $ git config set pull.rebase=false
>     error: missing value to set to the variable 'pull.rebase=false'
>     hint: did you mean "git config set pull.rebase false"?
>
> When the prefix before "=" is not a valid key, drop the hint:
>
>     $ git config set foo=bar
>     error: missing value to set to a variable with an invalid name 'foo=bar'
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---
>     config: suggest the correct form when key contains "="
>
>      * Skip the hint when the inferred value contains whitespace, so git
>        config set pull.rebase=false "hello world" no longer suggests a
>        malformed command.
>      * Replace the inline actions == 0 check with a named actions_implicit
>        flag, simplfied the code.
>
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2302%2FHaraldNordgren%2Fconfig-hint-equals-key-v4
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2302/HaraldNordgren/config-hint-equals-key-v4
> Pull-Request: https://github.com/git/git/pull/2302
>
> Range-diff vs v3:
>
>  1:  6b9d66361d ! 1:  780b99409c config: suggest the correct form when key contains "=" in set context
>      @@ Metadata
>       Author: Harald Nordgren <haraldnordgren@gmail.com>
>
>        ## Commit message ##
>      -    config: suggest the correct form when key contains "=" in set context
>      +    config: improve diagnostic for "set" with missing value
>
>      -    A user who types "git config pull.rebase=false" gets only "error:
>      -    invalid key: pull.rebase=false" with no clue what went wrong.
>      +    "git config set pull.rebase=false" currently fails with "wrong
>      +    number of arguments", and the implicit form "git config
>      +    pull.rebase=false" fails with "invalid key". Neither points at
>      +    the real problem: the value is missing.
>
>      -    Emit a "did you mean ..." hint suggesting the split form.  Restrict it
>      -    to plausible-set contexts ("git config set", bare "git config <key>",
>      -    and their 2-arg forms); explicit "get"/"unset" keep the existing error.
>      +    Report that directly, and when the argument has the shape
>      +    "<valid-key>=<value>", also suggest the split form:
>
>      -    "=" is legal inside a subsection, so only fire when "=" lands after
>      -    the last ".".  When the user supplied a separate value, use it in the
>      -    suggestion instead of the suffix after "=":
>      +        $ git config set pull.rebase=false
>      +        error: missing value to set to the variable 'pull.rebase=false'
>      +        hint: did you mean "git config set pull.rebase false"?
>
>      -        $ git config set pull.rebase=false true
>      -        error: invalid key: pull.rebase=false
>      -        hint: did you mean "git config set pull.rebase true"?
>      +    When the prefix before "=" is not a valid key, drop the hint:
>      +
>      +        $ git config set foo=bar
>      +        error: missing value to set to a variable with an invalid name 'foo=bar'
>
>      -    Signed-off-by: Harald Nordgren <harald.nordgren@kostdoktorn.se>
>           Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
>
>        ## builtin/config.c ##
>      @@ builtin/config.c: static void check_argc(int argc, int min, int max)
>         exit(129);
>        }
>
>      -+static void advise_setting_with_equals(const char *key, const char *value)
>      ++static int is_valid_key(const char *key)
>       +{
>       + const char *last_dot = strrchr(key, '.');
>      -+ const char *eq;
>       +
>      -+ if (!last_dot)
>      -+         return;
>      -+ eq = strchr(last_dot + 1, '=');
>      -+ if (!eq)
>      -+         return;
>      -+ if (!value)
>      -+         value = eq + 1;
>      -+ if (!*value || strpbrk(value, " \t\n"))
>      -+         return;
>      -+ advise(_("did you mean \"git config set %.*s %s\"?"),
>      -+        (int)(eq - key), key, value);
>      ++ return last_dot && isalpha(last_dot[1]);
>      ++}
>      ++
>      ++static NORETURN void die_missing_set_value(const char *arg)
>      ++{
>      ++ const char *last_dot = strrchr(arg, '.');
>      ++ const char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;
>      ++ char *prefix = eq ? xstrndup(arg, eq - arg) : NULL;
>      ++
>      ++ if (prefix && is_valid_key(prefix)) {
>      ++         error(_("missing value to set to the variable '%s'"), arg);
>      ++         advise(_("did you mean \"git config set %s %s\"?"),
>      ++                prefix, eq + 1);
>      ++ } else if (is_valid_key(arg)) {
>      ++         error(_("missing value to set to the variable '%s'"), arg);
>      ++ } else {
>      ++         error(_("missing value to set to a variable with an invalid name '%s'"),
>      ++               arg);
>      ++ }
>      ++ free(prefix);
>      ++ exit(129);
>       +}
>       +
>        static void show_config_origin(const struct config_display_options *opts,
>      @@ builtin/config.c: static int cmd_config_set(int argc, const char **argv, const c
>
>         argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,
>                              PARSE_OPT_STOP_AT_NON_OPTION);
>      -+ if (argc == 1 && strchr(argv[0], '=')) {
>      -+         error(_("wrong number of arguments, should be 2"));
>      -+         advise_setting_with_equals(argv[0], NULL);
>      -+         exit(129);
>      -+ }
>      ++ if (argc == 1)
>      ++         die_missing_set_value(argv[0]);
>         check_argc(argc, 2, 2);
>
>         if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
>      -@@ builtin/config.c: static int cmd_config_set(int argc, const char **argv, const char *prefix,
>      -                  error(_("cannot overwrite multiple values with a single value\n"
>      -                  "       Use --value=<pattern>, --append or --all to change %s."), argv[0]);
>      -  }
>      -+ if (ret == CONFIG_INVALID_KEY)
>      -+         advise_setting_with_equals(argv[0], argv[1]);
>      -
>      -  location_options_release(&location_opts);
>      -  free(comment);
>       @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)
>         };
>         char *value = NULL, *comment = NULL;
>      @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, con
>                 case 1: actions = ACTION_GET; break;
>                 case 2: actions = ACTION_SET; break;
>       @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)
>      -          if (ret == CONFIG_NOTHING_SET)
>      -                  error(_("cannot overwrite multiple values with a single value\n"
>      -                  "       Use a regexp, --add or --replace-all to change %s."), argv[0]);
>      -+         else if (ret == CONFIG_INVALID_KEY)
>      -+                 advise_setting_with_equals(argv[0], argv[1]);
>      -  }
>      -  else if (actions == ACTION_SET_ALL) {
>      -          check_write(&location_opts.source);
>      -@@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)
>      -          check_argc(argc, 1, 2);
>      -          ret = get_value(&location_opts, &display_opts, argv[0], argv[1],
>      -                          0, flags);
>      -+         if (ret == CONFIG_INVALID_KEY && actions_implicit)
>      -+                 advise_setting_with_equals(argv[0], NULL);
>      -  }
>      -  else if (actions == ACTION_GET_ALL) {
>      -          check_argc(argc, 1, 2);
>      +                  error(_("no action specified"));
>      +                  exit(129);
>      +          }
>      ++ if (actions_implicit && argc == 1) {
>      ++         const char *last_dot = strrchr(argv[0], '.');
>      ++         if (last_dot && strchr(last_dot + 1, '='))
>      ++                 die_missing_set_value(argv[0]);
>      ++ }
>      +  if (display_opts.omit_values &&
>      +      !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {
>      +          error(_("--name-only is only applicable to --list or --get-regexp"));
>
>        ## t/t1300-config.sh ##
>       @@ t/t1300-config.sh: test_expect_success 'invalid key' '
>         test_must_fail git config inval.2key blabla
>        '
>
>      -+test_expect_success 'misplaced "=" in key: bare 1-arg form hints' '
>      -+ test_must_fail git config pull.rebase=false 2>err &&
>      -+ test_grep "invalid key: pull\\.rebase=false" err &&
>      ++test_expect_success 'set with 1 arg of "key=value": valid key suggests split form' '
>      ++ test_must_fail git config set pull.rebase=false 2>err &&
>      ++ test_grep "missing value to set to the variable .pull\\.rebase=false." err &&
>       + test_grep "did you mean .git config set pull\\.rebase false." err
>       +'
>       +
>      -+test_expect_success 'misplaced "=" in key: bare 2-arg form uses given value' '
>      -+ test_must_fail git config pull.rebase=false true 2>err &&
>      -+ test_grep "did you mean .git config set pull\\.rebase true." err
>      -+'
>      -+
>      -+test_expect_success 'misplaced "=" in key: set subcommand uses given value' '
>      -+ test_must_fail git config set pull.rebase=false true 2>err &&
>      -+ test_grep "did you mean .git config set pull\\.rebase true." err
>      -+'
>      -+
>      -+test_expect_success 'misplaced "=" in key: set with single arg hints' '
>      -+ test_must_fail git config set pull.rebase=false 2>err &&
>      -+ test_grep "wrong number of arguments" err &&
>      ++test_expect_success 'set with 1 arg of "key=value": implicit form suggests split form' '
>      ++ test_must_fail git config pull.rebase=false 2>err &&
>      ++ test_grep "missing value to set to the variable .pull\\.rebase=false." err &&
>       + test_grep "did you mean .git config set pull\\.rebase false." err
>       +'
>       +
>      -+test_expect_success 'misplaced "=" in key: explicit --get does not hint' '
>      -+ test_must_fail git config --get pull.rebase=false 2>err &&
>      -+ test_grep "invalid key: pull\\.rebase=false" err &&
>      ++test_expect_success 'set with 1 arg of "key=value": invalid key does not suggest split form' '
>      ++ test_must_fail git config set foo=bar 2>err &&
>      ++ test_grep "missing value to set to a variable with an invalid name .foo=bar." err &&
>       + test_grep ! "did you mean" err
>       +'
>       +
>      -+test_expect_success 'misplaced "=" in key: get subcommand does not hint' '
>      -+ test_must_fail git config get pull.rebase=false 2>err &&
>      ++test_expect_success 'set with 1 arg: variable name starting with digit is invalid' '
>      ++ test_must_fail git config set foo.1bar=baz 2>err &&
>      ++ test_grep "missing value to set to a variable with an invalid name .foo\\.1bar=baz." err &&
>       + test_grep ! "did you mean" err
>       +'
>       +
>      -+test_expect_success 'misplaced "=" in key: unset subcommand does not hint' '
>      -+ test_must_fail git config unset pull.rebase=false 2>err &&
>      ++test_expect_success 'set with 1 arg of valid key reports missing value' '
>      ++ test_must_fail git config set pull.rebase 2>err &&
>      ++ test_grep "missing value to set to the variable .pull\\.rebase." err &&
>       + test_grep ! "did you mean" err
>       +'
>       +
>      -+test_expect_success 'misplaced "=" in key: value with whitespace skips hint' '
>      -+ test_must_fail git config set pull.rebase=false "hello world" 2>err &&
>      -+ test_grep "invalid key: pull\\.rebase=false" err &&
>      ++test_expect_success 'set with 2 args including "=" in invalid key does not suggest' '
>      ++ test_must_fail git config set pull.rebase=false true 2>err &&
>       + test_grep ! "did you mean" err
>       +'
>       +
>      -+test_expect_success '"=" inside subsection is valid, no hint' '
>      ++test_expect_success '"=" inside subsection is valid' '
>       + test_when_finished "rm -f subsection.cfg" &&
>      -+ git config set -f subsection.cfg foo.bar=baz.boo qux 2>err &&
>      -+ test_grep ! "did you mean" err &&
>      ++ git config set -f subsection.cfg foo.bar=baz.boo qux &&
>       + echo qux >expect &&
>       + git config get -f subsection.cfg foo.bar=baz.boo >actual &&
>       + test_cmp expect actual
>
>
>  builtin/config.c  | 39 ++++++++++++++++++++++++++++++++++++++-
>  t/t1300-config.sh | 43 +++++++++++++++++++++++++++++++++++++++++++
>  2 files changed, 81 insertions(+), 1 deletion(-)
>
> diff --git a/builtin/config.c b/builtin/config.c
> index cf4ba0f7cc..6fe2d85814 100644
> --- a/builtin/config.c
> +++ b/builtin/config.c
> @@ -1,6 +1,7 @@
>  #define USE_THE_REPOSITORY_VARIABLE
>  #include "builtin.h"
>  #include "abspath.h"
> +#include "advice.h"
>  #include "config.h"
>  #include "color.h"
>  #include "date.h"
> @@ -210,6 +211,33 @@ static void check_argc(int argc, int min, int max)
>         exit(129);
>  }
>
> +static int is_valid_key(const char *key)
> +{
> +       const char *last_dot = strrchr(key, '.');
> +
> +       return last_dot && isalpha(last_dot[1]);
> +}
> +
> +static NORETURN void die_missing_set_value(const char *arg)
> +{
> +       const char *last_dot = strrchr(arg, '.');
> +       const char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;
> +       char *prefix = eq ? xstrndup(arg, eq - arg) : NULL;
> +
> +       if (prefix && is_valid_key(prefix)) {
> +               error(_("missing value to set to the variable '%s'"), arg);
> +               advise(_("did you mean \"git config set %s %s\"?"),
> +                      prefix, eq + 1);
> +       } else if (is_valid_key(arg)) {
> +               error(_("missing value to set to the variable '%s'"), arg);
> +       } else {
> +               error(_("missing value to set to a variable with an invalid name '%s'"),
> +                     arg);
> +       }
> +       free(prefix);
> +       exit(129);
> +}
> +
>  static void show_config_origin(const struct config_display_options *opts,
>                                const struct key_value_info *kvi,
>                                struct strbuf *buf)
> @@ -1133,6 +1161,8 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix,
>
>         argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,
>                              PARSE_OPT_STOP_AT_NON_OPTION);
> +       if (argc == 1)
> +               die_missing_set_value(argv[0]);
>         check_argc(argc, 2, 2);
>
>         if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
> @@ -1371,6 +1401,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)
>         };
>         char *value = NULL, *comment = NULL;
>         int ret = 0;
> +       int actions_implicit;
>         struct key_value_info default_kvi = KVI_INIT;
>
>         argc = parse_options(argc, argv, prefix, opts,
> @@ -1385,7 +1416,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)
>                 exit(129);
>         }
>
> -       if (actions == 0)
> +       actions_implicit = (actions == 0);
> +       if (actions_implicit)
>                 switch (argc) {
>                 case 1: actions = ACTION_GET; break;
>                 case 2: actions = ACTION_SET; break;
> @@ -1394,6 +1426,11 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)
>                         error(_("no action specified"));
>                         exit(129);
>                 }
> +       if (actions_implicit && argc == 1) {
> +               const char *last_dot = strrchr(argv[0], '.');
> +               if (last_dot && strchr(last_dot + 1, '='))
> +                       die_missing_set_value(argv[0]);
> +       }
>         if (display_opts.omit_values &&
>             !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {
>                 error(_("--name-only is only applicable to --list or --get-regexp"));
> diff --git a/t/t1300-config.sh b/t/t1300-config.sh
> index 11fc976f3a..4a8a381bd8 100755
> --- a/t/t1300-config.sh
> +++ b/t/t1300-config.sh
> @@ -469,6 +469,49 @@ test_expect_success 'invalid key' '
>         test_must_fail git config inval.2key blabla
>  '
>
> +test_expect_success 'set with 1 arg of "key=value": valid key suggests split form' '
> +       test_must_fail git config set pull.rebase=false 2>err &&
> +       test_grep "missing value to set to the variable .pull\\.rebase=false." err &&
> +       test_grep "did you mean .git config set pull\\.rebase false." err
> +'
> +
> +test_expect_success 'set with 1 arg of "key=value": implicit form suggests split form' '
> +       test_must_fail git config pull.rebase=false 2>err &&
> +       test_grep "missing value to set to the variable .pull\\.rebase=false." err &&
> +       test_grep "did you mean .git config set pull\\.rebase false." err
> +'
> +
> +test_expect_success 'set with 1 arg of "key=value": invalid key does not suggest split form' '
> +       test_must_fail git config set foo=bar 2>err &&
> +       test_grep "missing value to set to a variable with an invalid name .foo=bar." err &&
> +       test_grep ! "did you mean" err
> +'
> +
> +test_expect_success 'set with 1 arg: variable name starting with digit is invalid' '
> +       test_must_fail git config set foo.1bar=baz 2>err &&
> +       test_grep "missing value to set to a variable with an invalid name .foo\\.1bar=baz." err &&
> +       test_grep ! "did you mean" err
> +'
> +
> +test_expect_success 'set with 1 arg of valid key reports missing value' '
> +       test_must_fail git config set pull.rebase 2>err &&
> +       test_grep "missing value to set to the variable .pull\\.rebase." err &&
> +       test_grep ! "did you mean" err
> +'
> +
> +test_expect_success 'set with 2 args including "=" in invalid key does not suggest' '
> +       test_must_fail git config set pull.rebase=false true 2>err &&
> +       test_grep ! "did you mean" err
> +'
> +
> +test_expect_success '"=" inside subsection is valid' '
> +       test_when_finished "rm -f subsection.cfg" &&
> +       git config set -f subsection.cfg foo.bar=baz.boo qux &&
> +       echo qux >expect &&
> +       git config get -f subsection.cfg foo.bar=baz.boo >actual &&
> +       test_cmp expect actual
> +'
> +
>  test_expect_success 'correct key' '
>         git config 123456.a123 987
>  '
>
> base-commit: 56a4f3c3a221adf1df9b39da69b8a6890f803157
> --
> gitgitgadget

^ permalink raw reply

* [PATCH v4] config: improve diagnostic for "set" with missing value
From: Harald Nordgren via GitGitGadget @ 2026-05-26 19:21 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2302.v3.git.git.1779697995418.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

"git config set pull.rebase=false" currently fails with "wrong
number of arguments", and the implicit form "git config
pull.rebase=false" fails with "invalid key". Neither points at
the real problem: the value is missing.

Report that directly, and when the argument has the shape
"<valid-key>=<value>", also suggest the split form:

    $ git config set pull.rebase=false
    error: missing value to set to the variable 'pull.rebase=false'
    hint: did you mean "git config set pull.rebase false"?

When the prefix before "=" is not a valid key, drop the hint:

    $ git config set foo=bar
    error: missing value to set to a variable with an invalid name 'foo=bar'

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
    config: suggest the correct form when key contains "="
    
     * Skip the hint when the inferred value contains whitespace, so git
       config set pull.rebase=false "hello world" no longer suggests a
       malformed command.
     * Replace the inline actions == 0 check with a named actions_implicit
       flag, simplfied the code.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2302%2FHaraldNordgren%2Fconfig-hint-equals-key-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2302/HaraldNordgren/config-hint-equals-key-v4
Pull-Request: https://github.com/git/git/pull/2302

Range-diff vs v3:

 1:  6b9d66361d ! 1:  780b99409c config: suggest the correct form when key contains "=" in set context
     @@ Metadata
      Author: Harald Nordgren <haraldnordgren@gmail.com>
      
       ## Commit message ##
     -    config: suggest the correct form when key contains "=" in set context
     +    config: improve diagnostic for "set" with missing value
      
     -    A user who types "git config pull.rebase=false" gets only "error:
     -    invalid key: pull.rebase=false" with no clue what went wrong.
     +    "git config set pull.rebase=false" currently fails with "wrong
     +    number of arguments", and the implicit form "git config
     +    pull.rebase=false" fails with "invalid key". Neither points at
     +    the real problem: the value is missing.
      
     -    Emit a "did you mean ..." hint suggesting the split form.  Restrict it
     -    to plausible-set contexts ("git config set", bare "git config <key>",
     -    and their 2-arg forms); explicit "get"/"unset" keep the existing error.
     +    Report that directly, and when the argument has the shape
     +    "<valid-key>=<value>", also suggest the split form:
      
     -    "=" is legal inside a subsection, so only fire when "=" lands after
     -    the last ".".  When the user supplied a separate value, use it in the
     -    suggestion instead of the suffix after "=":
     +        $ git config set pull.rebase=false
     +        error: missing value to set to the variable 'pull.rebase=false'
     +        hint: did you mean "git config set pull.rebase false"?
      
     -        $ git config set pull.rebase=false true
     -        error: invalid key: pull.rebase=false
     -        hint: did you mean "git config set pull.rebase true"?
     +    When the prefix before "=" is not a valid key, drop the hint:
     +
     +        $ git config set foo=bar
     +        error: missing value to set to a variable with an invalid name 'foo=bar'
      
     -    Signed-off-by: Harald Nordgren <harald.nordgren@kostdoktorn.se>
          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
      
       ## builtin/config.c ##
     @@ builtin/config.c: static void check_argc(int argc, int min, int max)
       	exit(129);
       }
       
     -+static void advise_setting_with_equals(const char *key, const char *value)
     ++static int is_valid_key(const char *key)
      +{
      +	const char *last_dot = strrchr(key, '.');
     -+	const char *eq;
      +
     -+	if (!last_dot)
     -+		return;
     -+	eq = strchr(last_dot + 1, '=');
     -+	if (!eq)
     -+		return;
     -+	if (!value)
     -+		value = eq + 1;
     -+	if (!*value || strpbrk(value, " \t\n"))
     -+		return;
     -+	advise(_("did you mean \"git config set %.*s %s\"?"),
     -+	       (int)(eq - key), key, value);
     ++	return last_dot && isalpha(last_dot[1]);
     ++}
     ++
     ++static NORETURN void die_missing_set_value(const char *arg)
     ++{
     ++	const char *last_dot = strrchr(arg, '.');
     ++	const char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;
     ++	char *prefix = eq ? xstrndup(arg, eq - arg) : NULL;
     ++
     ++	if (prefix && is_valid_key(prefix)) {
     ++		error(_("missing value to set to the variable '%s'"), arg);
     ++		advise(_("did you mean \"git config set %s %s\"?"),
     ++		       prefix, eq + 1);
     ++	} else if (is_valid_key(arg)) {
     ++		error(_("missing value to set to the variable '%s'"), arg);
     ++	} else {
     ++		error(_("missing value to set to a variable with an invalid name '%s'"),
     ++		      arg);
     ++	}
     ++	free(prefix);
     ++	exit(129);
      +}
      +
       static void show_config_origin(const struct config_display_options *opts,
     @@ builtin/config.c: static int cmd_config_set(int argc, const char **argv, const c
       
       	argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,
       			     PARSE_OPT_STOP_AT_NON_OPTION);
     -+	if (argc == 1 && strchr(argv[0], '=')) {
     -+		error(_("wrong number of arguments, should be 2"));
     -+		advise_setting_with_equals(argv[0], NULL);
     -+		exit(129);
     -+	}
     ++	if (argc == 1)
     ++		die_missing_set_value(argv[0]);
       	check_argc(argc, 2, 2);
       
       	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
     -@@ builtin/config.c: static int cmd_config_set(int argc, const char **argv, const char *prefix,
     - 			error(_("cannot overwrite multiple values with a single value\n"
     - 			"       Use --value=<pattern>, --append or --all to change %s."), argv[0]);
     - 	}
     -+	if (ret == CONFIG_INVALID_KEY)
     -+		advise_setting_with_equals(argv[0], argv[1]);
     - 
     - 	location_options_release(&location_opts);
     - 	free(comment);
      @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)
       	};
       	char *value = NULL, *comment = NULL;
     @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, con
       		case 1: actions = ACTION_GET; break;
       		case 2: actions = ACTION_SET; break;
      @@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)
     - 		if (ret == CONFIG_NOTHING_SET)
     - 			error(_("cannot overwrite multiple values with a single value\n"
     - 			"       Use a regexp, --add or --replace-all to change %s."), argv[0]);
     -+		else if (ret == CONFIG_INVALID_KEY)
     -+			advise_setting_with_equals(argv[0], argv[1]);
     - 	}
     - 	else if (actions == ACTION_SET_ALL) {
     - 		check_write(&location_opts.source);
     -@@ builtin/config.c: static int cmd_config_actions(int argc, const char **argv, const char *prefix)
     - 		check_argc(argc, 1, 2);
     - 		ret = get_value(&location_opts, &display_opts, argv[0], argv[1],
     - 				0, flags);
     -+		if (ret == CONFIG_INVALID_KEY && actions_implicit)
     -+			advise_setting_with_equals(argv[0], NULL);
     - 	}
     - 	else if (actions == ACTION_GET_ALL) {
     - 		check_argc(argc, 1, 2);
     + 			error(_("no action specified"));
     + 			exit(129);
     + 		}
     ++	if (actions_implicit && argc == 1) {
     ++		const char *last_dot = strrchr(argv[0], '.');
     ++		if (last_dot && strchr(last_dot + 1, '='))
     ++			die_missing_set_value(argv[0]);
     ++	}
     + 	if (display_opts.omit_values &&
     + 	    !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {
     + 		error(_("--name-only is only applicable to --list or --get-regexp"));
      
       ## t/t1300-config.sh ##
      @@ t/t1300-config.sh: test_expect_success 'invalid key' '
       	test_must_fail git config inval.2key blabla
       '
       
     -+test_expect_success 'misplaced "=" in key: bare 1-arg form hints' '
     -+	test_must_fail git config pull.rebase=false 2>err &&
     -+	test_grep "invalid key: pull\\.rebase=false" err &&
     ++test_expect_success 'set with 1 arg of "key=value": valid key suggests split form' '
     ++	test_must_fail git config set pull.rebase=false 2>err &&
     ++	test_grep "missing value to set to the variable .pull\\.rebase=false." err &&
      +	test_grep "did you mean .git config set pull\\.rebase false." err
      +'
      +
     -+test_expect_success 'misplaced "=" in key: bare 2-arg form uses given value' '
     -+	test_must_fail git config pull.rebase=false true 2>err &&
     -+	test_grep "did you mean .git config set pull\\.rebase true." err
     -+'
     -+
     -+test_expect_success 'misplaced "=" in key: set subcommand uses given value' '
     -+	test_must_fail git config set pull.rebase=false true 2>err &&
     -+	test_grep "did you mean .git config set pull\\.rebase true." err
     -+'
     -+
     -+test_expect_success 'misplaced "=" in key: set with single arg hints' '
     -+	test_must_fail git config set pull.rebase=false 2>err &&
     -+	test_grep "wrong number of arguments" err &&
     ++test_expect_success 'set with 1 arg of "key=value": implicit form suggests split form' '
     ++	test_must_fail git config pull.rebase=false 2>err &&
     ++	test_grep "missing value to set to the variable .pull\\.rebase=false." err &&
      +	test_grep "did you mean .git config set pull\\.rebase false." err
      +'
      +
     -+test_expect_success 'misplaced "=" in key: explicit --get does not hint' '
     -+	test_must_fail git config --get pull.rebase=false 2>err &&
     -+	test_grep "invalid key: pull\\.rebase=false" err &&
     ++test_expect_success 'set with 1 arg of "key=value": invalid key does not suggest split form' '
     ++	test_must_fail git config set foo=bar 2>err &&
     ++	test_grep "missing value to set to a variable with an invalid name .foo=bar." err &&
      +	test_grep ! "did you mean" err
      +'
      +
     -+test_expect_success 'misplaced "=" in key: get subcommand does not hint' '
     -+	test_must_fail git config get pull.rebase=false 2>err &&
     ++test_expect_success 'set with 1 arg: variable name starting with digit is invalid' '
     ++	test_must_fail git config set foo.1bar=baz 2>err &&
     ++	test_grep "missing value to set to a variable with an invalid name .foo\\.1bar=baz." err &&
      +	test_grep ! "did you mean" err
      +'
      +
     -+test_expect_success 'misplaced "=" in key: unset subcommand does not hint' '
     -+	test_must_fail git config unset pull.rebase=false 2>err &&
     ++test_expect_success 'set with 1 arg of valid key reports missing value' '
     ++	test_must_fail git config set pull.rebase 2>err &&
     ++	test_grep "missing value to set to the variable .pull\\.rebase." err &&
      +	test_grep ! "did you mean" err
      +'
      +
     -+test_expect_success 'misplaced "=" in key: value with whitespace skips hint' '
     -+	test_must_fail git config set pull.rebase=false "hello world" 2>err &&
     -+	test_grep "invalid key: pull\\.rebase=false" err &&
     ++test_expect_success 'set with 2 args including "=" in invalid key does not suggest' '
     ++	test_must_fail git config set pull.rebase=false true 2>err &&
      +	test_grep ! "did you mean" err
      +'
      +
     -+test_expect_success '"=" inside subsection is valid, no hint' '
     ++test_expect_success '"=" inside subsection is valid' '
      +	test_when_finished "rm -f subsection.cfg" &&
     -+	git config set -f subsection.cfg foo.bar=baz.boo qux 2>err &&
     -+	test_grep ! "did you mean" err &&
     ++	git config set -f subsection.cfg foo.bar=baz.boo qux &&
      +	echo qux >expect &&
      +	git config get -f subsection.cfg foo.bar=baz.boo >actual &&
      +	test_cmp expect actual


 builtin/config.c  | 39 ++++++++++++++++++++++++++++++++++++++-
 t/t1300-config.sh | 43 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+), 1 deletion(-)

diff --git a/builtin/config.c b/builtin/config.c
index cf4ba0f7cc..6fe2d85814 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -1,6 +1,7 @@
 #define USE_THE_REPOSITORY_VARIABLE
 #include "builtin.h"
 #include "abspath.h"
+#include "advice.h"
 #include "config.h"
 #include "color.h"
 #include "date.h"
@@ -210,6 +211,33 @@ static void check_argc(int argc, int min, int max)
 	exit(129);
 }
 
+static int is_valid_key(const char *key)
+{
+	const char *last_dot = strrchr(key, '.');
+
+	return last_dot && isalpha(last_dot[1]);
+}
+
+static NORETURN void die_missing_set_value(const char *arg)
+{
+	const char *last_dot = strrchr(arg, '.');
+	const char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL;
+	char *prefix = eq ? xstrndup(arg, eq - arg) : NULL;
+
+	if (prefix && is_valid_key(prefix)) {
+		error(_("missing value to set to the variable '%s'"), arg);
+		advise(_("did you mean \"git config set %s %s\"?"),
+		       prefix, eq + 1);
+	} else if (is_valid_key(arg)) {
+		error(_("missing value to set to the variable '%s'"), arg);
+	} else {
+		error(_("missing value to set to a variable with an invalid name '%s'"),
+		      arg);
+	}
+	free(prefix);
+	exit(129);
+}
+
 static void show_config_origin(const struct config_display_options *opts,
 			       const struct key_value_info *kvi,
 			       struct strbuf *buf)
@@ -1133,6 +1161,8 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix,
 
 	argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,
 			     PARSE_OPT_STOP_AT_NON_OPTION);
+	if (argc == 1)
+		die_missing_set_value(argv[0]);
 	check_argc(argc, 2, 2);
 
 	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
@@ -1371,6 +1401,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)
 	};
 	char *value = NULL, *comment = NULL;
 	int ret = 0;
+	int actions_implicit;
 	struct key_value_info default_kvi = KVI_INIT;
 
 	argc = parse_options(argc, argv, prefix, opts,
@@ -1385,7 +1416,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)
 		exit(129);
 	}
 
-	if (actions == 0)
+	actions_implicit = (actions == 0);
+	if (actions_implicit)
 		switch (argc) {
 		case 1: actions = ACTION_GET; break;
 		case 2: actions = ACTION_SET; break;
@@ -1394,6 +1426,11 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)
 			error(_("no action specified"));
 			exit(129);
 		}
+	if (actions_implicit && argc == 1) {
+		const char *last_dot = strrchr(argv[0], '.');
+		if (last_dot && strchr(last_dot + 1, '='))
+			die_missing_set_value(argv[0]);
+	}
 	if (display_opts.omit_values &&
 	    !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) {
 		error(_("--name-only is only applicable to --list or --get-regexp"));
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index 11fc976f3a..4a8a381bd8 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -469,6 +469,49 @@ test_expect_success 'invalid key' '
 	test_must_fail git config inval.2key blabla
 '
 
+test_expect_success 'set with 1 arg of "key=value": valid key suggests split form' '
+	test_must_fail git config set pull.rebase=false 2>err &&
+	test_grep "missing value to set to the variable .pull\\.rebase=false." err &&
+	test_grep "did you mean .git config set pull\\.rebase false." err
+'
+
+test_expect_success 'set with 1 arg of "key=value": implicit form suggests split form' '
+	test_must_fail git config pull.rebase=false 2>err &&
+	test_grep "missing value to set to the variable .pull\\.rebase=false." err &&
+	test_grep "did you mean .git config set pull\\.rebase false." err
+'
+
+test_expect_success 'set with 1 arg of "key=value": invalid key does not suggest split form' '
+	test_must_fail git config set foo=bar 2>err &&
+	test_grep "missing value to set to a variable with an invalid name .foo=bar." err &&
+	test_grep ! "did you mean" err
+'
+
+test_expect_success 'set with 1 arg: variable name starting with digit is invalid' '
+	test_must_fail git config set foo.1bar=baz 2>err &&
+	test_grep "missing value to set to a variable with an invalid name .foo\\.1bar=baz." err &&
+	test_grep ! "did you mean" err
+'
+
+test_expect_success 'set with 1 arg of valid key reports missing value' '
+	test_must_fail git config set pull.rebase 2>err &&
+	test_grep "missing value to set to the variable .pull\\.rebase." err &&
+	test_grep ! "did you mean" err
+'
+
+test_expect_success 'set with 2 args including "=" in invalid key does not suggest' '
+	test_must_fail git config set pull.rebase=false true 2>err &&
+	test_grep ! "did you mean" err
+'
+
+test_expect_success '"=" inside subsection is valid' '
+	test_when_finished "rm -f subsection.cfg" &&
+	git config set -f subsection.cfg foo.bar=baz.boo qux &&
+	echo qux >expect &&
+	git config get -f subsection.cfg foo.bar=baz.boo >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'correct key' '
 	git config 123456.a123 987
 '

base-commit: 56a4f3c3a221adf1df9b39da69b8a6890f803157
-- 
gitgitgadget

^ permalink raw reply related

* Re: git mv after the fact
From: Frieder Hannenheim @ 2026-05-26 16:50 UTC (permalink / raw)
  To: Chris Torek; +Cc: git
In-Reply-To: <CAPx1Gvd9+z0th9whCbcA60_bWproPp+kwp3qDmhQOe4G=0=E6A@mail.gmail.com>

In my particular use case I changed a patch to be a git patch with a
commit message and different filename so the move was not discovered
automatically. But I'm not sure if I staged the files so maybe it would
have been discovered.

Frieder

On 26.05.26 18:40, Chris Torek wrote:
> On Tue, May 26, 2026 at 6:18 AM Frieder Hannenheim <mail@fhannenheim.net> wrote:
>> I'd like to propose a new flag for git mv, that updates the index
>> like git mv normally would but does not move the file. ...
> You may already know this, but technically no flag is needed:
> you can just "git add" the new name and "git rm" the old one,
> with the same effect.
>
> A flag for "git mv" would be convenient (and slightly more
> efficient, not in terms of storage but in terms of CPU time
> spent discovering that the contents under the new name
> already exist in the object database). But Git will discover
> the rename on its own in the usual way regardless of how
> you get to that point.
>
> Chris

^ permalink raw reply

* [PATCH 5/5] git-son: add tests
From: Evan Haque via GitGitGadget @ 2026-05-26 16:47 UTC (permalink / raw)
  To: git; +Cc: Evan Haque, Evan Haque
In-Reply-To: <pull.2122.git.1779814052.gitgitgadget@gmail.com>

From: Evan Haque <evanhaque1@gmail.com>

Add t5151-son.sh with nine test cases covering:

 - basic child repository creation
 - parent remote configuration in the child
 - .gitignore update in the parent
 - initial commit presence in the child
 - failure when the target directory already exists
 - --branch without --inherit is rejected cleanly
 - no leftover directory on validation failure
 - --inherit fetches parent history

Register the test in t/meson.build so the meson build system
discovers and runs it.

Assisted-by: Claude Opus 4.6
Signed-off-by: Evan Haque <evanhaque1@gmail.com>
---
 t/meson.build  |  1 +
 t/t5151-son.sh | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 64 insertions(+)
 create mode 100755 t/t5151-son.sh

diff --git a/t/meson.build b/t/meson.build
index fd955f44ef..523062df66 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -591,6 +591,7 @@ integration_tests = [
   't5004-archive-corner-cases.sh',
   't5100-mailinfo.sh',
   't5150-request-pull.sh',
+  't5151-son.sh',
   't5200-update-server-info.sh',
   't5300-pack-object.sh',
   't5301-sliding-window.sh',
diff --git a/t/t5151-son.sh b/t/t5151-son.sh
new file mode 100755
index 0000000000..826cbbfa66
--- /dev/null
+++ b/t/t5151-son.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+test_description='Test git son command.'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+test_expect_success 'setup parent repository' '
+	echo "parent content" >file.txt &&
+	git add file.txt &&
+	git commit -m "Initial parent commit"
+'
+
+test_expect_success 'son creates child repository' '
+	git son my-child &&
+	test -d my-child &&
+	test -d my-child/.git
+'
+
+test_expect_success 'son sets parent remote in child' '
+	(
+		cd my-child &&
+		git remote get-url parent
+	)
+'
+
+test_expect_success 'son adds child to parent .gitignore' '
+	grep "my-child/" .gitignore
+'
+
+test_expect_success 'son child has initial commit' '
+	(
+		cd my-child &&
+		test $(git log --oneline | wc -l) -eq 1
+	)
+'
+
+test_expect_success 'son fails if target already exists' '
+	test_must_fail git son my-child
+'
+
+test_expect_success 'son with --branch requires --inherit' '
+	test_must_fail git son --branch main branch-child
+'
+
+test_expect_success 'son with --branch leaves no directory on failure' '
+	! test -e branch-child
+'
+
+test_expect_success 'son with --inherit fetches parent history' '
+	git init --bare "$TRASH_DIRECTORY/parent.git" &&
+	git push "$TRASH_DIRECTORY/parent.git" main &&
+	git remote add origin "file://$TRASH_DIRECTORY/parent.git" &&
+	git son --inherit inherited-child &&
+	(
+		cd inherited-child &&
+		git log --oneline parent/main
+	)
+'
+
+test_done
-- 
gitgitgadget

^ permalink raw reply related

* [PATCH 4/5] git-son: add documentation
From: Evan Haque via GitGitGadget @ 2026-05-26 16:47 UTC (permalink / raw)
  To: git; +Cc: Evan Haque, Evan Haque
In-Reply-To: <pull.2122.git.1779814052.gitgitgadget@gmail.com>

From: Evan Haque <evanhaque1@gmail.com>

Add a man page (git-son.adoc) documenting the synopsis, options, and
usage examples for the new command. Register the page in
Documentation/meson.build so it is built by the meson doc target.

Assisted-by: Claude Opus 4.6
Signed-off-by: Evan Haque <evanhaque1@gmail.com>
---
 Documentation/git-son.adoc | 64 ++++++++++++++++++++++++++++++++++++++
 Documentation/meson.build  |  1 +
 2 files changed, 65 insertions(+)
 create mode 100644 Documentation/git-son.adoc

diff --git a/Documentation/git-son.adoc b/Documentation/git-son.adoc
new file mode 100644
index 0000000000..17ec992bfd
--- /dev/null
+++ b/Documentation/git-son.adoc
@@ -0,0 +1,64 @@
+git-son(1)
+==========
+
+NAME
+----
+git-son - Create an independent child repository that knows its parent
+
+SYNOPSIS
+--------
+[verse]
+'git son' [--inherit] [--branch <branch>] <name>
+
+DESCRIPTION
+-----------
+
+Create a new independent Git repository inside the current working
+tree as a subdirectory named `<name>`. Unlike a submodule, the child
+repository is not tracked by the parent; instead, `<name>/` is added
+to the parent's `.gitignore`.
+
+The child repository is configured with a remote called `parent`
+pointing back to the parent repository's origin URL (or local path
+if no origin is set), allowing the child to fetch from the parent
+at any time.
+
+OPTIONS
+-------
+--inherit::
+	Fetch the parent's history into the child repository at
+	creation time. Without this flag, the child starts with a
+	single initial commit.
+
+--branch <branch>::
+	When used with `--inherit`, check out the given branch from
+	the parent instead of the default branch. This option
+	requires `--inherit`.
+
+<name>::
+	The name of the subdirectory (and child repository) to create.
+	Must not already exist.
+
+EXAMPLES
+--------
+
+Create a simple child repository:
+
+	git son my-tool
+
+Create a child that inherits the parent's history:
+
+	git son --inherit my-fork
+
+Create a child starting from a specific parent branch:
+
+	git son --inherit --branch feature my-experiment
+
+Later, from within the child, fetch updates from the parent:
+
+	cd my-tool
+	git fetch parent
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index f4854f802d..1ae7e5f644 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -139,6 +139,7 @@ manpages = {
   'git-show-ref.adoc' : 1,
   'git-show.adoc' : 1,
   'git-sh-setup.adoc' : 1,
+  'git-son.adoc' : 1,
   'git-sparse-checkout.adoc' : 1,
   'git-stage.adoc' : 1,
   'git-stash.adoc' : 1,
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH 3/5] git-son: add to command list as mainporcelain
From: Evan Haque via GitGitGadget @ 2026-05-26 16:47 UTC (permalink / raw)
  To: git; +Cc: Evan Haque, Evan Haque
In-Reply-To: <pull.2122.git.1779814052.gitgitgadget@gmail.com>

From: Evan Haque <evanhaque1@gmail.com>

Register git-son in command-list.txt as a mainporcelain command so
that it appears in "git help" output and is discoverable through the
standard help machinery.

Assisted-by: Claude Opus 4.6
Signed-off-by: Evan Haque <evanhaque1@gmail.com>
---
 command-list.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/command-list.txt b/command-list.txt
index 21b802c420..880177e0fd 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -186,6 +186,7 @@ git-show                                mainporcelain           info
 git-show-branch                         ancillaryinterrogators          complete
 git-show-index                          plumbinginterrogators
 git-show-ref                            plumbinginterrogators
+git-son                                 mainporcelain
 git-sparse-checkout                     mainporcelain
 git-stage                                                               complete
 git-stash                               mainporcelain
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH 2/5] git-son: register in Makefile and meson build system
From: Evan Haque via GitGitGadget @ 2026-05-26 16:47 UTC (permalink / raw)
  To: git; +Cc: Evan Haque, Evan Haque
In-Reply-To: <pull.2122.git.1779814052.gitgitgadget@gmail.com>

From: Evan Haque <evanhaque1@gmail.com>

Add git-son.sh to SCRIPT_SH in the Makefile and to the scripts_sh
array in meson.build so that the script is installed alongside the
other shell-based Git commands during "make install" and meson builds.

Also add /git-son to .gitignore so that the build artifact produced
from git-son.sh is not flagged as an untracked file.

Assisted-by: Claude Opus 4.6
Signed-off-by: Evan Haque <evanhaque1@gmail.com>
---
 .gitignore  | 1 +
 Makefile    | 1 +
 meson.build | 1 +
 3 files changed, 3 insertions(+)

diff --git a/.gitignore b/.gitignore
index 4da58c6754..5f329179c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -164,6 +164,7 @@
 /git-show-branch
 /git-show-index
 /git-show-ref
+/git-son
 /git-sparse-checkout
 /git-stage
 /git-stash
diff --git a/Makefile b/Makefile
index fb50c57e4f..4791f47af1 100644
--- a/Makefile
+++ b/Makefile
@@ -728,6 +728,7 @@ SCRIPT_SH += git-merge-resolve.sh
 SCRIPT_SH += git-mergetool.sh
 SCRIPT_SH += git-quiltimport.sh
 SCRIPT_SH += git-request-pull.sh
+SCRIPT_SH += git-son.sh
 SCRIPT_SH += git-submodule.sh
 SCRIPT_SH += git-web--browse.sh
 
diff --git a/meson.build b/meson.build
index 052c81f288..538bd4025f 100644
--- a/meson.build
+++ b/meson.build
@@ -1973,6 +1973,7 @@ scripts_sh = [
   'git-mergetool.sh',
   'git-quiltimport.sh',
   'git-request-pull.sh',
+  'git-son.sh',
   'git-sh-i18n.sh',
   'git-sh-setup.sh',
   'git-submodule.sh',
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH 1/5] git-son: add command to create independent child repositories
From: Evan Haque via GitGitGadget @ 2026-05-26 16:47 UTC (permalink / raw)
  To: git; +Cc: Evan Haque, Evan Haque
In-Reply-To: <pull.2122.git.1779814052.gitgitgadget@gmail.com>

From: Evan Haque <evanhaque1@gmail.com>

Introduce git-son, a new porcelain command that creates an independent
child repository inside the current working tree. Unlike submodules,
the child is not tracked by the parent; instead its directory is added
to the parent's .gitignore and a "parent" remote is configured in the
child pointing back to the parent's origin URL or local path.

This gives users a lightweight way to spin off a related repository
that knows where it came from without the coupling that submodules
impose.

The command supports two optional flags:

  --inherit    fetch the parent's history into the child at creation
  --branch     check out a specific parent branch (requires --inherit)

Assisted-by: Claude Opus 4.6
Signed-off-by: Evan Haque <evanhaque1@gmail.com>
---
 git-son.sh | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 97 insertions(+)
 create mode 100755 git-son.sh

diff --git a/git-son.sh b/git-son.sh
new file mode 100755
index 0000000000..a212c7b69f
--- /dev/null
+++ b/git-son.sh
@@ -0,0 +1,97 @@
+#!/bin/sh
+#
+# git-son: create an independent child repository that knows its parent
+#
+
+SUBDIRECTORY_OK='Yes'
+OPTIONS_SPEC='git son [options] <name>
+--
+inherit    fetch parent history into the son
+branch=    start the son from a specific parent branch
+'
+
+. git-sh-setup
+require_work_tree
+cd_to_toplevel
+
+inherit=
+branch=
+while test $# -gt 0
+do
+	case "$1" in
+	--inherit)
+		inherit=1 ;;
+	--branch)
+		shift
+		branch="$1" ;;
+	--)
+		shift; break ;;
+	-*)
+		usage ;;
+	*)
+		break ;;
+	esac
+	shift
+done
+
+name="$1"
+test -n "$name" || usage
+
+if test -n "$branch" && test -z "$inherit"
+then
+	die "fatal: --branch requires --inherit"
+fi
+
+parent_dir="$(pwd)"
+parent_remote="$(git remote get-url origin 2>/dev/null)" || parent_remote=
+
+if test -e "$name"
+then
+	die "fatal: '$name' already exists"
+fi
+
+mkdir "$name" || die "fatal: could not create directory '$name'"
+
+if ! echo "$name/" >> "$parent_dir/.gitignore" 2>/dev/null
+then
+	rm -rf "$name"
+	die "fatal: could not update .gitignore"
+fi
+
+cd "$name" || die "fatal: could not enter directory '$name'"
+
+if ! git init
+then
+	rm -rf "$parent_dir/$name"
+	die "fatal: could not initialize repository in '$name'"
+fi
+
+if test -n "$parent_remote"
+then
+	git remote add parent "$parent_remote"
+else
+	git remote add parent "$parent_dir"
+fi
+
+if test -n "$inherit"
+then
+	git fetch parent || die "fatal: could not fetch from parent"
+	if test -n "$branch"
+	then
+		git checkout -b "$branch" "parent/$branch" ||
+			die "fatal: could not checkout branch '$branch'"
+	else
+		git checkout -b main parent/HEAD 2>/dev/null ||
+			git checkout -b main "parent/$(git remote show parent | sed -n 's/.*HEAD branch: //p')" 2>/dev/null ||
+			echo "warning: could not determine parent HEAD, starting empty"
+	fi
+else
+	echo "# $name" > README.md
+	git add README.md
+	git commit -q -m "Initial commit"
+fi
+
+echo ""
+echo "Created son repository '$name'"
+echo "  parent: ${parent_remote:-$parent_dir}"
+echo "  inherit: ${inherit:-no}"
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH 0/5] git son: add command to create independent child repositories
From: Evan Haque via GitGitGadget @ 2026-05-26 16:47 UTC (permalink / raw)
  To: git; +Cc: Evan Haque


Motivation
==========

When spinning off a new project that is related to an existing repository,
there is no built-in way to create a child repository that maintains a link
back to its parent without the tight coupling of submodules. Submodules pin
the child to a specific commit and require the parent to track the child in
its index, which is too heavyweight when the child is meant to be fully
independent.

The typical workflow today is manual: git init, git remote add, update
.gitignore — three steps that are easy to forget or get wrong. git son
automates this and establishes a lightweight convention for the parent-child
relationship: a remote named parent in the child, and nothing in the parent
except an ignore rule.


Summary
=======

This series introduces git son, a new porcelain command that creates an
independent child repository inside the current working tree. Unlike a
submodule, the child is not tracked by the parent; instead, its subdirectory
is added to the parent's .gitignore. The child is configured at creation
time with a remote named parent pointing back to the parent repository's
origin URL (or local path if no origin exists), making the lineage explicit
and recoverable via standard Git commands.


Changes
=======

 * git-son.sh: new shell script implementing the command, supporting
   --inherit to fetch parent history at creation time and --branch <branch>
   (requires --inherit) to check out a specific parent branch
 * git-son registered in command-list.txt as mainporcelain
 * git-son.sh added to SCRIPT_SH in Makefile and to scripts_sh in
   meson.build
 * Documentation/git-son.adoc: new man page covering synopsis, option
   descriptions, and worked examples
 * Documentation/meson.build: git-son.adoc added to the manpage build list
 * t/t5151-son.sh: new test script covering basic creation, parent remote
   configuration, .gitignore update, idempotency failure, flag validation,
   and --inherit with a bare remote


Details
=======

The key design property is independence: the child is a fully self-contained
repository with no entry in the parent's index. The parent remote is the
only artifact linking the two, which means git fetch parent and git log
parent/<branch> work as expected from within the child without any special
tooling.

The --branch flag is intentionally restricted to --inherit mode. Without
fetching, there is no remote-tracking branch to check out from, so accepting
--branch alone would be misleading; the command dies with a clear diagnostic
in that case.

When no origin URL is available in the parent, the parent remote is set to
the parent's absolute local path. This covers the common case of
repositories that have never been pushed to a remote.


Testing
=======

t/t5151-son.sh covers the following scenarios:

 * Basic child repository creation (directory exists, .git present)
 * parent remote is correctly recorded in the child
 * Child directory is appended to the parent's .gitignore
 * Child starts with exactly one initial commit when --inherit is not used
 * Command fails without leaving a directory when the target already exists
 * --branch without --inherit is rejected before any filesystem changes
 * --inherit fetches parent history and the remote-tracking branch is
   reachable in the child

Evan Haque (5):
  git-son: add command to create independent child repositories
  git-son: register in Makefile and meson build system
  git-son: add to command list as mainporcelain
  git-son: add documentation
  git-son: add tests

 .gitignore                 |  1 +
 Documentation/git-son.adoc | 64 +++++++++++++++++++++++++
 Documentation/meson.build  |  1 +
 Makefile                   |  1 +
 command-list.txt           |  1 +
 git-son.sh                 | 97 ++++++++++++++++++++++++++++++++++++++
 meson.build                |  1 +
 t/meson.build              |  1 +
 t/t5151-son.sh             | 63 +++++++++++++++++++++++++
 9 files changed, 230 insertions(+)
 create mode 100644 Documentation/git-son.adoc
 create mode 100755 git-son.sh
 create mode 100755 t/t5151-son.sh


base-commit: aec3f587505a472db67e9462d0702e7d463a449d
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2122%2FEvandabest%2Fgit-son-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2122/Evandabest/git-son-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2122
-- 
gitgitgadget

^ permalink raw reply

* Re: git mv after the fact
From: Chris Torek @ 2026-05-26 16:40 UTC (permalink / raw)
  To: Frieder Hannenheim; +Cc: git
In-Reply-To: <02663c67-01ad-4dd1-aae6-9e9706f3d040@fhannenheim.net>

On Tue, May 26, 2026 at 6:18 AM Frieder Hannenheim <mail@fhannenheim.net> wrote:
> I'd like to propose a new flag for git mv, that updates the index
> like git mv normally would but does not move the file. ...

You may already know this, but technically no flag is needed:
you can just "git add" the new name and "git rm" the old one,
with the same effect.

A flag for "git mv" would be convenient (and slightly more
efficient, not in terms of storage but in terms of CPU time
spent discovering that the contents under the new name
already exist in the object database). But Git will discover
the rename on its own in the usual way regardless of how
you get to that point.

Chris

^ permalink raw reply

* [PATCH v2] completion: hide dotfiles for selected path completion
From: Zakariyah Ali via GitGitGadget @ 2026-05-26 15:23 UTC (permalink / raw)
  To: git; +Cc: Zakariyah Ali, Zakariyah Ali
In-Reply-To: <pull.2311.git.git.1779590184752.gitgitgadget@gmail.com>

From: Zakariyah Ali <zakariyahali100@gmail.com>

The completion helper for index paths uses git ls-files rather than
shell filename completion. As a result, leading-dot paths such as a
tracked .gitignore were offered even when the user had not started the
path with ".".

Hide leading-dot path components for git rm, git mv, and git ls-files
when completing an empty path component. Explicit dot completion is
still preserved, so git rm . can still complete .gitignore.

This matches standard shell filename completion behavior, where dotfiles
are hidden by default unless the user starts their input with a dot.
This also resolves four TODO comments in t/9902-completion.sh which
have been present since 2013 (commit ddf07bddef9a, "completion: add file
completion tests", 2013-04-27), expecting that .gitignore would not be
shown when completing on an empty path component.

Signed-off-by: Zakariyah Ali <zakariyahali100@gmail.com>
---
    completion: hide dotfiles for selected path completion
    
    The completion helper for index paths uses git ls-files rather than
    shell filename completion. As a result, leading-dot paths such as a
    tracked .gitignore were offered even when the user had not started the
    path with ..
    
    Hide leading-dot path components for git rm, git mv, and git ls-files
    when completing an empty path component. Explicit dot completion is
    still preserved, so git rm . can still complete .gitignore.
    
    This removes the existing TODO expectations in t/t9902-completion.sh and
    adds coverage for explicit dot completion.
    
    Validation:
    
     * git diff --check -- contrib/completion/git-completion.bash
       t/t9902-completion.sh
     * bash -n contrib/completion/git-completion.bash
     * ./t9902-completion.sh

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2311%2Falibaba0010%2Fcompletion-hide-dotfiles-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2311/alibaba0010/completion-hide-dotfiles-v2
Pull-Request: https://github.com/git/git/pull/2311

Range-diff vs v1:

 1:  5ccb408fd5 ! 1:  056e239e06 completion: hide dotfiles for selected path completion
     @@ Metadata
       ## Commit message ##
          completion: hide dotfiles for selected path completion
      
     +    The completion helper for index paths uses git ls-files rather than
     +    shell filename completion. As a result, leading-dot paths such as a
     +    tracked .gitignore were offered even when the user had not started the
     +    path with ".".
     +
     +    Hide leading-dot path components for git rm, git mv, and git ls-files
     +    when completing an empty path component. Explicit dot completion is
     +    still preserved, so git rm . can still complete .gitignore.
     +
     +    This matches standard shell filename completion behavior, where dotfiles
     +    are hidden by default unless the user starts their input with a dot.
     +    This also resolves four TODO comments in t/9902-completion.sh which
     +    have been present since 2013 (commit ddf07bddef9a, "completion: add file
     +    completion tests", 2013-04-27), expecting that .gitignore would not be
     +    shown when completing on an empty path component.
     +
          Signed-off-by: Zakariyah Ali <zakariyahali100@gmail.com>
      
       ## contrib/completion/git-completion.bash ##


 contrib/completion/git-completion.bash | 36 +++++++++++++++++---------
 t/t9902-completion.sh                  | 10 ++-----
 2 files changed, 26 insertions(+), 20 deletions(-)

diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index a8e7c6ddbf..e8f8fab125 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -638,25 +638,33 @@ __git_ls_files_helper ()
 }
 
 
-# __git_index_files accepts 1 or 2 arguments:
+# __git_index_files accepts 1 to 4 arguments:
 # 1: Options to pass to ls-files (required).
 # 2: A directory path (optional).
 #    If provided, only files within the specified directory are listed.
 #    Sub directories are never recursed.  Path must have a trailing
 #    slash.
 # 3: List only paths matching this path component (optional).
+# 4: Hide paths whose first component starts with a dot if this is
+#    "hide-dotfiles" and the third argument is empty (optional).
 __git_index_files ()
 {
-	local root="$2" match="$3"
+	local root="$2" match="$3" hide_dotfiles="${4-}"
+	local hide_dotfiles_awk=0
+	if [ "$hide_dotfiles" = "hide-dotfiles" ] && [ -z "$match" ]; then
+		hide_dotfiles_awk=1
+	fi
 
 	__git_ls_files_helper "$root" "$1" "${match:-?}" |
-	awk -F / -v pfx="${2//\\/\\\\}" '{
+	awk -F / -v pfx="${2//\\/\\\\}" -v hide_dotfiles="$hide_dotfiles_awk" '{
 		paths[$1] = 1
 	}
 	END {
 		for (p in paths) {
 			if (substr(p, 1, 1) != "\"") {
 				# No special characters, easy!
+				if (hide_dotfiles == 1 && substr(p, 1, 1) == ".")
+					continue
 				print pfx p
 				continue
 			}
@@ -675,8 +683,10 @@ __git_index_files ()
 				# We have seen the same directory unquoted,
 				# skip it.
 				continue
-			else
-				print pfx p
+
+			if (hide_dotfiles == 1 && substr(p, 1, 1) == ".")
+				continue
+			print pfx p
 		}
 	}
 	function dequote(p,    bs_idx, out, esc, esc_idx, dec) {
@@ -721,13 +731,15 @@ __git_index_files ()
 	}'
 }
 
-# __git_complete_index_file requires 1 argument:
+# __git_complete_index_file accepts 1 or 2 arguments:
 # 1: the options to pass to ls-file
+# 2: Hide paths whose first component starts with a dot if this is
+#    "hide-dotfiles" and the current word is empty (optional).
 #
 # The exception is --committable, which finds the files appropriate commit.
 __git_complete_index_file ()
 {
-	local dequoted_word pfx="" cur_
+	local dequoted_word pfx="" cur_ hide_dotfiles="${2-}"
 
 	__git_dequote "$cur"
 
@@ -740,7 +752,7 @@ __git_complete_index_file ()
 		cur_="$dequoted_word"
 	esac
 
-	__gitcomp_file_direct "$(__git_index_files "$1" "$pfx" "$cur_")"
+	__gitcomp_file_direct "$(__git_index_files "$1" "$pfx" "$cur_" "$hide_dotfiles")"
 }
 
 # Lists branches from the local repository.
@@ -2164,7 +2176,7 @@ _git_ls_files ()
 
 	# XXX ignore options like --modified and always suggest all cached
 	# files.
-	__git_complete_index_file "--cached"
+	__git_complete_index_file "--cached" hide-dotfiles
 }
 
 _git_ls_remote ()
@@ -2397,9 +2409,9 @@ _git_mv ()
 	if [ $(__git_count_arguments "mv") -gt 0 ]; then
 		# We need to show both cached and untracked files (including
 		# empty directories) since this may not be the last argument.
-		__git_complete_index_file "--cached --others --directory"
+		__git_complete_index_file "--cached --others --directory" hide-dotfiles
 	else
-		__git_complete_index_file "--cached"
+		__git_complete_index_file "--cached" hide-dotfiles
 	fi
 }
 
@@ -3219,7 +3231,7 @@ _git_rm ()
 		;;
 	esac
 
-	__git_complete_index_file "--cached"
+	__git_complete_index_file "--cached" hide-dotfiles
 }
 
 _git_shortlog ()
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index 28f61f08fb..02aaf71876 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -2811,17 +2811,15 @@ test_expect_success 'complete files' '
 
 	touch untracked &&
 
-	: TODO .gitignore should not be here &&
 	test_completion "git rm " <<-\EOF &&
-	.gitignore
 	modified
 	EOF
 
+	test_completion "git rm ." ".gitignore" &&
+
 	test_completion "git clean " "untracked" &&
 
-	: TODO .gitignore should not be here &&
 	test_completion "git mv " <<-\EOF &&
-	.gitignore
 	modified
 	EOF
 
@@ -2832,9 +2830,7 @@ test_expect_success 'complete files' '
 
 	mkdir untracked-dir &&
 
-	: TODO .gitignore should not be here &&
 	test_completion "git mv modified " <<-\EOF &&
-	.gitignore
 	dir
 	modified
 	untracked
@@ -2843,9 +2839,7 @@ test_expect_success 'complete files' '
 
 	test_completion "git commit " "modified" &&
 
-	: TODO .gitignore should not be here &&
 	test_completion "git ls-files " <<-\EOF &&
-	.gitignore
 	dir
 	modified
 	EOF

base-commit: 9b7fa37559a1b95ee32e32858b0d038b4cf583e5
-- 
gitgitgadget

^ permalink raw reply related

* What's cooking in git.git (May 2026, #07)
From: Junio C Hamano @ 2026-05-26 13:56 UTC (permalink / raw)
  To: git

Here are the topics that have been cooking in my tree.  Commits
prefixed with '+' are in 'next' (being in 'next' is a sign that a
topic is stable enough to be used and is a candidate to be in a
future release).  Commits prefixed with '-' are only in 'seen', and
aren't considered "accepted" at all and may be annotated with a URL
to a message that raises issues but they are by no means exhaustive.
A topic without enough support may be discarded after a long period
of no activity (of course they can be resubmitted when new interests
arise).

Copies of the source code to Git live in many repositories, and the
following is a list of the ones I push into or their mirrors.  Some
repositories have only a subset of branches.

With maint, master, next, seen, todo:

	git://git.kernel.org/pub/scm/git/git.git/
	git://repo.or.cz/alt-git.git/
	https://kernel.googlesource.com/pub/scm/git/git/
	https://github.com/git/git/
	https://gitlab.com/git-scm/git/

With all the integration branches and topics broken out:

	https://github.com/gitster/git/

Even though the preformatted documentation in HTML and man format
are not sources, they are published in these repositories for
convenience (replace "htmldocs" with "manpages" for the manual
pages):

	git://git.kernel.org/pub/scm/git/git-htmldocs.git/
	https://github.com/gitster/git-htmldocs.git/

Release tarballs are available at:

	https://www.kernel.org/pub/software/scm/git/

--------------------------------------------------
[Graduated to 'master']

* ag/sequencer-remove-unused-struct-member (2026-05-11) 1 commit
  (merged to 'next' on 2026-05-17 at 8553437ae1)
 + sequencer: remove todo_add_branch_context.commit

 Code clean-up.
 cf. <agLKVn6RF4UBYd_8@pks.im>
 source: <pull.2111.git.1778502113485.gitgitgadget@gmail.com>


* dk/doc-exclude-is-shared-per-repo (2026-05-12) 1 commit
  (merged to 'next' on 2026-05-17 at ddc761aec6)
 + ignore: note info/exclude lives in GIT_COMMON_DIR, not GIT_DIR

 Document the fact that .git/info/exclude is shared across worktrees
 linked to the same repository.
 cf. <bea48414-217b-4860-9279-fe94e3687c28@gmail.com>
 source: <ec97ad3f054e90b675f099a36a81a23bb4b2a0ed.1778620784.git.ben.knoble+github@gmail.com>


* jk/dumb-http-alternate-fix (2026-05-12) 1 commit
  (merged to 'next' on 2026-05-17 at c1a51214fb)
 + http: handle absolute-path alternates from server root

 The HTTP walker misinterpreted the alternates file that gives an
 absolute path when the server URL does not have the final slash
 (i.e., "https://example.com" not "https://example.com/").
 source: <20260512162619.GA69813@coredump.intra.peff.net>


* jk/pretty-no-strbuf-presizing (2026-05-12) 1 commit
  (merged to 'next' on 2026-05-17 at ee684c614f)
 + pretty: drop strbuf pre-sizing from add_rfc2047()

 Remove ineffective strbuf presizing that would have computed an
 allocation that would not have fit in the available memory anyway,
 or too small due to integer wraparound to cause immediate automatic
 growing.
 source: <20260512162022.GA69669@coredump.intra.peff.net>


* kk/paint-down-to-common-optim (2026-05-11) 2 commits
  (merged to 'next' on 2026-05-17 at 2e39c767e5)
 + commit-reach: early exit paint_down_to_common for single merge-base
 + commit-reach: introduce merge_base_flags enum

 "git merge-base" optimization.
 source: <pull.2109.v4.git.1778504352.gitgitgadget@gmail.com>


* mm/diff-U-takes-no-negative-values (2026-05-12) 4 commits
  (merged to 'next' on 2026-05-17 at d81439a049)
 + parse-options: clarify what "negated" means for PARSE_OPT_NONEG
 + xdiff: guard against negative context lengths
 + diff: reject negative values for -U/--unified
 + diff: reject negative values for --inter-hunk-context

 The command line parser for "git diff" learned a few options take
 only non-negative integers.
 source: <pull.2105.v2.git.1778609423.gitgitgadget@gmail.com>

--------------------------------------------------
[New Topics]

* kk/fetch-store-ref-optimization (2026-05-24) 1 commit
 - fetch: pass transport to post-fetch connectivity check

 When fetching from a transport that provides a self-contained pack,
 pass the transport pointer to the post-fetch `check_connected()` call
 to optimize connectivity check.

 Will merge to 'next'?
 source: <pull.2123.git.1779625693328.gitgitgadget@gmail.com>


* ds/restore-sparse-index (2026-05-24) 2 commits
 - restore: avoid sparse index expansion
 - t1092: test 'git restore' with sparse index

 'git restore --staged' has been optimized to avoid unnecessarily expanding
 the sparse index when operating on paths within the sparse checkout
 definition, by handling sparse directory entries at the tree level.

 Will merge to 'next'?
 source: <pull.2121.git.1779644412.gitgitgadget@gmail.com>


* kk/commit-reach-optim (2026-05-25) 3 commits
 - commit-reach: replace queue_has_nonstale() scan with O(1) tracking
 - commit-reach: deduplicate queue entries in paint_down_to_common
 - object.h: fix stale entries in object flag allocation table

 The check for non-stale commits in the priority queue used by
 `paint_down_to_common` and `ahead_behind` has been optimized by
 replacing an O(N) scan with an O(1) counter, yielding performance
 improvements in repositories with wide histories.

 Will merge to 'next'?
 source: <pull.2124.v2.git.1779719286.gitgitgadget@gmail.com>


* ar/receive-pack-worktree-env (2026-05-25) 1 commit
 - receive-pack: fix updateInstead with core.worktree

 The GIT_WORK_TREE variable prepared to invoke the push-to-checkout
 hook was leaking into the environment even when there was no hook
 used and broke the default push-to-deploy (i.e., let "git checkout"
 update the working tree only when the working tree is clean).

 Will merge to 'next'.
 source: <20260525162311.66240-2-hi@alyssa.is>


* ib/doc-push-default-simple (2026-05-25) 1 commit
 - doc: clarify push.default=simple behavior

 The documentation for `push.default = simple` has been clarified to
 better explain its behavior, making it clear that it pushes the
 current branch to a same-named branch on the remote, and detailing
 the upstream requirements for centralized workflows.

 Comments?
 source: <pull.2115.v2.git.1779767888508.gitgitgadget@gmail.com>


* jc/doc-monitor-ghci (2026-05-24) 1 commit
 - SubmittingPatches: proactively monitor GHCI pages

 Encourage original authors to monitor the CI status.

 Will merge to 'next'?
 source: <xmqq1pf0gpp3.fsf@gitster.g>


* ec/commit-fixup-options (2026-05-26) 2 commits
 - commit: allow -c/-C for all kinds of --fixup
 - commit: allow -m/-F for all kinds of --fixup

 The -m/-F/-c/-C options to supply commit log message from outside the
 editor are now supported for all "git commit --fixup" variations.

 Comments?
 source: <cover.1779792311.git.erik@cervined.in>

--------------------------------------------------
[Cooking]

* gh/jump-auto-mode (2026-05-21) 1 commit
 - git-jump: pick a mode automatically when invoked without arguments

 The 'git-jump' command (in contrib/) has been taught to automatically
 pick a mode (merge, diff, or ws) when invoked without arguments.

 Comments?
 source: <pull.2108.v3.git.1779371110195.gitgitgadget@gmail.com>


* sp/doc-range-diff-takes-notes (2026-05-20) 1 commit
  (merged to 'next' on 2026-05-22 at 020bec81b7)
 + Documentation/git-range-diff: add missing notes options in synopsis

 Docfix.

 Will merge to 'master'.
 source: <20260521052841.73775-1-siddh.raman.pant@oracle.com>


* ps/odb-source-loose (2026-05-21) 19 commits
 - odb/source-loose: drop pointer to the "files" source
 - odb/source-loose: stub out remaining callbacks
 - odb/source-loose: wire up `write_object_stream()` callback
 - object-file: refactor writing objects to use loose source
 - odb/source-loose: wire up `write_object()` callback
 - loose: refactor object map to operate on `struct odb_source_loose`
 - odb/source-loose: wire up `freshen_object()` callback
 - odb/source-loose: drop `odb_source_loose_has_object()`
 - odb/source-loose: wire up `count_objects()` callback
 - odb/source-loose: wire up `find_abbrev_len()` callback
 - odb/source-loose: wire up `for_each_object()` callback
 - odb/source-loose: wire up `read_object_stream()` callback
 - odb/source-loose: wire up `read_object_info()` callback
 - odb/source-loose: wire up `close()` callback
 - odb/source-loose: wire up `reprepare()` callback
 - odb/source-loose: start converting to a proper `struct odb_source`
 - odb/source-loose: store pointer to "files" instead of generic source
 - odb/source-loose: move loose source into "odb/" subsystem
 - Merge branch 'ps/odb-in-memory' into ps/odb-source-loose
 (this branch uses jt/odb-transaction-write and ps/odb-in-memory.)

 The loose object source has been refactored into a proper `struct
 odb_source`.

 Comments?
 source: <20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im>


* ps/setup-centralize-odb-creation (2026-05-25) 9 commits
 - setup: construct object database in `apply_repository_format()`
 - repository: stop reading loose object map twice on repo init
 - setup: stop initializing object database without repository
 - setup: stop creating the object database in `setup_git_env()`
 - repository: stop initializing the object database in `repo_set_gitdir()`
 - setup: deduplicate logic to apply repository format
 - setup: drop `setup_git_env()`
 - t0001: plug test gaps for git-init(1) with GIT_OBJECT_DIRECTORY
 - Merge branch 'ps/setup-wo-the-repository' into ps/setup-centralize-odb-creation
 (this branch uses ps/setup-wo-the-repository.)

 The setup logic to discover and configure repositories has been
 refactored, and the initialization of the object database has been
 centralized.

 Comments?
 source: <20260526-b4-pks-setup-centralize-odb-creation-v2-0-2fa5b385c13e@pks.im>


* ps/gitlab-ci-macOS-improvements (2026-05-21) 2 commits
  (merged to 'next' on 2026-05-22 at aaa3c7021e)
 + gitlab-ci: update macOS image
 + gitlab-ci: upgrade macOS runners

 Update GitLab CI jobs that exercise macOS.

 Will merge to 'master'.
 source: <20260521-b4-pks-gitlab-ci-updates-v1-0-53bb46ed33e0@pks.im>


* kh/doc-hook (2026-05-21) 4 commits
  (merged to 'next' on 2026-05-25 at 5e41d13adf)
 + doc: hook: don’t self-link via config include
 + doc: config: include existing git-hook(1) section
 + doc: hook: consistently capitalize Git
 + doc: hook: remove stray backtick

 Doc updates.

 Will merge to 'master'.
 cf. <2832179.mvXUDI8C0e@piment-oiseau>
 source: <CV_doc_hook.6f0@msgid.xyz>


* kh/doc-replay-config (2026-05-21) 4 commits
 - doc: replay: move “default” to the right-hand-side
 - doc: replay: use a nested definition list
 - doc: replay: simplify replay.refAction description
 - doc: link to config for git-replay(1)

 Doc update for "git replay" to actually refer to its configuration
 variables.

 Comments?
 source: <CV_doc_replay_config.709@msgid.xyz>


* jk/commit-graph-lazy-load-fallback (2026-05-18) 1 commit
  (merged to 'next' on 2026-05-22 at d1188df466)
 + commit: fall back to full read when maybe_tree is NULL

 The logic to lazy-load trees from the commit-graph has been made
 more robust by falling back to reading the commit object when
 the commit-graph is no longer available.

 Will merge to 'master'.
 source: <20260519061534.GA1709881@coredump.intra.peff.net>


* jk/connect-service-enum (2026-05-21) 2 commits
  (merged to 'next' on 2026-05-24 at 293561cbc5)
 + transport-helper: fix typo in BUG() message
  (merged to 'next' on 2026-05-21 at fd80c61e21)
 + connect: use "service" enum for "name" argument

 The "name" argument in git_connect() and related functions has been
 converted to a "service" enum to improve type safety and clarify its
 purpose.

 Will merge to 'master'.
 source: <20260519052219.GA1703179@coredump.intra.peff.net>
 source: <20260522044352.GA861761@coredump.intra.peff.net>


* jk/sq-dequote-cleanup (2026-05-18) 3 commits
  (merged to 'next' on 2026-05-21 at fbedf2daea)
 + quote: simplify internals of dequoting
 + quote: drop sq_dequote_to_argv()
 + quote.h: bump strvec forward declaration to the top

 Code simplification.

 Will merge to 'master'.
 source: <20260519011837.GA1615637@coredump.intra.peff.net>


* aj/stash-patch-optimize-temporary-index (2026-05-22) 1 commit
 - stash: reuse cached index entries in --patch temporary index

 "git stash -p" has been optimized by reusing cached index
 entries in its temporary index, avoiding unnecessary lstat()
 calls on unchanged files.

 Will merge to 'next'?
 source: <pull.2306.v2.git.git.1779491545531.gitgitgadget@gmail.com>


* tb/bitmap-build-performance (2026-05-19) 9 commits
 - pack-bitmap: build pseudo-merge bitmaps after regular bitmaps
 - pack-bitmap: remember pseudo-merge parents
 - pack-bitmap: sort bitmaps before XORing
 - pack-bitmap: cache object positions during fill
 - pack-bitmap: consolidate `find_object_pos()` success path
 - pack-bitmap: reuse stored selected bitmaps
 - pack-bitmap: check subtree bits before recursing
 - pack-bitmap: pass object position to `fill_bitmap_tree()`
 - Merge branch 'tb/pseudo-merge-bugfixes' into tb/bitmap-build-performance
 (this branch uses tb/pseudo-merge-bugfixes.)

 Reachability bitmap generation has been significantly optimized. By
 reordering tree traversal, caching object positions, and refining how
 pseudo-merge bitmaps are constructed, the performance of "git repack
 --write-midx-bitmaps" is improved, especially for large repositories
 and when using pseudo-merges.
 source: <cover.1779207127.git.me@ttaylorr.com>


* hn/status-pull-advice-qualified (2026-05-21) 1 commit
 - remote: qualify "git pull" advice for non-upstream compareBranches

 Advice shown by "git status" when the local branch is behind or has
 diverged from its push branch has been updated to suggest "git pull
 <remote> <branch>".

 Comments?
 source: <pull.2301.v4.git.git.1779372367317.gitgitgadget@gmail.com>


* kk/merge-octopus-optim (2026-05-11) 1 commit
  (merged to 'next' on 2026-05-20 at afe427dc66)
 + merge: use repo_in_merge_bases for octopus up-to-date check

 The logic to determine that branches in an octopus merge are
 independent has been optimized.

 Will merge to 'master'.
 cf. <c5b333f1-0db6-4aec-a369-6503cb924e7f@gmail.com>
 source: <pull.2110.git.1778566286543.gitgitgadget@gmail.com>


* rs/strbuf-add-uint (2026-05-12) 4 commits
 - ls-tree: use strbuf_add_uint()
 - ls-files: use strbuf_add_uint()
 - cat-file: use strbuf_add_uint()
 - strbuf: add strbuf_add_uint()

 Adding a decimal integer with strbuf_addf("%u") appears commonly;
 they have been optimized by using a custom formatter.

 Comments?
 source: <20260512115603.80780-1-l.s.r@web.de>


* ta/approxidate-noon-fix (2026-05-21) 4 commits
  (merged to 'next' on 2026-05-25 at 2dd9ce3c54)
 + approxidate: use deferred mday adjustments for "specials"
 + approxidate: make "specials" respect fixed day-of-month
 + t0006: add support for approxidate test date adjustment
 + approxidate: make "today" wrap to midnight

 "Friday noon" asked in the morning on Sunday was parsed to be one
 day before the specified time, which has been corrected.

 Will merge to 'master'.
 source: <20260521105408.8222-1-taahol@utu.fi>


* mm/doc-word-diff (2026-05-13) 1 commit
 - doc: clarify that --word-diff operates on line-level hunks

 The documentation for "--word-diff" has been extended with a bit of
 implementation detail of where these different words come from.

 Comments?
 source: <pull.2113.git.1778686956622.gitgitgadget@gmail.com>


* rs/strbuf-add-oid-hex (2026-05-13) 1 commit
 - hex: add and use strbuf_add_oid_hex()

 Formatting object name in full hexadecimal form has been optimized
 by using a new strbuf_add_oid_hex() helper function.

 Comments?
 source: <183aa0fd-d455-4ec9-9c42-d511fac8b3e4@web.de>


* kk/limit-list-optim (2026-05-14) 1 commit
  (merged to 'next' on 2026-05-19 at f17450dd1b)
 + revision: use priority queue in limit_list()

 The limit_list() function that is one of the core part of the
 revision traversal infrastructure has been optimized by replacing
 its use of linear list with priority queue.

 Will merge to 'master'.
 source: <pull.2114.git.1778777491939.gitgitgadget@gmail.com>


* ed/check-connected-close-err-fd (2026-05-16) 1 commit
  (merged to 'next' on 2026-05-22 at 00d592399e)
 + Merge branch 'ed/check-connected-close-err-fd-2.53' into ed/check-connected-close-err-fd
 (this branch uses ed/check-connected-close-err-fd-2.53.)

 File descriptor leak fix.

 Will merge to 'master'.
 (this branch uses ed/check-connected-close-err-fd-2.53.)


* ed/check-connected-close-err-fd-2.53 (2026-05-14) 1 commit
  (merged to 'next' on 2026-05-22 at 1017d0e022)
 + connected: close err_fd in promisor fast-path
 (this branch is used by ed/check-connected-close-err-fd.)

 File descriptor leak fix (for 2.54 maintenance track).

 Will merge to 'master'.
 source: <pull.2303.git.git.1778827194448.gitgitgadget@gmail.com>


* kk/tips-reachable-from-bases-optim (2026-05-16) 2 commits
  (merged to 'next' on 2026-05-22 at 87d6b8e666)
 + t6600: add tests for duplicate tips in tips_reachable_from_bases()
 + commit-reach: use object flags for tips_reachable_from_bases()

 Revision traversal optimization.

 Will merge to 'master'.
 source: <pull.2116.v3.git.1778947182.gitgitgadget@gmail.com>


* pb/doc-diff-format-updates (2026-05-15) 3 commits
  (merged to 'next' on 2026-05-20 at fe8d31e9f9)
 + diff-format.adoc: mode and hash are 0* for unmerged paths from index only
 + diff-format.adoc: 'git diff-files' prints two lines for unmerged files
 + diff-format.adoc: remove mention of diff-tree specific output

 Doc updates.

 Will merge to 'master'.
 source: <pull.2304.git.git.1778860091.gitgitgadget@gmail.com>


* ps/t3903-cover-stash-include-untracked (2026-05-16) 1 commit
  (merged to 'next' on 2026-05-20 at f1e7ac1cbd)
 + stash: add coverage for show --include-untracked

 Test coverage has been added to "git stash --include-untracked".

 Will merge to 'master'.
 source: <20260516183347.4323-2-pushkarkumarsingh1970@gmail.com>


* rs/trailer-fold-optim (2026-05-15) 1 commit
  (merged to 'next' on 2026-05-20 at 38c9fb15c2)
 + trailer: change strbuf in-place in unfold_value()

 Code simplification.

 Will merge to 'master'.
 source: <816be07e-2cd6-48fe-ae93-57fa0f2543ed@web.de>


* rs/use-builtin-add-overflow-explicitly-on-clang (2026-05-18) 2 commits
  (merged to 'next' on 2026-05-21 at c223b71079)
 + use __builtin_add_overflow() in st_add() with Clang
 + strbuf: use st_add3() in strbuf_grow()

 Micro optimization of codepaths that compute allocation sizes carefully.

 Will merge to 'master'.
 source: <20260518202502.25682-1-l.s.r@web.de>


* tc/generate-configlist-fix-for-older-ninja (2026-05-15) 1 commit
  (merged to 'next' on 2026-05-22 at 8322bfb8f2)
 + generate-configlist: collapse depfile for older Ninja

 Build update.

 Will merge to 'master'.
 source: <20260515-toon-fix-almalinux8-v3-1-b545a0647f0f@iotcl.com>


* hn/config-typo-advice (2026-05-25) 1 commit
 - config: suggest the correct form when key contains "=" in set context

 "git config foo.bar=baz" is not likely to be a request to read the
 value of such a variable with '=' in its name; rather it is plausible
 that the user meant "git config set foo.bar baz".  Give advice when
 giving an error message.

 Comments?
 source: <pull.2302.v3.git.git.1779697995418.gitgitgadget@gmail.com>


* ja/doc-synopsis-style-again (2026-05-25) 6 commits
 - doc: convert git-imap-send synopsis and options to new style
 - doc: convert git-apply synopsis and options to new style
 - doc: convert git-am synopsis and options to new style
 - doc: convert git-grep synopsis and options to new style
 - doc: git bisect: clarify the usage of the synopsis vs actual command
 - doc: convert git-bisect to synopsis style

 A batch of documentation pages has been updated to use the modern
 synopsis style.

 Will merge to 'next'?
 source: <pull.2117.v2.git.1779704908.gitgitgadget@gmail.com>


* kn/refs-fsck-skip-lock-files (2026-05-17) 1 commit
  (merged to 'next' on 2026-05-21 at 91e30e3543)
 + refs/files: skip lock files during consistency checks

 The consistency checks for the files reference backend have been updated
 to skip lock files earlier, avoiding unnecessary parsing of
 intermediate files.

 Will merge to 'master'.
 source: <20260517-refs-fsck-skip-lock-files-v3-1-b24dfd673c7e@gmail.com>


* jt/config-lock-timeout (2026-05-17) 1 commit
 - config: retry acquiring config.lock, configurable via core.configLockTimeout

 Configuration file locking now retries for a short period, avoiding
 failures when multiple processes attempt to update the configuration
 simultaneously.

 Comments?
 cf. <xmqqzf1xbl4i.fsf@gitster.g>
 source: <20260517132111.1014901-1-joerg@thalheim.io>


* hn/branch-prune-merged (2026-05-22) 6 commits
 - branch: add --dry-run for --prune-merged
 - branch: add branch.<name>.pruneMerged opt-out
 - branch: add --prune-merged <branch>
 - branch: prepare delete_branches for a bulk caller
 - branch: let delete_branches warn instead of error on bulk refusal
 - branch: add --forked <branch>

 "git branch" command learned "--prune-merged" option to remove
 local branches that have already been merged to the remote-tracking
 branches they track.

 Comments?
 source: <pull.2285.v11.git.git.1779449498.gitgitgadget@gmail.com>


* st/daemon-sockaddr-fixes (2026-05-14) 3 commits
 - daemon: guard NULL REMOTE_PORT in execute() logging
 - daemon: fix IPv6 address truncation in ip2str()
 - daemon: fix IPv6 address corruption in lookup_hostname()

 Correct use of sockaddr API in "git daemon".

 Waiting for response(s) to review comment(s).
 cf. <agGLRC1ziF5F8Okh@pks.im>
 source: <pull.2300.git.git.1778773592.gitgitgadget@gmail.com>


* ob/more-repo-config-values (2026-04-23) 8 commits
 - env: move "warn_on_object_refname_ambiguity" into `struct repo_config_values`
 - env: move "sparse_expect_files_outside_of_patterns" into `repo_config_values`
 - env: move "core_sparse_checkout_cone" into `struct repo_config_values`
 - environment: move "precomposed_unicode" into `struct repo_config_values`
 - environment: move "pack_compression_level" into `struct repo_config_values`
 - environment: move `zlib_compression_level` into `struct repo_config_values`
 - environment: move "check_stat" into `struct repo_config_values`
 - environment: move "trust_ctime" into `struct repo_config_values`

 Expecting a reroll.
 cf. <CAD=f0L8-_3sDGGkCzF4WA0xmUtaY_qiz__3zq5AemLgwTsqvsg@mail.gmail.com>
 cf. <xmqqlddqu013.fsf@gitster.g>
 source: <20260423165432.143598-1-belkid98@gmail.com>


* cc/promisor-auto-config-url-more (2026-05-19) 9 commits
 - doc: promisor: improve acceptFromServer entry
 - promisor-remote: auto-configure unknown remotes
 - promisor-remote: trust known remotes matching acceptFromServerUrl
 - promisor-remote: introduce promisor.acceptFromServerUrl
 - promisor-remote: add 'local_name' to 'struct promisor_info'
 - urlmatch: add url_normalize_pattern() helper
 - urlmatch: change 'allow_globs' arg to bool
 - t5710: simplify 'mkdir X' followed by 'git -C X init'
 - Merge branch 'cc/promisor-auto-config-url' into cc/promisor-auto-config-url-more

 The handling of promisor-remote protocol capability has been
 loosened to allow the other side to add to the list of promisor
 remotes via the promisor.acceptFromServerURL configuration
 variable.

 Comments?
 source: <20260519153808.494105-1-christian.couder@gmail.com>


* hn/checkout-track-fetch (2026-05-23) 2 commits
 - checkout: extend --track with a "fetch" mode to refresh start-point
 - branch: expose helpers for finding the remote owning a tracking ref

 "git checkout --track=..." learned to optionally fetch the branch
 from the remote the new branch will work with.

 Comments?
 source: <pull.2281.v13.git.git.1779565714.gitgitgadget@gmail.com>


* mf/revision-max-count-oldest (2026-05-18) 1 commit
 - revision.c: implement --max-count-oldest

 "git rev-list" (and "git log" family of commands) learned a new "--max-count-oldest"
 that picks oldest N commits in the range instead of the usual newest.

 Comments?
 source: <8210d60832b9a58aa4d71fc3790e44d8989564ce.1779152064.git.mroik@delayed.space>


* mm/line-log-cleanup (2026-05-25) 3 commits
 - line-log: allow non-patch diff formats with -L
 - line-log: integrate -L output with the standard log-tree pipeline
 - revision: move -L setup before output_format-to-diff derivation

 The `git log -L` implementation has been refactored to use the
 standard diff output pipeline, enabling pickaxe and diff-filter to
 work as expected. Additionally, metadata-only diff formats like
 --raw and --name-only are now supported with -L.

 Will merge to 'next'?
 source: <pull.2120.v2.git.1779733799.gitgitgadget@gmail.com>


* ds/path-walk-filters (2026-05-22) 14 commits
  (merged to 'next' on 2026-05-25 at eccb829b10)
 + path-walk: support `combine` filter
 + path-walk: support `object:type` filter
 + path-walk: support `tree:0` filter
 + t6601: tag otherwise-unreachable trees
 + pack-objects: support sparse:oid filter with path-walk
 + path-walk: add pl_sparse_trees to control tree pruning
 + path-walk: support blob size limit filter
 + backfill: die on incompatible filter options
 + path-walk: support blobless filter
 + path-walk: always emit directly-requested objects
 + t/perf: add pack-objects filter and path-walk benchmark
 + pack-objects: pass --objects with --path-walk
 + t5620: make test work with path-walk var
 + Merge branch 'en/backfill-fixes-and-edges' into ds/path-walk-filters

 The "git pack-objects --path-walk" traversal has been integrated
 with several object filters, including blobless and sparse filters.

 Will merge to 'master'.
 source: <pull.2101.v5.git.1779474277.gitgitgadget@gmail.com>


* en/ort-harden-against-corrupt-trees (2026-04-20) 5 commits
 - cache-tree: fix verify_cache() to catch non-adjacent D/F conflicts
 - merge-ort: abort merge when trees have duplicate entries
 - merge-ort: free diff pairs queue in clear_or_reinit_internal_opts()
 - merge-ort: drop unnecessary show_all_errors from collect_merge_info()
 - merge-ort: propagate callback errors from traverse_trees_wrapper()

 "ort" merge backend handles merging corrupt trees better by
 aborting when it should.

 Needs review.
 source: <pull.2096.git.1776731171.gitgitgadget@gmail.com>


* pw/status-rebase-todo (2026-05-01) 2 commits
 - status: improve rebase todo list parsing
 - sequencer: factor out parsing of todo commands

 The display of the rebase todo list in "git status" has been
 improved to correctly abbreviate object IDs for more commands and
 avoid misinterpreting refs as object IDs.

 Needs review.
 source: <cover.1777648598.git.phillip.wood@dunelm.org.uk>


* tb/pseudo-merge-bugfixes (2026-05-11) 9 commits
  (merged to 'next' on 2026-05-19 at ecee155d5c)
 + pack-bitmap: prevent pattern leak on pseudo-merge re-assignment
 + Documentation: fix broken `sampleRate` in gitpacking(7)
 + pack-bitmap: reject pseudo-merge "sampleRate" of 0
 + pack-bitmap: parse commits in `find_pseudo_merge_group_for_ref()`
 + pack-bitmap: fix pseudo-merge lookup for shared commits
 + pack-bitmap: fix inverted binary search in `pseudo_merge_at()`
 + pack-bitmap-write: sort pseudo-merge commit lookup table in pack order
 + t5333: demonstrate various pseudo-merge bugs
 + t/helper: add 'test-tool bitmap write' subcommand
 (this branch is used by tb/bitmap-build-performance.)

 Fixes many bugs in pseudo-merge code.

 Will merge to 'master'.
 source: <cover.1778546804.git.me@ttaylorr.com>


* ds/fetch-negotiation-options (2026-05-19) 8 commits
  (merged to 'next' on 2026-05-21 at ff57fd9c97)
 + send-pack: pass negotiation config in push
 + remote: add remote.*.negotiationInclude config
 + fetch: add --negotiation-include option for negotiation
 + negotiator: add have_sent() interface
 + remote: add remote.*.negotiationRestrict config
 + transport: rename negotiation_tips
 + fetch: add --negotiation-restrict option
 + t5516: fix test order flakiness

 The negotiation tip options in "git fetch" have been reworked to
 allow requiring certain refs to be sent as "have" lines, and to
 restrict negotiation to a specific set of refs.

 Will merge to 'master'.
 source: <pull.2085.v6.git.1779207896.gitgitgadget@gmail.com>


* en/batch-prefetch (2026-05-14) 4 commits
  (merged to 'next' on 2026-05-20 at 722acf81c8)
 + grep: prefetch necessary blobs
 + builtin/log: prefetch necessary blobs for `git cherry`
 + patch-ids.h: add missing trailing parenthesis in documentation comment
 + promisor-remote: document caller filtering contract

 In a lazy clone, "git cherry" and "git grep" often fetch necessary
 blob objects one by one from promisor remotes.  It has been corrected
 to collect necessary object names and fetch them in bulk to gain
 reasonable performance.

 Will merge to 'master'.
 cf. <0da4f159-8d4b-49e2-93c1-25aa0bf69371@gmail.com>
 source: <pull.2089.v3.git.1778775928.gitgitgadget@gmail.com>


* ps/odb-in-memory (2026-04-10) 18 commits
  (merged to 'next' on 2026-05-21 at c8709aa17f)
 + t/unit-tests: add tests for the in-memory object source
 + odb: generic in-memory source
 + odb/source-inmemory: stub out remaining functions
 + odb/source-inmemory: implement `freshen_object()` callback
 + odb/source-inmemory: implement `count_objects()` callback
 + odb/source-inmemory: implement `find_abbrev_len()` callback
 + odb/source-inmemory: implement `for_each_object()` callback
 + odb/source-inmemory: convert to use oidtree
 + oidtree: add ability to store data
 + cbtree: allow using arbitrary wrapper structures for nodes
 + odb/source-inmemory: implement `write_object_stream()` callback
 + odb/source-inmemory: implement `write_object()` callback
 + odb/source-inmemory: implement `read_object_stream()` callback
 + odb/source-inmemory: implement `read_object_info()` callback
 + odb: fix unnecessary call to `find_cached_object()`
 + odb/source-inmemory: implement `free()` callback
 + odb: introduce "in-memory" source
 + Merge branch 'jt/odb-transaction-write' into ps/odb-in-memory
 (this branch is used by ps/odb-source-loose; uses jt/odb-transaction-write.)

 Add a new odb "in-memory" source that is meant to only hold
 tentative objects (like the virtual blob object that represents the
 working tree file used by "git blame").

 Will merge to 'master'.
 source: <20260410-b4-pks-odb-source-inmemory-v3-0-22fd0fad58fe@pks.im>


* cl/conditional-config-on-worktree-path (2026-05-24) 2 commits
 - config: add "worktree" and "worktree/i" includeIf conditions
 - config: refactor include_by_gitdir() into include_by_path()

 The [includeIf "condition"] conditional inclusion facility for
 configuration files has learned to use the location of worktree
 in its condition.

 Ready?
 source: <20260525-includeif-worktree-v5-0-1efe525d025a@black-desk.cn>


* ps/shift-root-in-graph (2026-04-27) 1 commit
 - graph: add indentation for commits preceded by a parentless commit

 In a history with more than one root commit, "git log --graph
 --oneline" stuffed an unrelated commit immediately below a root
 commit, which has been corrected by making the spot below a root
 unavailable.

 Waiting for response(s) to review comment(s).
 cf. <20260513230216.GA1378627@coredump.intra.peff.net>
 source: <20260427102838.44867-2-pabloosabaterr@gmail.com>


* lp/repack-propagate-promisor-debugging-info (2026-04-18) 6 commits
 - repack-promisor: add missing headers
 - t7703: test for promisor file content after geometric repack
 - t7700: test for promisor file content after repack
 - repack-promisor: preserve content of promisor files after repack
 - repack-promisor add helper to fill promisor file after repack
 - pack-write: add explanation to promisor file content

 When fetching objects into a lazily cloned repository, .promisor
 files are created with information meant to help debugging.  "git
 repack" has been taught to carry this information forward to
 packfiles that are newly created.

 Needs review.
 cf. <xmqqse7xm8av.fsf@gitster.g>
 source: <cover.1776384902.git.lorenzo.pegorari2002@gmail.com>


* th/promisor-quiet-per-repo (2026-04-06) 1 commit
 - promisor-remote: fix promisor.quiet to use the correct repository

 The "promisor.quiet" configuration variable was not used from
 relevant submodules when commands like "grep --recurse-submodules"
 triggered a lazy fetch, which has been corrected.

 Comments?
 source: <20260406183041.783800-1-vikingtc4@gmail.com>


* jt/odb-transaction-write (2026-05-14) 7 commits
  (merged to 'next' on 2026-05-21 at 61108abe4d)
 + odb/transaction: make `write_object_stream()` pluggable
 + object-file: generalize packfile writes to use odb_write_stream
 + object-file: avoid fd seekback by checking object size upfront
 + object-file: remove flags from transaction packfile writes
 + odb: update `struct odb_write_stream` read() callback
 + odb/transaction: use pluggable `begin_transaction()`
 + odb: split `struct odb_transaction` into separate header
 (this branch is used by ps/odb-in-memory and ps/odb-source-loose.)

 ODB transaction interface is being reworked to explicitly handle
 object writes.

 Will merge to 'master'.
 source: <20260514183740.1505171-1-jltobler@gmail.com>


* sa/cat-file-batch-mailmap-switch (2026-04-15) 1 commit
  (merged to 'next' on 2026-05-22 at 197a9bad73)
 + cat-file: add mailmap subcommand to --batch-command

 "git cat-file --batch" learns an in-line command "mailmap"
 that lets the user toggle use of mailmap.

 Will merge to 'master'.
 cf. <xmqqwlwy4v7t.fsf@gitster.g>
 source: <20260416033250.4327-2-siddharthasthana31@gmail.com>


* tb/incremental-midx-part-3.3 (2026-05-19) 16 commits
  (merged to 'next' on 2026-05-21 at 6c11c1a739)
 + repack: allow `--write-midx=incremental` without `--geometric`
 + repack: introduce `--write-midx=incremental`
 + repack: implement incremental MIDX repacking
 + packfile: ensure `close_pack_revindex()` frees in-memory revindex
 + builtin/repack.c: convert `--write-midx` to an `OPT_CALLBACK`
 + repack-geometry: prepare for incremental MIDX repacking
 + repack-midx: extract `repack_fill_midx_stdin_packs()`
 + repack-midx: factor out `repack_prepare_midx_command()`
 + midx: expose `midx_layer_contains_pack()`
 + repack: track the ODB source via existing_packs
 + midx: support custom `--base` for incremental MIDX writes
 + midx: introduce `--no-write-chain-file` for incremental MIDX writes
 + midx: use `strvec` for `keep_hashes`
 + midx: build `keep_hashes` array in order
 + midx: use `strset` for retained MIDX files
 + midx-write: handle noop writes when converting incremental chains

 The repacking code has been refactored and compaction of MIDX layers
 have been implemented, and incremental strategy that does not require
 all-into-one repacking has been introduced.

 Will merge to 'master'.
 source: <cover.1779206239.git.me@ttaylorr.com>


* jd/unpack-trees-wo-the-repository (2026-03-31) 2 commits
 - unpack-trees: use repository from index instead of global
 - unpack-trees: use repository from index instead of global

 A handful of inappropriate uses of the_repository have been
 rewritten to use the right repository structure instance in the
 unpack-trees.c codepath.

 Comments?
 source: <pull.2258.v2.git.git.1774971267.gitgitgadget@gmail.com>


* ps/setup-wo-the-repository (2026-05-19) 18 commits
  (merged to 'next' on 2026-05-21 at d8fb5a7b3e)
 + setup: stop using `the_repository` in `init_db()`
 + setup: stop using `the_repository` in `create_reference_database()`
 + setup: stop using `the_repository` in `initialize_repository_version()`
 + setup: stop using `the_repository` in `check_repository_format()`
 + setup: stop using `the_repository` in `upgrade_repository_format()`
 + setup: stop using `the_repository` in `setup_git_directory()`
 + setup: stop using `the_repository` in `setup_git_directory_gently()`
 + setup: stop using `the_repository` in `setup_git_env()`
 + setup: stop using `the_repository` in `set_git_work_tree()`
 + setup: stop using `the_repository` in `setup_work_tree()`
 + setup: stop using `the_repository` in `enter_repo()`
 + setup: stop using `the_repository` in `verify_non_filename()`
 + setup: stop using `the_repository` in `verify_filename()`
 + setup: stop using `the_repository` in `path_inside_repo()`
 + setup: stop using `the_repository` in `prefix_path()`
 + setup: stop using `the_repository` in `is_inside_work_tree()`
 + setup: stop using `the_repository` in `is_inside_git_dir()`
 + setup: replace use of `the_repository` in static functions
 (this branch is used by ps/setup-centralize-odb-creation.)

 Many uses of the_repository has been updated to use a more
 appropriate struct repository instance in setup.c codepath.

 Will merge to 'master'.
 source: <20260519-pks-setup-wo-the-repository-v3-0-a00d8ea8b07f@pks.im>


* kh/doc-trailers (2026-04-13) 9 commits
 - doc: interpret-trailers: document comment line treatment
 - doc: interpret-trailers: commit to “trailer block” term
 - doc: interpret-trailers: add key format example
 - doc: interpret-trailers: explain key format
 - doc: interpret-trailers: explain the format after the intro
 - doc: interpret-trailers: not just for commit messages
 - doc: interpret-trailers: use “metadata” in Name as well
 - doc: interpret-trailers: replace “lines” with “metadata”
 - doc: interpret-trailers: stop fixating on RFC 822

 Documentation updates.

 Needs review.
 cf. <xmqq1pfivfa3.fsf@gitster.g>
 source: <V2_CV_doc_int-tr_key_format.613@msgid.xyz>


* ps/graph-lane-limit (2026-03-27) 3 commits
  (merged to 'next' on 2026-05-22 at ca1c5e8432)
 + graph: add truncation mark to capped lanes
 + graph: add --graph-lane-limit option
 + graph: limit the graph width to a hard-coded max

 The graph output from commands like "git log --graph" can now be
 limited to a specified number of lanes, preventing overly wide output
 in repositories with many branches.

 Will merge to 'master'.
 cf. <bdff0a5d-b738-4053-9b72-08eba88156de@kdbg.org>
 source: <20260328001113.1275291-1-pabloosabaterr@gmail.com>


* jr/bisect-custom-terms-in-output (2026-05-14) 3 commits
  (merged to 'next' on 2026-05-22 at 1ccd1056c9)
 + rev-parse: use selected alternate terms to look up refs
 + bisect: print bisect terms in single quotes
 + bisect: use selected alternate terms in status output

 "git bisect" now uses the selected terms (e.g., old/new) more
 consistently in its output.

 Will merge to 'master'.
 source: <20260514-bisect-terms-v4-0-b3e3cf1b06ce@schlaraffenlan.de>


* ua/push-remote-group (2026-05-03) 3 commits
 - push: support pushing to a remote group
 - remote: move remote group resolution to remote.c
 - remote: fix sign-compare warnings in push_cas_option

 "git push" learned to take a "remote group" name to push to, which
 causes pushes to multiple places, just like "git fetch" would do.

 Comments?
 source: <20260503153402.1333220-1-usmanakinyemi202@gmail.com>


* js/parseopt-subcommand-autocorrection (2026-04-27) 11 commits
 - SQUASH???
 - doc: document autocorrect API
 - parseopt: add tests for subcommand autocorrection
 - parseopt: enable subcommand autocorrection for git-remote and git-notes
 - parseopt: autocorrect mistyped subcommands
 - autocorrect: provide config resolution API
 - autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT
 - autocorrect: use mode and delay instead of magic numbers
 - help: move tty check for autocorrection to autocorrect.c
 - help: make autocorrect handling reusable
 - parseopt: extract subcommand handling from parse_options_step()

 The parse-options library learned to auto-correct misspelled
 subcommand names.

 Expecting a reroll.
 cf. <xmqqcxz2tzpr.fsf@gitster.g>
 source: <SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM>


* jc/neuter-sideband-post-3.0 (2026-03-05) 2 commits
 - sideband: delay sanitizing by default to Git v3.0
 - Merge branch 'jc/neuter-sideband-fixup' into jc/neuter-sideband-post-3.0

 The final step, split from earlier attempt by Dscho, to loosen the
 sideband restriction for now and tighten later at Git v3.0 boundary.

 On hold to help the base topic with wider exposure.
 (this branch uses jc/neuter-sideband-fixup.)
 source: <20260305233452.3727126-8-gitster@pobox.com>


* cs/subtree-split-recursion (2026-03-05) 3 commits
 - contrib/subtree: reduce recursion during split
 - contrib/subtree: functionalize split traversal
 - contrib/subtree: reduce function side-effects

 When processing large history graphs on Debian or Ubuntu, "git
 subtree" can die with a "recursion depth reached" error.

 Comments?
 source: <20260305-cs-subtree-split-recursion-v2-0-7266be870ba9@howdoi.land>


* pt/fsmonitor-linux (2026-04-15) 13 commits
  (merged to 'next' on 2026-05-22 at 5d99c1765d)
 + fsmonitor: convert shown khash to strset in do_handle_client
 + fsmonitor: add tests for Linux
 + fsmonitor: add timeout to daemon stop command
 + fsmonitor: close inherited file descriptors and detach in daemon
 + run-command: add close_fd_above_stderr option
 + fsmonitor: implement filesystem change listener for Linux
 + fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c
 + fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c
 + fsmonitor: use pthread_cond_timedwait for cookie wait
 + compat/win32: add pthread_cond_timedwait
 + fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon
 + fsmonitor: fix khash memory leak in do_handle_client
 + t9210, t9211: disable GIT_TEST_SPLIT_INDEX for scalar clone tests

 The fsmonitor daemon has been implemented for Linux.

 Will merge to 'master'.
 cf. <xmqqa4u5nnxq.fsf@gitster.g>
 source: <pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com>

^ permalink raw reply

* git mv after the fact
From: Frieder Hannenheim @ 2026-05-26 12:57 UTC (permalink / raw)
  To: git

Hi,

I'd like to propose a new flag for git mv, that updates the index
like git mv normally would but does not move the file. This would come
in handy when the file has already been moved, or is renamed by some
external tool and the user wants to fix up the index afterwards.

Thank you for considering this.

Sincerely,
Frieder Hannenheim


^ permalink raw reply

* [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


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