Git development
 help / color / mirror / Atom feed
* [PATCH v3 2/8] environment: move "check_stat" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-04-23 16:54 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260423165432.143598-1-belkid98@gmail.com>

The `core.checkstat` configuration is currently stored in the global
variable `check_stat`, which makes it shared across repository
instances within a single process.

Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `core.checkstat` is parsed eagerly
because it controls how `match_stat_data()` and related functions
decide file freshness; a lazy parse could lead to unexpected
behavior or complicate libification. This preserves the existing
eager‑parsing behavior while tying the value to the repository it
was read from, avoiding cross‑repository state leakage, and
continuing the effort to reduce reliance on global configuration
state.

Update all references to use `repo_config_values()`.

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
 entry.c       |  3 ++-
 environment.c |  6 +++---
 environment.h |  2 +-
 statinfo.c    | 10 +++++-----
 4 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/entry.c b/entry.c
index 7817aee362..c55e867d8a 100644
--- a/entry.c
+++ b/entry.c
@@ -443,7 +443,8 @@ static int check_path(const char *path, int len, struct stat *st, int skiplen)
 static void mark_colliding_entries(const struct checkout *state,
 				   struct cache_entry *ce, struct stat *st)
 {
-	int trust_ino = check_stat;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
+	int trust_ino = cfg->check_stat;
 
 #if defined(GIT_WINDOWS_NATIVE) || defined(__CYGWIN__)
 	trust_ino = 0;
diff --git a/environment.c b/environment.c
index 0a9067729e..8542ac3141 100644
--- a/environment.c
+++ b/environment.c
@@ -42,7 +42,6 @@ static int pack_compression_seen;
 static int zlib_compression_seen;
 
 int trust_executable_bit = 1;
-int check_stat = 1;
 int has_symlinks = 1;
 int minimum_abbrev = 4, default_abbrev = -1;
 int ignore_case;
@@ -315,9 +314,9 @@ int git_default_core_config(const char *var, const char *value,
 		if (!value)
 			return config_error_nonbool(var);
 		if (!strcasecmp(value, "default"))
-			check_stat = 1;
+			cfg->check_stat = 1;
 		else if (!strcasecmp(value, "minimal"))
-			check_stat = 0;
+			cfg->check_stat = 0;
 		else
 			return error(_("invalid value for '%s': '%s'"),
 				     var, value);
@@ -721,4 +720,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
 	cfg->apply_sparse_checkout = 0;
 	cfg->branch_track = BRANCH_TRACK_REMOTE;
 	cfg->trust_ctime = 1;
+	cfg->check_stat = 1;
 }
diff --git a/environment.h b/environment.h
index 64d537686e..1d3e2e4f23 100644
--- a/environment.h
+++ b/environment.h
@@ -92,6 +92,7 @@ struct repo_config_values {
 	char *attributes_file;
 	int apply_sparse_checkout;
 	int trust_ctime;
+	int check_stat;
 
 	/* section "branch" config values */
 	enum branch_track branch_track;
@@ -162,7 +163,6 @@ extern char *git_work_tree_cfg;
 
 /* Environment bits from configuration mechanism */
 extern int trust_executable_bit;
-extern int check_stat;
 extern int has_symlinks;
 extern int minimum_abbrev, default_abbrev;
 extern int ignore_case;
diff --git a/statinfo.c b/statinfo.c
index 4fc12053f4..5e00af127d 100644
--- a/statinfo.c
+++ b/statinfo.c
@@ -68,19 +68,19 @@ int match_stat_data(const struct stat_data *sd, struct stat *st)
 
 	if (sd->sd_mtime.sec != (unsigned int)st->st_mtime)
 		changed |= MTIME_CHANGED;
-	if (cfg->trust_ctime && check_stat &&
+	if (cfg->trust_ctime && cfg->check_stat &&
 	    sd->sd_ctime.sec != (unsigned int)st->st_ctime)
 		changed |= CTIME_CHANGED;
 
 #ifdef USE_NSEC
-	if (check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st))
+	if (cfg->check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st))
 		changed |= MTIME_CHANGED;
-	if (cfg->trust_ctime && check_stat &&
+	if (cfg->trust_ctime && cfg->check_stat &&
 	    sd->sd_ctime.nsec != ST_CTIME_NSEC(*st))
 		changed |= CTIME_CHANGED;
 #endif
 
-	if (check_stat) {
+	if (cfg->check_stat) {
 		if (sd->sd_uid != (unsigned int) st->st_uid ||
 			sd->sd_gid != (unsigned int) st->st_gid)
 			changed |= OWNER_CHANGED;
@@ -94,7 +94,7 @@ int match_stat_data(const struct stat_data *sd, struct stat *st)
 	 * clients will have different views of what "device"
 	 * the filesystem is on
 	 */
-	if (check_stat && sd->sd_dev != (unsigned int) st->st_dev)
+	if (cfg->check_stat && sd->sd_dev != (unsigned int) st->st_dev)
 			changed |= INODE_CHANGED;
 #endif
 
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v3 1/8] environment: move "trust_ctime" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-04-23 16:54 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260423165432.143598-1-belkid98@gmail.com>

The `core.trustctime` configuration is currently stored in the global
variable `trust_ctime`, which makes it shared across repository
instances in a single process.

Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `core.trustctime` is parsed eagerly
because it is used in low‑level stat‑matching functions
(`match_stat_data()`), where a lazy parse could cause unexpected
fatal errors and complicate libification efforts. This preserves
that behavior while tying the value to the repository from which it
was read, avoiding cross‑repository state leakage and continuing the
effort to reduce reliance on global configuration state.

Update all references to use repo_config_values().

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
 environment.c | 4 ++--
 environment.h | 2 +-
 statinfo.c    | 6 ++++--
 3 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/environment.c b/environment.c
index fc3ed8bb1c..0a9067729e 100644
--- a/environment.c
+++ b/environment.c
@@ -42,7 +42,6 @@ static int pack_compression_seen;
 static int zlib_compression_seen;
 
 int trust_executable_bit = 1;
-int trust_ctime = 1;
 int check_stat = 1;
 int has_symlinks = 1;
 int minimum_abbrev = 4, default_abbrev = -1;
@@ -309,7 +308,7 @@ int git_default_core_config(const char *var, const char *value,
 		return 0;
 	}
 	if (!strcmp(var, "core.trustctime")) {
-		trust_ctime = git_config_bool(var, value);
+		cfg->trust_ctime = git_config_bool(var, value);
 		return 0;
 	}
 	if (!strcmp(var, "core.checkstat")) {
@@ -721,4 +720,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
 	cfg->attributes_file = NULL;
 	cfg->apply_sparse_checkout = 0;
 	cfg->branch_track = BRANCH_TRACK_REMOTE;
+	cfg->trust_ctime = 1;
 }
diff --git a/environment.h b/environment.h
index 123a71cdc8..64d537686e 100644
--- a/environment.h
+++ b/environment.h
@@ -91,6 +91,7 @@ struct repo_config_values {
 	/* section "core" config values */
 	char *attributes_file;
 	int apply_sparse_checkout;
+	int trust_ctime;
 
 	/* section "branch" config values */
 	enum branch_track branch_track;
@@ -161,7 +162,6 @@ extern char *git_work_tree_cfg;
 
 /* Environment bits from configuration mechanism */
 extern int trust_executable_bit;
-extern int trust_ctime;
 extern int check_stat;
 extern int has_symlinks;
 extern int minimum_abbrev, default_abbrev;
diff --git a/statinfo.c b/statinfo.c
index 30a164b0e6..4fc12053f4 100644
--- a/statinfo.c
+++ b/statinfo.c
@@ -3,6 +3,7 @@
 #include "git-compat-util.h"
 #include "environment.h"
 #include "statinfo.h"
+#include "repository.h"
 
 /*
  * Munge st_size into an unsigned int.
@@ -63,17 +64,18 @@ void fake_lstat_data(const struct stat_data *sd, struct stat *st)
 int match_stat_data(const struct stat_data *sd, struct stat *st)
 {
 	int changed = 0;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	if (sd->sd_mtime.sec != (unsigned int)st->st_mtime)
 		changed |= MTIME_CHANGED;
-	if (trust_ctime && check_stat &&
+	if (cfg->trust_ctime && check_stat &&
 	    sd->sd_ctime.sec != (unsigned int)st->st_ctime)
 		changed |= CTIME_CHANGED;
 
 #ifdef USE_NSEC
 	if (check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st))
 		changed |= MTIME_CHANGED;
-	if (trust_ctime && check_stat &&
+	if (cfg->trust_ctime && check_stat &&
 	    sd->sd_ctime.nsec != ST_CTIME_NSEC(*st))
 		changed |= CTIME_CHANGED;
 #endif
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v3 0/8] environment: move core config globals into repo_config_values
From: Olamide Caleb Bello @ 2026-04-23 16:54 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <CAOLa=ZQDXn7181VfHpcWtNOSjTh9nzM3YnDTG_X1Vqh_v64bwg@mail.gmail.com>

This series continues the effort to remove repository-dependent global
configuration state by moving several core.* configuration variables
into struct repo_config_values.

This ensures configuration values are tied to the repository from which
they were read, avoiding cross-repository state leakage in processes
handling multiple repositories, while preserving existing behavior.

All affected configuration values are eagerly parsed. Storing them in
repo_config_values preserves current semantics and avoids introducing
lazy parsing into runtime code paths.

This v3 addresses review feedback by explicitly clarifying the reason
these values belong in repo_config_values (eager parsing) and improving
commit message clarity.

Olamide Caleb Bello (8):
  environment: move "trust_ctime" into `struct repo_config_values`
  environment: move "check_stat" into `struct repo_config_values`
  environment: move `zlib_compression_level` into `struct
    repo_config_values`
  environment: move "pack_compression_level" into `struct
    repo_config_values`
  environment: move "precomposed_unicode" into `struct
    repo_config_values`
  env: move "core_sparse_checkout_cone" into `struct repo_config_values`
  env: move "sparse_expect_files_outside_of_patterns" into
    `repo_config_values`
  env: move "warn_on_object_refname_ambiguity" into `struct
    repo_config_values`

 builtin/cat-file.c        |  7 ++++---
 builtin/fast-import.c     |  8 +++++---
 builtin/index-pack.c      |  3 ++-
 builtin/mv.c              |  2 +-
 builtin/pack-objects.c    | 24 +++++++++++++----------
 builtin/sparse-checkout.c | 37 +++++++++++++++++++++---------------
 compat/precompose_utf8.c  | 20 +++++++++++++-------
 diff.c                    |  3 ++-
 dir.c                     |  3 ++-
 entry.c                   |  3 ++-
 environment.c             | 40 +++++++++++++++++++++------------------
 environment.h             | 19 ++++++++++---------
 http-push.c               |  3 ++-
 object-file.c             |  6 ++++--
 object-name.c             |  3 ++-
 revision.c                |  7 ++++---
 sparse-index.c            |  4 ++--
 statinfo.c                | 12 +++++++-----
 submodule.c               |  7 ++++---
 upload-pack.c             |  3 ++-
 20 files changed, 126 insertions(+), 88 deletions(-)

-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply

* Re: [PATCH v3 0/8] repo_config_values: migrate more globals
From: Bello Olamide @ 2026-04-23 16:37 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me
In-Reply-To: <20260423160832.114816-1-belkid98@gmail.com>

Hi all,

Apologies, this v3 series was generated with an incorrect commit range,
and includes unrelated upstream commits. Please ignore this series.

I will resend a corrected version shortly based only on the intended
environment.* refactoring changes.

Thanks,
Olamide Bello

^ permalink raw reply

* [PATCH v3 8/8] Git 2.54-rc2
From: Olamide Caleb Bello @ 2026-04-23 16:08 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me
In-Reply-To: <20260423160832.114816-1-belkid98@gmail.com>

From: Junio C Hamano <gitster@pobox.com>

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 GIT-VERSION-GEN | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN
index 9c55beb496..fb11bace12 100755
--- a/GIT-VERSION-GEN
+++ b/GIT-VERSION-GEN
@@ -1,6 +1,6 @@
 #!/bin/sh
 
-DEF_VER=v2.54.0-rc1
+DEF_VER=v2.54.0-rc2
 
 LF='
 '
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v3 7/8] Hopefully the final tweak before -rc2
From: Olamide Caleb Bello @ 2026-04-23 16:08 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me
In-Reply-To: <20260423160832.114816-1-belkid98@gmail.com>

From: Junio C Hamano <gitster@pobox.com>

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/RelNotes/2.54.0.adoc | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/Documentation/RelNotes/2.54.0.adoc b/Documentation/RelNotes/2.54.0.adoc
index 3fa25e06f2..2ad73ff473 100644
--- a/Documentation/RelNotes/2.54.0.adoc
+++ b/Documentation/RelNotes/2.54.0.adoc
@@ -526,6 +526,12 @@ Fixes since v2.53
    conflicts.
    (merge c0ce43376b ng/add-files-to-cache-wo-rename later to maint).
 
+ * Doc mark-up update for entries in the glossary with bulleted lists.
+   (merge a65cbd87ea jk/doc-markup-sub-list-indentation later to maint).
+
+ * CI dependency updates.
+   (merge 4bdb17e3a8 jc/ci-github-actions-use-checkout-v5 later to maint).
+
  * Other code cleanup, docfix, build fix, etc.
    (merge d79fff4a11 jk/remote-tracking-ref-leakfix later to maint).
    (merge 7a747f972d dd/t5403-modernise later to maint).
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v3 6/8] gitglossary: fix indentation of sub-lists
From: Olamide Caleb Bello @ 2026-04-23 16:08 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Jeff King
In-Reply-To: <20260423160832.114816-1-belkid98@gmail.com>

From: Jeff King <peff@peff.net>

The glossary entry is a list of terms and their definitions, so
multi-paragraph definitions need "+" continuation lines to indicate
that they are part of a single entry.

When an entry contains a sub-list (say, a bulleted list), the final "+"
may become ambiguous: is it connecting the next paragraph to the final
entry of the sub-list, or to the original list of definition paragraphs?

Asciidoc generally connects it to the former, even when we mean the
latter, and you end up with the next paragraph indented incorrectly,
like this:

  glob
    ...defines glob...

    Two consecutive asterisks ("**") in patterns matched
    against full pathname may have special meaning:

    - ...some special meaning of **...

    - ...another special meaning of **...

    - Other consecutive asterisks are considered invalid.

      Glob magic is incompatible with literal magic.

That final "Glob magic is incompatible" paragraph is in the wrong spot.
It should be at the same level as "Two consecutive asterisks", as it is
not part of the final "Other consecutive asterisks" bullet point.

The same problem appears in several other spots in the glossary.

Usually we'd fix this by using "--" markers, which put the sub-list into
its own block. But there's a catch: in some of these spots we are
already in an open block, and nesting open blocks is a problem. It seems
to work for me using Asciidoc 10.2.1, but Asciidoctor 2.0.26 makes a
mess of it (our intent to open a new block seems to close the old one).

Fortunately there's a work-around: when using a "+" list-continuation,
the number of empty lines above the continuation indicates which level
of parent list to continue. So by adding an empty line after our
unordered list (before the "+"), we should be able to continue the
definition list item.

But asciidoc being asciidoc, of course that is not the end of the story.
That technique works fine for the "glob" and "attr" lists in this patch,
but under the "refs" item it works for only 1 of the 2 lists! I can't
figure out why, and this may be an asciidoctor bug. But we can work
around it by using "--" open-block markers here, since we're not
already in an open block.

So using the extra blank line for the first two instances, and "--"
markers for the second two, this patch produces identical output from
"doc-diff HEAD^ HEAD" for both --asciidoctor and --asciidoc modes.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/glossary-content.adoc | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/Documentation/glossary-content.adoc b/Documentation/glossary-content.adoc
index 20ba121314..8c4e9dd3be 100644
--- a/Documentation/glossary-content.adoc
+++ b/Documentation/glossary-content.adoc
@@ -430,6 +430,7 @@ full pathname may have special meaning:
    matches "`a/b`", "`a/x/b`", "`a/x/y/b`" and so on.
 
  - Other consecutive asterisks are considered invalid.
+
 +
 Glob magic is incompatible with literal magic.
 
@@ -452,6 +453,7 @@ these forms:
 
 - "`!ATTR`" requires that the attribute `ATTR` be
   unspecified.
+
 +
 Note that when matching against a tree object, attributes are still
 obtained from working tree, not from the given tree object.
@@ -560,14 +562,17 @@ The ref namespace is hierarchical.
 Ref names must either start with `refs/` or be located in the root of
 the hierarchy. For the latter, their name must follow these rules:
 +
+--
  - The name consists of only upper-case characters or underscores.
 
  - The name ends with "`_HEAD`" or is equal to "`HEAD`".
+--
 +
 There are some irregular refs in the root of the hierarchy that do not
 match these rules. The following list is exhaustive and shall not be
 extended in the future:
 +
+--
  - `AUTO_MERGE`
 
  - `BISECT_EXPECTED_REV`
@@ -577,6 +582,7 @@ extended in the future:
  - `NOTES_MERGE_REF`
 
  - `MERGE_AUTOSTASH`
+--
 +
 Different subhierarchies are used for different purposes. For example,
 the `refs/heads/` hierarchy is used to represent local branches whereas
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v3 5/8] CI: bump actions/checkout from 4 to 5 for rust-analysis job
From: Olamide Caleb Bello @ 2026-04-23 16:08 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me
In-Reply-To: <20260423160832.114816-1-belkid98@gmail.com>

From: Junio C Hamano <gitster@pobox.com>

GitHub Actions started complaining about use of Node.js 20 and I was
wondering why only one job uses actions/checkout@v4, while everybody
else already uses actions/checkout@v5.

It turns out that it is caused by a semantic mismerge between
e75cd059 (ci: check formatting of our Rust code, 2025-10-15) that
added a new use of actions/checkout@v4 that happened very close to
another change 63541ed9 (build(deps): bump actions/checkout from 4
to 5, 2025-10-16) that updated all uses of actions/checkout@v4 to
use vactions/checkout@v5.

Update the leftover and the last use of actions/checkout@v4 to use
actions/checkout@v5 to help ourselves to move away from Node.js 20.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 .github/workflows/main.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f2e93f5461..8ed268fc09 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -480,7 +480,7 @@ jobs:
       group: rust-analysis-${{ github.ref }}
       cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v5
     - run: ci/install-dependencies.sh
     - run: ci/run-rust-checks.sh
   sparse:
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v3 4/8] doc: am: correct to full --no-message-id
From: Olamide Caleb Bello @ 2026-04-23 16:08 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Kristoffer Haugsbakk
In-Reply-To: <20260423160832.114816-1-belkid98@gmail.com>

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/git-am.adoc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Documentation/git-am.adoc b/Documentation/git-am.adoc
index 14f83a8920..ab71ab7490 100644
--- a/Documentation/git-am.adoc
+++ b/Documentation/git-am.adoc
@@ -91,7 +91,7 @@ OPTIONS
 
 --no-message-id::
 	Do not add the Message-ID header to the commit message.
-	`no-message-id` is useful to override `am.messageid`.
+	`--no-message-id` is useful to override `am.messageid`.
 
 -q::
 --quiet::
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v3 3/8] doc: am: revert Message-ID trailer claim
From: Olamide Caleb Bello @ 2026-04-23 16:08 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Kristoffer Haugsbakk
In-Reply-To: <20260423160832.114816-1-belkid98@gmail.com>

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

I claimed in 3c18135b (doc: am: say that --message-id adds a trailer,
2026-02-09) that `git am --message-id` adds a Git trailer. But that
isn’t the case; for the case of a commit message with a subject, body,
and no trailer block:

    <subject>

    <paragrah>

It just appends the line right after `paragraph`:

    <subject>

    <paragraph>
    Message-ID: <message-id_trailer.323@msgid.xyz>

It does work for two other cases though, namely subject-only and with an
existing trailer block.

This is at best an inconsistency and arguably a bug, but we’re at the
trailing end of the release cycle now. So reverting the doc is safer
than making msg-id act as a trailer, for now.

Revert this hunk from commit 3c18135b except the only useful
change (“Also use inline-verbatim for `Message-ID`”).

Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/git-am.adoc | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/Documentation/git-am.adoc b/Documentation/git-am.adoc
index 403181baa9..14f83a8920 100644
--- a/Documentation/git-am.adoc
+++ b/Documentation/git-am.adoc
@@ -84,11 +84,10 @@ OPTIONS
 
 -m::
 --message-id::
-	Pass the `-m` flag to linkgit:git-mailinfo[1], so that the
-	`Message-ID` header is added as a trailer (see
-	linkgit:git-interpret-trailers[1]).  The `am.messageid`
-	configuration variable can be used to specify the default
-	behaviour.
+	Pass the `-m` flag to linkgit:git-mailinfo[1],
+	so that the `Message-ID` header is added to the commit message.
+	The `am.messageid` configuration variable can be used to specify
+	the default behaviour.
 
 --no-message-id::
 	Do not add the Message-ID header to the commit message.
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v3 2/8] rust: we are way beyond 2.53
From: Olamide Caleb Bello @ 2026-04-23 16:08 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, brian m. carlson, Derrick Stolee
In-Reply-To: <20260423160832.114816-1-belkid98@gmail.com>

From: Junio C Hamano <gitster@pobox.com>

Earlier we timelined that we'd tune our build procedures to build
with Rust by default in Git 2.53, but we are already in prerelease
freeze for 2.54 now.  Update the BreakingChanges document to delay
it until Git 2.55 (slated for the end of June 2026).

Noticed-by: brian m. carlson <sandals@crustytoothpaste.net>
Helped-by: Derrick Stolee <stolee@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/BreakingChanges.adoc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Documentation/BreakingChanges.adoc b/Documentation/BreakingChanges.adoc
index f814450d2f..af59c43f42 100644
--- a/Documentation/BreakingChanges.adoc
+++ b/Documentation/BreakingChanges.adoc
@@ -190,7 +190,7 @@ milestones for the introduction of Rust:
 1. Initially, with Git 2.52, support for Rust will be auto-detected by Meson and
    disabled in our Makefile so that the project can sort out the initial
    infrastructure.
-2. In Git 2.53, both build systems will default-enable support for Rust.
+2. In Git 2.55, both build systems will default-enable support for Rust.
    Consequently, builds will break by default if Rust is not available on the
    build host. The use of Rust can still be explicitly disabled via build
    flags.
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v3 1/8] Revert "compat/posix: introduce writev(3p) wrapper"
From: Olamide Caleb Bello @ 2026-04-23 16:08 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me
In-Reply-To: <20260423160832.114816-1-belkid98@gmail.com>

From: Junio C Hamano <gitster@pobox.com>

This reverts commit 3b9b2c2a29a1d529ca9884fa0a6529f6e2496abe; let's
not use writev() for now.
---
 Makefile         |  4 ----
 compat/posix.h   | 14 --------------
 compat/writev.c  | 44 --------------------------------------------
 config.mak.uname |  2 --
 meson.build      |  1 -
 5 files changed, 65 deletions(-)
 delete mode 100644 compat/writev.c

diff --git a/Makefile b/Makefile
index 5d22394c2e..cedc234173 100644
--- a/Makefile
+++ b/Makefile
@@ -2029,10 +2029,6 @@ ifdef NO_PREAD
 	COMPAT_CFLAGS += -DNO_PREAD
 	COMPAT_OBJS += compat/pread.o
 endif
-ifdef NO_WRITEV
-	COMPAT_CFLAGS += -DNO_WRITEV
-	COMPAT_OBJS += compat/writev.o
-endif
 ifdef NO_FAST_WORKING_DIRECTORY
 	BASIC_CFLAGS += -DNO_FAST_WORKING_DIRECTORY
 endif
diff --git a/compat/posix.h b/compat/posix.h
index 94699a03fa..faaae1b655 100644
--- a/compat/posix.h
+++ b/compat/posix.h
@@ -137,9 +137,6 @@
 #include <sys/socket.h>
 #include <sys/ioctl.h>
 #include <sys/statvfs.h>
-#ifndef NO_WRITEV
-#include <sys/uio.h>
-#endif
 #include <termios.h>
 #ifndef NO_SYS_SELECT_H
 #include <sys/select.h>
@@ -326,17 +323,6 @@ int git_lstat(const char *, struct stat *);
 ssize_t git_pread(int fd, void *buf, size_t count, off_t offset);
 #endif
 
-#ifdef NO_WRITEV
-#define writev git_writev
-#define iovec git_iovec
-struct git_iovec {
-	void *iov_base;
-	size_t iov_len;
-};
-
-ssize_t git_writev(int fd, const struct iovec *iov, int iovcnt);
-#endif
-
 #ifdef NO_SETENV
 #define setenv gitsetenv
 int gitsetenv(const char *, const char *, int);
diff --git a/compat/writev.c b/compat/writev.c
deleted file mode 100644
index 3a94870a2f..0000000000
--- a/compat/writev.c
+++ /dev/null
@@ -1,44 +0,0 @@
-#include "../git-compat-util.h"
-#include "../wrapper.h"
-
-ssize_t git_writev(int fd, const struct iovec *iov, int iovcnt)
-{
-	size_t total_written = 0;
-	size_t sum = 0;
-
-	/*
-	 * According to writev(3p), the syscall shall error with EINVAL in case
-	 * the sum of `iov_len` overflows `ssize_t`.
-	 */
-	 for (int i = 0; i < iovcnt; i++) {
-		if (iov[i].iov_len > maximum_signed_value_of_type(ssize_t) ||
-		    iov[i].iov_len + sum > maximum_signed_value_of_type(ssize_t)) {
-			errno = EINVAL;
-			return -1;
-		}
-
-		sum += iov[i].iov_len;
-	}
-
-	for (int i = 0; i < iovcnt; i++) {
-		const char *bytes = iov[i].iov_base;
-		size_t iovec_written = 0;
-
-		while (iovec_written < iov[i].iov_len) {
-			ssize_t bytes_written = xwrite(fd, bytes + iovec_written,
-						       iov[i].iov_len - iovec_written);
-			if (bytes_written < 0) {
-				if (total_written)
-					goto out;
-				return bytes_written;
-			}
-			if (!bytes_written)
-				goto out;
-			iovec_written += bytes_written;
-			total_written += bytes_written;
-		}
-	}
-
-out:
-	return (ssize_t) total_written;
-}
diff --git a/config.mak.uname b/config.mak.uname
index ccb3f71881..5feb582558 100644
--- a/config.mak.uname
+++ b/config.mak.uname
@@ -459,7 +459,6 @@ ifeq ($(uname_S),Windows)
 	SANE_TOOL_PATH ?= $(msvc_bin_dir_msys)
 	HAVE_ALLOCA_H = YesPlease
 	NO_PREAD = YesPlease
-	NO_WRITEV = YesPlease
 	NEEDS_CRYPTO_WITH_SSL = YesPlease
 	NO_LIBGEN_H = YesPlease
 	NO_POLL = YesPlease
@@ -675,7 +674,6 @@ ifeq ($(uname_S),MINGW)
 	pathsep = ;
 	HAVE_ALLOCA_H = YesPlease
 	NO_PREAD = YesPlease
-	NO_WRITEV = YesPlease
 	NEEDS_CRYPTO_WITH_SSL = YesPlease
 	NO_LIBGEN_H = YesPlease
 	NO_POLL = YesPlease
diff --git a/meson.build b/meson.build
index 8309942d18..11488623bf 100644
--- a/meson.build
+++ b/meson.build
@@ -1429,7 +1429,6 @@ checkfuncs = {
   'initgroups' : [],
   'strtoumax' : ['strtoumax.c', 'strtoimax.c'],
   'pread' : ['pread.c'],
-  'writev' : ['writev.c'],
 }
 
 if host_machine.system() == 'windows'
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v3 0/8] repo_config_values: migrate more globals
From: Olamide Caleb Bello @ 2026-04-23 16:08 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, gitster, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <CAOLa=ZQDXn7181VfHpcWtNOSjTh9nzM3YnDTG_X1Vqh_v64bwg@mail.gmail.com>

This series continues the effort to remove repository-dependent global
configuration state by moving several core.* configuration variables
into struct `repo_config_values`.

This ensures configuration values are tied to the repository from which
they were read, avoiding cross-repository state leakage in processes
handling multiple repositories, while preserving existing behavior.

All affected configuration values are eagerly parsed. Storing them in
repo_config_values preserves current semantics and avoids introducing
lazy parsing into runtime code paths.

This v3 addresses review feedback by explicitly clarifying the reason
these values belong in repo_config_values (eager parsing) and improving
commit message clarity.

Olamide Caleb Bello (8):
  environment: move "trust_ctime" into struct repo_config_values
  environment: move "check_stat" into struct repo_config_values
  environment: move "zlib_compression_level" into repo_config_values
  environment: move "pack_compression_level" into struct repo_config_values
  environment: move "precomposed_unicode" into struct repo_config_values
  env: move "core_sparse_checkout_cone" into repo_config_values
  env: put "sparse_expect_files_outside_of_patterns" in repo_config_values
  env: move "warn_on_object_refname_ambiguity" into repo_config_values
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply

* [PATCH] parse-options: fix sparse 'plain integer as NULL pointer'
From: Ramsay Jones @ 2026-04-23 16:05 UTC (permalink / raw)
  To: Jiamu Sun; +Cc: GIT Mailing-list, Junio C Hamano


Signed-off-by: Ramsay Jones <ramsay@ramsayjones.plus.com>
---

Hi Jiamu Sun,

If you need to re-roll your 'js/parseopt-subcommand-autocorrection'
branch, could you please squash this into the patch corresponding
to commit b9e6a2d30a ("parseopt: autocorrect mistyped subcommands",
2026-04-23).

Thanks.

ATB,
Ramsay Jones

 parse-options.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/parse-options.c b/parse-options.c
index d60e7bd3c9..14f3f385eb 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -658,7 +658,7 @@ static const char *autocorrect_subcommand(const char *cmd,
 
 	for_each_string_list_item(cand, cmds) {
 		if (starts_with(cand->string, cmd)) {
-			cand->util = 0;
+			cand->util = NULL;
 		} else {
 			int edit = levenshtein(cmd, cand->string,
 					       0, 2, 1, 3) + 1;
-- 
2.54.0

^ permalink raw reply related

* Re: What's cooking in git.git (Apr 2026, #08)
From: Phillip Wood @ 2026-04-23 15:18 UTC (permalink / raw)
  To: Junio C Hamano, git
In-Reply-To: <xmqqv7dix8pi.fsf@gitster.g>

On 23/04/2026 11:38, Junio C Hamano wrote:
> 
> * hn/git-checkout-m-with-stash (2026-04-15) 5 commits
>   - checkout -m: autostash when switching branches
>   - checkout: rollback lock on early returns in merge_working_tree
>   - sequencer: teach autostash apply to take optional conflict marker labels
>   - sequencer: allow create_autostash to run silently
>   - stash: add --label-ours, --label-theirs, --label-base for apply
> 
>   "git checkout -m another-branch" was invented to deal with local
>   changes to paths that are different between the current and the new
>   branch, but it gave only one chance to resolve conflicts.  The command
>   was taught to create a stash to save the local changes.
> 
>   Will merge to 'next'?
>   source: <pull.2234.v14.git.git.1776270259.gitgitgadget@gmail.com>

If you can hold off for a couple of days, I'm planning to review the 
lastest round tomorrow

Thanks

Phillip


^ permalink raw reply

* Re: [PATCH v2 2/3] builtin/log: prefetch necessary blobs for `git cherry`
From: Phillip Wood @ 2026-04-23 15:15 UTC (permalink / raw)
  To: Elijah Newren, phillip.wood; +Cc: Elijah Newren via GitGitGadget, git
In-Reply-To: <CABPp-BF4woakYQ5RZ32J8SzDs_VpvT2Wv+Y2WaHTnFnM=96Kzg@mail.gmail.com>

Hi Elijah

On 21/04/2026 22:28, Elijah Newren wrote:
> On Sun, Apr 19, 2026 at 7:04 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>> On 18/04/2026 01:32, Elijah Newren via GitGitGadget wrote:
>>> From: Elijah Newren <newren@gmail.com>
>>>
>>> In partial clones, `git cherry` fetches necessary blobs on-demand one
>>> at a time, which can be very slow.  We would like to prefetch all
>>> necessary blobs upfront.  To do so, we need to be able to first figure
>>> out which blobs are needed.
>>
>> "git rebase" without "--reapply-cherry-picks" suffers from this problem
>> as well as it does the equivalent of "git log --cherry-pick". Is there
>> any way to share prefetch_cherry_blobs() with the cherry-pick detection
>> in revision.c?
> 
> Yes, you're right; git rebase without --reapply-cherry-picks and git
> log --cherry-pick both go through cherry_pick_list() in revision.c,
> which has exactly the same shape as the patch-ids loop in
> cmd_cherry(): build a hashmap of one side via add_commit_patch_id(),
> then look up the other side via patch_id_iter_first(). The on-demand
> blob fetches come from the same patch_id_neq() callback.
> 
> After poking around, I think the approximate scope of the fix would
> be: Move collect_diff_blob_oids(), always_match(), and
> prefetch_cherry_blobs() from builtin/log.c to patch-ids.c and expose
> the last one in patch-ids.h. In cherry_pick_list(), between the
> add_commit_patch_id loop and the comparison loop, build a temporary
> list of just the lookup-side commits (filtering by
> SYMMETRIC_LEFT/BOUNDARY as the existing loop already does) and call
> prefetch_cherry_blobs() on it.

Thanks for taking a look

> That said, I'd rather leave this out of the current series. The bigger
> picture is that I have reservations about expanding partial-clone
> support further into this area. git cherry, git log --cherry-pick, and
> the default cherry-pick detection in git rebase all exist to answer
> "has this patch already landed upstream?" -- a question that, in
> repositories large enough to need partial clones, I feel is rarely
> worth the cost of computing patch-ids across arbitrary amounts of
> history. The honest guidance I would probably give for users on a
> large repo is "pass --reapply-cherry-picks (with rebase) and skip this
> entirely" or to narrow the range under consideration.

"--reapply-cherry-picks --empty=drop" is certainly more efficient. When 
we're computing patch ids do we do it for every upstream commit or just 
the ones that modify the set of paths that are modified in the branch 
we're rebasing?

It is a shame that we don't have a config setting for 
"-reapply-cherry-picks" as it is easy to forget to pass that option. 
Unfortunately it is not supported by the apply backend which makes such 
a setting potentially confusing.

>  The omission of
> a --no-reapply-cherry-picks option in git-replay wasn't a lack of
> effort or oversight, but a deliberate choice where I'd rather hold off
> (possibly indefinitely) on implementing it.  So I'm a bit reluctant to
> make the performance hazard less visible without also asking whether
> we should even be doing that piece of the operation.
> 
> I only implemented the git cherry fix because of a specific customer
> situation where the operation was already baked into tooling, and
> prefetching at least makes the worst case tolerable.

I'm a bit surprised customers aren't complaining about tools that use 
"git rebase" being slow.

> I don't want to
> hold myself to doing the same for the cherry_pick_list() path, but I'm
> fairly confident the code here can be re-used for those other cases
> and I'd help review a patch from anyone who wants to carry it forward.
> 
> Anyway, you are making the right connection, it's just that my
> personal answer is to let some other interested individual do it.

Fair enough

Thanks

Phillip


^ permalink raw reply

* [PATCH] [ci] Update GitHub Actions to latest major release / GitHub #2278
From: Christoph Grüninger @ 2026-04-23 14:20 UTC (permalink / raw)
  To: git; +Cc: johannes.schindelin

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

Dear Git developer!

I repost my suggested contribution from:

https://github.com/git/git/pull/2278

I updated all GitHub Actions to their latest major release. In contrast 
to "ci: GitHub Actions updates" (brought to you by Dependabot 
(https://lore.kernel.org/git/pull.2097.git.1776775319.gitgitgadget@gmail.com/T/#t), 
I update some more standard actions and mshick/add-pr-comment.
They fix deprecation warnings that GitHub Action deprecated Node20.js.

Kind regards,
Christoph


-- 
Most customers will not accept source code with compile errors in it.
                  Dan Saks, CppCon 2016 (https://youtu.be/D7Sd8A6_fYU)

[-- Attachment #2: 0001-ci-Update-GitHub-Actions-to-latest-major-release.patch --]
[-- Type: text/x-patch, Size: 10077 bytes --]

From c55bac722b6e6e5df6ea698f5985e97d625db457 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Christoph=20Gr=C3=BCninger?= <foss@grueninger.de>
Date: Wed, 22 Apr 2026 23:34:12 +0200
Subject: [PATCH] [ci] Update GitHub Actions to latest major release

Fix deprecation warning that Node20.js will stop working
in June.
---
 .github/workflows/check-whitespace.yml |  2 +-
 .github/workflows/coverity.yml         |  2 +-
 .github/workflows/l10n.yml             |  2 +-
 .github/workflows/main.yml             | 50 +++++++++++++-------------
 4 files changed, 28 insertions(+), 28 deletions(-)

diff --git a/.github/workflows/check-whitespace.yml b/.github/workflows/check-whitespace.yml
index 928fd4c..ea6f49f 100644
--- a/.github/workflows/check-whitespace.yml
+++ b/.github/workflows/check-whitespace.yml
@@ -19,7 +19,7 @@ jobs:
   check-whitespace:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
       with:
         fetch-depth: 0
 
diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml
index 3435bae..89bef26 100644
--- a/.github/workflows/coverity.yml
+++ b/.github/workflows/coverity.yml
@@ -38,7 +38,7 @@ jobs:
       COVERITY_LANGUAGE: cxx
       COVERITY_PLATFORM: overridden-below
     steps:
-      - uses: actions/checkout@v5
+      - uses: actions/checkout@v6
       - name: install minimal Git for Windows SDK
         if: contains(matrix.os, 'windows')
         uses: git-for-windows/setup-git-for-windows-sdk@v1
diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml
index 95e5513..114a12a 100644
--- a/.github/workflows/l10n.yml
+++ b/.github/workflows/l10n.yml
@@ -92,7 +92,7 @@ jobs:
           cat git-po-helper.out
           exit $exit_code
       - name: Create comment in pull request for report
-        uses: mshick/add-pr-comment@v2
+        uses: mshick/add-pr-comment@v3
         if: >-
           always() &&
           github.event_name == 'pull_request_target' &&
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 6f3d94e..e992562 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -63,7 +63,7 @@ jobs:
           echo "skip_concurrent=$skip_concurrent" >>$GITHUB_OUTPUT
       - name: skip if the commit or tree was already tested
         id: skip-if-redundant
-        uses: actions/github-script@v8
+        uses: actions/github-script@v9
         if: steps.check-ref.outputs.enabled == 'yes'
         with:
           github-token: ${{secrets.GITHUB_TOKEN}}
@@ -112,7 +112,7 @@ jobs:
       group: windows-build-${{ github.ref }}
       cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - uses: git-for-windows/setup-git-for-windows-sdk@v1
     - name: build
       shell: bash
@@ -123,7 +123,7 @@ jobs:
     - name: zip up tracked files
       run: git archive -o artifacts/tracked.tar.gz HEAD
     - name: upload tracked files and build artifacts
-      uses: actions/upload-artifact@v5
+      uses: actions/upload-artifact@v7
       with:
         name: windows-artifacts
         path: artifacts
@@ -140,7 +140,7 @@ jobs:
       cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
     steps:
     - name: download tracked files and build artifacts
-      uses: actions/download-artifact@v6
+      uses: actions/download-artifact@v8
       with:
         name: windows-artifacts
         path: ${{github.workspace}}
@@ -157,7 +157,7 @@ jobs:
       run: ci/print-test-failures.sh
     - name: Upload failed tests' directories
       if: failure() && env.FAILED_TEST_ARTIFACTS != ''
-      uses: actions/upload-artifact@v5
+      uses: actions/upload-artifact@v7
       with:
         name: failed-tests-windows-${{ matrix.nr }}
         path: ${{env.FAILED_TEST_ARTIFACTS}}
@@ -173,10 +173,10 @@ jobs:
       group: vs-build-${{ github.ref }}
       cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - uses: git-for-windows/setup-git-for-windows-sdk@v1
     - name: initialize vcpkg
-      uses: actions/checkout@v5
+      uses: actions/checkout@v6
       with:
         repository: 'microsoft/vcpkg'
         path: 'compat/vcbuild/vcpkg'
@@ -186,7 +186,7 @@ jobs:
         repository: git/git
         definitionId: 9
     - name: add msbuild to PATH
-      uses: microsoft/setup-msbuild@v2
+      uses: microsoft/setup-msbuild@v3
     - name: copy dlls to root
       shell: cmd
       run: compat\vcbuild\vcpkg_copy_dlls.bat release
@@ -208,7 +208,7 @@ jobs:
     - name: zip up tracked files
       run: git archive -o artifacts/tracked.tar.gz HEAD
     - name: upload tracked files and build artifacts
-      uses: actions/upload-artifact@v5
+      uses: actions/upload-artifact@v7
       with:
         name: vs-artifacts
         path: artifacts
@@ -226,7 +226,7 @@ jobs:
     steps:
     - uses: git-for-windows/setup-git-for-windows-sdk@v1
     - name: download tracked files and build artifacts
-      uses: actions/download-artifact@v6
+      uses: actions/download-artifact@v8
       with:
         name: vs-artifacts
         path: ${{github.workspace}}
@@ -244,7 +244,7 @@ jobs:
       run: ci/print-test-failures.sh
     - name: Upload failed tests' directories
       if: failure() && env.FAILED_TEST_ARTIFACTS != ''
-      uses: actions/upload-artifact@v5
+      uses: actions/upload-artifact@v7
       with:
         name: failed-tests-windows-vs-${{ matrix.nr }}
         path: ${{env.FAILED_TEST_ARTIFACTS}}
@@ -258,7 +258,7 @@ jobs:
       group: windows-meson-build-${{ github.ref }}
       cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - uses: actions/setup-python@v6
     - name: Set up dependencies
       shell: pwsh
@@ -270,7 +270,7 @@ jobs:
       shell: pwsh
       run: meson compile -C build
     - name: Upload build artifacts
-      uses: actions/upload-artifact@v5
+      uses: actions/upload-artifact@v7
       with:
         name: windows-meson-artifacts
         path: build
@@ -286,13 +286,13 @@ jobs:
       group: windows-meson-test-${{ matrix.nr }}-${{ github.ref }}
       cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - uses: actions/setup-python@v6
     - name: Set up dependencies
       shell: pwsh
       run: pip install meson ninja
     - name: Download build artifacts
-      uses: actions/download-artifact@v6
+      uses: actions/download-artifact@v8
       with:
         name: windows-meson-artifacts
         path: build
@@ -305,7 +305,7 @@ jobs:
       run: ci/print-test-failures.sh
     - name: Upload failed tests' directories
       if: failure() && env.FAILED_TEST_ARTIFACTS != ''
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v7
       with:
         name: failed-tests-windows-meson-${{ matrix.nr }}
         path: ${{env.FAILED_TEST_ARTIFACTS}}
@@ -341,7 +341,7 @@ jobs:
       TEST_OUTPUT_DIRECTORY: ${{github.workspace}}/t
     runs-on: ${{matrix.vector.pool}}
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - run: ci/install-dependencies.sh
     - run: ci/run-build-and-tests.sh
     - name: print test failures
@@ -349,7 +349,7 @@ jobs:
       run: ci/print-test-failures.sh
     - name: Upload failed tests' directories
       if: failure() && env.FAILED_TEST_ARTIFACTS != ''
-      uses: actions/upload-artifact@v5
+      uses: actions/upload-artifact@v7
       with:
         name: failed-tests-${{matrix.vector.jobname}}
         path: ${{env.FAILED_TEST_ARTIFACTS}}
@@ -362,7 +362,7 @@ jobs:
       CI_JOB_IMAGE: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - run: ci/install-dependencies.sh
     - run: ci/run-build-and-minimal-fuzzers.sh
   dockerized:
@@ -439,7 +439,7 @@ jobs:
         else
           apt-get -q update && apt-get -q -y install git
         fi
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - run: ci/install-dependencies.sh
     - run: useradd builder --create-home
     - run: chown -R builder .
@@ -449,7 +449,7 @@ jobs:
       run: sudo --preserve-env --set-home --user=builder ci/print-test-failures.sh
     - name: Upload failed tests' directories
       if: failure() && env.FAILED_TEST_ARTIFACTS != ''
-      uses: actions/upload-artifact@v5
+      uses: actions/upload-artifact@v6
       with:
         name: failed-tests-${{matrix.vector.jobname}}
         path: ${{env.FAILED_TEST_ARTIFACTS}}
@@ -464,7 +464,7 @@ jobs:
       group: static-analysis-${{ github.ref }}
       cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - run: ci/install-dependencies.sh
     - run: ci/run-static-analysis.sh
     - run: ci/check-directional-formatting.bash
@@ -480,7 +480,7 @@ jobs:
       group: rust-analysis-${{ github.ref }}
       cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - run: ci/install-dependencies.sh
     - run: ci/run-rust-checks.sh
   sparse:
@@ -494,7 +494,7 @@ jobs:
       group: sparse-${{ github.ref }}
       cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - name: Install other dependencies
       run: ci/install-dependencies.sh
     - run: make sparse
@@ -510,6 +510,6 @@ jobs:
       CI_JOB_IMAGE: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
     - run: ci/install-dependencies.sh
     - run: ci/test-documentation.sh
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 3/3] builtin/history: introduce "fixup" subcommand
From: Patrick Steinhardt @ 2026-04-23 14:21 UTC (permalink / raw)
  To: git; +Cc: Elijah Newren, D. Ben Knoble, Tian Yuchen
In-Reply-To: <20260423-b4-pks-history-fixup-v2-0-d7571c6d36eb@pks.im>

The newly introduced git-history(1) command provides functionality to
easily edit commit history while also rebasing dependent branches. The
functionality exposed by this command is still somewhat limited though.

One common use case when editing commit history that is not yet covered
is fixing up a specific commit. Introduce a new subcommand that allows
the user to do exactly that by performing a three-way merge into the
target's commit tree, using HEAD's tree as the merge base. The flow is
thus essentially:

    $ echo changes >file
    $ git add file
    $ git history fixup HEAD~

Like with the other commands, this will automatically rebase dependent
branches, as well. Unlike the other commands though:

  - The command does not work in a bare repository as it interacts with
    the index.

  - The command may run into merge conflicts. If so, the command will
    simply abort.

Especially the second item limits the usefulness of this command a bit.
But there are plans to introduce first-class conflicts into Git, which
will help use cases like this one.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-history.adoc |  77 ++++-
 builtin/history.c              | 246 ++++++++++++++-
 t/meson.build                  |   1 +
 t/t3453-history-fixup.sh       | 680 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 998 insertions(+), 6 deletions(-)

diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 24dc907033..6576379f77 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,6 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
 SYNOPSIS
 --------
 [synopsis]
+git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
 git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
 git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
 
@@ -22,8 +23,9 @@ THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
 This command is related to linkgit:git-rebase[1] in that both commands can be
 used to rewrite history. There are a couple of major differences though:
 
-* linkgit:git-history[1] can work in a bare repository as it does not need to
-  touch either the index or the worktree.
+* Most subcommands of linkgit:git-history[1] can work in a bare repository as
+  they do not need to touch either the index or the worktree. The `fixup`
+  subcommand is an exception to this, as it reads staged changes from the index.
 * linkgit:git-history[1] does not execute any linkgit:githooks[5] at the
   current point in time. This may change in the future.
 * linkgit:git-history[1] by default updates all branches that are descendants
@@ -48,11 +50,27 @@ conflicts. This limitation is by design as history rewrites are not intended to
 be stateful operations. The limitation can be lifted once (if) Git learns about
 first-class conflicts.
 
+When using `fixup` with `--empty=drop`, dropping the root commit is not yet
+supported.
+
 COMMANDS
 --------
 
 The following commands are available to rewrite history in different ways:
 
+`fixup <commit>`::
+	Apply the currently staged changes to the specified commit. This
+	is done by performing a three-way merge between the HEAD commit,
+	the target commit and the tree generated from staged changes.
+	This is using the same logic as linkgit:git-cherry-pick[1].
++
+The commit message and authorship of the target commit are preserved by
+default, unless you specify `--reedit-message`.
++
+If applying the staged changes would result in a conflict, the command
+aborts with an error. All branches that are descendants of the original
+commit are updated to point to the rewritten history.
+
 `reword <commit>`::
 	Rewrite the commit message of the specified commit. All the other
 	details of this commit remain unchanged. This command will spawn an
@@ -87,6 +105,31 @@ OPTIONS
 	objects will be written into the repository, so applying these printed
 	ref updates is generally safe.
 
+`--reedit-message`::
+	Open an editor to modify the target commit's message.
+
+`--empty=(drop|keep|abort)`::
+	Control what happens when a commit becomes empty as a result of the
+	fixup. This can happen in two situations:
++
+--
+* The fixup target itself becomes empty because the staged changes exactly
+  cancel out all changes introduced by that commit.
+
+* A descendant commit becomes empty during replay because it introduced the
+  same change that was just fixed up into an ancestor.
+--
++
+With `drop` (the default), empty commits are removed from the rewritten
+history. Descendants of a dropped target commit are replayed directly onto
+the target's parent. Note that dropping the root commit is not supported;
+see LIMITATIONS.
++
+With `keep`, empty commits are retained in the rewritten history as-is.
++
+With `abort`, the command stops with an error if any commit would become
+empty.
+
 `--update-refs=(branches|head)`::
 	Control which references will be updated by the command, if any. With
 	`branches`, all local branches that point to commits which are
@@ -96,6 +139,36 @@ OPTIONS
 EXAMPLES
 --------
 
+Fixup a commit
+~~~~~~~~~~~~~~
+
+----------
+$ git log --oneline --stat
+abc1234 (HEAD -> main) third
+ third.txt | 1 +
+def5678 second
+ second.txt | 1 +
+ghi9012 first
+ first.txt | 1 +
+
+$ echo "change" >>unrelated.txt
+$ git add unrelated.txt
+$ git history fixup ghi9012
+
+$ git log --oneline --stat
+jkl3456 (HEAD -> main) third
+ third.txt | 1 +
+mno7890 second
+ second.txt | 1 +
+pqr1234 first
+ first.txt     | 1 +
+ unrelated.txt | 1 +
+----------
+
+The staged addition of `unrelated.txt` has been incorporated into the `first`
+commit. All descendant commits have been replayed on top of the rewritten
+history.
+
 Split a commit
 ~~~~~~~~~~~~~~
 
diff --git a/builtin/history.c b/builtin/history.c
index 549e352c74..0fc06fb204 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -10,6 +10,7 @@
 #include "gettext.h"
 #include "hex.h"
 #include "lockfile.h"
+#include "merge-ort.h"
 #include "oidmap.h"
 #include "parse-options.h"
 #include "path.h"
@@ -23,6 +24,8 @@
 #include "unpack-trees.h"
 #include "wt-status.h"
 
+#define GIT_HISTORY_FIXUP_USAGE \
+	N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
 #define GIT_HISTORY_REWORD_USAGE \
 	N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
 #define GIT_HISTORY_SPLIT_USAGE \
@@ -335,10 +338,13 @@ static int handle_reference_updates(struct rev_info *revs,
 				    struct commit *original,
 				    struct commit *rewritten,
 				    const char *reflog_msg,
-				    int dry_run)
+				    int dry_run,
+				    enum replay_empty_commit_action empty)
 {
 	const struct name_decoration *decoration;
-	struct replay_revisions_options opts = { 0 };
+	struct replay_revisions_options opts = {
+		.empty = empty,
+	};
 	struct replay_result result = { 0 };
 	struct ref_transaction *transaction = NULL;
 	struct strbuf err = STRBUF_INIT;
@@ -434,6 +440,236 @@ static int handle_reference_updates(struct rev_info *revs,
 	return ret;
 }
 
+static int commit_became_empty(struct repository *repo,
+			       struct commit *original,
+			       struct tree *result)
+{
+	struct commit *parent = original->parents ? original->parents->item : NULL;
+	struct object_id parent_tree_oid;
+
+	if (parent) {
+		if (repo_parse_commit(repo, parent))
+			return error(_("unable to parse parent of %s"),
+				     oid_to_hex(&original->object.oid));
+
+		parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
+	} else {
+		oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+	}
+
+	return oideq(&result->object.oid, &parent_tree_oid);
+}
+
+static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
+{
+	enum replay_empty_commit_action *value = opt->value;
+
+	BUG_ON_OPT_NEG(unset);
+
+	if (!strcmp(arg, "drop"))
+		*value = REPLAY_EMPTY_COMMIT_DROP;
+	else if (!strcmp(arg, "keep"))
+		*value = REPLAY_EMPTY_COMMIT_KEEP;
+	else if (!strcmp(arg, "abort"))
+		*value = REPLAY_EMPTY_COMMIT_ABORT;
+	else
+		die(_("unrecognized '--empty=' action '%s'; "
+		      "valid values are \"drop\", \"keep\", and \"abort\"."), arg);
+
+	return 0;
+}
+
+static int cmd_history_fixup(int argc,
+			     const char **argv,
+			     const char *prefix,
+			     struct repository *repo)
+{
+	const char * const usage[] = {
+		GIT_HISTORY_FIXUP_USAGE,
+		NULL,
+	};
+	enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
+	enum ref_action action = REF_ACTION_DEFAULT;
+	enum commit_tree_flags flags = 0;
+	int dry_run = 0;
+	struct option options[] = {
+		OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
+			       N_("control which refs should be updated"),
+			       PARSE_OPT_NONEG, parse_ref_action),
+		OPT_BOOL('n', "dry-run", &dry_run,
+			 N_("perform a dry-run without updating any refs")),
+		OPT_BIT(0, "reedit-message", &flags,
+			N_("open an editor to modify the commit message"),
+			COMMIT_TREE_EDIT_MESSAGE),
+		OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
+			       N_("how to handle commits that become empty"),
+			       PARSE_OPT_NONEG, parse_opt_empty),
+		OPT_END(),
+	};
+	struct merge_result merge_result = { 0 };
+	struct merge_options merge_opts = { 0 };
+	struct strbuf reflog_msg = STRBUF_INIT;
+	struct commit *head_commit, *original, *rewritten;
+	struct tree *head_tree, *original_tree, *index_tree;
+	struct rev_info revs = { 0 };
+	bool skip_commit = false;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, options, usage, 0);
+	if (argc != 1) {
+		ret = error(_("command expects a single revision"));
+		goto out;
+	}
+	repo_config(repo, git_default_config, NULL);
+
+	if (action == REF_ACTION_DEFAULT)
+		action = REF_ACTION_BRANCHES;
+
+	if (is_bare_repository()) {
+		ret = error(_("cannot run fixup in a bare repository"));
+		goto out;
+	}
+
+	/* Resolve the original commit, which is the one we want to fix up. */
+	original = lookup_commit_reference_by_name(argv[0]);
+	if (!original) {
+		ret = error(_("commit cannot be found: %s"), argv[0]);
+		goto out;
+	}
+
+	/*
+	 * Resolve HEAD so we can use its tree as the merge base: the staged
+	 * changes are expressed as a diff from HEAD's tree to the index tree.
+	 */
+	head_commit = lookup_commit_reference_by_name("HEAD");
+	if (!head_commit) {
+		ret = error(_("cannot look up HEAD"));
+		goto out;
+	}
+
+	head_tree = repo_get_commit_tree(repo, head_commit);
+	if (!head_tree) {
+		ret = error(_("cannot get tree for HEAD"));
+		goto out;
+	}
+
+	if (repo_read_index(repo) < 0) {
+		ret = error(_("unable to read index"));
+		goto out;
+	}
+
+	if (!repo_index_has_changes(repo, head_tree, NULL)) {
+		ret = error(_("nothing to fixup: no staged changes"));
+		goto out;
+	}
+
+	/*
+	 * Write the index as a tree object. This is the "theirs" side of the
+	 * three-way merge: it is HEAD's tree with the staged changes applied.
+	 */
+	index_tree = write_in_core_index_as_tree(repo, repo->index);
+	if (!index_tree) {
+		ret = error(_("unable to write index as a tree"));
+		goto out;
+	}
+
+	original_tree = repo_get_commit_tree(repo, original);
+	if (!original_tree) {
+		ret = error(_("cannot get tree for commit %s"), argv[0]);
+		goto out;
+	}
+
+	/*
+	 * Perform the three-way merge to reapply changes in the index onto the
+	 * target commit. This is using basically the same logic as a
+	 * cherry-pick, where the base commit is our HEAD, ours is the original
+	 * tree and theirs is the index tree.
+	 */
+	init_basic_merge_options(&merge_opts, repo);
+	merge_opts.ancestor = "HEAD";
+	merge_opts.branch1 = argv[0];
+	merge_opts.branch2 = "staged";
+	merge_incore_nonrecursive(&merge_opts, head_tree,
+				  original_tree, index_tree, &merge_result);
+
+	if (merge_result.clean < 0) {
+		ret = error(_("merge failed while applying fixup"));
+		goto out;
+	}
+
+	if (!merge_result.clean) {
+		ret = error(_("fixup would produce conflicts; aborting"));
+		goto out;
+	}
+
+	ret = commit_became_empty(repo, original, merge_result.tree);
+	if (ret < 0)
+		goto out;
+	if (ret > 0) {
+		switch (empty) {
+		case REPLAY_EMPTY_COMMIT_DROP:
+			/*
+			 * Drop the target commit by replaying its descendants
+			 * directly onto its parent.
+			 */
+			rewritten = original->parents ? original->parents->item : NULL;
+
+			/*
+			 * TODO: we don't yet have the ability to drop root
+			 * commits, but there's ultimately no good reason for
+			 * this restriction to exist other than a technical
+			 * limitation.
+			 */
+			if (!rewritten) {
+				ret = error(_("cannot drop root commit %s: "
+					      "it has no parent to replay onto"),
+					    argv[0]);
+				goto out;
+			}
+
+			skip_commit = true;
+			break;
+		case REPLAY_EMPTY_COMMIT_KEEP:
+			/* Proceed and record the empty commit. */
+			break;
+		case REPLAY_EMPTY_COMMIT_ABORT:
+			ret = error(_("fixup makes commit %s empty"), argv[0]);
+			goto out;
+		}
+	}
+
+	ret = setup_revwalk(repo, action, original, &revs);
+	if (ret)
+		goto out;
+
+	if (!skip_commit) {
+		ret = commit_tree_ext(repo, "fixup", original, original->parents,
+				      &original_tree->object.oid, &merge_result.tree->object.oid,
+				      &rewritten, flags);
+		if (ret < 0) {
+			ret = error(_("failed writing fixed-up commit"));
+			goto out;
+		}
+	}
+
+	strbuf_addf(&reflog_msg, "fixup: updating %s", argv[0]);
+
+	ret = handle_reference_updates(&revs, action, original, rewritten,
+				       reflog_msg.buf, dry_run, empty);
+	if (ret < 0) {
+		ret = error(_("failed replaying descendants"));
+		goto out;
+	}
+
+	ret = 0;
+
+out:
+	merge_finalize(&merge_opts, &merge_result);
+	strbuf_release(&reflog_msg);
+	release_revisions(&revs);
+	return ret;
+}
+
 static int cmd_history_reword(int argc,
 			      const char **argv,
 			      const char *prefix,
@@ -487,7 +723,7 @@ static int cmd_history_reword(int argc,
 	strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
 
 	ret = handle_reference_updates(&revs, action, original, rewritten,
-				       reflog_msg.buf, dry_run);
+				       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
 	if (ret < 0) {
 		ret = error(_("failed replaying descendants"));
 		goto out;
@@ -724,7 +960,7 @@ static int cmd_history_split(int argc,
 	strbuf_addf(&reflog_msg, "split: updating %s", argv[0]);
 
 	ret = handle_reference_updates(&revs, action, original, rewritten,
-				       reflog_msg.buf, dry_run);
+				       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
 	if (ret < 0) {
 		ret = error(_("failed replaying descendants"));
 		goto out;
@@ -745,12 +981,14 @@ int cmd_history(int argc,
 		struct repository *repo)
 {
 	const char * const usage[] = {
+		GIT_HISTORY_FIXUP_USAGE,
 		GIT_HISTORY_REWORD_USAGE,
 		GIT_HISTORY_SPLIT_USAGE,
 		NULL,
 	};
 	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
+		OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup),
 		OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
 		OPT_SUBCOMMAND("split", &fn, cmd_history_split),
 		OPT_END(),
diff --git a/t/meson.build b/t/meson.build
index 7528e5cda5..f502ad8ec9 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -397,6 +397,7 @@ integration_tests = [
   't3450-history.sh',
   't3451-history-reword.sh',
   't3452-history-split.sh',
+  't3453-history-fixup.sh',
   't3500-cherry.sh',
   't3501-revert-cherry-pick.sh',
   't3502-cherry-pick-merge.sh',
diff --git a/t/t3453-history-fixup.sh b/t/t3453-history-fixup.sh
new file mode 100755
index 0000000000..868298e248
--- /dev/null
+++ b/t/t3453-history-fixup.sh
@@ -0,0 +1,680 @@
+#!/bin/sh
+
+test_description='tests for git-history fixup subcommand'
+
+. ./test-lib.sh
+
+fixup_with_message () {
+	cat >message &&
+	write_script fake-editor.sh <<-\EOF &&
+	cp message "$1"
+	EOF
+	test_set_editor "$(pwd)"/fake-editor.sh &&
+	git history fixup --reedit-message "$@" &&
+	rm fake-editor.sh message
+}
+
+expect_changes () {
+	git log --format="%s" --numstat "$@" >actual.raw &&
+	sed '/^$/d' <actual.raw >actual &&
+	cat >expect &&
+	test_cmp expect actual
+}
+
+test_expect_success 'errors on missing commit argument' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		test_must_fail git history fixup 2>err &&
+		test_grep "command expects a single revision" err
+	)
+'
+
+test_expect_success 'errors on too many arguments' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		test_must_fail git history fixup HEAD HEAD 2>err &&
+		test_grep "command expects a single revision" err
+	)
+'
+
+test_expect_success 'errors on unknown revision' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		test_must_fail git history fixup does-not-exist 2>err &&
+		test_grep "commit cannot be found: does-not-exist" err
+	)
+'
+
+test_expect_success 'errors when nothing is staged' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		test_must_fail git history fixup HEAD 2>err &&
+		test_grep "nothing to fixup: no staged changes" err
+	)
+'
+
+test_expect_success 'errors in a bare repository' '
+	test_when_finished "rm -rf repo repo.git" &&
+	git init repo &&
+	test_commit -C repo initial &&
+	git clone --bare repo repo.git &&
+	test_must_fail git -C repo.git history fixup HEAD 2>err &&
+	test_grep "cannot run fixup in a bare repository" err
+'
+
+test_expect_success 'errors with invalid --empty= value' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	test_must_fail git -C repo history fixup --empty=bogus HEAD 2>err &&
+	test_grep "unrecognized.*--empty.*bogus" err
+'
+
+test_expect_success 'can fixup the tip commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		echo content >file.txt &&
+		git add file.txt &&
+		git commit -m "add file" &&
+
+		echo fix >>file.txt &&
+		git add file.txt &&
+
+		expect_changes <<-\EOF &&
+		add file
+		1	0	file.txt
+		initial
+		1	0	initial.t
+		EOF
+
+		git symbolic-ref HEAD >branch-expect &&
+		git history fixup HEAD &&
+		git symbolic-ref HEAD >branch-actual &&
+		test_cmp branch-expect branch-actual &&
+
+		expect_changes <<-\EOF &&
+		add file
+		2	0	file.txt
+		initial
+		1	0	initial.t
+		EOF
+
+		# Verify the fix is in the tip commit tree
+		git show HEAD:file.txt >actual &&
+		printf "content\nfix\n" >expect &&
+		test_cmp expect actual &&
+
+		git reflog >reflog &&
+		test_grep "fixup: updating HEAD" reflog
+	)
+'
+
+test_expect_success 'can fixup a commit in the middle of history' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		echo content >file.txt &&
+		git add file.txt &&
+		git commit -m "add file" &&
+		test_commit third &&
+
+		echo fix >>file.txt &&
+		git add file.txt &&
+
+		expect_changes <<-\EOF &&
+		third
+		1	0	third.t
+		add file
+		1	0	file.txt
+		first
+		1	0	first.t
+		EOF
+
+		git history fixup HEAD~ &&
+
+		expect_changes <<-\EOF &&
+		third
+		1	0	third.t
+		add file
+		2	0	file.txt
+		first
+		1	0	first.t
+		EOF
+
+		# Verify the fix landed in the "add file" commit.
+		git show HEAD~:file.txt >actual &&
+		printf "content\nfix\n" >expect &&
+		test_cmp expect actual &&
+
+		# And verify that the replayed commit also has the change.
+		git show HEAD:file.txt >actual &&
+		printf "content\nfix\n" >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'can fixup root commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		echo initial >root.txt &&
+		git add root.txt &&
+		git commit -m "root" &&
+		test_commit second &&
+
+		expect_changes <<-\EOF &&
+		second
+		1	0	second.t
+		root
+		1	0	root.txt
+		EOF
+
+		echo fix >>root.txt &&
+		git add root.txt &&
+		git history fixup HEAD~ &&
+
+		expect_changes <<-\EOF &&
+		second
+		1	0	second.t
+		root
+		2	0	root.txt
+		EOF
+
+		git show HEAD~:root.txt >actual &&
+		printf "initial\nfix\n" >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'preserves commit message and authorship' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		echo content >file.txt &&
+		git add file.txt &&
+		git commit --author="Original <original@example.com>" -m "original message" &&
+
+		echo fix >>file.txt &&
+		git add file.txt &&
+		git history fixup HEAD &&
+
+		# Message preserved
+		git log -1 --format="%s" >actual &&
+		echo "original message" >expect &&
+		test_cmp expect actual &&
+
+		# Authorship preserved
+		git log -1 --format="%an <%ae>" >actual &&
+		echo "Original <original@example.com>" >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'updates all descendant branches by default' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit base &&
+		git branch branch &&
+		test_commit ours &&
+		git switch branch &&
+		test_commit theirs &&
+		git switch main &&
+
+		echo fix >fix.txt &&
+		git add fix.txt &&
+		git history fixup base &&
+
+		expect_changes --branches <<-\EOF &&
+		theirs
+		1	0	theirs.t
+		ours
+		1	0	ours.t
+		base
+		1	0	base.t
+		1	0	fix.txt
+		EOF
+
+		# Both branches should have the fix in the base
+		git show main~:fix.txt >actual &&
+		echo fix >expect &&
+		test_cmp expect actual &&
+		git show branch~:fix.txt >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'can fixup commit on a different branch' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit base &&
+		git branch theirs &&
+		test_commit ours &&
+		git switch theirs &&
+		test_commit theirs &&
+
+		# Stage a change while on "theirs"
+		echo fix >fix.txt &&
+		git add fix.txt &&
+
+		# Ensure that "ours" does not change, as it does not contain
+		# the commit in question.
+		git rev-parse ours >ours-before &&
+		git history fixup theirs &&
+		git rev-parse ours >ours-after &&
+		test_cmp ours-before ours-after &&
+
+		git show HEAD:fix.txt >actual &&
+		echo fix >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '--dry-run prints ref updates without modifying repo' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit base &&
+		git branch branch &&
+		test_commit main-tip &&
+		git switch branch &&
+		test_commit branch-tip &&
+		git switch main &&
+
+		echo fix >fix.txt &&
+		git add fix.txt &&
+
+		git refs list >refs-before &&
+		git history fixup --dry-run base >updates &&
+		git refs list >refs-after &&
+		test_cmp refs-before refs-after &&
+
+		test_grep "update refs/heads/main" updates &&
+		test_grep "update refs/heads/branch" updates &&
+
+		expect_changes --branches <<-\EOF &&
+		branch-tip
+		1	0	branch-tip.t
+		main-tip
+		1	0	main-tip.t
+		base
+		1	0	base.t
+		EOF
+
+		git update-ref --stdin <updates &&
+		expect_changes --branches <<-\EOF
+		branch-tip
+		1	0	branch-tip.t
+		main-tip
+		1	0	main-tip.t
+		base
+		1	0	base.t
+		1	0	fix.txt
+		EOF
+	)
+'
+
+test_expect_success '--update-refs=head updates only HEAD' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit base &&
+		git branch branch &&
+		test_commit main-tip &&
+		git switch branch &&
+		test_commit branch-tip &&
+
+		echo fix >fix.txt &&
+		git add fix.txt &&
+
+		# Only HEAD (branch) should be updated
+		git history fixup --update-refs=head base &&
+
+		# The main branch should be unaffected.
+		expect_changes main <<-\EOF &&
+		main-tip
+		1	0	main-tip.t
+		base
+		1	0	base.t
+		EOF
+
+		# But the currently checked out branch should be modified.
+		expect_changes branch <<-\EOF
+		branch-tip
+		1	0	branch-tip.t
+		base
+		1	0	base.t
+		1	0	fix.txt
+		EOF
+	)
+'
+
+test_expect_success '--update-refs=head refuses to rewrite commits not in HEAD ancestry' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit base &&
+		git branch other &&
+		test_commit main-tip &&
+		git switch other &&
+		test_commit other-tip &&
+
+		echo fix >fix.txt &&
+		git add fix.txt &&
+
+		test_must_fail git history fixup --update-refs=head main-tip 2>err &&
+		test_grep "rewritten commit must be an ancestor of HEAD" err
+	)
+'
+
+test_expect_success 'aborts when fixup would produce conflicts' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+
+		echo "line one" >file.txt &&
+		git add file.txt &&
+		git commit -m "first" &&
+
+		echo "line two" >file.txt &&
+		git add file.txt &&
+		git commit -m "second" &&
+
+		echo "conflicting change" >file.txt &&
+		git add file.txt &&
+
+		git refs list >refs-before &&
+		test_must_fail git history fixup HEAD~ 2>err &&
+		test_grep "fixup would produce conflicts" err &&
+		git refs list >refs-after &&
+		test_cmp refs-before refs-after
+	)
+'
+
+test_expect_success '--reedit-message opens editor for the commit message' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		echo content >file.txt &&
+		git add file.txt &&
+		git commit -m "add file" &&
+
+		echo fix >>file.txt &&
+		git add file.txt &&
+
+		fixup_with_message HEAD <<-\EOF &&
+		add file with fix
+		EOF
+
+		expect_changes --branches <<-\EOF
+		add file with fix
+		2	0	file.txt
+		initial
+		1	0	initial.t
+		EOF
+	)
+'
+
+test_expect_success 'retains unstaged working tree changes after fixup' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		touch a b &&
+		git add . &&
+		git commit -m "initial commit" &&
+		echo staged >a &&
+		echo unstaged >b &&
+		git add a &&
+		git history fixup HEAD &&
+
+		# b is still modified in the worktree but not staged
+		cat >expect <<-\EOF &&
+		 M b
+		EOF
+		git status --porcelain --untracked-files=no >actual &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'index is clean after fixup when target is HEAD' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+
+		test_commit initial &&
+		echo fix >fix.txt &&
+		git add fix.txt &&
+		git history fixup HEAD &&
+
+		git status --porcelain --untracked-files=no >actual &&
+		test_must_be_empty actual
+	)
+'
+
+test_expect_success 'index is unchanged on conflict' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+
+		echo base >file.txt &&
+		git add file.txt &&
+		git commit -m base &&
+		echo change >file.txt &&
+		git add file.txt &&
+		git commit -m change &&
+
+		echo conflict >file.txt &&
+		git add file.txt &&
+
+		git diff --cached >index-before &&
+		test_must_fail git history fixup HEAD~ &&
+		git diff --cached >index-after &&
+		test_cmp index-before index-after
+	)
+'
+
+test_expect_success '--empty=drop removes target commit and replays descendants onto its parent' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+
+		test_commit first &&
+		test_commit second &&
+		test_commit third &&
+
+		git rm second.t &&
+		git history fixup --empty=drop HEAD~ &&
+
+		expect_changes <<-\EOF &&
+		third
+		1	0	third.t
+		first
+		1	0	first.t
+		EOF
+		test_must_fail git show HEAD:second.t
+	)
+'
+
+test_expect_success '--empty=drop errors out when dropping the root commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+
+		test_commit first &&
+		test_commit second &&
+
+		git rm first.t &&
+		test_must_fail git history fixup --empty=drop HEAD~ 2>err &&
+		test_grep "cannot drop root commit" err
+	)
+'
+
+test_expect_success '--empty=drop can drop the HEAD commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+
+		test_commit first &&
+		test_commit second &&
+
+		git rm second.t &&
+		git history fixup --empty=drop HEAD &&
+
+		expect_changes <<-\EOF
+		first
+		1	0	first.t
+		EOF
+	)
+'
+
+test_expect_success '--empty=drop drops empty replayed commits' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+
+		touch base remove-me &&
+		git add . &&
+		git commit -m "base" &&
+		git rm remove-me &&
+		git commit -m "remove" &&
+		touch reintroduce remove-me &&
+		git add . &&
+		git commit -m "reintroduce" &&
+
+		git rm remove-me &&
+		git history fixup --empty=drop HEAD~2 &&
+
+		expect_changes <<-\EOF
+		reintroduce
+		0	0	reintroduce
+		0	0	remove-me
+		base
+		0	0	base
+		EOF
+	)
+'
+
+test_expect_success '--empty=keep keeps commit when fixup target becomes empty' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+
+		test_commit first &&
+		test_commit second &&
+		test_commit third &&
+
+		git rm second.t &&
+		git history fixup --empty=keep HEAD~ &&
+
+		expect_changes <<-\EOF
+		third
+		1	0	third.t
+		second
+		first
+		1	0	first.t
+		EOF
+	)
+'
+
+test_expect_success '--empty=keep keeps commit when replayed commit becomes empty' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+
+		touch base remove-me &&
+		git add . &&
+		git commit -m "base" &&
+		git rm remove-me &&
+		git commit -m "remove" &&
+		touch reintroduce remove-me &&
+		git add . &&
+		git commit -m "reintroduce" &&
+
+		git rm remove-me &&
+		git history fixup --empty=keep HEAD~2 &&
+
+		expect_changes <<-\EOF
+		reintroduce
+		0	0	reintroduce
+		0	0	remove-me
+		remove
+		base
+		0	0	base
+		EOF
+	)
+'
+
+test_expect_success '--empty=abort errors out when fixup target becomes empty' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+
+		test_commit first &&
+		test_commit second &&
+
+		git rm first.t &&
+		test_must_fail git history fixup --empty=abort HEAD~ 2>err &&
+		test_grep "fixup makes commit.*empty" err
+	)
+'
+
+test_expect_success '--empty=abort errors out when a descendant becomes empty during replay' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+
+		touch base remove-me &&
+		git add . &&
+		git commit -m "base" &&
+		git rm remove-me &&
+		git commit -m "remove" &&
+		touch reintroduce remove-me &&
+		git add . &&
+		git commit -m "reintroduce" &&
+
+		git rm remove-me &&
+		test_must_fail git history fixup --empty=abort HEAD~2 2>err &&
+		test_grep "became empty after replay" err
+	)
+'
+
+test_done

