Git development
 help / color / mirror / Atom feed
* [PATCH v2 4/4] doc: clarify http.emptyAuth values
From: Matthew John Cheetham via GitGitGadget @ 2026-04-30 10:54 UTC (permalink / raw)
  To: git
  Cc: gitster, johannes.schindelin, Matthew John Cheetham,
	Matthew John Cheetham, Matthew John Cheetham
In-Reply-To: <pull.2087.v2.git.1777546472.gitgitgadget@gmail.com>

From: Matthew John Cheetham <mjcheetham@outlook.com>

The existing description of http.emptyAuth explains the purpose of the
setting but never says what values it accepts. Readers have to infer
from context (or read the source) that it takes 'true', 'false', or
'auto', and what each one means.

Document the three accepted values explicitly:

* 'auto' (the default) only sends empty credentials when the server's
  401 response advertises a mechanism that requires them, such as
  GSS-Negotiate. This matches the long-standing auto-detection
  behaviour added in 40a18fc77c (http: add an "auto" mode for
  http.emptyauth, 2017-02-25).

* 'true' unconditionally sends empty credentials on the very first
  request, before any 401 response, for callers that know they want
  this behaviour up front.

* 'false' disables the feature entirely; mechanisms that depend on
  empty credentials, such as GSS-Negotiate, will not work in this
  mode.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
---
 Documentation/config/http.adoc | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc
index 849c89f36c..792a71b413 100644
--- a/Documentation/config/http.adoc
+++ b/Documentation/config/http.adoc
@@ -59,7 +59,18 @@ http.emptyAuth::
 	Attempt authentication without seeking a username or password.  This
 	can be used to attempt GSS-Negotiate authentication without specifying
 	a username in the URL, as libcurl normally requires a username for
-	authentication.
+	authentication. Possible values are:
++
+--
+* `auto` (default) - Send empty credentials only if the server's 401 response
+  advertises an authentication mechanism that requires them (such as
+  GSS-Negotiate); otherwise fall back to prompting via the credential helper.
+* `true` - Always send empty credentials on the very first request, before
+  receiving any 401 response from the server.
+* `false` - Never send empty credentials. Mechanisms that require
+  empty credentials or an explicit username, such as GSS-Negotiate, will not
+  work.
+--
 
 http.proactiveAuth::
 	Attempt authentication without first making an unauthenticated attempt and
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH v6 0/6] Xdiff cleanup part 3
From: Phillip Wood @ 2026-04-30 13:35 UTC (permalink / raw)
  To: Ezekiel Newren via GitGitGadget, git
  Cc: Yee Cheng Chin, René Scharfe, Jeff King, D. Ben Knoble,
	SZEDER Gábor, Ezekiel Newren
In-Reply-To: <pull.2156.v6.git.git.1777500495.gitgitgadget@gmail.com>

Hi Ezekiel

On 29/04/2026 23:08, Ezekiel Newren via GitGitGadget wrote:
> Changes in v6:
> 
>   * implement suggestions by Phillip Wood [1,2]
> 
> Phillip's second "if" in [1] differs from his first one. In my changes I
> made both of them structurally the same.

I was in two minds about whether to do that or not, all the changes here 
look good to me.

Juino - are you happy to rebase pw/xdiff-shrink-memory-consumption, or 
do you want be to send a re-roll?

> Something I'm confused by is the range-diff of patch 5. I'm confused why
> range-diff states that this is different at all. I don't think this is a
> problem, I just don't like not being able to explain a difference pointed
> out by range-diff.

The context line below the insertion of "action1[i] = INVESTIGATE;" has 
changed do to the changes to patch 4. It would be nice if there was a 
way to tell range-diff to ignore hunks where only the context lines have 
changed but nobody has implemented that yet.

Thanks

Phillip

> 5: 88c68fa89a ! 5: 099b08c33f xdiff/xdl_cleanup_records: make setting action easier to follow
 > @@ xdiff/xprepare.c: static int xdl_cleanup_records(xdlclassifier_t 
cf, xdfile_t *
 >  +			action1[i] = > INVESTIGATE;
 >   	}>