-- 
2.54.0.545.g6539524ca2.dirty


^ permalink raw reply related

* [PATCH v2 2/3] builtin/history: generalize function to commit trees
From: Patrick Steinhardt @ 2026-04-23 14:21 UTC (permalink / raw)
  To: git; +Cc: Elijah Newren, D. Ben Knoble, Tian Yuchen
In-Reply-To: <20260423-b4-pks-history-fixup-v2-0-d7571c6d36eb@pks.im>

The function `commit_tree_with_edited_message_ext()` can be used to
commit a tree with a specific list of parents with an edited commit
message. This function is useful outside of editing the commit message
though, as it also performs the plumbing to extract the original commit
message and strip some headers from it.

Refactor the function to receive a flags field that allows the caller to
control whether or not the commit message should be edited, or whether
it should be retained as-is. This will be used in a subsequent commit.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/history.c | 45 ++++++++++++++++++++++++++-------------------
 1 file changed, 26 insertions(+), 19 deletions(-)

diff --git a/builtin/history.c b/builtin/history.c
index 9526938085..549e352c74 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -91,13 +91,18 @@ static int fill_commit_message(struct repository *repo,
 	return 0;
 }
 
-static int commit_tree_with_edited_message_ext(struct repository *repo,
-					       const char *action,
-					       struct commit *commit_with_message,
-					       const struct commit_list *parents,
-					       const struct object_id *old_tree,
-					       const struct object_id *new_tree,
-					       struct commit **out)
+enum commit_tree_flags {
+	COMMIT_TREE_EDIT_MESSAGE = (1 << 0),
+};
+
+static int commit_tree_ext(struct repository *repo,
+			   const char *action,
+			   struct commit *commit_with_message,
+			   const struct commit_list *parents,
+			   const struct object_id *old_tree,
+			   const struct object_id *new_tree,
+			   struct commit **out,
+			   enum commit_tree_flags flags)
 {
 	const char *exclude_gpgsig[] = {
 		/* We reencode the message, so the encoding needs to be stripped. */
@@ -122,10 +127,14 @@ static int commit_tree_with_edited_message_ext(struct repository *repo,
 		original_author = xmemdupz(ptr, len);
 	find_commit_subject(original_message, &original_body);
 
-	ret = fill_commit_message(repo, old_tree, new_tree,
-				  original_body, action, &commit_message);
-	if (ret < 0)
-		goto out;
+	if (flags & COMMIT_TREE_EDIT_MESSAGE) {
+		ret = fill_commit_message(repo, old_tree, new_tree,
+					  original_body, action, &commit_message);
+		if (ret < 0)
+			goto out;
+	} else {
+		strbuf_addstr(&commit_message, original_body);
+	}
 
 	original_extra_headers = read_commit_extra_headers(commit_with_message,
 							   exclude_gpgsig);
@@ -168,8 +177,8 @@ static int commit_tree_with_edited_message(struct repository *repo,
 		oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
 	}
 
-	return commit_tree_with_edited_message_ext(repo, action, original, original->parents,
-						   &parent_tree_oid, tree_oid, out);
+	return commit_tree_ext(repo, action, original, original->parents,
+			       &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
 }
 
 enum ref_action {
@@ -616,9 +625,8 @@ static int split_commit(struct repository *repo,
 	 * The first commit is constructed from the split-out tree. The base
 	 * that shall be diffed against is the parent of the original commit.
 	 */
-	ret = commit_tree_with_edited_message_ext(repo, "split-out", original,
-						  original->parents, &parent_tree_oid,
-						  &split_tree->object.oid, &first_commit);
+	ret = commit_tree_ext(repo, "split-out", original, original->parents, &parent_tree_oid,
+			      &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
 	if (ret < 0) {
 		ret = error(_("failed writing first commit"));
 		goto out;
@@ -634,9 +642,8 @@ static int split_commit(struct repository *repo,
 	old_tree_oid = &repo_get_commit_tree(repo, first_commit)->object.oid;
 	new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
 
-	ret = commit_tree_with_edited_message_ext(repo, "split-out", original,
-						  parents, old_tree_oid,
-						  new_tree_oid, &second_commit);
+	ret = commit_tree_ext(repo, "split-out", original, parents, old_tree_oid,
+			      new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);
 	if (ret < 0) {
 		ret = error(_("failed writing second commit"));
 		goto out;

-- 
2.54.0.545.g6539524ca2.dirty


^ permalink raw reply related

* [PATCH v2 1/3] replay: allow callers to control what happens with empty commits
From: Patrick Steinhardt @ 2026-04-23 14:21 UTC (permalink / raw)
  To: git; +Cc: Elijah Newren, D. Ben Knoble, Tian Yuchen
In-Reply-To: <20260423-b4-pks-history-fixup-v2-0-d7571c6d36eb@pks.im>

When replaying commits it may happen that some of the commits become
empty relative to their parent. Such commits are for now automatically
dropped by the replay subsystem without much control from the user.

Introduce a new enum that allows the caller to drop, keep or abort in
this case.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 replay.c | 29 ++++++++++++++++++++++++-----
 replay.h | 19 +++++++++++++++++++
 2 files changed, 43 insertions(+), 5 deletions(-)

diff --git a/replay.c b/replay.c
index f96f1f6551..4ef8abb607 100644
--- a/replay.c
+++ b/replay.c
@@ -269,7 +269,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
 					  struct commit *onto,
 					  struct merge_options *merge_opt,
 					  struct merge_result *result,
-					  enum replay_mode mode)
+					  enum replay_mode mode,
+					  enum replay_empty_commit_action empty)
 {
 	struct commit *base, *replayed_base;
 	struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -321,12 +322,25 @@ static struct commit *pick_regular_commit(struct repository *repo,
 	}
 	merge_opt->ancestor = NULL;
 	merge_opt->branch2 = NULL;
+
 	if (!result->clean)
 		return NULL;
-	/* Drop commits that become empty */
+
+	/* Handle commits that become empty */
 	if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
-	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid))
-		return replayed_base;
+	    !oideq(&pickme_tree->object.oid, &base_tree->object.oid)) {
+		switch (empty) {
+		case REPLAY_EMPTY_COMMIT_DROP:
+			return replayed_base;
+		case REPLAY_EMPTY_COMMIT_KEEP:
+			break;
+		case REPLAY_EMPTY_COMMIT_ABORT:
+			result->clean = error(_("commit %s became empty after replay"),
+					      oid_to_hex(&pickme->object.oid));
+			return NULL;
+		}
+	}
+
 	return create_commit(repo, result->tree, pickme, replayed_base, mode);
 }
 
@@ -417,7 +431,7 @@ int replay_revisions(struct rev_info *revs,
 
 		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
 						  mode == REPLAY_MODE_REVERT ? last_commit : onto,
-						  &merge_opt, &result, mode);
+						  &merge_opt, &result, mode, opts->empty);
 		if (!last_commit)
 			break;
 
@@ -458,6 +472,11 @@ int replay_revisions(struct rev_info *revs,
 		}
 	}
 
+	if (result.clean < 0) {
+		ret = -1;
+		goto out;
+	}
+
 	if (!result.clean) {
 		ret = 1;
 		goto out;
diff --git a/replay.h b/replay.h
index 0ab74b9805..1851a07705 100644
--- a/replay.h
+++ b/replay.h
@@ -6,6 +6,19 @@
 struct repository;
 struct rev_info;
 
+/*
+ * Controls what happens when a replayed commit becomes empty (i.e. its tree
+ * is identical to its parent's tree after the replay).
+ */
+enum replay_empty_commit_action {
+	/* Silently discard the empty commit. */
+	REPLAY_EMPTY_COMMIT_DROP,
+	/* Keep the empty commit as-is. */
+	REPLAY_EMPTY_COMMIT_KEEP,
+	/* Abort with an error. */
+	REPLAY_EMPTY_COMMIT_ABORT,
+};
+
 /*
  * A set of options that can be passed to `replay_revisions()`.
  */
@@ -43,6 +56,12 @@ struct replay_revisions_options {
 	 * Requires `onto` to be set.
 	 */
 	int contained;
+
+	/*
+	 * Controls what to do when a replayed commit becomes empty.
+	 * Defaults to REPLAY_EMPTY_COMMIT_DROP.
+	 */
+	enum replay_empty_commit_action empty;
 };
 
 /* This struct is used as an out-parameter by `replay_revisions()`. */

-- 
2.54.0.545.g6539524ca2.dirty


^ permalink raw reply related

* [PATCH v2 0/3] builtin/history: introduce "fixup" subcommand
From: Patrick Steinhardt @ 2026-04-23 14:21 UTC (permalink / raw)
  To: git; +Cc: Elijah Newren, D. Ben Knoble, Tian Yuchen
In-Reply-To: <20260422-b4-pks-history-fixup-v1-0-48d4484243de@pks.im>

Hi,

this short patch series introduces a new "fixup" subcommand. This
command is the first one that I felt is missing in my day to day work,
as I end up doing fixup commits quite often.

The flow is rather simple: the user stages some changes, and then they
execute `git history fixup <commit>` to amend those changes to the given
commit. As with the other subcommands, dependent branches will then be
rebased automatically.

This is the first command that may result in merge conflicts. For now we
simply abort in such cases, but there are plans to introduce first-class
conflicts into Git. So once we have them, we'll also be able to handle
such cases more gracefully. I still think that the command is useful
even without that conflict handling.

Changes in v2:
  - Introduce "--empty=(keep|drop|abort)" to specify what happens with
    empty commits.
  - Adapt documentation a bit to hopefully clarify how changes are
    backported.
  - Link to v1: https://patch.msgid.link/20260422-b4-pks-history-fixup-v1-0-48d4484243de@pks.im

Thanks!

Patrick

---
Patrick Steinhardt (3):
      replay: allow callers to control what happens with empty commits
      builtin/history: generalize function to commit trees
      builtin/history: introduce "fixup" subcommand

 Documentation/git-history.adoc |  77 ++++-
 builtin/history.c              | 291 ++++++++++++++++--
 replay.c                       |  29 +-
 replay.h                       |  19 ++
 t/meson.build                  |   1 +
 t/t3453-history-fixup.sh       | 680 +++++++++++++++++++++++++++++++++++++++++
 6 files changed, 1067 insertions(+), 30 deletions(-)

Range-diff versus v1:

-:  ---------- > 1:  79b53c5c27 replay: allow callers to control what happens with empty commits
1:  3bbe1f8b98 = 2:  79573cb5bf builtin/history: generalize function to commit trees
2:  44f22df21e ! 3:  afdfd49f96 builtin/history: introduce "fixup" subcommand
    @@ Documentation/git-history.adoc: git-history - EXPERIMENTAL: Rewrite history
      SYNOPSIS
      --------
      [synopsis]
    -+git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message]
    ++git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
      git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
      git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
      
    @@ Documentation/git-history.adoc: THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY C
      * linkgit:git-history[1] does not execute any linkgit:githooks[5] at the
        current point in time. This may change in the future.
      * linkgit:git-history[1] by default updates all branches that are descendants
    -@@ Documentation/git-history.adoc: COMMANDS
    +@@ Documentation/git-history.adoc: conflicts. This limitation is by design as history rewrites are not intended to
    + be stateful operations. The limitation can be lifted once (if) Git learns about
    + first-class conflicts.
    + 
    ++When using `fixup` with `--empty=drop`, dropping the root commit is not yet
    ++supported.
    ++
    + COMMANDS
    + --------
      
      The following commands are available to rewrite history in different ways:
      
     +`fixup <commit>`::
    -+	Apply the currently staged changes to the specified commit. The staged
    -+	changes are incorporated into the target commit's tree via a three-way
    -+	merge, using HEAD's tree as the merge base, which is equivalent to
    -+	linkgit:git-cherry-pick[1].
    ++	Apply the currently staged changes to the specified commit. This
    ++	is done by performing a three-way merge between the HEAD commit,
    ++	the target commit and the tree generated from staged changes.
    ++	This is using the same logic as linkgit:git-cherry-pick[1].
     ++
     +The commit message and authorship of the target commit are preserved by
     +default, unless you specify `--reedit-message`.
    @@ Documentation/git-history.adoc: OPTIONS
      
     +`--reedit-message`::
     +	Open an editor to modify the target commit's message.
    ++
    ++`--empty=(drop|keep|abort)`::
    ++	Control what happens when a commit becomes empty as a result of the
    ++	fixup. This can happen in two situations:
    +++
    ++--
    ++* The fixup target itself becomes empty because the staged changes exactly
    ++  cancel out all changes introduced by that commit.
    ++
    ++* A descendant commit becomes empty during replay because it introduced the
    ++  same change that was just fixed up into an ancestor.
    ++--
    +++
    ++With `drop` (the default), empty commits are removed from the rewritten
    ++history. Descendants of a dropped target commit are replayed directly onto
    ++the target's parent. Note that dropping the root commit is not supported;
    ++see LIMITATIONS.
    +++
    ++With `keep`, empty commits are retained in the rewritten history as-is.
    +++
    ++With `abort`, the command stops with an error if any commit would become
    ++empty.
     +
      `--update-refs=(branches|head)`::
      	Control which references will be updated by the command, if any. With
    @@ builtin/history.c
      #include "wt-status.h"
      
     +#define GIT_HISTORY_FIXUP_USAGE \
    -+	N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message]")
    ++	N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
      #define GIT_HISTORY_REWORD_USAGE \
      	N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
      #define GIT_HISTORY_SPLIT_USAGE \
    +@@ builtin/history.c: static int handle_reference_updates(struct rev_info *revs,
    + 				    struct commit *original,
    + 				    struct commit *rewritten,
    + 				    const char *reflog_msg,
    +-				    int dry_run)
    ++				    int dry_run,
    ++				    enum replay_empty_commit_action empty)
    + {
    + 	const struct name_decoration *decoration;
    +-	struct replay_revisions_options opts = { 0 };
    ++	struct replay_revisions_options opts = {
    ++		.empty = empty,
    ++	};
    + 	struct replay_result result = { 0 };
    + 	struct ref_transaction *transaction = NULL;
    + 	struct strbuf err = STRBUF_INIT;
     @@ builtin/history.c: static int handle_reference_updates(struct rev_info *revs,
      	return ret;
      }
      
    ++static int commit_became_empty(struct repository *repo,
    ++			       struct commit *original,
    ++			       struct tree *result)
    ++{
    ++	struct commit *parent = original->parents ? original->parents->item : NULL;
    ++	struct object_id parent_tree_oid;
    ++
    ++	if (parent) {
    ++		if (repo_parse_commit(repo, parent))
    ++			return error(_("unable to parse parent of %s"),
    ++				     oid_to_hex(&original->object.oid));
    ++
    ++		parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
    ++	} else {
    ++		oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
    ++	}
    ++
    ++	return oideq(&result->object.oid, &parent_tree_oid);
    ++}
    ++
    ++static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
    ++{
    ++	enum replay_empty_commit_action *value = opt->value;
    ++
    ++	BUG_ON_OPT_NEG(unset);
    ++
    ++	if (!strcmp(arg, "drop"))
    ++		*value = REPLAY_EMPTY_COMMIT_DROP;
    ++	else if (!strcmp(arg, "keep"))
    ++		*value = REPLAY_EMPTY_COMMIT_KEEP;
    ++	else if (!strcmp(arg, "abort"))
    ++		*value = REPLAY_EMPTY_COMMIT_ABORT;
    ++	else
    ++		die(_("unrecognized '--empty=' action '%s'; "
    ++		      "valid values are \"drop\", \"keep\", and \"abort\"."), arg);
    ++
    ++	return 0;
    ++}
    ++
     +static int cmd_history_fixup(int argc,
     +			     const char **argv,
     +			     const char *prefix,
    @@ builtin/history.c: static int handle_reference_updates(struct rev_info *revs,
     +		GIT_HISTORY_FIXUP_USAGE,
     +		NULL,
     +	};
    ++	enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
     +	enum ref_action action = REF_ACTION_DEFAULT;
    -+	int dry_run = 0;
     +	enum commit_tree_flags flags = 0;
    ++	int dry_run = 0;
     +	struct option options[] = {
     +		OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
     +			       N_("control which refs should be updated"),
    @@ builtin/history.c: static int handle_reference_updates(struct rev_info *revs,
     +		OPT_BIT(0, "reedit-message", &flags,
     +			N_("open an editor to modify the commit message"),
     +			COMMIT_TREE_EDIT_MESSAGE),
    ++		OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
    ++			       N_("how to handle commits that become empty"),
    ++			       PARSE_OPT_NONEG, parse_opt_empty),
     +		OPT_END(),
     +	};
     +	struct merge_result merge_result = { 0 };
    @@ builtin/history.c: static int handle_reference_updates(struct rev_info *revs,
     +	struct commit *head_commit, *original, *rewritten;
     +	struct tree *head_tree, *original_tree, *index_tree;
     +	struct rev_info revs = { 0 };
    ++	bool skip_commit = false;
     +	int ret;
     +
     +	argc = parse_options(argc, argv, prefix, options, usage, 0);
    @@ builtin/history.c: static int handle_reference_updates(struct rev_info *revs,
     +		goto out;
     +	}
     +
    ++	ret = commit_became_empty(repo, original, merge_result.tree);
    ++	if (ret < 0)
    ++		goto out;
    ++	if (ret > 0) {
    ++		switch (empty) {
    ++		case REPLAY_EMPTY_COMMIT_DROP:
    ++			/*
    ++			 * Drop the target commit by replaying its descendants
    ++			 * directly onto its parent.
    ++			 */
    ++			rewritten = original->parents ? original->parents->item : NULL;
    ++
    ++			/*
    ++			 * TODO: we don't yet have the ability to drop root
    ++			 * commits, but there's ultimately no good reason for
    ++			 * this restriction to exist other than a technical
    ++			 * limitation.
    ++			 */
    ++			if (!rewritten) {
    ++				ret = error(_("cannot drop root commit %s: "
    ++					      "it has no parent to replay onto"),
    ++					    argv[0]);
    ++				goto out;
    ++			}
    ++
    ++			skip_commit = true;
    ++			break;
    ++		case REPLAY_EMPTY_COMMIT_KEEP:
    ++			/* Proceed and record the empty commit. */
    ++			break;
    ++		case REPLAY_EMPTY_COMMIT_ABORT:
    ++			ret = error(_("fixup makes commit %s empty"), argv[0]);
    ++			goto out;
    ++		}
    ++	}
    ++
     +	ret = setup_revwalk(repo, action, original, &revs);
     +	if (ret)
     +		goto out;
     +
    -+	ret = commit_tree_ext(repo, "fixup", original, original->parents,
    -+			      &original_tree->object.oid, &merge_result.tree->object.oid,
    -+			      &rewritten, flags);
    -+	if (ret < 0) {
    -+		ret = error(_("failed writing fixed-up commit"));
    -+		goto out;
    ++	if (!skip_commit) {
    ++		ret = commit_tree_ext(repo, "fixup", original, original->parents,
    ++				      &original_tree->object.oid, &merge_result.tree->object.oid,
    ++				      &rewritten, flags);
    ++		if (ret < 0) {
    ++			ret = error(_("failed writing fixed-up commit"));
    ++			goto out;
    ++		}
     +	}
     +
     +	strbuf_addf(&reflog_msg, "fixup: updating %s", argv[0]);
     +
     +	ret = handle_reference_updates(&revs, action, original, rewritten,
    -+				       reflog_msg.buf, dry_run);
    ++				       reflog_msg.buf, dry_run, empty);
     +	if (ret < 0) {
     +		ret = error(_("failed replaying descendants"));
     +		goto out;
    @@ builtin/history.c: static int handle_reference_updates(struct rev_info *revs,
      static int cmd_history_reword(int argc,
      			      const char **argv,
      			      const char *prefix,
    +@@ builtin/history.c: static int cmd_history_reword(int argc,
    + 	strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
    + 
    + 	ret = handle_reference_updates(&revs, action, original, rewritten,
    +-				       reflog_msg.buf, dry_run);
    ++				       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
    + 	if (ret < 0) {
    + 		ret = error(_("failed replaying descendants"));
    + 		goto out;
    +@@ builtin/history.c: static int cmd_history_split(int argc,
    + 	strbuf_addf(&reflog_msg, "split: updating %s", argv[0]);
    + 
    + 	ret = handle_reference_updates(&revs, action, original, rewritten,
    +-				       reflog_msg.buf, dry_run);
    ++				       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
    + 	if (ret < 0) {
    + 		ret = error(_("failed replaying descendants"));
    + 		goto out;
     @@ builtin/history.c: int cmd_history(int argc,
      		struct repository *repo)
      {
    @@ t/t3453-history-fixup.sh (new)
     +	test_grep "cannot run fixup in a bare repository" err
     +'
     +
    ++test_expect_success 'errors with invalid --empty= value' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo &&
    ++	test_must_fail git -C repo history fixup --empty=bogus HEAD 2>err &&
    ++	test_grep "unrecognized.*--empty.*bogus" err
    ++'
    ++
     +test_expect_success 'can fixup the tip commit' '
     +	test_when_finished "rm -rf repo" &&
     +	git init repo &&
    @@ t/t3453-history-fixup.sh (new)
     +	)
     +'
     +
    ++test_expect_success '--empty=drop removes target commit and replays descendants onto its parent' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo --initial-branch=main &&
    ++	(
    ++		cd repo &&
    ++
    ++		test_commit first &&
    ++		test_commit second &&
    ++		test_commit third &&
    ++
    ++		git rm second.t &&
    ++		git history fixup --empty=drop HEAD~ &&
    ++
    ++		expect_changes <<-\EOF &&
    ++		third
    ++		1	0	third.t
    ++		first
    ++		1	0	first.t
    ++		EOF
    ++		test_must_fail git show HEAD:second.t
    ++	)
    ++'
    ++
    ++test_expect_success '--empty=drop errors out when dropping the root commit' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo &&
    ++	(
    ++		cd repo &&
    ++
    ++		test_commit first &&
    ++		test_commit second &&
    ++
    ++		git rm first.t &&
    ++		test_must_fail git history fixup --empty=drop HEAD~ 2>err &&
    ++		test_grep "cannot drop root commit" err
    ++	)
    ++'
    ++
    ++test_expect_success '--empty=drop can drop the HEAD commit' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo &&
    ++	(
    ++		cd repo &&
    ++
    ++		test_commit first &&
    ++		test_commit second &&
    ++
    ++		git rm second.t &&
    ++		git history fixup --empty=drop HEAD &&
    ++
    ++		expect_changes <<-\EOF
    ++		first
    ++		1	0	first.t
    ++		EOF
    ++	)
    ++'
    ++
    ++test_expect_success '--empty=drop drops empty replayed commits' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo &&
    ++	(
    ++		cd repo &&
    ++
    ++		touch base remove-me &&
    ++		git add . &&
    ++		git commit -m "base" &&
    ++		git rm remove-me &&
    ++		git commit -m "remove" &&
    ++		touch reintroduce remove-me &&
    ++		git add . &&
    ++		git commit -m "reintroduce" &&
    ++
    ++		git rm remove-me &&
    ++		git history fixup --empty=drop HEAD~2 &&
    ++
    ++		expect_changes <<-\EOF
    ++		reintroduce
    ++		0	0	reintroduce
    ++		0	0	remove-me
    ++		base
    ++		0	0	base
    ++		EOF
    ++	)
    ++'
    ++
    ++test_expect_success '--empty=keep keeps commit when fixup target becomes empty' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo &&
    ++	(
    ++		cd repo &&
    ++
    ++		test_commit first &&
    ++		test_commit second &&
    ++		test_commit third &&
    ++
    ++		git rm second.t &&
    ++		git history fixup --empty=keep HEAD~ &&
    ++
    ++		expect_changes <<-\EOF
    ++		third
    ++		1	0	third.t
    ++		second
    ++		first
    ++		1	0	first.t
    ++		EOF
    ++	)
    ++'
    ++
    ++test_expect_success '--empty=keep keeps commit when replayed commit becomes empty' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo &&
    ++	(
    ++		cd repo &&
    ++
    ++		touch base remove-me &&
    ++		git add . &&
    ++		git commit -m "base" &&
    ++		git rm remove-me &&
    ++		git commit -m "remove" &&
    ++		touch reintroduce remove-me &&
    ++		git add . &&
    ++		git commit -m "reintroduce" &&
    ++
    ++		git rm remove-me &&
    ++		git history fixup --empty=keep HEAD~2 &&
    ++
    ++		expect_changes <<-\EOF
    ++		reintroduce
    ++		0	0	reintroduce
    ++		0	0	remove-me
    ++		remove
    ++		base
    ++		0	0	base
    ++		EOF
    ++	)
    ++'
    ++
    ++test_expect_success '--empty=abort errors out when fixup target becomes empty' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo &&
    ++	(
    ++		cd repo &&
    ++
    ++		test_commit first &&
    ++		test_commit second &&
    ++
    ++		git rm first.t &&
    ++		test_must_fail git history fixup --empty=abort HEAD~ 2>err &&
    ++		test_grep "fixup makes commit.*empty" err
    ++	)
    ++'
    ++
    ++test_expect_success '--empty=abort errors out when a descendant becomes empty during replay' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo --initial-branch=main &&
    ++	(
    ++		cd repo &&
    ++
    ++		touch base remove-me &&
    ++		git add . &&
    ++		git commit -m "base" &&
    ++		git rm remove-me &&
    ++		git commit -m "remove" &&
    ++		touch reintroduce remove-me &&
    ++		git add . &&
    ++		git commit -m "reintroduce" &&
    ++
    ++		git rm remove-me &&
    ++		test_must_fail git history fixup --empty=abort HEAD~2 2>err &&
    ++		test_grep "became empty after replay" err
    ++	)
    ++'
    ++
     +test_done

---
base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
change-id: 20260422-b4-pks-history-fixup-be27e0c4a03e


^ permalink raw reply

* en/backfill-fixes-and-edges (was Re: What's cooking in git.git (Apr 2026, #08))
From: Derrick Stolee @ 2026-04-23 13:41 UTC (permalink / raw)
  To: Junio C Hamano, git, Elijah Newren
In-Reply-To: <xmqqv7dix8pi.fsf@gitster.g>

On 4/23/2026 6:38 AM, Junio C Hamano wrote:

> * en/backfill-fixes-and-edges (2026-04-15) 3 commits
>  - backfill: default to grabbing edge blobs too
>  - backfill: document acceptance of revision-range in more standard manner
>  - backfill: reject rev-list arguments that do not make sense
> 
>  The 'git backfill' command now rejects revision-limiting options that
>  are incompatible with its operation, uses standard documentation for
>  revision ranges, and includes blobs from boundary commits by default
>  to improve performance of subsequent operations.
> 
>  Needs review.
>  source: <pull.2088.git.1776297482.gitgitgadget@gmail.com>
I carefully reviewed these patches and think they look good
to go in version 1. Maybe you need a second opinion?

Thanks,
-Stolee


^ permalink raw reply

* [PATCH] push: add push.showProgress config option
From: Harald Nordgren via GitGitGadget @ 2026-04-23 12:30 UTC (permalink / raw)
  To: git; +Cc: Harald Nordgren, Harald Nordgren

From: Harald Nordgren <haraldnordgren@gmail.com>

Add a `push.showProgress` boolean config that sets the default for
progress reporting during `git push`. Setting it to `false` suppresses
the pack-objects progress output (Enumerating/Counting/Compressing/
Writing objects) without silencing the ref update summary line the
way `--quiet` does. An explicit `--progress` or `--no-progress` on the
command line still overrides the config.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
    push: add showProgress config option
    
    The amount of output shown for each push is excessive in my opinion.
    
    It can be silenced by '-q', but this has the bad side-effect that the
    success message is not shown. '--no-progress' exists, so would make
    sense to allow this to be always be turned on.
    
    Enumerating objects: 17, done.
    Counting objects: 100% (17/17), done.
    Delta compression using up to 8 threads
    Compressing objects: 100% (9/9), done.
    Writing objects: 100% (9/9), 1.32 KiB | 1.32 MiB/s, done.
    Total 9 (delta 8), reused 0 (delta 0), pack-reused 0 (from 0)
    remote: Resolving deltas: 100% (8/8), completed with 8 local objects.
    To github.com:HaraldNordgren/git.git
     + 3b9fc3aac6...6d326b0098 push-use-progress-config -> push-use-progress-config (forced update)
    

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2279%2FHaraldNordgren%2Fpush-use-progress-config-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2279/HaraldNordgren/push-use-progress-config-v1
Pull-Request: https://github.com/git/git/pull/2279

 Documentation/config/push.adoc |  8 ++++++++
 builtin/push.c                 |  3 +++
 t/t5523-push-upstream.sh       | 23 +++++++++++++++++++++++
 3 files changed, 34 insertions(+)

diff --git a/Documentation/config/push.adoc b/Documentation/config/push.adoc
index d9112b2260..92f22c8ec3 100644
--- a/Documentation/config/push.adoc
+++ b/Documentation/config/push.adoc
@@ -137,3 +137,11 @@ This will result in only b (a and c are cleared).
 	If set to `false`, disable use of bitmaps for `git push` even if
 	`pack.useBitmaps` is `true`, without preventing other git operations
 	from using bitmaps. Default is `true`.
+
+`push.showProgress`::
+	If set to `false`, suppress progress reporting during `git push`,
+	equivalent to passing `--no-progress` on the command line. If set
+	to `true`, force progress reporting, equivalent to `--progress`.
+	If unset, progress is reported when standard error is connected to
+	a terminal. An explicit `--progress` or `--no-progress` on the
+	command line overrides this configuration.
diff --git a/builtin/push.c b/builtin/push.c
index 7100ffba5d..d35f816740 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -539,6 +539,9 @@ static int git_push_config(const char *k, const char *v,
 		else
 			*flags &= ~TRANSPORT_PUSH_FORCE_IF_INCLUDES;
 		return 0;
+	} else if (!strcmp(k, "push.showprogress")) {
+		progress = git_config_bool(k, v);
+		return 0;
 	}
 
 	return git_default_config(k, v, ctx, NULL);
diff --git a/t/t5523-push-upstream.sh b/t/t5523-push-upstream.sh
index 22d3e1162c..27aa87ee01 100755
--- a/t/t5523-push-upstream.sh
+++ b/t/t5523-push-upstream.sh
@@ -120,6 +120,29 @@ test_expect_success TTY 'push --no-progress suppresses progress' '
 	test_grep ! "Writing objects" err
 '
 
+test_expect_success TTY 'push.showProgress=false suppresses progress' '
+	ensure_fresh_upstream &&
+
+	test_terminal git -c push.showProgress=false push -u upstream main \
+		>out 2>err &&
+	test_grep ! "Writing objects" err
+'
+
+test_expect_success 'push.showProgress=true forces progress on non-tty' '
+	ensure_fresh_upstream &&
+
+	git -c push.showProgress=true push -u upstream main >out 2>err &&
+	test_grep "Writing objects" err
+'
+
+test_expect_success TTY '--progress overrides push.showProgress=false' '
+	ensure_fresh_upstream &&
+
+	test_terminal git -c push.showProgress=false push -u --progress \
+		upstream main >out 2>err &&
+	test_grep "Writing objects" err
+'
+
 test_expect_success TTY 'quiet push' '
 	ensure_fresh_upstream &&
 

base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
-- 
gitgitgadget

^ permalink raw reply related

* What's cooking in git.git (Apr 2026, #08)
From: Junio C Hamano @ 2026-04-23 10:38 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).

Git 2.54 has been released, and the tip of 'next' has acquired a
handful more topics, but no update to 'master' yet to kick off the
new cycle has happened (yet).  I am still mostly offline for a
couple of weeks, but just had a chance to pick up updates to a
handful of topics.

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/

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

* en/ort-cached-rename-with-trivial-resolution (2026-04-20) 1 commit
 - merge-ort: handle cached rename & trivial resolution interaction better

 "ort" merge backend improvements.

 Will merge to 'next'.
 source: <pull.2095.git.1776724214171.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>


* jk/revert-aa-reap-transport-child-processes (2026-04-22) 1 commit
 - Revert "transport-helper, connect: use clean_on_exit to reap children on abnormal exit"

 Revert a recent change that introduced a regression to help mksh users.

 Will merge to 'next'.
 source: <20260422230020.GA1839627@coredump.intra.peff.net>


* js/ci-github-actions-update (2026-04-21) 4 commits
 - ci: bump actions/checkout from v5 to v6
 - ci: bump actions/github-script from v8 to v9
 - ci: bump actions/{upload,download}-artifact to v7 and v8
 - ci: bump microsoft/setup-msbuild from v2 to v3

 Update various GitHub Actions versions.

 Will merge to 'next'.
 source: <pull.2097.git.1776775319.gitgitgadget@gmail.com>


* mf/format-patch-cover-letter-format-docfix (2026-04-22) 1 commit
 - Fix docs for format.commitListFormat

 Docfix.

 Will merge to 'next'.
 source: <576d29f15e016889e02c253713656cd8cbf1f04c.1776894255.git.mroik@delayed.space>


* sg/t6112-unwanted-tilde-expansion-fix (2026-04-21) 1 commit
 - t6112: avoid tilde expansion

 Test fix.

 Will merge to 'next'?
 source: <20260421192132.51172-1-szeder.dev@gmail.com>

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

* pw/status-rebase-todo (2026-04-20) 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.1776697483.git.phillip.wood@dunelm.org.uk>


* sb/userdiff-lisp-family (2026-04-14) 2 commits
  (merged to 'next' on 2026-04-20 at 5897c04899)
 + userdiff: extend Scheme support to cover other Lisp dialects
 + userdiff: tighten word-diff test case of the scheme driver

 The userdiff driver for the Scheme language has been extended to
 cover other Lisp dialects.

 Will merge to 'master'.
 source: <pull.2000.v4.git.1776220063.gitgitgadget@gmail.com>


* ss/t7004-unhide-git-failures (2026-04-20) 3 commits
 - t7004: avoid subshells to capture git exit codes
 - t7004: dynamically grab expected state in tests
 - t7004: drop hardcoded tag count for state verification

 Test clean-up.

 Will merge to 'next'.
 cf. <aecNc-BNwaqFlg5c@pks.im>
 source: <20260421053334.5414-1-r.siddharth.shrimali@gmail.com>


* tb/pseudo-merge-bugfixes (2026-04-21) 9 commits
 - 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

 Fixes many bugs in pseudo-merge code.

 Expecting (hopefully minor and final) reroll.
 cf. <CABPp-BGkfavqezk2SV3+K6iF8MLm8j_=ijHiPDLmv_U_o_Ykgg@mail.gmail.com>
 source: <cover.1776801694.git.me@ttaylorr.com>


* ds/fetch-negotiation-options (2026-04-22) 7 commits
 - send-pack: pass negotiation config in push
 - remote: add remote.*.negotiationInclude config
 - fetch: add --negotiation-include option for negotiation
 - 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.

 Needs review.
 source: <pull.2085.v3.git.1776871546.gitgitgadget@gmail.com>


* en/backfill-fixes-and-edges (2026-04-15) 3 commits
 - backfill: default to grabbing edge blobs too
 - backfill: document acceptance of revision-range in more standard manner
 - backfill: reject rev-list arguments that do not make sense

 The 'git backfill' command now rejects revision-limiting options that
 are incompatible with its operation, uses standard documentation for
 revision ranges, and includes blobs from boundary commits by default
 to improve performance of subsequent operations.

 Needs review.
 source: <pull.2088.git.1776297482.gitgitgadget@gmail.com>


* mc/http-emptyauth-negotiate-fix (2026-04-16) 3 commits
  (merged to 'next' on 2026-04-20 at 6539524ca2)
 + t5563: add tests for http.emptyAuth with Negotiate
 + http: attempt Negotiate auth in http.emptyAuth=auto mode
 + http: extract http_reauth_prepare() from retry paths

 The 'http.emptyAuth=auto' configuration now correctly attempts
 Negotiate authentication before falling back to manual credentials.
 This allows seamless Kerberos ticket-based authentication without
 requiring users to explicitly set 'http.emptyAuth=true'.

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


* en/batch-prefetch (2026-04-17) 3 commits
 - grep: prefetch necessary blobs
 - builtin/log: prefetch necessary blobs for `git cherry`
 - patch-ids.h: add missing trailing parenthesis in documentation comment

 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.

 Needs review.
 source: <pull.2089.v2.git.1776472347.gitgitgadget@gmail.com>


* en/diffstat-utf8-truncation-fix (2026-04-20) 1 commit
 - diff: fix out-of-bounds reads and NULL deref in diffstat UTF-8 truncation

 The computation to shorten the filenames shown in diffstat measured
 width of individual UTF-8 characters to add up, but forgot to take
 into account error cases (e.g., an invalid UTF-8 sequence, or a
 control character).

 Will merge to 'next'?
 source: <pull.2093.v3.git.1776699778177.gitgitgadget@gmail.com>


* sp/refs-reduce-the-repository (2026-04-04) 3 commits
  (merged to 'next' on 2026-04-09 at bb1d626802)
 + refs/reftable-backend: drop uses of the_repository
 + refs: remove the_hash_algo global state
 + refs: add struct repository parameter in get_files_ref_lock_timeout_ms()

 Code clean-up to use the right instance of a repository instance in
 calls inside refs subsystem.

 Will merge to 'master'.
 source: <20260404135914.61195-1-shreyanshpaliwalcmsmn@gmail.com>


* ps/odb-in-memory (2026-04-10) 18 commits
 - 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 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 'next'?
 source: <20260410-b4-pks-odb-source-inmemory-v3-0-22fd0fad58fe@pks.im>


* jc/doc-timestamps-in-stat (2026-04-10) 1 commit
  (merged to 'next' on 2026-04-20 at 0680260012)
 + CodingGuidelines: st_mtimespec vs st_mtim vs st_mtime

 Doc update.

 Will merge to 'master'.
 source: <xmqqzf3aofdj.fsf_-_@gitster.g>


* ps/test-set-e-clean (2026-04-21) 12 commits
  (merged to 'next' on 2026-04-23 at 4f69b47b94)
 + t: detect errors outside of test cases
 + t9902: fix use of `read` with `set -e`
 + t6002: fix use of `expr` with `set -e`
 + t1301: don't fail in case setfacl(1) doesn't exist or fails
 + t0008: silence error in subshell when using `grep -v`
 + t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`
 + t: prepare execution of potentially failing commands for `set -e`
 + t: prepare conditional test execution for `set -e`
 + t: prepare `git config --unset` calls for `set -e`
 + t: prepare `stop_git_daemon ()` for `set -e`
 + t: prepare `test_must_fail ()` for `set -e`
 + t: prepare `test_match_signal ()` calls for `set -e`

 The test suite harness and many individual test scripts have been
 updated to work correctly when 'set -e' is in effect, which helps
 detect misspelled test commands.

 Will merge to 'master'.
 source: <20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im>


* js/adjust-tests-to-explicitly-access-bare-repo (2026-04-02) 17 commits
 - git p4 clone --bare: need to be explicit about the gitdir
 - t9700: stop relying on implicit bare repo discovery
 - t9210: pass `safe.bareRepository=all` to `scalar register`
 - t6020: use `-C` for worktree, `--git-dir` for bare repository
 - t5619: wrap `test_commit_bulk` in `GIT_DIR` subshell for bare repo
 - t5540/t5541: avoid accessing a bare repository via `-C <dir>`
 - t5509: specify bare repository path explicitly
 - t5505: export `GIT_DIR` after `git init --bare`
 - t5503: avoid discovering a bare repository
 - t2406: use `--git-dir=.` for bare repository worktree repair
 - t2400: explicitly specify bare repo for `git worktree add`
 - t1900: avoid using `-C <dir>` for a bare repository
 - t1020: use `--git-dir` instead of subshell for bare repo
 - t0056: allow implicit bare repo discovery for `-C` work-tree tests
 - t0003: use `--git-dir` for bare repo attribute tests
 - t0001: replace `cd`+`git` with `git --git-dir` in `check_config`
 - t0001: allow implicit bare repo discovery for aliased-command test

 Some tests assume that bare repository accesses are by default
 allowed; rewrite some of them to avoid the assumption, rewrite
 others to explicitly set safe.bareRepository to allow them.

 Waiting for review response.
 cf. <xmqq1pgsdrdw.fsf@gitster.g>
 source: <pull.2076.git.1775140403.gitgitgadget@gmail.com>


* cl/conditional-config-on-worktree-path (2026-04-03) 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.

 Comments?
 source: <20260403-includeif-worktree-v3-0-109ce5782b03@black-desk.cn>


* bc/rust-by-default (2026-04-09) 4 commits
  (merged to 'next' on 2026-04-23 at fb9310bfae)
 + Enable Rust by default
 + Linux: link against libdl
 + ci: install cargo on Alpine
 + docs: update version with default Rust support

 Rust support is enabled by default (but still allows opting out) in
 some future version of Git.

 Will merge to 'master'.
 source: <20260409224434.1861422-1-sandals@crustytoothpaste.net>


* ps/shift-root-in-graph (2026-04-04) 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.

 Will merge to 'next'?
 source: <20260404092425.550346-2-pabloosabaterr@gmail.com>


* jt/config-lock-timeout (2026-04-03) 1 commit
 - config: retry acquiring config.lock for 100ms

 The code path to update the configuration file has been taught to
 use a short timeout to retry.

 Waiting for a review response.
 cf. <adYvSZeN0ZVqwRhi@pks.im>
 source: <20260403100135.3901610-1-joerg@thalheim.io>


* dl/cache-tree-fully-valid-fix (2026-04-06) 1 commit
  (merged to 'next' on 2026-04-13 at 68c82a9f37)
 + cache-tree: fix inverted object existence check in cache_tree_fully_valid

 The check that implements the logic to see if an in-core cache-tree
 is fully ready to write out a tree object was broken, which has
 been corrected.

 Will merge to 'master'.
 source: <20260406192711.68870-1-davidlin@stripe.com>


* ja/doc-difftool-synopsis-style (2026-04-04) 4 commits
  (merged to 'next' on 2026-04-13 at 0e6c98f313)
 + doc: convert git-describe manual page to synopsis style
 + doc: convert git-shortlog manual page to synopsis style
 + doc: convert git-range-diff manual page to synopsis style
 + doc: convert git-difftool manual page to synopsis style

 Doc mark-up updates.

 Will merge to 'master'.
 source: <pull.2077.git.1775322767.gitgitgadget@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.

 Comments?
 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-04-02) 7 commits
 - 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.)

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

 Will merge to 'next'?
 source: <20260402213220.2651523-1-jltobler@gmail.com>


* sa/cat-file-batch-mailmap-switch (2026-04-15) 1 commit
 - 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 'next'?
 source: <20260416033250.4327-2-siddharthasthana31@gmail.com>


* tb/incremental-midx-part-3.3 (2026-03-29) 16 commits
 - 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 `--checksum-only` for incremental MIDX writes
 - midx: use `strvec` for `keep_hashes`
 - strvec: introduce `strvec_init_alloc()`
 - midx: use `string_list` 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.

 Expecting a reroll.
 cf. <acxBUkHDolY9VCnR@nand.local>
 source: <cover.1774820449.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-04-20) 18 commits
 - 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_git_dir()`
 - setup: stop using `the_repository` in `is_inside_worktree()`
 - setup: replace use of `the_repository` in static functions

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

 Needs review.
 source: <20260420-pks-setup-wo-the-repository-v1-0-f4a81c4988e8@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.
 source: <V2_CV_doc_int-tr_key_format.613@msgid.xyz>


* ps/graph-lane-limit (2026-03-27) 3 commits
 - 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.

 Needs review.
 cf. <bdff0a5d-b738-4053-9b72-08eba88156de@kdbg.org>
 source: <20260328001113.1275291-1-pabloosabaterr@gmail.com>


* cc/promisor-auto-config-url (2026-04-07) 10 commits
  (merged to 'next' on 2026-04-13 at 289fcba081)
 + t5710: use proper file:// URIs for absolute paths
 + promisor-remote: remove the 'accepted' strvec
 + promisor-remote: keep accepted promisor_info structs alive
 + promisor-remote: refactor accept_from_server()
 + promisor-remote: refactor has_control_char()
 + promisor-remote: refactor should_accept_remote() control flow
 + promisor-remote: reject empty name or URL in advertised remote
 + promisor-remote: clarify that a remote is ignored
 + promisor-remote: pass config entry to all_fields_match() directly
 + promisor-remote: try accepted remotes before others in get_direct()

 Promisor remote handling has been refactored and fixed in
 preparation for auto-configuration of advertised remotes.

 Will merge to 'master'.
 source: <20260407115243.358642-1-christian.couder@gmail.com>


* jr/bisect-custom-terms-in-output (2026-04-17) 2 commits
 - rev-parse: use selected alternate terms to look up refs
 - bisect: use selected alternate terms in status output

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

 Needs review.
 source: <20260417-bisect-terms-v3-0-d659fa547261@schlaraffenlan.de>


* ua/push-remote-group (2026-03-27) 3 commits
 - SQUASH??? - futureproof against the attack of the "main"
 - push: support pushing to a remote group
 - remote: move remote group resolution to remote.c

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

 Expecting a reroll.
 cf. <xmqq7bqzu1xh.fsf@gitster.g>
 cf. <xmqqse9kj4rh.fsf@gitster.g>
 source: <20260325190906.1153080-1-usmanakinyemi202@gmail.com>


* hn/git-checkout-m-with-stash (2026-04-15) 5 commits
 - checkout -m: autostash when switching branches
 - checkout: rollback lock on early returns in merge_working_tree
 - sequencer: teach autostash apply to take optional conflict marker labels
 - sequencer: allow create_autostash to run silently
 - stash: add --label-ours, --label-theirs, --label-base for apply

 "git checkout -m another-branch" was invented to deal with local
 changes to paths that are different between the current and the new
 branch, but it gave only one chance to resolve conflicts.  The command
 was taught to create a stash to save the local changes.

 Will merge to 'next'?
 source: <pull.2234.v14.git.git.1776270259.gitgitgadget@gmail.com>


* kh/name-rev-custom-format (2026-03-20) 2 commits
 - name-rev: learn --format=<pretty>
 - name-rev: wrap both blocks in braces

 "git name-rev" learned to use custom format instead of the object
 name in an extended SHA-1 expression form.

 Expecting a reroll.
 cf. </1873f57e-76b4-48d0-8034-73f72f5fe93d@app.fastmail.com>
 source: <V2_CV_name-rev_--format.51b@msgid.xyz>


* js/parseopt-subcommand-autocorrection (2026-04-22) 10 commits
 - 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.

 Comments?
 source: <SY0P300MB0801AE56F740AD087D22B35ACE2D2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM>


* ab/clone-default-object-filter (2026-03-14) 1 commit
 - clone: add clone.<url>.defaultObjectFilter config

 "git clone" learns to pay attention to "clone.<url>.defaultObjectFilter"
 configuration and behave as if the "--filter=<filter-spec>" option
 was given on the command line.

 Expecting review responses.
 cf. <abe1l8ONmFIhzaxi@pks.im>
 source: <pull.2058.v6.git.1773553022381.gitgitgadget@gmail.com>


* jc/neuter-sideband-fixup (2026-03-05) 6 commits
  (merged to 'next' on 2026-03-13 at 5a4098b0cd)
 + sideband: drop 'default' configuration
 + sideband: offer to configure sanitizing on a per-URL basis
 + sideband: add options to allow more control sequences to be passed through
 + sideband: do allow ANSI color sequences by default
 + sideband: introduce an "escape hatch" to allow control characters
 + sideband: mask control characters
 (this branch is used by jc/neuter-sideband-post-3.0.)

 Try to resurrect and reboot a stalled "avoid sending risky escape
 sequences taken from sideband to the terminal" topic by Dscho.  The
 plan is to keep it in 'next' long enough to see if anybody screams
 with the "everything dropped except for ANSI color escape sequences"
 default.

 Will keep in 'next' a bit longer than usual.
 source: <20260305233452.3727126-1-gitster@pobox.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
 (this branch uses jc/neuter-sideband-fixup.)

 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, until jc/neuter-sideband-fixup cooks long enough in 'next'.
 (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/promisor-lazy-fetch-no-recurse (2026-03-11) 1 commit
 - promisor-remote: prevent lazy-fetch recursion in child fetch

 The mechanism to avoid recursive lazy-fetch from promisor remotes
 was not propagated properly to child "git fetch" processes, which
 has been corrected.

 Will discard.
 cf. <xmqqik9s6qvd.fsf@gitster.g>
 source: <pull.2224.v3.git.git.1773238778894.gitgitgadget@gmail.com>


* pt/fsmonitor-linux (2026-04-15) 13 commits
 - 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 'next'?
 source: <pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com>


* ar/parallel-hooks (2026-04-10) 13 commits
  (merged to 'next' on 2026-04-13 at 0a6acf0d17)
 + t1800: test SIGPIPE with parallel hooks
 + hook: allow hook.jobs=-1 to use all available CPU cores
 + hook: add hook.<event>.enabled switch
 + hook: move is_known_hook() to hook.c for wider use
 + hook: warn when hook.<friendly-name>.jobs is set
 + hook: add per-event jobs config
 + hook: add -j/--jobs option to git hook run
 + hook: mark non-parallelizable hooks
 + hook: allow pre-push parallel execution
 + hook: allow parallel hook execution
 + hook: parse the hook.jobs config
 + config: add a repo_config_get_uint() helper
 + repository: fix repo_init() memleak due to missing _clear()

 Will merge to 'master'.
 source: <20260410090608.75283-1-adrian.ratiu@collabora.com>


* pw/xdiff-shrink-memory-consumption (2026-04-02) 5 commits
 - xdiff: reduce the size of array
 - xprepare: simplify error handling
 - xdiff: cleanup xdl_clean_mmatch()
 - xdiff: reduce size of action arrays
 - Merge branch 'en/xdiff-cleanup-3' into pw/xdiff-shrink-memory-consumption
 (this branch uses en/xdiff-cleanup-3.)

 Shrink wasted memory in Myers diff that does not account for common
 prefix and suffix removal.

 On hold, waiting for the base topic.
 source: <cover.1775141855.git.phillip.wood@dunelm.org.uk>


* en/xdiff-cleanup-3 (2026-04-08) 6 commits
 - xdiff/xdl_cleanup_records: put braces around the else clause
 - xdiff/xdl_cleanup_records: make setting action easier to follow
 - xdiff/xdl_cleanup_records: make limits more clear
 - xdiff/xdl_cleanup_records: use unambiguous types
 - xdiff: use unambiguous types in xdl_bogo_sqrt()
 - xdiff/xdl_cleanup_records: delete local recs pointer
 (this branch is used by pw/xdiff-shrink-memory-consumption.)

 Preparation of the xdiff/ codebase to work with Rust.

 Expecting a (hopefully small and final) reroll?
 cf. <32c34d0d-9358-43e3-9d58-5999b3ffd6c2@gmail.com>
 source: <pull.2156.v5.git.git.1775679988.gitgitgadget@gmail.com>

^ permalink raw reply

* Re: git grep bug with --column and --only-matching
From: Phillip Wood @ 2026-04-23  9:44 UTC (permalink / raw)
  To: René Scharfe, Brandon Chinn
  Cc: git, Taylor Blau, Jeff King, Junio C Hamano
In-Reply-To: <3ce1906a-85e3-4fb1-9ebc-a5639f3194c9@web.de>

On 21/04/2026 21:33, René Scharfe wrote:
> On 4/21/26 7:03 AM, Brandon Chinn wrote:
>> I'm encountering a bug when using `git grep` with both `--column` and
>> `--only-matching`, is this a known limitation?
>>
>> Repro:
>>
>> ```
>> $ echo 'x   x   x' > repro.txt
>>
>> $ grep -bo x repro.txt
>> 0:x
>> 4:x
>> 8:x
>>
>> $ git grep --no-index -o -n --column x repro.txt
>> repro.txt:1:  1:x
>> repro.txt:1:  2:x
>> repro.txt:1:  6:x
> 
> The first column value matches (1-based for git grep, 0-based for
> grep(1)), the remaining ones are different.  grep(1) shows the offset
> like for the first match, but git grep adds the relative position of the
> end of the previous match.

I'm having a hard time understanding what git is doing. Looking at your 
example with the ruler

>    (`man gitcvs-migration` or `git help cvs-migration` if git is
>          ^^^                   ^^^                        ^^^
>             1111111111222222222233333333334444444444555555555566
>    123456789 123456789 123456789 123456789 123456789 123456789 1
>             
>    7:git
>    16:git
>    38:git

The first match ends at column 9 and the second match starts at column 
29 so how do we end up with 16 as the "column" of the second match when 
the difference between them is 20?

> I don't know how to use the resulting numbers and I agree that it seems
> like a bug -- showing the column of each match within its line makes
> more sense for an option named --column.

Indeed

> However, the original submission of this feature in
> https://lore.kernel.org/git/cover.1529961706.git.me@ttaylorr.com/
> gave similar examples in its commit message and its tests, called
> them "as one would expect" and was not challenged on that, so I may be
> missing something.

I wonder how hard anyone looked at the examples and tests, the 
discussion seemed to have focused on other things.

Thanks

Phillip

> Here's its first example with underlined matches, a ruler and the
> intended output (without unnecessary headers):
> 
>    (`man gitcvs-migration` or `git help cvs-migration` if git is
>          ^^^                   ^^^                        ^^^
>             1111111111222222222233333333334444444444555555555566
>    123456789 123456789 123456789 123456789 123456789 123456789 1
>             
>    7:git
>    16:git
>    38:git
> 
> And here's the last line of the test file from t7810 with underlined
> matches (the other four lines are very similar), a ruler and the
> expected output (unncessary headers removed):
> 
>    foo_mmap bar mmap baz
>        ^^^^     ^^^^
>             111111111122
>    123456789 123456789 1
> 
>    5:mmap
>    13:mmap
> 
> If we wanted to show the column of matches 2 and beyond then we could do
> something like this:
> 
> 
> diff --git a/grep.c b/grep.c
> index c7e1dc1e0e..a54e5d86a9 100644
> --- a/grep.c
> +++ b/grep.c
> @@ -1267,6 +1267,7 @@ static void show_line(struct grep_opt *opt,
>   		regmatch_t match;
>   		enum grep_context ctx = GREP_CONTEXT_BODY;
>   		int eflags = 0;
> +		const char *start = bol;
>   
>   		if (want_color(opt->color)) {
>   			if (sign == ':')
> @@ -1285,6 +1286,7 @@ static void show_line(struct grep_opt *opt,
>   			if (match.rm_so == match.rm_eo)
>   				break;
>   
> +			cno = bol - start + match.rm_so + 1;
>   			if (opt->only_matching)
>   				show_line_header(opt, name, lno, cno, sign);
>   			else
> @@ -1294,7 +1296,6 @@ static void show_line(struct grep_opt *opt,
>   			if (opt->only_matching)
>   				opt->output(opt, "\n", 1);
>   			bol += match.rm_eo;
> -			cno += match.rm_eo;
>   			rest -= match.rm_eo;
>   			eflags = REG_NOTBOL;
>   		}
> diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh
> index 64ac4f04ee..bd439563d6 100755
> --- a/t/t7810-grep.sh
> +++ b/t/t7810-grep.sh
> @@ -322,11 +322,11 @@ do
>   		${HC}file:1:5:mmap
>   		${HC}file:2:5:mmap
>   		${HC}file:3:5:mmap
> -		${HC}file:3:13:mmap
> +		${HC}file:3:14:mmap
>   		${HC}file:4:5:mmap
> -		${HC}file:4:13:mmap
> +		${HC}file:4:14:mmap
>   		${HC}file:5:5:mmap
> -		${HC}file:5:13:mmap
> +		${HC}file:5:14:mmap
>   		EOF
>   		git grep --column -n -o -e mmap $H >actual &&
>   		test_cmp expected actual
> 
> 


^ 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