> - 	for (i = xdf2->dstart; i <= xdf2->dend; i++) {
> + 	if (need_min) {
> +@@ xdiff/xprepare.c: static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd
>              size_t mph2 = xdf2->recs[i].minimal_perfect_hash;
>              rcrec = cf->rcrecs[mph2];
>              nm = rcrec ? rcrec->len1 : 0;
> 
> 
> [1] limits
> https://lore.kernel.org/git/d88af7e1-e8dd-4423-9c6c-977e1f1dc074@gmail.com/
> [2] action execution
> https://lore.kernel.org/git/df244360-e9a9-44c0-946d-29288e6dd269@gmail.com/
> 
> Changes in v5:
> 
>   * drop commit "xdiff/xdl_cleanup_records: simplify INVESTIGATE handling for
>     clarity".
>   * add braces around the else clause
> 
> I didn't see a better way to rewrite how action is used so I reverted to
> what it used to be.
> 
> Changes in v4:
> 
>   * Change SIZE_MAX to PTRDIFF_MAX.
> 
> Changes in v3:
> 
>   * run make DEVELOPER=1 on each commit and fix all compiler issues
> 
> v2 is a radical departure from v1 Changes in v2:
> 
>   * make the flow of xdl_cleanup_records() easier to follow
> 
> There is no performance or behavioral change introduced in this patch
> series.
> 
> === original cover letter bellow ===
> 
> Patch series summary:
> 
>   * patch 1: Introduce the ivec type
>   * patch 2: Create the function xdl_do_classic_diff()
>   * patches 3-4: generic cleanup
>   * patches 5-8: convert from dstart/dend (in xdfile_t) to
>     delta_start/delta_end (in xdfenv_t)
>   * patches 9-10: move xdl_cleanup_records(), and related, from xprepare.c to
>     xdiffi.c
> 
> Things that will be addressed in future patch series:
> 
>   * Make xdl_cleanup_records() easier to read
>   * convert recs/nrec into an ivec
>   * convert changed to an ivec
>   * remove reference_index/nreff from xdfile_t and turn it into an ivec
>   * splitting minimal_perfect_hash out as its own ivec
>   * improve the performance of the classifier and parsing/hashing lines
> 
> === before this patch series typedef struct s_xdfile { xrecord_t *recs;
> size_t nrec; ptrdiff_t dstart, dend; bool *changed; size_t *reference_index;
> size_t nreff; } xdfile_t;
> 
> typedef struct s_xdfenv { xdfile_t xdf1, xdf2; } xdfenv_t;
> 
> === after this patch series typedef struct s_xdfile { xrecord_t *recs;
> size_t nrec; bool *changed; size_t *reference_index; size_t nreff; }
> xdfile_t;
> 
> typedef struct s_xdfenv { xdfile_t xdf1, xdf2; size_t delta_start,
> delta_end; size_t mph_size; } xdfenv_t;
> 
> Ezekiel Newren (6):
>    xdiff/xdl_cleanup_records: delete local recs pointer
>    xdiff: use unambiguous types in xdl_bogo_sqrt()
>    xdiff/xdl_cleanup_records: use unambiguous types
>    xdiff/xdl_cleanup_records: make limits more clear
>    xdiff/xdl_cleanup_records: make setting action easier to follow
>    xdiff/xdl_cleanup_records: make execution of action easier to follow
> 
>   xdiff/xdiffi.c   |  2 +-
>   xdiff/xprepare.c | 97 ++++++++++++++++++++++++++++++++++--------------
>   xdiff/xutils.c   |  4 +-
>   xdiff/xutils.h   |  2 +-
>   4 files changed, 73 insertions(+), 32 deletions(-)
> 
> 
> base-commit: ca1db8a0f7dc0dbea892e99f5b37c5fe5861be71
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2156%2Fezekielnewren%2Fxdiff-cleanup-3-v6
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2156/ezekielnewren/xdiff-cleanup-3-v6
> Pull-Request: https://github.com/git/git/pull/2156
> 
> Range-diff vs v5:
> 
>   1:  b31924a949 = 1:  b31924a949 xdiff/xdl_cleanup_records: delete local recs pointer
>   2:  1822166fef = 2:  1822166fef xdiff: use unambiguous types in xdl_bogo_sqrt()
>   3:  85aa0da90c = 3:  85aa0da90c xdiff/xdl_cleanup_records: use unambiguous types
>   4:  fec2b0f38a ! 4:  51c62ed454 xdiff/xdl_cleanup_records: make limits more clear
>       @@ Commit message
>              * The additional condition `!need_min` is redudant now, remove it.
>            Best viewed with --color-words.
>        
>       +    Helped-by: Phillip Wood
>            Signed-off-by: Ezekiel Newren <ezekielnewren@gmail.com>
>        
>         ## xdiff/xprepare.c ##
>       @@ xdiff/xprepare.c: static bool xdl_clean_mmatch(uint8_t const *action, ptrdiff_t
>         	uint8_t *action1 = NULL, *action2 = NULL;
>         	bool need_min = !!(cf->flags & XDF_NEED_MINIMAL);
>        @@ xdiff/xprepare.c: static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd
>       - 		goto cleanup;
>       - 	}
>       -
>       -+	if (need_min) {
>       -+		/* i.e. infinity */
>       -+		mlim1 = PTRDIFF_MAX;
>       -+		mlim2 = PTRDIFF_MAX;
>       -+	} else {
>       -+		mlim1 = XDL_MIN(xdl_bogosqrt(xdf1->nrec), XDL_MAX_EQLIMIT);
>       -+		mlim2 = XDL_MIN(xdl_bogosqrt(xdf2->nrec), XDL_MAX_EQLIMIT);
>       -+	}
>       -+
>         	/*
>         	 * Initialize temporary arrays with DISCARD, KEEP, or INVESTIGATE.
>         	 */
>        -	if ((mlim = (long)xdl_bogosqrt((uint64_t)xdf1->nrec)) > XDL_MAX_EQLIMIT)
>        -		mlim = XDL_MAX_EQLIMIT;
>       ++	if (need_min) {
>       ++		/* i.e. infinity */
>       ++		mlim1 = PTRDIFF_MAX;
>       ++	} else {
>       ++		mlim1 = xdl_bogosqrt((uint64_t)xdf1->nrec);
>       ++		if (mlim1 > XDL_MAX_EQLIMIT)
>       ++			mlim1 = XDL_MAX_EQLIMIT;
>       ++	}
>         	for (i = xdf1->dstart; i <= xdf1->dend; i++) {
>         		size_t mph1 = xdf1->recs[i].minimal_perfect_hash;
>         		rcrec = cf->rcrecs[mph1];
>       @@ xdiff/xprepare.c: static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *
>         
>        -	if ((mlim = (long)xdl_bogosqrt((uint64_t)xdf2->nrec)) > XDL_MAX_EQLIMIT)
>        -		mlim = XDL_MAX_EQLIMIT;
>       ++	if (need_min) {
>       ++		/* i.e. infinity */
>       ++		mlim2 = PTRDIFF_MAX;
>       ++	} else {
>       ++		mlim2 = xdl_bogosqrt((uint64_t)xdf2->nrec);
>       ++		if (mlim2 > XDL_MAX_EQLIMIT)
>       ++			mlim2 = XDL_MAX_EQLIMIT;
>       ++	}
>         	for (i = xdf2->dstart; i <= xdf2->dend; i++) {
>         		size_t mph2 = xdf2->recs[i].minimal_perfect_hash;
>         		rcrec = cf->rcrecs[mph2];
>   5:  88c68fa89a ! 5:  45ad2ae62d xdiff/xdl_cleanup_records: make setting action easier to follow
>       @@ xdiff/xprepare.c: static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *
>        +			action1[i] = INVESTIGATE;
>         	}
>         
>       - 	for (i = xdf2->dstart; i <= xdf2->dend; i++) {
>       + 	if (need_min) {
>       +@@ xdiff/xprepare.c: static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd
>         		size_t mph2 = xdf2->recs[i].minimal_perfect_hash;
>         		rcrec = cf->rcrecs[mph2];
>         		nm = rcrec ? rcrec->len1 : 0;
>   6:  699e198fa9 ! 6:  a5174802f4 xdiff/xdl_cleanup_records: put braces around the else clause
>       @@ Metadata
>        Author: Ezekiel Newren <ezekielnewren@gmail.com>
>        
>         ## Commit message ##
>       -    xdiff/xdl_cleanup_records: put braces around the else clause
>       +    xdiff/xdl_cleanup_records: make execution of action easier to follow
>        
>       +    Helped-by: Phillip Wood
>            Signed-off-by: Ezekiel Newren <ezekielnewren@gmail.com>
>        
>         ## xdiff/xprepare.c ##
>        @@ xdiff/xprepare.c: static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd
>       - 		    (action1[i] == INVESTIGATE && !xdl_clean_mmatch(action1, i, xdf1->dstart, xdf1->dend))) {
>       + 	 */
>       + 	xdf1->nreff = 0;
>       + 	for (i = xdf1->dstart; i <= xdf1->dend; i++) {
>       +-		if (action1[i] == KEEP ||
>       +-		    (action1[i] == INVESTIGATE && !xdl_clean_mmatch(action1, i, xdf1->dstart, xdf1->dend))) {
>       ++		uint8_t action = action1[i];
>       ++
>       ++		if (action == INVESTIGATE) {
>       ++			if (!xdl_clean_mmatch(action1, i, xdf1->dstart, xdf1->dend))
>       ++				action = KEEP;
>       ++			else
>       ++				action = DISCARD;
>       ++		}
>       ++
>       ++		if (action == KEEP) {
>         			xdf1->reference_index[xdf1->nreff++] = i;
>       - 			/* changed[i] remains false, i.e. keep */
>       +-			/* changed[i] remains false, i.e. keep */
>        -		} else
>       -+		} else {
>       ++			/* changed[i] remains false */
>       ++		} else if (action == DISCARD) {
>         			xdf1->changed[i] = true;
>       - 			/* i.e. discard */
>       +-			/* i.e. discard */
>       ++		} else {
>       ++			BUG("Illegal state for action");
>        +		}
>         	}
>         
>         	xdf2->nreff = 0;
>       -@@ xdiff/xprepare.c: static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd
>       - 		    (action2[i] == INVESTIGATE && !xdl_clean_mmatch(action2, i, xdf2->dstart, xdf2->dend))) {
>       + 	for (i = xdf2->dstart; i <= xdf2->dend; i++) {
>       +-		if (action2[i] == KEEP ||
>       +-		    (action2[i] == INVESTIGATE && !xdl_clean_mmatch(action2, i, xdf2->dstart, xdf2->dend))) {
>       ++		uint8_t action = action2[i];
>       ++
>       ++		if (action == INVESTIGATE) {
>       ++			if (!xdl_clean_mmatch(action2, i, xdf2->dstart, xdf2->dend))
>       ++				action = KEEP;
>       ++			else
>       ++				action = DISCARD;
>       ++		}
>       ++
>       ++		if (action == KEEP) {
>         			xdf2->reference_index[xdf2->nreff++] = i;
>       - 			/* changed[i] remains false, i.e. keep */
>       +-			/* changed[i] remains false, i.e. keep */
>        -		} else
>       -+		} else {
>       ++			/* changed[i] remains false */
>       ++		} else if (action == DISCARD) {
>         			xdf2->changed[i] = true;
>       - 			/* i.e. discard */
>       +-			/* i.e. discard */
>       ++		} else {
>       ++			BUG("Illegal state for action");
>        +		}
>         	}
>         
> 


^ permalink raw reply

* Re: git interactive rebase does not allow editing commits at once anymore
From: Phillip Wood @ 2026-04-30 13:43 UTC (permalink / raw)
  To: David Jordanes, git@vger.kernel.org
In-Reply-To: <DB7PR03MB3881199B8D12CC7A981ADF0CA8352@DB7PR03MB3881.eurprd03.prod.outlook.com>

Hi David

On 30/04/2026 08:46, David Jordanes wrote:
> Git version: git version 2.53.0.windows.2
> OS: Windows 11
> 
> Steps to reproduce:
> 1. Create a dummy repo
> 2. Create a couple of commits
> 2. Run git rebase -i HEAD~2
> 
> Observed:
> Lines in todo appear as:
> pick <hash> # commit 1
> pick <hash> # commit 2
> 
> After editing:
> reword <hash> commit A
> reword <hash> commit B
> 
> Then git loops each commit to edit.

If you mean git opens the editor for you to edit the first commit 
message and then when you close the editor it opens the second message, 
it is working as it always has done. There is an external program called 
"git-revise" which I believe lets you edit multiple commit message at 
once though I've not used it myself.

Processing the commits one at a time lets you inspect the changes for 
the commit you're editing the message for and if you have 
"commit.verbose" enabled then git will show you the diff for the commit 
below the commit message in the editor.

Thanks

Phillip


> Expected:
> After editing, all changes should be applied at once (no loop).
> 
> Question:
> Is this intended behavior? By whom and why this flow was decided? What problem it solves? If I have to rebase interactively 10 or 15 commits I have to go through all those commits one by one???


^ permalink raw reply

* Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command
From: Phillip Wood @ 2026-04-30 13:54 UTC (permalink / raw)
  To: kristofferhaugsbakk, git; +Cc: Kristoffer Haugsbakk, ben.knoble
In-Reply-To: <V3_name-rev_factor.66d@msgid.xyz>

Hi Kristoffer

On 28/04/2026 23:25, kristofferhaugsbakk@fastmail.com wrote:
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
> 
> @@ -516,6 +534,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
>   
>   	for (p_start = p; *p; p++) {
>   #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
> +	start:
>   		if (!ishex(*p)) {
>   			counter = 0;
>   		} else if (++counter == hexsz &&
> @@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
>   			const char *name = NULL;
>   			char c = *(p + 1);
>   			int p_len = p - p_start + 1;
> +			struct object *o = NULL;
> +			int oid_ret = 1;
>   
>   			counter = 0;
>   
>   			*(p + 1) = 0;
> -			if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
> -				struct object *o =
> -					lookup_object(the_repository, &oid);
> +			oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);

It would be safer to restore *(p + 1) here rather that relying on each 
case block to do it.

			*(p + 1) = c;
> +
> +			switch (cmd->type) {
> +			case NAME_REV:
> +				if (!oid_ret)
> +					o = lookup_object(the_repository, &oid);
>   				if (o)
>   					name = get_rev_name(o, &buf);
> +				*(p + 1) = c;
> +				if (!name)
> +					goto start;

The pre-image uses "continue" which will increment p - why the change in 
behavior?

Thanks

Phillip

> +				if (cmd->u.name_only)
> +					printf("%.*s%s", p_len - hexsz, p_start, name);
> +				else
> +					printf("%.*s (%s)", p_len, p_start, name);
> +				break;
> +			default:
> +				BUG("uncovered case: %d", cmd->type);
>   			}
> -			*(p + 1) = c;
> -
> -			if (!name)
> -				continue;
>   
> -			if (data->name_only)
> -				printf("%.*s%s", p_len - hexsz, p_start, name);
> -			else
> -				printf("%.*s (%s)", p_len, p_start, name);
>   			p_start = p + 1;
>   		}
>   	}
> @@ -567,6 +593,7 @@ int cmd_name_rev(int argc,
>   #endif
>   	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
>   	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
> +	struct command cmd;
>   	struct option opts[] = {
>   		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
>   		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
> @@ -596,6 +623,7 @@ int cmd_name_rev(int argc,
>   	init_commit_rev_name(&rev_names);
>   	repo_config(the_repository, git_default_config, NULL);
>   	argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
> +	init_name_rev_command(&cmd, data.name_only);
>   
>   #ifndef WITH_BREAKING_CHANGES
>   	if (transform_stdin) {
> @@ -663,7 +691,7 @@ int cmd_name_rev(int argc,
>   
>   		while (strbuf_getline(&sb, stdin) != EOF) {
>   			strbuf_addch(&sb, '\n');
> -			name_rev_line(sb.buf, &data);
> +			name_rev_line(sb.buf, &cmd);
>   		}
>   		strbuf_release(&sb);
>   	} else if (all) {


^ permalink raw reply

* Re: [PATCH 1/6] index-pack, unpack-objects: use size_t for object size
From: Torsten Bögershausen @ 2026-04-30 14:13 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <dc660106ea8511e6adc44d2b70e9a4ae8b18090e.1777393580.git.gitgitgadget@gmail.com>

On Tue, Apr 28, 2026 at 04:26:15PM +0000, Johannes Schindelin via GitGitGadget wrote:
> From: Johannes Schindelin <johannes.schindelin@gmx.de>
> 
> When unpacking objects from a packfile, the object size is decoded
> from a variable-length encoding. On platforms where unsigned long is
> 32-bit (such as Windows, even in 64-bit builds), the shift operation
> overflows when decoding sizes larger than 4GB. The result is a
> truncated size value, causing the unpacked object to be corrupted or
> rejected.
> 
> Fix this by changing the size variable to size_t, which is 64-bit on
> 64-bit platforms, and ensuring the shift arithmetic occurs in 64-bit
> space.
> 
> This was originally authored by LordKiRon <https://github.com/LordKiRon>,
> who preferred not to reveal their real name and therefore agreed that I
> take over authorship.

Good to see things moving forward.

See even
https://github.com/git-for-windows/git/pull/2179
which is probably obsolete soon.

^ permalink raw reply

* Draft of Git Rev News edition 134
From: Christian Couder @ 2026-04-30 15:17 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Jakub Narebski, Markus Jansen, Kaartic Sivaraam,
	Štěpán Němec, Taylor Blau,
	Johannes Schindelin, Michael Montalbo, Kristoffer Haugsbakk,
	Matthew Hughes, Paulo Gomes, Toon Claes

Hi everyone,

A draft of a new Git Rev News edition is available here:

  https://github.com/git/git.github.io/blob/master/rev_news/drafts/edition-134.md

Everyone is welcome to contribute in any section either by editing the
above page on GitHub and sending a pull request, or by commenting on
this GitHub issue:

  https://github.com/git/git.github.io/issues/842

You can also reply to this email.

In general all kinds of contributions, for example proofreading,
suggestions for articles or links, help on the issues in GitHub,
volunteering for being interviewed and so on, are very much
appreciated.

I tried to Cc everyone who appears in this edition, but maybe I missed
some people, sorry about that.

Jakub, Markus, Kaartic and I plan to publish this edition on Saturday
May 2nd, 2026.

Thanks,
Christian.

^ permalink raw reply

* Re: [PATCH v3 1/1] git-gui: handle missing worktree and separated gitdir
From: Mark Levedahl @ 2026-04-30 16:18 UTC (permalink / raw)
  To: Shroom Moo, git; +Cc: j6t
In-Reply-To: <tencent_8A236D9D4A8D8CCA7DAA083157AA8543700A@qq.com>



On 4/30/26 6:02 AM, Shroom Moo wrote:
> When git-gui is started from a directory that Git recognizes as a
> valid repository but the working tree is not accessible (e.g., a
> separated gitdir created by `git clone --separate-git-dir`, a bare
> repository, or a case where the worktree directory was removed),
> it previously called `rev-parse --show-toplevel` without error
> handling, causing a fatal Tcl error ("this operation must be run
> in a work tree").
>
> Wrap the call in a `catch` and handle the failure as follows:
>
> - For bare repositories, keep `_gitworktree` empty so that the
>   existing `is_bare` check shows "Cannot use bare repository" and
>   exits.  No behavioral change.
>
> - For non‑bare repositories, try to locate the worktree from the
>   parent directory using `git -C $parent rev-parse --show-toplevel`.
>   If the parent is a valid worktree, change to it; this covers the
>   legitimate case of starting git-gui from within the .git
>   subdirectory of a normal working tree.
>
> - If the parent directory is not a worktree, refuse to start with
>   a clear error message.  This prevents dangerous operations in a
>   separated gitdir, where ordinary Git commands like `git status`
>   would themselves refuse to run.
>
> The approach intentionally avoids two pitfalls:
>
>   - Testing `--is-inside-git-dir` before calling `--show-toplevel`
>     would break the normal use case of starting git-gui from within
>     a .git subdirectory (where --show-toplevel would succeed).
>
>   - A simple “non‑bare” check after a failed --show-toplevel would
>     reject a normal repository whose worktree was only temporarily
>     removed.
>
> The chosen method keeps the original behavior for bare repositories
> and for regular working trees, fixes the crash, and properly blocks
> separated gitdirs without a reachable worktree.
>
> Signed-off-by: Shroom Moo <egg_mushroomcow@foxmail.com>
> ---
>  git-gui/git-gui.sh | 23 ++++++++++++++++++++++-
>  1 file changed, 22 insertions(+), 1 deletion(-)
>
> diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh
> index 23fe76e498..2392282df3 100755
> --- a/git-gui/git-gui.sh
> +++ b/git-gui/git-gui.sh
> @@ -1169,7 +1169,28 @@ if {![file isdirectory $_gitdir]} {
>  load_config 0
>  apply_config
>  
> -set _gitworktree [git rev-parse --show-toplevel]
> +if {[catch {set _gitworktree [git rev-parse --show-toplevel]}]} {
> +	# For bare repositories, use the existing error handling
> +	if {![catch {set bare [git rev-parse --is-bare-repository]}] && $bare eq {true}} {
> +		set _gitworktree {}
> +	} else {
> +		# Non-bare: try to find the worktree from the parent directory
> +		set parent [file dirname [pwd]]
> +		# Cannot go higher than the root directory; leave _gitworktree empty
> +		if {[file normalize $parent] eq [file normalize [pwd]]} {
> +			# Already at the filesystem root; let existing paths cope
> +			set _gitworktree {}
> +		} elseif {![catch {
> +			set _gitworktree [git -C $parent rev-parse --show-toplevel]
> +		}]} {
> +			cd $parent
> +		} else {
> +			catch {wm withdraw .}
> +			error_popup [mc "Cannot start git-gui from inside the Git directory."]
> +			exit 1
> +		}
> +	}
> +}
>  
>  if {$_prefix ne {}} {
>  	if {$_gitworktree eq {}} {

A bare repository can be contained in a workdir / worktree pointing at a different gitdir:
the logic above can thus a workdir that doesn't use the gitdir where git-gui was started.
The bigger issue is that a gitdir can support multiple checked-out directories with no
one-to-one mapping and no clear idea of which of those a user may have intended.

So, I believe the correct fix is to test "rev-parse --is-inside-git-dir, and if so throw a
clear error message and exit. This will give the user something to start with to solve the
problem of why they started git-gui in a gitdir, and not in a worktree.

Mark


^ permalink raw reply

* [PATCH v5] revision.c: implement --max-count-oldest
From: Mirko Faina @ 2026-04-30 19:52 UTC (permalink / raw)
  To: git
  Cc: Mirko Faina, Junio C Hamano, Jeff King, Jean-Noël Avila,
	Patrick Steinhardt, Tian Yuchen, Ben Knoble, Johannes Sixt,
	Chris Torek
In-Reply-To: <cover.1777249165.git.mroik@delayed.space>

--max-count is a commit limiting option sets a maximum amount of commits
to be shown. If a user wants to see only the first N commits of the
history (the oldest commits) they'd have to combine --max-count with
--skip. This is not very user-friendly.

Teach get_revision() the --max-count-oldest option.

Signed-off-by: Mirko Faina <mroik@delayed.space>
---
 Documentation/rev-list-options.adoc |  3 ++
 revision.c                          | 77 +++++++++++++++++++++++++++--
 revision.h                          |  2 +
 t/t4202-log.sh                      | 14 ++++++
 4 files changed, 93 insertions(+), 3 deletions(-)

diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc
index 2d195a1474..736f34efab 100644
--- a/Documentation/rev-list-options.adoc
+++ b/Documentation/rev-list-options.adoc
@@ -18,6 +18,9 @@ ordering and formatting options, such as `--reverse`.
 `--max-count=<number>`::
 	Limit the output to _<number>_ commits.
 
+`--max-count-oldest=<number>`::
+	Limit the output to the _<number>_ oldest commits.
+
 `--skip=<number>`::
 	Skip _<number>_ commits before starting to show the commit output.
 
diff --git a/revision.c b/revision.c
index 599b3a66c3..3aaa77ced5 100644
--- a/revision.c
+++ b/revision.c
@@ -2339,10 +2339,24 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
 	}
 
 	if ((argcount = parse_long_opt("max-count", argv, &optarg))) {
+		if (revs->max_count_type == 1)
+			die(_("can't use --max-count with --max-count-oldest"));
 		revs->max_count = parse_count(optarg);
 		revs->no_walk = 0;
+		revs->max_count_type = 0;
 		return argcount;
+	} else if ((argcount = parse_long_opt("max-count-oldest", argv, &optarg))) {
+		if (revs->max_count_type == 0 && revs->max_count != -1)
+			die(_("can't use --max-count with --max-count-oldest"));
+		if (revs->skip_count > 0)
+			die(_("con't use --max-count-oldest with --skip"));
+		revs->max_count = parse_count(optarg);
+		revs->no_walk = 0;
+		revs->max_count_type = 1;
+		revs->max_count_stage = 0;
 	} else if ((argcount = parse_long_opt("skip", argv, &optarg))) {
+		if (revs->max_count_type == 1)
+			die(_("con't use --max-count-oldest with --skip"));
 		revs->skip_count = parse_count(optarg);
 		return argcount;
 	} else if ((*arg == '-') && isdigit(arg[1])) {
@@ -4521,15 +4535,68 @@ static struct commit *get_revision_internal(struct rev_info *revs)
 	return c;
 }
 
+static void retrieve_oldest_commits(struct rev_info *revs,
+				    struct commit_list **queue)
+{
+	struct commit *c;
+	int max_count = revs->max_count;
+	int queuei_count = 0;
+	int queueo_count = 0;
+	struct commit_list *queueo = NULL;
+	struct commit_list *queuei = NULL;
+	struct commit_list *reversed_queue = NULL;
+
+	revs->max_count = -1;
+	while ((c = get_revision_internal(revs))) {
+		c->object.flags &= ~SHOWN;
+		commit_list_insert(c, &queuei);
+		queuei_count++;
+		while (queuei_count + queueo_count > max_count) {
+			if (!queueo_count) {
+				while (queuei_count > 0) {
+					c = pop_commit(&queuei);
+					queuei_count--;
+					commit_list_insert(c, &queueo);
+					queueo_count++;
+				}
+			}
+			pop_commit(&queueo);
+			queueo_count--;
+		}
+	}
+
+	while ((c = pop_commit(&queueo)))
+		commit_list_insert(c, &reversed_queue);
+	while ((c = pop_commit(&queuei)))
+		commit_list_insert(c, &queueo);
+	while ((c = pop_commit(&queueo)))
+		commit_list_insert(c, &reversed_queue);
+
+	while ((c = pop_commit(&reversed_queue)))
+		commit_list_insert(c, queue);
+}
+
 struct commit *get_revision(struct rev_info *revs)
 {
 	struct commit *c;
 	struct commit_list *reversed;
+	struct commit_list *queue = NULL;
+
+	if (revs->max_count_type == 1 && !revs->max_count_stage) {
+		retrieve_oldest_commits(revs, &queue);
+		commit_list_free(revs->commits);
+		revs->commits = queue;
+		revs->max_count_stage = 1;
+	}
 
 	if (revs->reverse) {
 		reversed = NULL;
-		while ((c = get_revision_internal(revs)))
-			commit_list_insert(c, &reversed);
+		if (revs->max_count_type == 1)
+			while ((c = pop_commit(&revs->commits)))
+				commit_list_insert(c, &reversed);
+		else
+			while ((c = get_revision_internal(revs)))
+				commit_list_insert(c, &reversed);
 		commit_list_free(revs->commits);
 		revs->commits = reversed;
 		revs->reverse = 0;
@@ -4543,7 +4610,11 @@ struct commit *get_revision(struct rev_info *revs)
 		return c;
 	}
 
-	c = get_revision_internal(revs);
+	if (revs->max_count_stage)
+		c = pop_commit(&revs->commits);
+	else
+		c = get_revision_internal(revs);
+
 	if (c && revs->graph)
 		graph_update(revs->graph, c);
 	if (!c) {
diff --git a/revision.h b/revision.h
index 584f1338b5..e157463cb1 100644
--- a/revision.h
+++ b/revision.h
@@ -309,6 +309,8 @@ struct rev_info {
 	/* special limits */
 	int skip_count;
 	int max_count;
+	unsigned int max_count_type:1;
+	unsigned int max_count_stage:1;
 	timestamp_t max_age;
 	timestamp_t max_age_as_filter;
 	timestamp_t min_age;
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index 05cee9e41b..668c231cf1 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -1882,6 +1882,20 @@ test_expect_success 'log --graph with --name-status' '
 	test_cmp_graph --name-status tangle..reach
 '
 
+test_expect_success 'log --max-count-oldest=3 --oneline' '
+	test_when_finished rm expect &&
+	git log --oneline | tail -n3 >expect &&
+	git log --oneline --max-count-oldest=3 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'log --max-count-oldest=3 --reverse --oneline' '
+	test_when_finished rm expect &&
+	git log --oneline | tail -n3 | tac >expect &&
+	git log --oneline --max-count-oldest=3 --reverse >actual &&
+	test_cmp expect actual
+'
+
 cat >expect <<-\EOF
 * reach
 |
-- 
2.54.0


^ permalink raw reply related

* [PATCH] pretty: add diff-stat log placeholders
From: Andrey Zarubin via GitGitGadget @ 2026-04-30 19:55 UTC (permalink / raw)
  To: git; +Cc: Andrey Zarubin, Andrey Zarubin

From: Andrey Zarubin <zarandr@gmail.com>

Currently, users who want per-commit line/file change counts in
a custom log format must post-process `git log --shortstat`
output because the pretty formatter exposes no equivalent
placeholders.

Introduce `%(diff-stat:files)`, `%(diff-stat:insertions)`,
`%(diff-stat:deletions)`, and `%(diff-stat:lines)`, computed
from the same diffstat machinery as `--shortstat` and cached
once per commit during format expansion.

Short aliases are provided as `%aF`, `%aA`, and `%aR`. The
requested `%aI` and `%aD` forms are unavailable because those
names already expand to author dates, so use additions/removals
mnemonics instead.

When log output is already walking a diff, the formatter reuses
the current diff queue. Otherwise it computes a private summary
lazily, so formats without these placeholders still pay no diff
cost.

Signed-off-by: Andrey Zarubin <zarandr@gmail.com>
---
    pretty: add diff-stat log placeholders

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2284%2Fzarandr%2Fmaster-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2284/zarandr/master-v1
Pull-Request: https://github.com/git/git/pull/2284

 Documentation/pretty-formats.adoc |  12 +++
 builtin/log.c                     |   5 +
 diff.c                            |  32 ++++--
 diff.h                            |   8 ++
 log-tree.c                        |   2 +
 pretty.c                          | 166 ++++++++++++++++++++++++++++++
 pretty.h                          |   3 +
 t/t4205-log-pretty-formats.sh     | 162 +++++++++++++++++++++++++++++
 8 files changed, 381 insertions(+), 9 deletions(-)

diff --git a/Documentation/pretty-formats.adoc b/Documentation/pretty-formats.adoc
index 2ae0eb11a9..d1b574f3ad 100644
--- a/Documentation/pretty-formats.adoc
+++ b/Documentation/pretty-formats.adoc
@@ -294,6 +294,18 @@ tags are added or removed at the same time.
 `exclude=<pattern>`;; Do not consider tags matching the given
    `glob(7)` _<pattern>_, excluding the `refs/tags/` prefix.
 
+++%(diff-stat:files)++:: show the number of files changed
+++%(diff-stat:insertions)++:: show the number of inserted lines
+++%(diff-stat:deletions)++:: show the number of deleted lines
+++%(diff-stat:lines)++:: show the total number of inserted and deleted lines
++
+  These placeholders are computed like `--shortstat`. By default,
+  merge commits expand to `0` unless a merge diff mode such as `-m`,
+  `-c`, or `--cc` is in effect.
++%aF+:: short alias for `%(diff-stat:files)`
++%aA+:: short alias for `%(diff-stat:insertions)`
++%aR+:: short alias for `%(diff-stat:deletions)`
+
 +%S+:: ref name given on the command line by which the commit was reached
        (like `git log --source`), only works with `git log`
 +%e+:: encoding
diff --git a/builtin/log.c b/builtin/log.c
index 8c0939dd42..017face2c0 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -321,6 +321,11 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,
 	memset(&w, 0, sizeof(w));
 	userformat_find_requirements(NULL, &w);
 
+	if (w.diffstat) {
+		rev->diff = 1;
+		rev->diffopt.output_format |= DIFF_FORMAT_NO_OUTPUT;
+	}
+
 	if (!rev->show_notes_given && (!rev->pretty_given || w.notes))
 		rev->show_notes = 1;
 	if (rev->show_notes)
diff --git a/diff.c b/diff.c
index 397e38b41c..2f018e801a 100644
--- a/diff.c
+++ b/diff.c
@@ -3195,12 +3195,14 @@ static void show_stats(struct diffstat_t *data, struct diff_options *options)
 	strbuf_release(&out);
 }
 
-static void show_shortstats(struct diffstat_t *data, struct diff_options *options)
+void summarize_diffstat(struct diffstat_t *data,
+			struct diff_stat_summary *summary)
 {
-	int i, adds = 0, dels = 0, total_files = data->nr;
+	int i;
 
-	if (data->nr == 0)
-		return;
+	summary->files = data->nr;
+	summary->insertions = 0;
+	summary->deletions = 0;
 
 	for (i = 0; i < data->nr; i++) {
 		int added = data->files[i]->added;
@@ -3208,13 +3210,25 @@ static void show_shortstats(struct diffstat_t *data, struct diff_options *option
 
 		if (data->files[i]->is_unmerged ||
 		    (!data->files[i]->is_interesting && (added + deleted == 0))) {
-			total_files--;
-		} else if (!data->files[i]->is_binary) { /* don't count bytes */
-			adds += added;
-			dels += deleted;
+			summary->files--;
+		} else if (!data->files[i]->is_binary) {
+			summary->insertions += added;
+			summary->deletions += deleted;
 		}
 	}
-	print_stat_summary_inserts_deletes(options, total_files, adds, dels);
+}
+
+static void show_shortstats(struct diffstat_t *data, struct diff_options *options)
+{
+	struct diff_stat_summary summary;
+
+	if (data->nr == 0)
+		return;
+
+	summarize_diffstat(data, &summary);
+	print_stat_summary_inserts_deletes(options, summary.files,
+					   summary.insertions,
+					   summary.deletions);
 }
 
 static void show_numstat(struct diffstat_t *data, struct diff_options *options)
diff --git a/diff.h b/diff.h
index 7eb84aadf4..798c52138d 100644
--- a/diff.h
+++ b/diff.h
@@ -449,6 +449,12 @@ struct diffstat_t {
 	} **files;
 };
 
+struct diff_stat_summary {
+	int files;
+	int insertions;
+	int deletions;
+};
+
 enum color_diff {
 	DIFF_RESET = 0,
 	DIFF_CONTEXT = 1,
@@ -581,6 +587,8 @@ struct diff_filepair *diff_unmerge(struct diff_options *, const char *path);
 
 void compute_diffstat(struct diff_options *options, struct diffstat_t *diffstat,
 		      struct diff_queue_struct *q);
+void summarize_diffstat(struct diffstat_t *diffstat,
+			struct diff_stat_summary *summary);
 void free_diffstat_info(struct diffstat_t *diffstat);
 
 #define DIFF_SETUP_REVERSE      	1
diff --git a/log-tree.c b/log-tree.c
index 7e048701d0..aa6f6dd27d 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -881,6 +881,8 @@ void show_log(struct rev_info *opt)
 	ctx.expand_tabs_in_log = opt->expand_tabs_in_log;
 	ctx.output_encoding = get_log_output_encoding();
 	ctx.rev = opt;
+	ctx.diff_parent = parent;
+	ctx.diff_queue_present = diff_queued_diff.nr > 0;
 	if (opt->from_ident.mail_begin && opt->from_ident.name_begin)
 		ctx.from_ident = &opt->from_ident;
 	if (opt->graph)
diff --git a/pretty.c b/pretty.c
index 814803980b..a50ecd31ce 100644
--- a/pretty.c
+++ b/pretty.c
@@ -10,6 +10,7 @@
 #include "hex.h"
 #include "utf8.h"
 #include "diff.h"
+#include "diffcore.h"
 #include "pager.h"
 #include "revision.h"
 #include "string-list.h"
@@ -893,6 +894,7 @@ struct format_commit_context {
 	const struct pretty_print_context *pretty_ctx;
 	unsigned commit_header_parsed:1;
 	unsigned commit_message_parsed:1;
+	unsigned diffstat_parsed:1;
 	struct signature_check signature_check;
 	enum flush_type flush_type;
 	enum trunc_type truncate;
@@ -911,6 +913,7 @@ struct format_commit_context {
 
 	/* The following ones are relative to the result struct strbuf. */
 	size_t wrap_start;
+	struct diff_stat_summary diffstat;
 };
 
 static void parse_commit_header(struct format_commit_context *context)
@@ -939,6 +942,145 @@ static void parse_commit_header(struct format_commit_context *context)
 	context->commit_header_parsed = 1;
 }
 
+enum diff_stat_placeholder {
+	DIFF_STAT_FILES,
+	DIFF_STAT_INSERTIONS,
+	DIFF_STAT_DELETIONS,
+	DIFF_STAT_LINES,
+};
+
+static void parse_commit_diffstat(struct format_commit_context *c)
+{
+	const struct pretty_print_context *pretty_ctx = c->pretty_ctx;
+	const struct rev_info *rev = pretty_ctx->rev;
+	struct diff_options opts;
+	struct diffstat_t diffstat;
+	const struct commit *commit = c->commit;
+	const struct commit *parent = pretty_ctx->diff_parent;
+	const struct object_id *tree_oid;
+	int copied_pathspec = 0;
+	int use_current_queue = 0;
+	int use_rev_opts = rev && rev->diffopt.repo;
+
+	if (c->diffstat_parsed)
+		return;
+	c->diffstat_parsed = 1;
+	memset(&c->diffstat, 0, sizeof(c->diffstat));
+
+	if (pretty_ctx->diff_queue_present) {
+		opts = rev->diffopt;
+		compute_diffstat(&opts, &diffstat, &diff_queued_diff);
+		summarize_diffstat(&diffstat, &c->diffstat);
+		free_diffstat_info(&diffstat);
+		return;
+	}
+
+	parse_commit_or_die((struct commit *)commit);
+	tree_oid = get_commit_tree_oid(commit);
+
+	if (use_rev_opts) {
+		memcpy(&opts, &rev->diffopt, sizeof(opts));
+		copy_pathspec(&opts.pathspec, &rev->diffopt.pathspec);
+		copied_pathspec = 1;
+	} else {
+		repo_diff_setup(c->repository, &opts);
+		init_diffstat_widths(&opts);
+		opts.flags.recursive = 1;
+		opts.flags.allow_textconv = 1;
+	}
+	opts.output_format = DIFF_FORMAT_SHORTSTAT;
+	diff_setup_done(&opts);
+
+	if (!commit->parents) {
+		if (use_rev_opts && !rev->show_root_diff)
+			goto out;
+		diff_root_tree_oid(tree_oid, "", &opts);
+		use_current_queue = 1;
+		goto diffstat;
+	}
+
+	if (!parent && commit->parents->next) {
+		if (!use_rev_opts)
+			goto out;
+		if (rev->combine_merges ||
+		    (rev->separate_merges && rev->first_parent_merges))
+			parent = commit->parents->item;
+		else
+			goto out;
+	} else if (!parent) {
+		parent = commit->parents->item;
+	}
+
+	parse_commit_or_die((struct commit *)parent);
+	diff_tree_oid(get_commit_tree_oid(parent), tree_oid, "", &opts);
+	use_current_queue = 1;
+
+diffstat:
+	diffcore_std(&opts);
+	compute_diffstat(&opts, &diffstat, &diff_queued_diff);
+	summarize_diffstat(&diffstat, &c->diffstat);
+	free_diffstat_info(&diffstat);
+out:
+	if (use_current_queue) {
+		opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+		diff_flush(&opts);
+	}
+	if (copied_pathspec)
+		clear_pathspec(&opts.pathspec);
+	else
+		diff_free(&opts);
+}
+
+static void format_commit_diffstat(struct strbuf *sb,
+				   struct format_commit_context *c,
+				   enum diff_stat_placeholder which)
+{
+	int value;
+
+	parse_commit_diffstat(c);
+
+	switch (which) {
+	case DIFF_STAT_FILES:
+		value = c->diffstat.files;
+		break;
+	case DIFF_STAT_INSERTIONS:
+		value = c->diffstat.insertions;
+		break;
+	case DIFF_STAT_DELETIONS:
+		value = c->diffstat.deletions;
+		break;
+	case DIFF_STAT_LINES:
+		value = c->diffstat.insertions + c->diffstat.deletions;
+		break;
+	default:
+		BUG("unknown diff stat placeholder");
+	}
+
+	strbuf_addf(sb, "%d", value);
+}
+
+static size_t parse_diff_stat_placeholder(struct strbuf *sb,
+					  const char *placeholder,
+					  struct format_commit_context *c)
+{
+	const char *arg;
+	enum diff_stat_placeholder which;
+
+	if (skip_prefix(placeholder, "(diff-stat:files)", &arg))
+		which = DIFF_STAT_FILES;
+	else if (skip_prefix(placeholder, "(diff-stat:insertions)", &arg))
+		which = DIFF_STAT_INSERTIONS;
+	else if (skip_prefix(placeholder, "(diff-stat:deletions)", &arg))
+		which = DIFF_STAT_DELETIONS;
+	else if (skip_prefix(placeholder, "(diff-stat:lines)", &arg))
+		which = DIFF_STAT_LINES;
+	else
+		return 0;
+
+	format_commit_diffstat(sb, c, which);
+	return arg - placeholder;
+}
+
 static int istitlechar(char c)
 {
 	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
@@ -1564,6 +1706,24 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 		return 7;
 	}
 
+	if (placeholder[0] == 'a') {
+		switch (placeholder[1]) {
+		case 'F':
+			format_commit_diffstat(sb, c, DIFF_STAT_FILES);
+			return 2;
+		case 'A':
+			format_commit_diffstat(sb, c, DIFF_STAT_INSERTIONS);
+			return 2;
+		case 'R':
+			format_commit_diffstat(sb, c, DIFF_STAT_DELETIONS);
+			return 2;
+		}
+	}
+
+	res = parse_diff_stat_placeholder(sb, placeholder, c);
+	if (res)
+		return res;
+
 	switch (placeholder[0]) {
 	case 'H':		/* commit hash */
 		strbuf_addstr(sb, diff_get_color(c->auto_color, DIFF_COMMIT));
@@ -1980,6 +2140,10 @@ void userformat_find_requirements(const char *fmt, struct userformat_want *w)
 			fmt++;
 
 		switch (*fmt) {
+		case 'a':
+			if (fmt[1] == 'F' || fmt[1] == 'A' || fmt[1] == 'R')
+				w->diffstat = 1;
+			break;
 		case 'N':
 			w->notes = 1;
 			break;
@@ -1993,6 +2157,8 @@ void userformat_find_requirements(const char *fmt, struct userformat_want *w)
 		case '(':
 			if (starts_with(fmt + 1, "decorate"))
 				w->decorate = 1;
+			else if (starts_with(fmt + 1, "diff-stat:"))
+				w->diffstat = 1;
 			break;
 		}
 	}
diff --git a/pretty.h b/pretty.h
index fac699033e..7f0491e512 100644
--- a/pretty.h
+++ b/pretty.h
@@ -58,6 +58,8 @@ struct pretty_print_context {
 	 */
 	struct string_list in_body_headers;
 	int graph_width;
+	const struct commit *diff_parent;
+	unsigned diff_queue_present:1;
 };
 
 /* Check whether commit format is mail. */
@@ -75,6 +77,7 @@ struct userformat_want {
 	unsigned notes:1;
 	unsigned source:1;
 	unsigned decorate:1;
+	unsigned diffstat:1;
 };
 void userformat_find_requirements(const char *fmt, struct userformat_want *w);
 
diff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh
index 3865f6abc7..230950baed 100755
--- a/t/t4205-log-pretty-formats.sh
+++ b/t/t4205-log-pretty-formats.sh
@@ -1227,4 +1227,166 @@ test_expect_failure 'wide and decomposed characters column counting' '
 	test_cmp expected actual
 '
 
+diffstat_log_shortstat_values () {
+	git -C diffstat log --shortstat --format=tformat:commit "$@" |
+	perl -ne '
+		chomp;
+		if ($_ eq "commit") {
+			if ($seen) {
+				print "$files $insertions $deletions ",
+				      $insertions + $deletions, "\n";
+			}
+			$seen = 1;
+			($files, $insertions, $deletions) = (0, 0, 0);
+		} elsif (/^\s*(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?$/) {
+			$files = $1;
+			$insertions = defined($2) ? $2 : 0;
+			$deletions = defined($3) ? $3 : 0;
+		}
+		END {
+			if ($seen) {
+				print "$files $insertions $deletions ",
+				      $insertions + $deletions, "\n";
+			}
+		}
+	'
+}
+
+test_diff_stat_placeholders () {
+	commit=$1
+	shift &&
+	diffstat_log_shortstat_values -1 "$@" "$commit" >expected &&
+	git -C diffstat log -1 \
+		--format="%(diff-stat:files) %(diff-stat:insertions) %(diff-stat:deletions) %(diff-stat:lines)" \
+		"$@" \
+		"$commit" >actual &&
+	sed "/^$/d" <expected >expect-nonblank &&
+	sed "/^$/d" <actual >actual-nonblank &&
+	test_cmp expect-nonblank actual-nonblank
+}
+
+test_expect_success 'set up diffstat pretty-format history' '
+	test_create_repo diffstat &&
+	(
+		cd diffstat &&
+		echo root >file &&
+		git add file &&
+		test_tick &&
+		git commit -m root &&
+		root=$(git rev-parse HEAD) &&
+		main_branch=$(git symbolic-ref --quiet --short HEAD) &&
+
+		printf "line two\nline three\n" >>file &&
+		git add file &&
+		test_tick &&
+		git commit -m text &&
+		text=$(git rev-parse HEAD) &&
+
+		printf "\000\001\002\003" >bin &&
+		git add bin &&
+		test_tick &&
+		git commit -m binary &&
+		binary=$(git rev-parse HEAD) &&
+
+		echo doomed >doomed &&
+		git add doomed &&
+		test_tick &&
+		git commit -m doomed &&
+
+		git rm doomed &&
+		test_tick &&
+		git commit -m delete-doomed &&
+		delete_only=$(git rev-parse HEAD) &&
+
+		git branch topic &&
+		git mv file renamed &&
+		test_tick &&
+		git commit -m rename &&
+		rename=$(git rev-parse HEAD) &&
+
+		git checkout topic &&
+		echo topic >topic &&
+		git add topic &&
+		test_tick &&
+		git commit -m topic &&
+
+		git checkout "$main_branch" &&
+		test_tick &&
+		git merge --no-ff -m merge topic &&
+		merge=$(git rev-parse HEAD) &&
+
+		cat >../diffstat-oids <<-EOF
+		root=$root
+		text=$text
+		binary=$binary
+		delete_only=$delete_only
+		rename=$rename
+		merge=$merge
+		EOF
+	)
+'
+
+load_diffstat_oids () {
+	. ./diffstat-oids
+}
+
+test_expect_success 'diff-stat placeholders match shortstat for root commit' '
+	load_diffstat_oids &&
+	test_diff_stat_placeholders "$root"
+'
+
+test_expect_success 'diff-stat placeholders match shortstat for normal commit' '
+	load_diffstat_oids &&
+	test_diff_stat_placeholders "$text"
+'
+
+test_expect_success 'diff-stat placeholders match shortstat for binary change' '
+	load_diffstat_oids &&
+	test_diff_stat_placeholders "$binary"
+'
+
+test_expect_success 'diff-stat placeholders match shortstat for delete-only commit' '
+	load_diffstat_oids &&
+	test_diff_stat_placeholders "$delete_only"
+'
+
+test_expect_success 'diff-stat placeholders match shortstat for rename commit' '
+	load_diffstat_oids &&
+	test_diff_stat_placeholders "$rename" -M
+'
+
+test_expect_success 'diff-stat placeholders match shortstat for merge commit' '
+	load_diffstat_oids &&
+	test_diff_stat_placeholders "$merge"
+'
+
+test_expect_success 'diff-stat placeholders match shortstat for -m merge output' '
+	load_diffstat_oids &&
+	test_diff_stat_placeholders "$merge" -m
+'
+
+test_expect_success 'diff-stat placeholders match shortstat for --cc merge output' '
+	load_diffstat_oids &&
+	test_diff_stat_placeholders "$merge" --cc
+'
+
+test_expect_success 'diff-stat aliases match shortstat' '
+	load_diffstat_oids &&
+	diffstat_log_shortstat_values -1 -M "$rename" >expected &&
+	cut -d" " -f1-3 expected >expect-alias &&
+	git -C diffstat log -1 -M --format="%aF %aA %aR" "$rename" >actual &&
+	test_cmp expect-alias actual
+'
+
+test_expect_success 'multiple diff-stat placeholders reuse one summary' '
+	load_diffstat_oids &&
+	set -- $(diffstat_log_shortstat_values -1 "$text") &&
+	printf "%s %s %s %s %s %s %s\n" \
+		"$1" "$1" "$2" "$2" "$3" "$3" "$4" >expected &&
+	git -C diffstat log -1 \
+		--format="%aF %(diff-stat:files) %aA %(diff-stat:insertions) %aR %(diff-stat:deletions) %(diff-stat:lines)" \
+		"$text" >actual &&
+	test_cmp expected actual
+'
+
 test_done

base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH v6 0/6] Xdiff cleanup part 3
From: Ezekiel Newren @ 2026-04-30 21:08 UTC (permalink / raw)
  To: phillip.wood
  Cc: Ezekiel Newren via GitGitGadget, git, Yee Cheng Chin,
	René Scharfe, Jeff King, D. Ben Knoble, SZEDER Gábor
In-Reply-To: <c8b48c6a-5a20-4981-9cd4-999b40c618fc@gmail.com>

On Thu, Apr 30, 2026 at 7:35 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
>
> Hi Ezekiel
>
> On 29/04/2026 23:08, Ezekiel Newren via GitGitGadget wrote:
> > Changes in v6:
> >
> >   * implement suggestions by Phillip Wood [1,2]
> >
> > Phillip's second "if" in [1] differs from his first one. In my changes I
> > made both of them structurally the same.
>
> I was in two minds about whether to do that or not, all the changes here
> look good to me.

Hopefully this is the last revision. These changes took way longer
than I expected to get through the review process. What do you think
Junio?

^ permalink raw reply

* git rename/moved status unreliable in ruby
From: sebastien.stettler @ 2026-05-01  5:05 UTC (permalink / raw)
  To: git@vger.kernel.org

1. What did you do before the bug happened? (Steps to reproduce your issue)

when moving ruby classes between namespaces they are marked as new files and the old
ones are marked as deleted

if i only change the class name it will mark it as renamed

2. What did you expect to happen? (Expected behavior)

in the namespace state i would expected it to be marked as moved since
nothing has fundementally changed

3. What happened instead? (Actual behavior)

the file was marked as new file and the old file was marked as deleted

What's different between what you expected and what actually happened

4. Anything else you want to add:

I have demonstrated the behavior here https://github.com/billybonks/git-rename

Mostly i would like to understand what is the expectation from gits point of view in these mutations.
If this is considered something that can be improved i am happy to build out more test cases, and help with implementation.

if not, understanding the reasoning would be great

Thank you.



[System Info]
git version:
git version 2.47.1
cpu: arm64
no commit associated with this build
sizeof-long: 8
sizeof-size_t: 8
shell-path: /bin/sh
feature: fsmonitor--daemon
libcurl: 8.7.1
zlib: 1.2.12
uname: Darwin 25.3.0 Darwin Kernel Version 25.3.0: Wed Jan 28 20:51:28 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6041 arm64
compiler info: clang: 16.0.0 (clang-1600.0.26.4)
libc info: no libc information available
$SHELL (typically, interactive shell): /bin/zsh


[Enabled Hooks]


Sent with Proton Mail secure email.

^ permalink raw reply

* Re: [PATCH 6/6] t5608: add regression test for >4GB object clone
From: Jeff King @ 2026-05-01  6:38 UTC (permalink / raw)
  To: Derrick Stolee
  Cc: Johannes Schindelin via GitGitGadget, git, Johannes Schindelin
In-Reply-To: <e1e8837f-7374-4079-ba87-ab95dd156e33@gmail.com>

On Wed, Apr 29, 2026 at 09:34:21AM -0400, Derrick Stolee wrote:

> As mentioned in the previous patch, constructing this large packfile
> takes ~4 minutes in CI pipelines. That's quite a lot to handle for
> every CI run.

And for local runs, too. ;) The test suite takes less than 90 seconds to
run on my laptop, but t5608 by itself 160 seconds. Even if running it in
parallel didn't slow down the rest of the suite (which is not true,
because it's obviously hogging a whole processor the whole time), that's
still almost doubling the run-time.

I'd also worry about assuming that the trash directory can hold 4+GB
(maybe 8GB+ since we clone it?), especially since many of us use ram
disks.

That said...

> > +test_expect_success SIZE_T_IS_64BIT 'set up repo with >4GB object' '
> 
> Your prereq here prevents it from running on 32-bit builds, which is
> good. However, I wonder if it would be worth also specifying these
> tests as expensive. It's less likely that these layers will be touched
> often, so it should be enough to run these on major occasions, such as
> testing a release candidate.

I think it is already skipped in most cases, because t5608 requires the
GIT_TEST_CLONE_2GB environment variable be set. Arguably it should just
be using EXPENSIVE, too, as I do not think there is much value in having
individual flags for all of the expensive tests. I think that test just
predates the modern prereq system entirely.

> I suppose this also is a question for Junio and our process for
> validating releases. Do we have a certain cadence where we run the
> expensive tests? What has been our threshold for hiding a test case
> behind the expensive label?

AFAIK the labeling of expensive things is mostly ad-hoc, and nobody is
systematically running them. Likewise for the t/perf tests, which are
super expensive but do (very occasionally) turn up interesting
regressions.

-Peff

^ permalink raw reply

* Re: [PATCH v3 00/16] repack: incremental MIDX/bitmap-based repacking
From: Jeff King @ 2026-05-01  6:46 UTC (permalink / raw)
  To: Taylor Blau; +Cc: git, Junio C Hamano, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1777507303.git.me@ttaylorr.com>

On Wed, Apr 29, 2026 at 08:13:04PM -0400, Taylor Blau wrote:

> I expect that this should be the final reroll absent any late-breaking
> feedback. The only changes since last time are the following:
> 
>  - Various stale references to "--checksum-only" have been eradicated
>    from commit message(s).
> 
>  - `repack_prepare_midx_command()` now uses `pipe_command()` to
>    eliminate the possibility of a deadlock.
> 
>  - `write_midx_included_packs()` now cleans up after itself properly
>    when receiving multiple lines of output.
> 
>  - The errant test in t7705 was moved to the final commit (where it
>    belongs) instead of the penultimate one.

Thanks, the new pipe_command() and leak-fix for patch 14 look good to
me.

> As usual, a range-diff is included below as well for convenience. Thanks
> in advance for reviewing!

Something funny going on here:

> Range-diff against v2:
>  -:  ----------- >  1:  d6c27317c25 midx-write: handle noop writes when converting incremental chains
>  -:  ----------- >  2:  629c8d23116 midx: use `strset` for retained MIDX files
>  -:  ----------- >  3:  e303bf6a4ac midx: build `keep_hashes` array in order
>  -:  ----------- >  4:  42d76c70060 midx: use `strvec` for `keep_hashes`
>  -:  ----------- >  5:  2c80aa34fac midx: introduce `--no-write-chain-file` for incremental MIDX writes
>  7:  d9acef1334a !  6:  2a05f4b86f3 repack: allow `--write-midx=incremental` without `--geometric`
>     @@ Metadata
>      Author: Taylor Blau <me@ttaylorr.com>
>      
>       ## Commit message ##
>     -    repack: allow `--write-midx=incremental` without `--geometric`
>     +    midx: support custom `--base` for incremental MIDX writes
> [...gigantic range-diff for this commit...]

I'm not sure what's happening here. The 6th patch in v2 was "midx:
support custom --base...". The series you sent looks right, I think, but
maybe your range-diff endpoints were wrong or something?

-Peff

^ permalink raw reply

* Re: [PATCH v2] t5564: use a short path for the SOCKS proxy socket
From: Jeff King @ 2026-05-01  6:47 UTC (permalink / raw)
  To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <pull.2100.v2.git.1777450974159.gitgitgadget@gmail.com>

On Wed, Apr 29, 2026 at 08:22:54AM +0000, Johannes Schindelin via GitGitGadget wrote:

> From: Johannes Schindelin <johannes.schindelin@gmx.de>
> 
> The SOCKS proxy test introduced in 0ca365c2ed4 (http: do not ignore
> proxy path, 2024-08-02) creates a Unix domain socket in
> `$TRASH_DIRECTORY`. When the trash directory path is long (e.g.
> when running from a deeply nested worktree), the socket path can
> exceed the 108-character limit for `struct sockaddr_un.sun_path` on
> Linux, causing the test to fail with "Path length ... is longer
> than maximum supported length (108)".
> 
> We cannot work around this using the chdir trick our own socket code
> employs, because both sides of the connection are outside our control:
> the socket is created by socks4-proxy.pl via Perl's IO::Socket::UNIX,
> and the client side is libcurl.
> 
> Use `mktemp -d` to create a unique temporary directory with a short
> path, and place the socket inside it. This avoids collisions between
> concurrent test runs (e.g. `--stress`) and tmpdir-race vulnerabilities
> that a static `/tmp` path would be susceptible to.

Thanks, this looks great to me (and thank you for fleshing out the
explanation in the commit message, too).

-Peff

^ permalink raw reply

* Re: git commit silently fails
From: Jeff King @ 2026-05-01  6:49 UTC (permalink / raw)
  To: Yuri; +Cc: Git Mailing List
In-Reply-To: <a650d44b-1ab1-4a20-9b17-c82cea604acb@rawbw.com>

On Tue, Apr 28, 2026 at 01:39:35PM -0700, Yuri wrote:

> But today git failed:
> [yuri@yv /usr/ports/devel/catch2]$ /usr/local/bin/git commit --verbose . -m
> "devel/catch2: update 3.13.0 → 3.14.0"
> [yuri@yv /usr/ports/devel/catch2]$ echo $?
> 1
> [yuri@yv /usr/ports/devel/catch2]$ /usr/local/bin/git --version
> git version 2.54.0
> 
> 
> No error messages, no verbose messages, just failure ...
> What might be wrong?

I can't reproduce the problem here, so a wild guess: do you have a pager
configured for the commit command (with pager.commit, for example)? That
could be eating all of the output (including stderr) if the pager
command fails to run.

-Peff

^ permalink raw reply

* Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting
From: Phillip Wood @ 2026-05-01 10:16 UTC (permalink / raw)
  To: kristofferhaugsbakk, git; +Cc: Kristoffer Haugsbakk, ben.knoble
In-Reply-To: <V3_format-rev_new_builtin.66f@msgid.xyz>

Hi Kristoffer

On 28/04/2026 23:25, kristofferhaugsbakk@fastmail.com wrote:
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
> 
> Introduce a new builtin for pretty formatting one revision expression
> per line or commit object names found in running text.
> 
> Sometimes you want to format commits. Most of the time you’re
> walking the graph, e.g. getting a range of commits like
> `master..topic`. That’s a job for git-log(1).
> 
> But there are times when you want to format commits that you encounter
> on demand:
> 
> • Full hashes in running text that you might want to pretty-print
> • git-last-modified(1) outputs full hashes that you can do the same
>    with
> • git-cherry(1) has `-v` for commit subject, but maybe you want
>    something else?
> 
> But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
> 
> • You can’t feed commits piecemeal to these commands, one input
>    for one output; they block until standard in is closed

So you can feed them piecemeal but you don't get any output until you 
close stdin. That can be helpful as it means the calling process can 
write to "git log --stdin" and then read the output without worrying 
about getting deadlocked. The Implementation below works fine if there 
are separate processes or threads writing to and reading from "git 
format-rev", but if we want a single process to be able to read from and 
write to "git format-rev --stdin-mode=text" there will need to be a way 
to delimit message boundaries so that git knows where the input message 
ends and the caller knows where the response ends. We'll also need to be 
careful about flushing the output at the end of a processed message.

For "--stdin-mode=revs" the caller cannot know how many lines the output 
will span because formats like %(trailers) will produce a variable 
number of lines depending on which trailers are present. It is also 
possible for a rev name to span more than one line. The following 
example finds the most recent commit that mentions 'cherry-pick' in the 
subject line

:/^[^
]cherry-pick

so we need a way to delimit the input and output records there as well. 
I think the functionality implemented here is useful (transforming the 
output of 'git blame' or 'git-last-modified' are convicing examples) and 
it is probably better to do it as a command rather than adding a 
"--format" option to name-rev.

> • You can’t feed a list of possibly duplicate commits, like the output
>    of git-last-modified(1); they effectively deduplicate the output

That is definitely a problem

> Beyond these two points there’s also the input massage problem: you

s/massagge/message/?

Thanks

Phillip

> cannot feed mixed input (revisions mixed with arbitrary text).
> 
> One might hope that git-cat-file(1) can save us. But it doesn’t
> support pretty formats.
> 
> But there is one command that already both handles revisions as
> arguments, revisions on standard input, and even revisions mixed in
> with arbitrary text. Namely git-name-rev(1): the command for outputting
> symbolic names for commits.
> 
> We made some room in `builtin/name-rev.c` two commits ago. Let’s
> now add this new git-format-rev(1) command. Taking inspiration from
> git-name-rev(1), there are two modes:
> 
> • revs: like git-name-rev(1) in argv mode, but one revision per line
>    on standard in
> • text: like git-name-rev(1) with `--annotate-stdin`
> 
> ***
> 
> We need to add this command to the exception list in
> `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
> in the usage line.
> 
> Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
> ---
> 
> Notes (series):
>      v3:
>      • And don’t forget to document --notes this time
>      
>            https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/
> 
>   Documentation/git-format-rev.adoc | 148 ++++++++++++++++++++++++
>   Makefile                          |   1 +
>   builtin.h                         |   1 +
>   builtin/name-rev.c                | 186 ++++++++++++++++++++++++++++++
>   command-list.txt                  |   1 +
>   git.c                             |   1 +
>   t/t1517-outside-repo.sh           |   3 +-
>   t/t6120-describe.sh               | 104 +++++++++++++++++
>   8 files changed, 444 insertions(+), 1 deletion(-)
>   create mode 100644 Documentation/git-format-rev.adoc
> 
> diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
> new file mode 100644
> index 00000000000..d960001d750
> --- /dev/null
> +++ b/Documentation/git-format-rev.adoc
> @@ -0,0 +1,148 @@
> +git-format-rev(1)
> +=================
> +
> +NAME
> +----
> +git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
> +
> +
> +SYNOPSIS
> +--------
> +[synopsis]
> +(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
> +
> +DESCRIPTION
> +-----------
> +
> +Pretty format revisions from standard input.
> +
> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
> +
> +OPTIONS
> +-------
> +
> +`--stdin-mode=<mode>`::
> +	How to interpret standard input data:
> ++
> +--
> +`revs`:: Each line is interpreted as a commit. Any kind of revision
> +	expression can be used (see linkgit:gitrevisions[7]). Annotated
> +	tags are peeled (see linkgit:gitglossary[7]).
> ++
> +The argument `rev` is also accepted.
> +`text`:: Formats all commit object names found in freeform text. These
> +	must the full object names, i.e. abbreviated hexidecimal object
> +	names will not be interpreted.
> +--
> +
> +`--format=<pretty>`::
> +	Pretty format string.
> +
> +`--notes=<ref>`::
> +`--no-notes`::
> +	Custom notes ref. Notes are displayed when using the `%N`
> +	atom. See linkgit:git-notes[1].
> +
> +EXAMPLES
> +--------
> +
> +The command linkgit:git-last-modified[1] shows the commit that each file
> +was last modified in.
> +
> +----
> +$ git last-modified -- README.md Makefile
> +7798034171030be0909c56377a4e0e10e6d2df93	Makefile
> +c50fbb2dd225e7e82abba4380423ae105089f4d7	README.md
> +----
> +
> +We can pipe the result to this command in order to replace the object
> +name with the commit author.
> +
> +----
> +$ git last-modified -- README.md Makefile |
> +    git format-rev --stdin-mode=text --format=%an
> +Junio C Hamano	Makefile
> +Todd Zullinger	README.md
> +----
> +
> +Another example is _formatting commits in commit messages_. Given this commit message:
> +
> +----
> +Fix off-by-one error
> +
> +Fix off-by-one error introduced in
> +e83c5163316f89bfbde7d9ab23ca2e25604af290.
> +
> +We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
> +that only covered 1/3 of the faulty cases.
> +----
> +
> +We can format the commits and use par(1) to reflow the text, say in a
> +`commit-msg` hook:
> +
> +----
> +$ git config set hook.reference-commits.event commit-msg
> +$ git config set hook.reference-commits.command reference-commits
> +$ cat $(which reference-commits)
> +#/bin/sh
> +
> +msg="$1"
> +rewritten=$(mktemp)
> +git format-rev --stdin-mode=text --format=reference <"$msg" |
> +    par >"$rewritten"
> +mv "$rewritten" "$msg"
> +----
> +
> +Which will produce something like this:
> +
> +----
> +Fix off-by-one error
> +
> +Fix off-by-one error introduced in e83c5163316 (Implement better memory
> +allocator, 2005-04-07).
> +
> +We thought we fixed this in 5569bf9bbed (Fix memory allocator,
> +2005-06-22) but that only covered 1/3 of the faulty cases.
> +----
> +
> +DISCUSSION
> +----------
> +
> +This command lets you format any number of revisions in any order
> +through one command invocation. Consider the
> +linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
> +
> +1. There might be hundreds of files
> +2. Commits can be repeated, i.e. two or more files were last modified in
> +   the same commit
> +
> +Two widely-used commands which pretty formats commits are
> +linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
> +not a good fit for the above use case.
> +
> +- The output of linkgit:git-last-modified[1] would have to be processed
> +  in stages since you need to transform the first column separately and
> +  then link the author to the filename. But this is surmountable.
> +- You can feed each commit to `git show` or `git show --no-walk -1`. But
> +  that means that you need to create a process for each line.
> +- Let’s say that you want to use one process, not one per line. So you
> +  want to feed all the commits to the command. Now you face the problem
> +  that you have to feed all the commits to the commands before you get
> +  any output (this is also the case for the `--stdin` modes). In other
> +  words, you cannot loop through each line, get the author for the
> +  commit, and output the author and the filename. You need to feed all
> +  the commits, get back all the output, and match the output with the
> +  filename.
> +- But the next problem is that commands will deduplicate the input and
> +  only output one commit one single time only. Thus you cannot make the
> +  output order match the input order, since a commit could have been
> +  repeated in the original input.
> +
> +In short, it is straightforward to use these two commands if you use one
> +process per line. It is much more work if you just want to use one
> +process, but still doable. In contrast, this problem is just another
> +shell pipeline with this command.
> +
> +GIT
> +---
> +Part of the linkgit:git[1] suite
> diff --git a/Makefile b/Makefile
> index 15b1ded1a0b..cbaf91fd846 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
>   BUILT_INS += git-cherry$X
>   BUILT_INS += git-cherry-pick$X
>   BUILT_INS += git-format-patch$X
> +BUILT_INS += git-format-rev$X
>   BUILT_INS += git-fsck-objects$X
>   BUILT_INS += git-init$X
>   BUILT_INS += git-maintenance$X
> diff --git a/builtin.h b/builtin.h
> index 235c51f30e5..63813c90125 100644
> --- a/builtin.h
> +++ b/builtin.h
> @@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
>   int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
>   int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
>   int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
> +int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
>   int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
>   int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
>   int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
> diff --git a/builtin/name-rev.c b/builtin/name-rev.c
> index dc4136f4de3..b60cc766279 100644
> --- a/builtin/name-rev.c
> +++ b/builtin/name-rev.c
> @@ -18,6 +18,9 @@
>   #include "commit-graph.h"
>   #include "wildmatch.h"
>   #include "mem-pool.h"
> +#include "pretty.h"
> +#include "revision.h"
> +#include "notes.h"
>   
>   /*
>    * One day.  See the 'name a rev shortly after epoch' test in t6120 when
> @@ -272,14 +275,26 @@ struct name_ref_data {
>   	struct string_list exclude_filters;
>   };
>   
> +struct pretty_format {
> +	struct pretty_print_context ctx;
> +	struct userformat_want want;
> +};
> +
>   enum command_type {
>   	NAME_REV = 1,
> +	FORMAT_REV = 2,
> +};
> +
> +enum stdin_mode {
> +    TEXT = 1,
> +    REVS = 2,
>   };
>   
>   struct command {
>   	enum command_type type;
>   	union {
>   		int name_only;
> +		struct pretty_format *pretty_format;
>   	} u;
>   };
>   
> @@ -290,6 +305,13 @@ static void init_name_rev_command(struct command *cmd,
>   	cmd->u.name_only = name_only;
>   }
>   
> +static void init_format_rev_command(struct command *cmd,
> +				    struct pretty_format *pretty_format)
> +{
> +	cmd->type = FORMAT_REV;
> +	cmd->u.pretty_format = pretty_format;
> +}
> +
>   static struct tip_table {
>   	struct tip_table_entry {
>   		struct object_id oid;
> @@ -495,6 +517,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
>   	}
>   }
>   
> +static const char *get_format_rev(const struct commit *c,
> +				  struct pretty_format *format_ctx,
> +				  struct strbuf *buf)
> +{
> +	strbuf_reset(buf);
> +
> +	if (format_ctx->want.notes) {
> +		struct strbuf notebuf = STRBUF_INIT;
> +
> +		format_display_notes(&c->object.oid, &notebuf,
> +				     get_log_output_encoding(),
> +				     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
> +		format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
> +	}
> +
> +	pretty_print_commit(&format_ctx->ctx, c, buf);
> +	FREE_AND_NULL(format_ctx->ctx.notes_message);
> +
> +	return buf->buf;
> +}
> +
>   static void show_name(const struct object *obj,
>   		      const char *caller_name,
>   		      int always, int allow_undefined, int name_only)
> @@ -565,6 +608,19 @@ static void name_rev_line(char *p, struct command *cmd)
>   				else
>   					printf("%.*s (%s)", p_len, p_start, name);
>   				break;
> +			case FORMAT_REV:
> +				if (!oid_ret)
> +					o = parse_object(the_repository, &oid);
> +				if (o && o->type == OBJ_COMMIT)
> +					name = get_format_rev((const struct commit *)o,
> +							      cmd->u.pretty_format,
> +							      &buf);
> +				*(p + 1) = c;
> +				if (name)
> +					printf("%.*s%s", p_len - hexsz, p_start, name);
> +				else
> +					printf("%.*s", p_len, p_start);
> +				break;
>   			default:
>   				BUG("uncovered case: %d", cmd->type);
>   			}
> @@ -718,3 +774,133 @@ int cmd_name_rev(int argc,
>   	object_array_clear(&revs);
>   	return 0;
>   }
> +
> +static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
> +{
> +	if (!strcmp(stdin_mode, "text"))
> +		return TEXT;
> +	else if (!strcmp(stdin_mode, "revs") ||
> +		 !strcmp(stdin_mode, "rev"))
> +		return REVS;
> +	else
> +		die(_("'%s' needs to be either text, revs, or rev"),
> +		    "--stdin-mode");
> +}
> +
> +static char const *const format_rev_usage[] = {
> +	N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
> +	NULL
> +};
> +
> +int cmd_format_rev(int argc,
> +		   const char **argv,
> +		   const char *prefix,
> +		   struct repository *repo UNUSED)
> +{
> +	const char *format = NULL;
> +	enum stdin_mode stdin_mode;
> +	const char *stdin_mode_arg = NULL;
> +	struct display_notes_opt format_notes_opt;
> +	struct rev_info format_rev = REV_INFO_INIT;
> +	struct pretty_format format_pp = { 0 };
> +	struct string_list notes = STRING_LIST_INIT_NODUP;
> +	struct strbuf scratch_buf = STRBUF_INIT;
> +	struct command cmd;
> +	struct option opts[] = {
> +		OPT_STRING(0, "format", &format, N_("format"),
> +			   N_("pretty format to use")),
> +		OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
> +			   N_("how revs are processed")),
> +		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
> +				N_("display notes for pretty format")),
> +		OPT_END(),
> +	};
> +
> +	argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
> +
> +	if (argc > 0) {
> +		error(_("too many arguments"));
> +		usage_with_options(format_rev_usage, opts);
> +	}
> +
> +	if (!format)
> +		die(_("'%s' is required"), "--format");
> +	if (!stdin_mode_arg)
> +		die(_("'%s' is required"), "--stdin-mode");
> +
> +	init_display_notes(&format_notes_opt);
> +	stdin_mode = parse_stdin_mode(stdin_mode_arg);
> +
> +	get_commit_format(format, &format_rev);
> +	format_pp.ctx.rev = &format_rev;
> +	format_pp.ctx.fmt = format_rev.commit_format;
> +	format_pp.ctx.abbrev = format_rev.abbrev;
> +	format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
> +	format_pp.ctx.date_mode = format_rev.date_mode;
> +	format_pp.ctx.color = GIT_COLOR_AUTO;
> +
> +	userformat_find_requirements(format,
> +				     &format_pp.want);
> +	if (format_pp.want.notes) {
> +		int ignore_show_notes = 0;
> +		struct string_list_item *n;
> +
> +		for_each_string_list_item(n, &notes)
> +			enable_ref_display_notes(&format_notes_opt,
> +						 &ignore_show_notes,
> +						 n->string);
> +		load_display_notes(&format_notes_opt);
> +	}
> +
> +	init_format_rev_command(&cmd, &format_pp);
> +
> +	switch (stdin_mode) {
> +	case TEXT:
> +		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
> +			strbuf_addch(&scratch_buf, '\n');
> +			name_rev_line(scratch_buf.buf, &cmd);
> +		}
> +		break;
> +	case REVS:
> +		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
> +			struct object_id oid;
> +			struct object *object;
> +			struct object *peeled;
> +			struct commit *commit;
> +
> +			if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
> +				fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
> +					scratch_buf.buf);
> +				continue;
> +			}
> +
> +			object = parse_object(the_repository, &oid);
> +			if (!object) {
> +				fprintf(stderr, "Could not get object for %s. Skipping.\n",
> +					scratch_buf.buf);
> +				continue;
> +			}
> +
> +			peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
> +			if (peeled && peeled->type == OBJ_COMMIT)
> +				commit = (struct commit *)peeled;
> +			if (!commit) {
> +				fprintf(stderr, "Could not get commit for %s. Skipping.\n",
> +					*argv);
> +				continue;
> +			}
> +
> +			get_format_rev(commit, &format_pp, &scratch_buf);
> +			printf("%s\n", scratch_buf.buf);
> +			strbuf_release(&scratch_buf);
> +		}
> +		break;
> +	default:
> +		BUG("uncovered case: %d", stdin_mode);
> +	}
> +
> +	strbuf_release(&scratch_buf);
> +	string_list_clear(&notes, 0);
> +	release_display_notes(&format_notes_opt);
> +	return 0;
> +}
> diff --git a/command-list.txt b/command-list.txt
> index f9005cf4597..df729872dca 100644
> --- a/command-list.txt
> +++ b/command-list.txt
> @@ -108,6 +108,7 @@ git-fmt-merge-msg                       purehelpers
>   git-for-each-ref                        plumbinginterrogators
>   git-for-each-repo                       plumbinginterrogators
>   git-format-patch                        mainporcelain
> +git-format-rev                          plumbinginterrogators
>   git-fsck                                ancillaryinterrogators          complete
>   git-gc                                  mainporcelain
>   git-get-tar-commit-id                   plumbinginterrogators
> diff --git a/git.c b/git.c
> index 2b212e6675d..af5b0422b00 100644
> --- a/git.c
> +++ b/git.c
> @@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
>   	{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
>   	{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
>   	{ "format-patch", cmd_format_patch, RUN_SETUP },
> +	{ "format-rev", cmd_format_rev, RUN_SETUP },
>   	{ "fsck", cmd_fsck, RUN_SETUP },
>   	{ "fsck-objects", cmd_fsck, RUN_SETUP },
>   	{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
> diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
> index c824c1a25cf..360a9323343 100755
> --- a/t/t1517-outside-repo.sh
> +++ b/t/t1517-outside-repo.sh
> @@ -114,7 +114,8 @@ do
>   	archimport | citool | credential-netrc | credential-libsecret | \
>   	credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
>   	daemon | \
> -	difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
> +	difftool--helper | filter-branch | format-rev | fsck-objects | \
> +	get-tar-commit-id | \
>   	gui | gui--askpass | \
>   	http-backend | http-fetch | http-push | init-db | \
>   	merge-octopus | merge-one-file | merge-resolve | mergetool | \
> diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
> index 62789f76381..725f7d81b6b 100755
> --- a/t/t6120-describe.sh
> +++ b/t/t6120-describe.sh
> @@ -801,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '
>   	test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
>   '
>   
> +test_expect_success 'name-rev --format setup' '
> +	mkdir repo-format &&
> +	git -C repo-format init &&
> +	test_commit -C repo-format first &&
> +	test_commit -C repo-format second &&
> +	test_commit -C repo-format third &&
> +	test_commit -C repo-format fourth &&
> +	test_commit -C repo-format fifth &&
> +	test_commit -C repo-format sixth &&
> +	test_commit -C repo-format seventh &&
> +	test_commit -C repo-format eighth
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=revs' '
> +	cat >expect <<-\EOF &&
> +	eighth
> +	seventh
> +	fifth
> +	EOF
> +	git -C repo-format format-rev --stdin-mode=revs \
> +		--format=%s >actual <<-\EOF &&
> +	HEAD
> +	HEAD~
> +	HEAD~3
> +	EOF
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
> +	git -C repo-format log --format=reference >expect &&
> +	test_file_not_empty expect &&
> +	git -C repo-format rev-list HEAD >list &&
> +	git -C repo-format format-rev --stdin-mode=text \
> +		--format=reference <list >actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
> +	cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
> +	reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
> +	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
> +	cat >expect <<-EOF &&
> +	We thought we fixed this in ${reference}.
> +	But look at this tree: ${tree}.
> +	EOF
> +	git -C repo-format format-rev --stdin-mode=text --format=reference \
> +		>actual <<-EOF &&
> +	We thought we fixed this in ${cmit_oid}.
> +	But look at this tree: ${tree}.
> +	EOF
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev with %N (note)' '
> +	test_when_finished "git -C repo-format notes remove" &&
> +	git -C repo-format notes add -m"Make a note" &&
> +	printf "Make a note\n\n\n" >expect &&
> +	git -C repo-format format-rev --stdin-mode=revs \
> +		--format="tformat:%N" \
> +		>actual <<-\EOF &&
> +	HEAD
> +	HEAD~
> +	EOF
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
> +	# One custom notes ref
> +	test_when_finished "git -C repo-format notes remove" &&
> +	test_when_finished "git -C repo-format notes --ref=word remove" &&
> +	git -C repo-format notes add -m"default" &&
> +	git -C repo-format notes --ref=word add -m"custom" &&
> +	printf "custom\n\n" >expect &&
> +	git -C repo-format format-rev --stdin-mode=revs \
> +		--format="tformat:%N" \
> +		--notes=word \
> +		>actual <<-\EOF &&
> +	HEAD
> +	EOF
> +	test_cmp expect actual &&
> +	# Glob all
> +	printf "default\ncustom\n\n" >expect &&
> +	git -C repo-format format-rev --stdin-mode=revs \
> +		--format="tformat:%N" \
> +		--notes=* >actual <<-\EOF &&
> +	HEAD
> +	EOF
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
> +	test_when_finished "git -C repo-format tag -d version" &&
> +	git -C repo-format tag -a -m"new version" version &&
> +	cat >expect <<-\EOF &&
> +	eighth
> +	EOF
> +	git -C repo-format format-rev --stdin-mode=revs \
> +		--format=%s \
> +		--notes=* >actual <<-\EOF &&
> +	version
> +	EOF
> +	test_cmp expect actual
> +'
> +
>   test_done


^ permalink raw reply

* Re: [PATCH v3 1/1] git-gui: handle missing worktree and separated
From: Shroom Moo @ 2026-05-01 10:22 UTC (permalink / raw)
  To: mlevedahl, git; +Cc: Shroom Moo, j6t
In-Reply-To: <3b0b37ed-1a5d-4fe1-b2b4-7db67a62a06d@gmail.com>

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #1: Type: text/plain, Size: 1255 bytes --]

Hi Mark,

Thanks for catching the multi‑worktree ambiguity. The parent‑directory
fallback can surely accidentally pick the wrong worktree. As a 
reminder, the current code deliberately supports starting git-gui from 
within a regular repository's .git directory.

The comment says:
# beware that from the .git dir this sets _gitdir to .
# and _prefix to the empty string

In that case, _gitdir is ".", _prefix is empty, and the later logic
falls back to using [file dirname $_gitdir] as the worktree. A blanket
"if --is-inside-git-dir then exit" would make that case useless.

I'll send a v4 that first checks --is-bare-repository (preserving the
original bare‑repo error), then checks --is-inside-git-dir and refuses
if inside a gitdir. This accepts the .git‑startup limitation in
exchange for safety, and keeps the bare‑repo message unchanged.

Two alternatives still exist if a different trade‑off is preferred: 
- Only check --is-inside-git-dir (simpler, but makes the
bare‑repo error_popup useless).
- After --is-inside-git-dir, consult git worktree list and
switch to the single worktree if unambiguous (keeps .git‑startup
but adds complexity and a runtime dependency).

Shroom

^ permalink raw reply

* [PATCH v4 1/1] git-gui: handle missing worktree and separated gitdir
From: Shroom Moo @ 2026-05-01 10:54 UTC (permalink / raw)
  To: git; +Cc: j6t, mlevedahl, Shroom Moo
In-Reply-To: <tencent_8A236D9D4A8D8CCA7DAA083157AA8543700A@qq.com>

When git-gui is started from a directory that Git recognizes as a
valid repository but the working tree is not accessible (e.g., a
separated gitdir created by `git clone --separate-git-dir`, a bare
repository, or a case where the worktree directory was removed),
it previously called `rev-parse --show-toplevel` without error
handling, causing a fatal Tcl error ("this operation must be run
in a work tree").

Wrap the call in a `catch` and handle the failure as follows:

- For bare repositories, keep `_gitworktree` empty so that the
  existing `is_bare` check shows "Cannot use bare repository" and
  exits.  No behavioral change.

- If we are inside the gitdir (i.e., `--is-inside-git-dir` is
  `true`), refuse to start with a clear message: "Cannot start
  git-gui from inside the Git directory. Please run git-gui from a
  working tree."  This prevents dangerous operations in a separated
  gitdir or when starting from a .git subdirectory, where ordinary
  Git commands like `git status` also refuse to run.

- Otherwise, leave `_gitworktree` empty so that the existing error
  paths (e.g., missing worktree) can display their own diagnostics.

This fix intentionally drops the previous ability to start git-gui
from within a regular repository's .git subdirectory.  That behavior
was never reliable (Git itself forbids operations inside .git) and
broke completely in the multi‑worktree case where the parent
directory is not guaranteed to be the intended worktree.  The error
message guides the user to start git-gui from a worktree instead.

Signed-off-by: Shroom Moo <egg_mushroomcow@foxmail.com>
---
 git-gui/git-gui.sh | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh
index 23fe76e498..9e848aa26b 100755
--- a/git-gui/git-gui.sh
+++ b/git-gui/git-gui.sh
@@ -1169,7 +1169,17 @@ if {![file isdirectory $_gitdir]} {
 load_config 0
 apply_config
 
-set _gitworktree [git rev-parse --show-toplevel]
+if {[catch {set _gitworktree [git rev-parse --show-toplevel]}]} {
+    if {![catch {set bare [git rev-parse --is-bare-repository]}] && $bare eq {true}} {
+        set _gitworktree {}
+    } elseif {![catch {set inside [git rev-parse --is-inside-git-dir]}] && $inside eq {true}} {
+        catch {wm withdraw .}
+        error_popup [mc "Cannot start git-gui from inside the Git directory\nPlease run git-gui from a working tree"]
+        exit 1
+    } else {
+        set _gitworktree {}
+    }
+}
 
 if {$_prefix ne {}} {
 	if {$_gitworktree eq {}} {
-- 
2.52.0.windows.1


^ permalink raw reply related

* Re: [PATCH v3 1/1] git-gui: handle missing worktree and separated gitdir
From: Johannes Sixt @ 2026-05-01 13:13 UTC (permalink / raw)
  To: Mark Levedahl, Shroom Moo; +Cc: git
In-Reply-To: <3b0b37ed-1a5d-4fe1-b2b4-7db67a62a06d@gmail.com>

Am 30.04.26 um 18:18 schrieb Mark Levedahl:
> 
> 
> On 4/30/26 6:02 AM, Shroom Moo wrote:
>> When git-gui is started from a directory that Git recognizes as a
>> valid repository but the working tree is not accessible (e.g., a
>> separated gitdir created by `git clone --separate-git-dir`, a bare
>> repository, or a case where the worktree directory was removed),
>> it previously called `rev-parse --show-toplevel` without error
>> handling, causing a fatal Tcl error ("this operation must be run
>> in a work tree").
>>
>> Wrap the call in a `catch` and handle the failure as follows:
>>
>> - For bare repositories, keep `_gitworktree` empty so that the
>>   existing `is_bare` check shows "Cannot use bare repository" and
>>   exits.  No behavioral change.
>>
>> - For non‑bare repositories, try to locate the worktree from the
>>   parent directory using `git -C $parent rev-parse --show-toplevel`.
>>   If the parent is a valid worktree, change to it; this covers the
>>   legitimate case of starting git-gui from within the .git
>>   subdirectory of a normal working tree.
>>
>> - If the parent directory is not a worktree, refuse to start with
>>   a clear error message.  This prevents dangerous operations in a
>>   separated gitdir, where ordinary Git commands like `git status`
>>   would themselves refuse to run.
>>
>> The approach intentionally avoids two pitfalls:
>>
>>   - Testing `--is-inside-git-dir` before calling `--show-toplevel`
>>     would break the normal use case of starting git-gui from within
>>     a .git subdirectory (where --show-toplevel would succeed).
>>
>>   - A simple “non‑bare” check after a failed --show-toplevel would
>>     reject a normal repository whose worktree was only temporarily
>>     removed.
>>
>> The chosen method keeps the original behavior for bare repositories
>> and for regular working trees, fixes the crash, and properly blocks
>> separated gitdirs without a reachable worktree.
>>
>> Signed-off-by: Shroom Moo <egg_mushroomcow@foxmail.com>
>> ---
>>  git-gui/git-gui.sh | 23 ++++++++++++++++++++++-
>>  1 file changed, 22 insertions(+), 1 deletion(-)
>>
>> diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh
>> index 23fe76e498..2392282df3 100755
>> --- a/git-gui/git-gui.sh
>> +++ b/git-gui/git-gui.sh
>> @@ -1169,7 +1169,28 @@ if {![file isdirectory $_gitdir]} {
>>  load_config 0
>>  apply_config
>>  
>> -set _gitworktree [git rev-parse --show-toplevel]
>> +if {[catch {set _gitworktree [git rev-parse --show-toplevel]}]} {
>> +	# For bare repositories, use the existing error handling
>> +	if {![catch {set bare [git rev-parse --is-bare-repository]}] && $bare eq {true}} {
>> +		set _gitworktree {}
>> +	} else {
>> +		# Non-bare: try to find the worktree from the parent directory
>> +		set parent [file dirname [pwd]]
>> +		# Cannot go higher than the root directory; leave _gitworktree empty
>> +		if {[file normalize $parent] eq [file normalize [pwd]]} {
>> +			# Already at the filesystem root; let existing paths cope
>> +			set _gitworktree {}
>> +		} elseif {![catch {
>> +			set _gitworktree [git -C $parent rev-parse --show-toplevel]
>> +		}]} {
>> +			cd $parent
>> +		} else {
>> +			catch {wm withdraw .}
>> +			error_popup [mc "Cannot start git-gui from inside the Git directory."]
>> +			exit 1
>> +		}
>> +	}
>> +}
>>  
>>  if {$_prefix ne {}} {
>>  	if {$_gitworktree eq {}} {
> 
> A bare repository can be contained in a workdir / worktree pointing at a different gitdir:
> the logic above can thus a workdir that doesn't use the gitdir where git-gui was started.
> The bigger issue is that a gitdir can support multiple checked-out directories with no
> one-to-one mapping and no clear idea of which of those a user may have intended.
> 
> So, I believe the correct fix is to test "rev-parse --is-inside-git-dir, and if so throw a
> clear error message and exit. This will give the user something to start with to solve the
> problem of why they started git-gui in a gitdir, and not in a worktree.
We have quite a bit of code that attempts to make Git GUI work from the
.git directory and also in bare repositories.

87cd09f43e56 ("git-gui: work from the .git dir", 2010-01-23) made the
first step. The original code just used the $_gitdir as the working
directory. However, at that time we did not have alternate worktrees,
and the old code, when used today, does not work in a `git
worktree`-created worktree. Later, the `git rev-parse --show-toplevel`
call came with 38ec8d3e2652 ("git-gui: correct assignment of work-tree",
2010-10-20). However, it also changes the fall-back code slightly, so
that running Git GUI from the .git directory would not work the same way
as before and takes the .git directory as the work tree (because in the
.git directory --show-cdup is not "..", but empty).

I think we need to restructure the existing flow a bit and not just fix
a single spot in the code. I suggest this order of operation:

1. Handle the bare repository case. If not enabled, fail. Otherwise, we
can work with an empty $_gitworktree.

2. Collect --show-toplevel into $_gitworktree.

2a. If this failed: If --is-inside-git-dir is true, and the last
$_gitdir directory component is exactly ".git", take the parent
repository as $_gitworktree. Otherwise, fail.

3. Handle all the other edge cases, if any, with the so determined
$_gitworktree. (I didn't think through, yet, what needs to be done.)

-- Hannes


^ permalink raw reply

* Re: [PATCH 6/6] t5608: add regression test for >4GB object clone
From: Derrick Stolee @ 2026-05-01 13:19 UTC (permalink / raw)
  To: Jeff King; +Cc: Johannes Schindelin via GitGitGadget, git, Johannes Schindelin
In-Reply-To: <20260501063805.GA2038915@coredump.intra.peff.net>

On 5/1/2026 2:38 AM, Jeff King wrote:
> On Wed, Apr 29, 2026 at 09:34:21AM -0400, Derrick Stolee wrote:

>>> +test_expect_success SIZE_T_IS_64BIT 'set up repo with >4GB object' '
>>
>> Your prereq here prevents it from running on 32-bit builds, which is
>> good. However, I wonder if it would be worth also specifying these
>> tests as expensive. It's less likely that these layers will be touched
>> often, so it should be enough to run these on major occasions, such as
>> testing a release candidate.
> 
> I think it is already skipped in most cases, because t5608 requires the
> GIT_TEST_CLONE_2GB environment variable be set. Arguably it should just
> be using EXPENSIVE, too, as I do not think there is much value in having
> individual flags for all of the expensive tests. I think that test just
> predates the modern prereq system entirely.

Thanks for the extra details here! That helps avoid the issues that I
was thinking about, but maybe doubling-down and adding EXPENSIVE is
still worth it. 
>> I suppose this also is a question for Junio and our process for
>> validating releases. Do we have a certain cadence where we run the
>> expensive tests? What has been our threshold for hiding a test case
>> behind the expensive label?
> 
> AFAIK the labeling of expensive things is mostly ad-hoc, and nobody is
> systematically running them. Likewise for the t/perf tests, which are
> super expensive but do (very occasionally) turn up interesting
> regressions.
I used to be more diligent about running the performance tests myself
around release windows. The EXPENSIVE tests would also be good to do
on rc0. I will contemplate how to put this into my routine.

Thanks,
-Stolee


^ permalink raw reply

* [PATCH v2 0/2] status: improve rebase todo list parsing
From: Phillip Wood @ 2026-05-01 15:16 UTC (permalink / raw)
  To: git; +Cc: Elijah Newren, Patrick Steinhardt, Phillip Wood
In-Reply-To: <cover.1776697483.git.phillip.wood@dunelm.org.uk>

When there is rebase in progress "git status" displays the last couple
of completed and the next couple of pending commands from the todo
list. When it does this is tries to abbreviate the object ids of
the commits to be picked. Unfortunately it does not abbreviate the
object ids when the line starts with "fixup -C" or "merge -C". It
also mistakenly replaces the refname in "reset main" and "update-ref
refs/heads/main" with the object id that the ref points to.

This series fixes that. The first patch factors out the sequencer
code that parses the command names in the todo list. The second patch
uses that function in "git status" to parse the command names so that
it knows whether the line may contain "-C" and whether there is an
object id that should be abbreviated.

Thanks to Elijah and Patrick for their comments in V1.

Changes since V1:

Patch 1 - Expanded commit message and added a code comment.

Patch 2 - Fixed some typos, added a code comment and clarified that -Wswitch
          is included by -Wall.

Base-Commit: 8c9303b1ffae5b745d1b0a1f98330cf7944d8db0
Published-As: https://github.com/phillipwood/git/releases/tag/pw%2Fimprove-status-todo-list-parsing%2Fv2
View-Changes-At: https://github.com/phillipwood/git/compare/8c9303b1f...b80bc1e0a
Fetch-It-Via: git fetch https://github.com/phillipwood/git pw/improve-status-todo-list-parsing/v2


Phillip Wood (2):
  sequencer: factor out parsing of todo commands
  status: improve rebase todo list parsing

 sequencer.c            |  45 +++++++++-----
 sequencer.h            |   8 +++
 t/t7512-status-help.sh |  74 +++++++++++++++--------
 wt-status.c            | 131 +++++++++++++++++++++++++++++++++--------
 4 files changed, 191 insertions(+), 67 deletions(-)

Range-diff against v1:
1:  3d5135a719 ! 1:  d27dddff93 sequencer: factor out parsing of todo commands
    @@ Metadata
      ## Commit message ##
         sequencer: factor out parsing of todo commands
     
    -    Move the code that parses todo commands into a separate function so that
    -    it can be shared with "git status" in the next commit.
    +    Move the code that parses todo commands into a separate function so
    +    that it can be shared with "git status" in the next commit. As we
    +    know the input is NUL terminated we do not pass a pointer to the end
    +    of the line and instead test for a blank line by looking for NUL, CR
    +    LF, or LF. We use starts_with() instead of starts_with_mem() for the
    +    same reason. This results in slightly different behavior when there
    +    a CR at the start of the line that is not followed by LF. Previously
    +    such a line was treated as a comment rather than an invalid line.
     
         Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
     
    @@ sequencer.h: int read_author_script(const char *path, char **name, char **email,
      int write_basic_state(struct replay_opts *opts, const char *head_name,
      		      struct commit *onto, const struct object_id *orig_head);
      void sequencer_post_commit_cleanup(struct repository *r, int verbose);
    ++
    ++/*
    ++ * Try to parse the todo command pointed to by *p. On success sets cmd,
    ++ * advances p and returns true. On failure returns false, leaves p and
    ++ * cmd unchanged.
    ++ */
     +bool sequencer_parse_todo_command(const char **p, enum todo_command *cmd);
    ++
      int sequencer_get_last_command(struct repository* r,
      			       enum replay_action *action);
      int sequencer_determine_whence(struct repository *r, enum commit_whence *whence);
2:  d20dc1f655 ! 2:  b80bc1e0a2 status: improve rebase todo list parsing
    @@ Commit message
     
         When there is rebase in progress "git status" displays the last couple
         of completed and the next couple of pending commands from the todo
    -    list. When it does this is tries to abbreviate the object ids of
    +    list. When it does this it tries to abbreviate the object ids of
         the commits to be picked. Unfortunately it does not abbreviate the
         object ids when the line starts with "fixup -C" or "merge -C". It
         also mistakenly replaces the refname in "reset main" and "update-ref
    @@ Commit message
         wider variety of commands. Only the pending commands in the tests
         are changed to avoid removing existing coverage.
     
    +    Helped-by: Elijah Newren <newren@gmail.com>
         Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
     
      ## t/t7512-status-help.sh ##
    @@ wt-status.c: static int split_commit_in_progress(struct wt_status *s)
      	return split_in_progress;
     +}
     +
    ++/*
    ++ * If the whitespace-delimited token starting at or just after *pp *
    ++ * is a hex object id that is longer than its default abbreviation, *
    ++ * abbreviate it in-place, shrinking `line` accordingly. On return
    ++ * *pp points one past the (possibly abbreviated) token. Leaves both
    ++ * `line` and *pp-advanced-past-the-token unchanged in all other cases
    ++ * (non-hex token, unresolvable, or a refname that happens to consist
    ++ * only of hex digits).
    ++ */
     +static void abbrev_oid_in_line(struct repository *r,
     +			       struct strbuf *line, char **pp)
     +{
    @@ wt-status.c: static int split_commit_in_progress(struct wt_status *s)
     +	p += strspn(p, " \t");
     +	end_of_object_name = p + strcspn(p, " \t");
     +	/*
    -+	 * The for "merge" and "reset" the object name may be a label or
    ++	 * For "merge" and "reset" the object name may be a label or
     +	 * ref rather than a hex object id. Only abbreviate the object
     +	 * name if it is a hex object id.
     +	 */
    @@ wt-status.c: static int split_commit_in_progress(struct wt_status *s)
     +	*pp = end_of_object_name;
     +}
     +
    -+static void skip_dash_c(char **pp) {
    ++static void skip_dash_c(char **pp)
    ++{
     +	char *p = *pp;
     +
     +	p += strspn(p, " \t");
    -+	/* The (void) cast is required to silence -Wunused_value */
    ++	/* The (void) cast is required to silence -Wunused-value */
     +	(void)(skip_prefix(p, "-C", &p) || skip_prefix(p, "-c", &p));
     +	*pp = p;
      }
    @@ wt-status.c: static int split_commit_in_progress(struct wt_status *s)
     +
     +	/*
     +	 * Avoid "default" and instead list all the other commands so
    -+	 * that -Wswitch warns if a new command is added without handling
    -+	 * it in this function.
    ++	 * that -Wswitch (which is included in -Wall) warns if a new
    ++	 * command is added without handling it in this function.
     +	 */
     +	case TODO_BREAK:
     +	case TODO_EXEC:
-- 
2.54.0.rc1.174.gd833f386ac5.dirty


^ permalink raw reply

* [PATCH v2 1/2] sequencer: factor out parsing of todo commands
From: Phillip Wood @ 2026-05-01 15:16 UTC (permalink / raw)
  To: git; +Cc: Elijah Newren, Patrick Steinhardt, Phillip Wood
In-Reply-To: <cover.1777648598.git.phillip.wood@dunelm.org.uk>

From: Phillip Wood <phillip.wood@dunelm.org.uk>

Move the code that parses todo commands into a separate function so
that it can be shared with "git status" in the next commit. As we
know the input is NUL terminated we do not pass a pointer to the end
of the line and instead test for a blank line by looking for NUL, CR
LF, or LF. We use starts_with() instead of starts_with_mem() for the
same reason. This results in slightly different behavior when there
a CR at the start of the line that is not followed by LF. Previously
such a line was treated as a comment rather than an invalid line.

Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
---
 sequencer.c | 45 ++++++++++++++++++++++++++++++---------------
 sequencer.h |  8 ++++++++
 2 files changed, 38 insertions(+), 15 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index b7d8dca47f..b8e860434a 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2625,6 +2625,27 @@ static int is_command(enum todo_command command, const char **bol)
 		return 1;
 	}
 	return 0;
+}
+
+bool sequencer_parse_todo_command(const char **p, enum todo_command *cmd)
+{
+	const char *s = *p;
+
+	for (int i = 0; i < TODO_COMMENT; i++)
+		if (is_command(i, p)) {
+			*cmd = i;
+			return true;
+		}
+
+	if (starts_with(s, comment_line_str)) {
+		*cmd = TODO_COMMENT;
+		return true;
+	} else if (s[0] == '\n' || (s[0] == '\r' && s[1] == '\n') || !s[0]) {
+		*cmd = TODO_COMMENT;
+		return true;
+	}
+
+	return false;
 }
 
 static int check_label_or_ref_arg(enum todo_command command, const char *arg)
@@ -2716,29 +2737,23 @@ static int parse_insn_line(struct repository *r, struct replay_opts *opts,
 {
 	struct object_id commit_oid;
 	char *end_of_object_name;
-	int i, saved, status, padding;
+	int saved, status, padding;
 
 	item->flags = 0;
 
 	/* left-trim */
 	bol += strspn(bol, " \t");
 
-	if (bol == eol || *bol == '\r' || starts_with_mem(bol, eol - bol, comment_line_str)) {
-		item->command = TODO_COMMENT;
-		item->commit = NULL;
-		item->arg_offset = bol - buf;
-		item->arg_len = eol - bol;
-		return 0;
-	}
-
-	for (i = 0; i < TODO_COMMENT; i++)
-		if (is_command(i, &bol)) {
-			item->command = i;
-			break;
-		}
-	if (i >= TODO_COMMENT)
+	if (!sequencer_parse_todo_command(&bol, &item->command))
 		return error(_("invalid command '%.*s'"),
 			     (int)strcspn(bol, " \t\r\n"), bol);
+
+	if (item->command == TODO_COMMENT) {
+		item->commit = NULL;
+		item->arg_offset = bol - buf;
+		item->arg_len = eol - bol;
+		return 0;
+	}
 
 	/* Eat up extra spaces/ tabs before object name */
 	padding = strspn(bol, " \t");
diff --git a/sequencer.h b/sequencer.h
index a6fa670c7c..28fabef926 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -262,6 +262,14 @@ int read_author_script(const char *path, char **name, char **email, char **date,
 int write_basic_state(struct replay_opts *opts, const char *head_name,
 		      struct commit *onto, const struct object_id *orig_head);
 void sequencer_post_commit_cleanup(struct repository *r, int verbose);
+
+/*
+ * Try to parse the todo command pointed to by *p. On success sets cmd,
+ * advances p and returns true. On failure returns false, leaves p and
+ * cmd unchanged.
+ */
+bool sequencer_parse_todo_command(const char **p, enum todo_command *cmd);
+
 int sequencer_get_last_command(struct repository* r,
 			       enum replay_action *action);
 int sequencer_determine_whence(struct repository *r, enum commit_whence *whence);
-- 
2.54.0.rc1.174.gd833f386ac5.dirty


^ permalink raw reply related

* [PATCH v2 2/2] status: improve rebase todo list parsing
From: Phillip Wood @ 2026-05-01 15:16 UTC (permalink / raw)
  To: git; +Cc: Elijah Newren, Patrick Steinhardt, Phillip Wood
In-Reply-To: <cover.1777648598.git.phillip.wood@dunelm.org.uk>

From: Phillip Wood <phillip.wood@dunelm.org.uk>

When there is rebase in progress "git status" displays the last couple
of completed and the next couple of pending commands from the todo
list. When it does this it tries to abbreviate the object ids of
the commits to be picked. Unfortunately it does not abbreviate the
object ids when the line starts with "fixup -C" or "merge -C". It
also mistakenly replaces the refname in "reset main" and "update-ref
refs/heads/main" with the object id that the ref points to. Use
the function added in the last commit to parse the command name and
only try to abbreviate the argument for commands that take an object
id. When trying to abbreviate an object id, only replace the object
name if it starts with the abbreviated object id so that labels or
branch names that contain only hex digits are left unchanged.

Comments are now processed after stripping any leading
whitespace from the line. This matches what the sequencer does in
parse_insn_line(). The existing test cases are updated to test a
wider variety of commands. Only the pending commands in the tests
are changed to avoid removing existing coverage.

Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
---
 t/t7512-status-help.sh |  74 +++++++++++++++--------
 wt-status.c            | 131 +++++++++++++++++++++++++++++++++--------
 2 files changed, 153 insertions(+), 52 deletions(-)

diff --git a/t/t7512-status-help.sh b/t/t7512-status-help.sh
index 08e82f7914..aca4b6d332 100755
--- a/t/t7512-status-help.sh
+++ b/t/t7512-status-help.sh
@@ -224,7 +224,7 @@ test_expect_success 'status when splitting a commit' '
 	COMMIT3=$(git rev-parse --short split_commit) &&
 	test_commit four_split main.txt four &&
 	COMMIT4=$(git rev-parse --short split_commit) &&
-	FAKE_LINES="1 edit 2 3" &&
+	FAKE_LINES="reword 1 edit 2 fixup_-C 3" &&
 	export FAKE_LINES &&
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD~3) &&
@@ -233,10 +233,10 @@ test_expect_success 'status when splitting a commit' '
 	cat >expected <<EOF &&
 interactive rebase in progress; onto $ONTO
 Last commands done (2 commands done):
-   pick $COMMIT2 # two_split
+   reword $COMMIT2 # two_split
    edit $COMMIT3 # three_split
 Next command to do (1 remaining command):
-   pick $COMMIT4 # four_split
+   fixup -C $COMMIT4 # four_split
   (use "git rebase --edit-todo" to view and edit)
 You are currently splitting a commit while rebasing branch '\''split_commit'\'' on '\''$ONTO'\''.
   (Once your working directory is clean, run "git rebase --continue")
@@ -297,7 +297,7 @@ test_expect_success 'prepare for several edits' '
 
 
 test_expect_success 'status: (continue first edit) second edit' '
-	FAKE_LINES="edit 1 edit 2 3" &&
+	FAKE_LINES="edit 1 edit 2 drop 3" &&
 	export FAKE_LINES &&
 	test_when_finished "git rebase --abort" &&
 	COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -312,7 +312,7 @@ Last commands done (2 commands done):
    edit $COMMIT2 # two_edits
    edit $COMMIT3 # three_edits
 Next command to do (1 remaining command):
-   pick $COMMIT4 # four_edits
+   drop $COMMIT4 # four_edits
   (use "git rebase --edit-todo" to view and edit)
 You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
   (use "git commit --amend" to amend the current commit)
@@ -327,7 +327,7 @@ EOF
 
 test_expect_success 'status: (continue first edit) second edit and split' '
 	git reset --hard several_edits &&
-	FAKE_LINES="edit 1 edit 2 3" &&
+	FAKE_LINES="edit 1 edit 2 squash 3" &&
 	export FAKE_LINES &&
 	test_when_finished "git rebase --abort" &&
 	COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -343,7 +343,7 @@ Last commands done (2 commands done):
    edit $COMMIT2 # two_edits
    edit $COMMIT3 # three_edits
 Next command to do (1 remaining command):
-   pick $COMMIT4 # four_edits
+   squash $COMMIT4 # four_edits
   (use "git rebase --edit-todo" to view and edit)
 You are currently splitting a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
   (Once your working directory is clean, run "git rebase --continue")
@@ -362,7 +362,7 @@ EOF
 
 test_expect_success 'status: (continue first edit) second edit and amend' '
 	git reset --hard several_edits &&
-	FAKE_LINES="edit 1 edit 2 3" &&
+	FAKE_LINES="edit 1 edit 2 fixup 3" &&
 	export FAKE_LINES &&
 	test_when_finished "git rebase --abort" &&
 	COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -378,7 +378,7 @@ Last commands done (2 commands done):
    edit $COMMIT2 # two_edits
    edit $COMMIT3 # three_edits
 Next command to do (1 remaining command):
-   pick $COMMIT4 # four_edits
+   fixup $COMMIT4 # four_edits
   (use "git rebase --edit-todo" to view and edit)
 You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
   (use "git commit --amend" to amend the current commit)
@@ -393,7 +393,7 @@ EOF
 
 test_expect_success 'status: (amend first edit) second edit' '
 	git reset --hard several_edits &&
-	FAKE_LINES="edit 1 edit 2 3" &&
+	FAKE_LINES="edit 1 edit 2 fixup_-c 3" &&
 	export FAKE_LINES &&
 	test_when_finished "git rebase --abort" &&
 	COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -409,7 +409,7 @@ Last commands done (2 commands done):
    edit $COMMIT2 # two_edits
    edit $COMMIT3 # three_edits
 Next command to do (1 remaining command):
-   pick $COMMIT4 # four_edits
+   fixup -c $COMMIT4 # four_edits
   (use "git rebase --edit-todo" to view and edit)
 You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
   (use "git commit --amend" to amend the current commit)
@@ -460,14 +460,20 @@ EOF
 
 test_expect_success 'status: (amend first edit) second edit and amend' '
 	git reset --hard several_edits &&
-	FAKE_LINES="edit 1 edit 2 3" &&
-	export FAKE_LINES &&
 	test_when_finished "git rebase --abort" &&
 	COMMIT2=$(git rev-parse --short several_edits^^) &&
 	COMMIT3=$(git rev-parse --short several_edits^) &&
 	COMMIT4=$(git rev-parse --short several_edits) &&
 	ONTO=$(git rev-parse --short HEAD~3) &&
-	git rebase -i HEAD~3 &&
+	cat >todo <<-EOF &&
+	edit several_edits^^ # two_edits
+	edit several_edits^ # three_edits
+	merge $(git rev-parse main) $(git rev-parse several_edits)
+	EOF
+	(
+		set_replace_editor todo &&
+		git rebase -i HEAD~3
+	) &&
 	git commit --amend -m "c" &&
 	git rebase --continue &&
 	git commit --amend -m "d" &&
@@ -477,7 +483,7 @@ Last commands done (2 commands done):
    edit $COMMIT2 # two_edits
    edit $COMMIT3 # three_edits
 Next command to do (1 remaining command):
-   pick $COMMIT4 # four_edits
+   merge $(git rev-parse --short main) $COMMIT4
   (use "git rebase --edit-todo" to view and edit)
 You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
   (use "git commit --amend" to amend the current commit)
@@ -525,14 +531,21 @@ EOF
 
 test_expect_success 'status: (split first edit) second edit and split' '
 	git reset --hard several_edits &&
-	FAKE_LINES="edit 1 edit 2 3" &&
-	export FAKE_LINES &&
 	test_when_finished "git rebase --abort" &&
 	COMMIT2=$(git rev-parse --short several_edits^^) &&
 	COMMIT3=$(git rev-parse --short several_edits^) &&
 	COMMIT4=$(git rev-parse --short several_edits) &&
+	cat >todo <<-EOF &&
+	edit several_edits^^ # two_edits
+	edit several_edits^ # three_edits
+	reset $(git rev-parse main)
+	merge -C several_edits topic # title
+	EOF
 	ONTO=$(git rev-parse --short HEAD~3) &&
-	git rebase -i HEAD~3 &&
+	(
+		set_replace_editor todo &&
+		git rebase -i HEAD~3
+	) &&
 	git reset HEAD^ &&
 	git add main.txt &&
 	git commit --amend -m "f" &&
@@ -543,8 +556,9 @@ interactive rebase in progress; onto $ONTO
 Last commands done (2 commands done):
    edit $COMMIT2 # two_edits
    edit $COMMIT3 # three_edits
-Next command to do (1 remaining command):
-   pick $COMMIT4 # four_edits
+Next commands to do (2 remaining commands):
+   reset $(git rev-parse --short main)
+   merge -C $COMMIT4 topic # title
   (use "git rebase --edit-todo" to view and edit)
 You are currently splitting a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
   (Once your working directory is clean, run "git rebase --continue")
@@ -563,14 +577,21 @@ EOF
 
 test_expect_success 'status: (split first edit) second edit and amend' '
 	git reset --hard several_edits &&
-	FAKE_LINES="edit 1 edit 2 3" &&
-	export FAKE_LINES &&
 	test_when_finished "git rebase --abort" &&
+	git branch cafe main &&
 	COMMIT2=$(git rev-parse --short several_edits^^) &&
 	COMMIT3=$(git rev-parse --short several_edits^) &&
-	COMMIT4=$(git rev-parse --short several_edits) &&
+	cat >todo <<-EOF &&
+	edit several_edits^^ # two_edits
+	edit several_edits^ # three_edits
+	update-ref refs/heads/main
+	reset cafe
+	EOF
 	ONTO=$(git rev-parse --short HEAD~3) &&
-	git rebase -i HEAD~3 &&
+	(
+		set_replace_editor todo &&
+		git rebase -i HEAD~3
+	) &&
 	git reset HEAD^ &&
 	git add main.txt &&
 	git commit --amend -m "g" &&
@@ -581,8 +602,9 @@ interactive rebase in progress; onto $ONTO
 Last commands done (2 commands done):
    edit $COMMIT2 # two_edits
    edit $COMMIT3 # three_edits
-Next command to do (1 remaining command):
-   pick $COMMIT4 # four_edits
+Next commands to do (2 remaining commands):
+   update-ref refs/heads/main
+   reset cafe
   (use "git rebase --edit-todo" to view and edit)
 You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
   (use "git commit --amend" to amend the current commit)
diff --git a/wt-status.c b/wt-status.c
index 479ccc3304..94c159d9d4 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -1363,6 +1363,61 @@ static int split_commit_in_progress(struct wt_status *s)
 	free(rebase_orig_head);
 
 	return split_in_progress;
+}
+
+/*
+ * If the whitespace-delimited token starting at or just after *pp *
+ * is a hex object id that is longer than its default abbreviation, *
+ * abbreviate it in-place, shrinking `line` accordingly. On return
+ * *pp points one past the (possibly abbreviated) token. Leaves both
+ * `line` and *pp-advanced-past-the-token unchanged in all other cases
+ * (non-hex token, unresolvable, or a refname that happens to consist
+ * only of hex digits).
+ */
+static void abbrev_oid_in_line(struct repository *r,
+			       struct strbuf *line, char **pp)
+{
+	char *p = *pp;
+	char *end_of_object_name, saved;
+	const char *abbrev;
+	struct object_id oid;
+	bool have_oid;
+
+	p += strspn(p, " \t");
+	end_of_object_name = p + strcspn(p, " \t");
+	/*
+	 * For "merge" and "reset" the object name may be a label or
+	 * ref rather than a hex object id. Only abbreviate the object
+	 * name if it is a hex object id.
+	 */
+	for (const char *q = p; q < end_of_object_name; q++) {
+		if (!isxdigit(*q))
+			goto out;
+	}
+	saved = *end_of_object_name;
+	*end_of_object_name = '\0';
+	have_oid = !repo_get_oid(r, p, &oid);
+	*end_of_object_name = saved;
+	if (!have_oid)
+		goto out; /* object name was a label */
+	abbrev = repo_find_unique_abbrev(r, &oid, DEFAULT_ABBREV);
+	if (!starts_with(p, abbrev))
+		goto out; /* object name was a refname containing only xdigits */
+	p += strlen(abbrev);
+	strbuf_remove(line, p - line->buf, end_of_object_name - p);
+	end_of_object_name = p;
+out:
+	*pp = end_of_object_name;
+}
+
+static void skip_dash_c(char **pp)
+{
+	char *p = *pp;
+
+	p += strspn(p, " \t");
+	/* The (void) cast is required to silence -Wunused-value */
+	(void)(skip_prefix(p, "-C", &p) || skip_prefix(p, "-c", &p));
+	*pp = p;
 }
 
 /*
@@ -1371,29 +1426,57 @@ static int split_commit_in_progress(struct wt_status *s)
  * into
  * "pick d6a2f03 some message"
  *
- * The function assumes that the line does not contain useless spaces
- * before or after the command.
+ * Returns false on comment lines, true otherwise
  */
-static void abbrev_oid_in_line(struct repository *r, struct strbuf *line)
+static bool format_todo_line(struct repository *r, struct strbuf *line)
 {
-	struct string_list split = STRING_LIST_INIT_DUP;
-	struct object_id oid;
-
-	if (starts_with(line->buf, "exec ") ||
-	    starts_with(line->buf, "x ") ||
-	    starts_with(line->buf, "label ") ||
-	    starts_with(line->buf, "l "))
-		return;
-
-	if ((2 <= string_list_split(&split, line->buf, " ", 2)) &&
-	    !repo_get_oid(r, split.items[1].string, &oid)) {
-		strbuf_reset(line);
-		strbuf_addf(line, "%s ", split.items[0].string);
-		strbuf_add_unique_abbrev(line, &oid, DEFAULT_ABBREV);
-		for (size_t i = 2; i < split.nr; i++)
-			strbuf_addf(line, " %s", split.items[i].string);
+	enum todo_command cmd;
+	char *p = line->buf;
+
+	if (!sequencer_parse_todo_command((const char**)&p, &cmd))
+		return true; /* keep invalid lines */
+
+	switch (cmd) {
+	case TODO_COMMENT:
+		return false;
+
+	case TODO_MERGE:
+		skip_dash_c(&p);
+		while (true) {
+			p += strspn(p, " \t");
+			if (!p[0] || (p[0] == '#' && (!p[1] || isspace(p[1]))))
+				break;
+			abbrev_oid_in_line(r, line, &p);
+		}
+		break;
+
+	case TODO_FIXUP:
+		skip_dash_c(&p);
+		/* fallthrough */
+	case TODO_DROP:
+	case TODO_EDIT:
+	case TODO_PICK:
+	case TODO_RESET:
+	case TODO_REVERT:
+	case TODO_REWORD:
+	case TODO_SQUASH:
+		abbrev_oid_in_line(r, line, &p);
+		break;
+
+	/*
+	 * Avoid "default" and instead list all the other commands so
+	 * that -Wswitch (which is included in -Wall) warns if a new
+	 * command is added without handling it in this function.
+	 */
+	case TODO_BREAK:
+	case TODO_EXEC:
+	case TODO_LABEL:
+	case TODO_NOOP:
+	case TODO_UPDATE_REF:
+		break;
 	}
-	string_list_clear(&split, 0);
+
+	return true;
 }
 
 static int read_rebase_todolist(struct repository *r, const char *fname, struct string_list *lines)
@@ -1411,13 +1494,9 @@ static int read_rebase_todolist(struct repository *r, const char *fname, struct
 			  repo_git_path_replace(r, &buf, "%s", fname));
 	}
 	while (!strbuf_getline_lf(&buf, f)) {
-		if (starts_with(buf.buf, comment_line_str))
-			continue;
 		strbuf_trim(&buf);
-		if (!buf.len)
-			continue;
-		abbrev_oid_in_line(r, &buf);
-		string_list_append(lines, buf.buf);
+		if (format_todo_line(r, &buf))
+			string_list_append(lines, buf.buf);
 	}
 	fclose(f);
 
-- 
2.54.0.rc1.174.gd833f386ac5.dirty


^ permalink raw reply related

* [PATCH] worktree: rename get_worktree_from_repository()
From: Phillip Wood @ 2026-05-01 15:20 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Junio C Hamano

From: Phillip Wood <phillip.wood@dunelm.org.uk>

get_worktree_from_repository() returns a struct worktree that
describes the worktree that the repository argument would operate
on. Since 0f779147602 (worktree: remove "the_repository" from
is_current_worktree(), 2026-03-26) that worktree is always the
"current" worktree. Change the name to get_current_worktee() to
reflect better what the function does.

Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
---
Base-Commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
Published-As: https://github.com/phillipwood/git/releases/tag/pw%2Frename-worktree-from-repository%2Fv1
View-Changes-At: https://github.com/phillipwood/git/compare/94f057755...bd4839613
Fetch-It-Via: git fetch https://github.com/phillipwood/git pw/rename-worktree-from-repository/v1

 worktree.c  | 2 +-
 worktree.h  | 2 +-
 wt-status.c | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/worktree.c b/worktree.c
index d874e23b4e..340b4ed777 100644
--- a/worktree.c
+++ b/worktree.c
@@ -66,7 +66,7 @@ static int is_current_worktree(struct worktree *wt)
 	return is_current;
 }
 
-struct worktree *get_worktree_from_repository(struct repository *repo)
+struct worktree *get_current_worktree(struct repository *repo)
 {
 	struct worktree *wt = xcalloc(1, sizeof(*wt));
 	char *gitdir = absolute_pathdup(repo->gitdir);
diff --git a/worktree.h b/worktree.h
index d19ec29dbb..1075409f9a 100644
--- a/worktree.h
+++ b/worktree.h
@@ -42,7 +42,7 @@ struct worktree **get_worktrees_without_reading_head(void);
  * Construct a struct worktree corresponding to repo->gitdir and
  * repo->worktree.
  */
-struct worktree *get_worktree_from_repository(struct repository *repo);
+struct worktree *get_current_worktree(struct repository *repo);
 
 /*
  * Returns 1 if linked worktrees exist, 0 otherwise.
diff --git a/wt-status.c b/wt-status.c
index 479ccc3304..c12fca7078 100644
--- a/wt-status.c
+++ b/wt-status.c
@@ -1827,7 +1827,7 @@ void wt_status_get_state(struct repository *r,
 	struct stat st;
 	struct object_id oid;
 	enum replay_action action;
-	struct worktree *wt = get_worktree_from_repository(r);
+	struct worktree *wt = get_current_worktree(r);
 
 	if (!stat(git_path_merge_head(r), &st)) {
 		wt_status_check_rebase(wt, state);
-- 
2.54.0.rc1.174.gd833f386ac5.dirty


^ permalink raw reply related

* Re: git rename/moved status unreliable in ruby
From: Phillip Wood @ 2026-05-01 15:30 UTC (permalink / raw)
  To: sebastien.stettler, git@vger.kernel.org
In-Reply-To: <OsOzcjEwvHCQSghLE8LD_wHb_jDlil9I88OUuhpiRONnVd1o9p3gStbK1mx4q7OwY3ePtbZO-BBgTNOCeJ2DMyvBsdlMhRmDrTP894KP5xo=@proton.me>

Hi Sebastien

On 01/05/2026 06:05, sebastien.stettler wrote:
> 1. What did you do before the bug happened? (Steps to reproduce your issue)
> 
> when moving ruby classes between namespaces they are marked as new files and the old
> ones are marked as deleted
> 
> if i only change the class name it will mark it as renamed

Rename detection is based on how similar the two files are. Looking at 
the example you linked to below you're changing a file that looks like

module Math
   class Calculator
     def add(a, b)
       a + b
       ...
     end
   end
end

to

module Math
   module Calculators
     class Calculator
       def add(a, b)
         a + b
         ...
       end
     end
   end
end

Which means that git sees that every line has changed because the 
indentation has changed. If you want git to realize that the file has 
been renamed you could move it in one commit and then add modify it in 
the next commit.

Thanks

Phillip

> 2. What did you expect to happen? (Expected behavior)
> 
> in the namespace state i would expected it to be marked as moved since
> nothing has fundementally changed
> 
> 3. What happened instead? (Actual behavior)
> 
> the file was marked as new file and the old file was marked as deleted
> 
> What's different between what you expected and what actually happened
> 
> 4. Anything else you want to add:
> 
> I have demonstrated the behavior here https://github.com/billybonks/git-rename
> 
> Mostly i would like to understand what is the expectation from gits point of view in these mutations.
> If this is considered something that can be improved i am happy to build out more test cases, and help with implementation.
> 
> if not, understanding the reasoning would be great
> 
> Thank you.
> 
> 
> 
> [System Info]
> git version:
> git version 2.47.1
> cpu: arm64
> no commit associated with this build
> sizeof-long: 8
> sizeof-size_t: 8
> shell-path: /bin/sh
> feature: fsmonitor--daemon
> libcurl: 8.7.1
> zlib: 1.2.12
> uname: Darwin 25.3.0 Darwin Kernel Version 25.3.0: Wed Jan 28 20:51:28 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6041 arm64
> compiler info: clang: 16.0.0 (clang-1600.0.26.4)
> libc info: no libc information available
> $SHELL (typically, interactive shell): /bin/zsh
> 
> 
> [Enabled Hooks]
> 
> 
> Sent with Proton Mail secure email.
> 


^ 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