Git development
 help / color / mirror / Atom feed
* [PATCH 03/11] reftable/block: check deflateInit() return value
From: Johannes Schindelin via GitGitGadget @ 2026-07-14 22:48 UTC (permalink / raw)
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin
In-Reply-To: <pull.2179.git.1784069325.gitgitgadget@gmail.com>

From: Johannes Schindelin <johannes.schindelin@gmx.de>

block_writer_init() allocates a z_stream and calls deflateInit()
to prepare it for compressing log records. The return value of
deflateInit() is silently discarded. If zlib initialization fails
(e.g., Z_MEM_ERROR when the system is under memory pressure), the
z_stream is left in an undefined state.

Subsequent deflate() calls in block_writer_finish() then operate
on this uninitialized stream. Depending on the zlib
implementation, this can produce silently corrupted compressed
data (which would be written to the reftable file and discovered
only when a later reader fails to inflate) or crash outright.

The function already uses REFTABLE_ZLIB_ERROR for deflate()
failures later in the code path (lines 171, 199), so returning
the same error code for deflateInit() failure is consistent.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 reftable/block.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/reftable/block.c b/reftable/block.c
index 920b3f4486..ec81fd0493 100644
--- a/reftable/block.c
+++ b/reftable/block.c
@@ -87,7 +87,8 @@ int block_writer_init(struct block_writer *bw, uint8_t typ, uint8_t *block,
 		REFTABLE_CALLOC_ARRAY(bw->zstream, 1);
 		if (!bw->zstream)
 			return REFTABLE_OUT_OF_MEMORY_ERROR;
-		deflateInit(bw->zstream, 9);
+		if (deflateInit(bw->zstream, 9) != Z_OK)
+			return REFTABLE_ZLIB_ERROR;
 	}
 
 	return 0;
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH 02/11] config: propagate launch_editor() failure in show_editor()
From: Johannes Schindelin via GitGitGadget @ 2026-07-14 22:48 UTC (permalink / raw)
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin
In-Reply-To: <pull.2179.git.1784069325.gitgitgadget@gmail.com>

From: Johannes Schindelin <johannes.schindelin@gmx.de>

show_editor() calls launch_editor() to open the user's editor on
the configuration file, but discards the return value and
unconditionally returns 0 (success). When the editor fails to
launch (e.g., $EDITOR is not found, or the editor exits with a
nonzero status), the caller receives no indication that anything
went wrong.

This affects "git config edit" and "git config --edit": the
command silently succeeds even when the editor could not be
started. In contrast, other editor-launching paths in git (such
as "git commit" and "git rebase --edit-todo") properly propagate
editor failures and exit with an error.

Check the return value and propagate the failure by returning -1.
The two callers (cmd_config_edit at line 1315 and the legacy
cmd_config at line 1478) both propagate this return to
handle_builtin, which translates negative returns into an error
exit.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/config.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/builtin/config.c b/builtin/config.c
index 8d8ec0beea..1307fdb0d6 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -1313,7 +1313,10 @@ static int show_editor(struct config_location_options *opts)
 		else if (errno != EEXIST)
 			die_errno(_("cannot create configuration file %s"), config_file);
 	}
-	launch_editor(config_file, NULL, NULL);
+	if (launch_editor(config_file, NULL, NULL)) {
+		free(config_file);
+		return -1;
+	}
 	free(config_file);
 
 	return 0;
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH 01/11] http: die on curl_easy_duphandle failure in get_active_slot
From: Johannes Schindelin via GitGitGadget @ 2026-07-14 22:48 UTC (permalink / raw)
  To: git; +Cc: Johannes Schindelin, Johannes Schindelin
In-Reply-To: <pull.2179.git.1784069325.gitgitgadget@gmail.com>

From: Johannes Schindelin <johannes.schindelin@gmx.de>

get_active_slot() duplicates the default curl handle via
curl_easy_duphandle() to create a per-slot session handle. The
return value is stored directly in slot->curl without checking
for NULL. curl_easy_duphandle() can return NULL when memory
allocation fails internally, and the libcurl documentation
explicitly states this possibility.

When this happens, slot->curl is NULL and the very next operation
(curl_easy_setopt on line 1632 for CURLOPT_COOKIEFILE) passes
NULL as the curl handle, which is undefined behavior in libcurl
and typically crashes.

Every HTTP operation in git goes through get_active_slot(), so
this affects all remote-https, remote-http, and HTTP-based
operations (clone, fetch, push over HTTP, bundle-uri downloads).

Add a NULL check and die() with a clear message. There is no
reasonable recovery from a failed handle duplication: the process
is out of memory and cannot perform any HTTP operation.

Pointed out by Coverity.

Assisted-by: Claude Opus 4.6
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 http.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/http.c b/http.c
index b4e7b8d00b..8f1d6d1f56 100644
--- a/http.c
+++ b/http.c
@@ -1608,6 +1608,8 @@ struct active_request_slot *get_active_slot(void)
 
 	if (!slot->curl) {
 		slot->curl = curl_easy_duphandle(curl_default);
+		if (!slot->curl)
+			die("curl_easy_duphandle failed");
 		curl_session_count++;
 	}
 
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH 00/11] coverity: fix unchecked returns
From: Johannes Schindelin via GitGitGadget @ 2026-07-14 22:48 UTC (permalink / raw)
  To: git; +Cc: Johannes Schindelin

This is the next batch of fixes in response to issues reported by Coverity.

Johannes Schindelin (11):
  http: die on curl_easy_duphandle failure in get_active_slot
  config: propagate launch_editor() failure in show_editor()
  reftable/block: check deflateInit() return value
  reftable tests: check reftable_table_init_ref_iterator() return
  last-modified: handle repo_parse_commit() failures
  compat/pread: check initial lseek for errors
  transport-helper: check dup() return in get_exporter
  transport-helper: warn when export-marks file cannot be finalized
  bisect: check strbuf_getline_lf return when reading terms
  bisect: check get_terms return at all call sites
  bisect: handle dup() failure when redirecting stdout

 bisect.c                        |  6 ++++--
 builtin/bisect.c                | 27 +++++++++++++++++++++++++--
 builtin/config.c                |  5 ++++-
 builtin/last-modified.c         |  9 ++++++---
 compat/pread.c                  |  2 ++
 http.c                          |  2 ++
 reftable/block.c                |  3 ++-
 t/unit-tests/u-reftable-table.c |  6 ++++--
 transport-helper.c              |  6 +++++-
 9 files changed, 54 insertions(+), 12 deletions(-)


base-commit: 55526a18268bbc1ddaf8a6b7850c33d984eac9e9
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2179%2Fdscho%2Fcoverity-fixes-unchecked-returns-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2179/dscho/coverity-fixes-unchecked-returns-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2179
-- 
gitgitgadget

^ permalink raw reply

* Re: [PATCH 1/6] SubmittingPatches: clarify expected structure of commit log message
From: D. Ben Knoble @ 2026-07-14 22:46 UTC (permalink / raw)
  To: Weijie Yuan; +Cc: Junio C Hamano, Michael Montalbo, git
In-Reply-To: <alTy306FaTAe2E8w@wyuan.org>

On Mon, Jul 13, 2026 at 10:42 AM Weijie Yuan <wy@wyuan.org> wrote:
>
[snip]
> I think this might confuse readers. Now you place these points in
> parallel:
>
>  1. Title
>  2. Body
>  3. Observation (The Status Quo)
>  4. Solution Design (The Approach)
>  5. Implementation (The Execution)

Without commenting on "confuse," I find this style of heading

    Thing (The Other Thing)

needlessly suggests an LLM's involvement with the text. That by itself
is not grounds for my objection; instead, I'll note that often the
parenthetical restates the original header in some way. That makes it
redundant. (In some cases in the wild I have seen examples where the 2
were not synonymous, which _is_ confusing :)


> But acatually you mean:
>
> 1. Title
> 2. Body
>    The body typically follows three parts:
>    a. Observation
>    b. Solution Design
>    c. Implementation
>
> But I haven't written much about adoc, so I don't know its syntax and
> how to write it.

This is nice. If I had to suggest anything further, it would be "don't
be afraid of long headings":

1. Title: Summarize the change
2. Body: Describe [Justify?] the change
    a. Observe the status quo
    b. Explain your approach [solution/design/etc.]
    c. Command the code to change [or: Describe the implementation/execution]

?

-- 
D. Ben Knoble

^ permalink raw reply

* Re: [PATCH] completion: zsh: support completion after "git -C <path>"
From: D. Ben Knoble @ 2026-07-14 22:34 UTC (permalink / raw)
  To: Lutz Lengemann via GitGitGadget; +Cc: git, Lutz Lengemann, Junio C Hamano
In-Reply-To: <CALnO6CD9P4+e=YPdKaLfSBOk-H3_ir64pBP-qMKNNvzUNqunXQ@mail.gmail.com>

Hi Lutz,

On Thu, Jun 18, 2026 at 1:43 PM D. Ben Knoble <ben.knoble@gmail.com> wrote:
>
> [apologies in advance for the strange format below]
>
> On Wed, Jun 17, 2026 at 11:37 AM Lutz Lengemann via GitGitGadget
> <gitgitgadget@gmail.com> wrote:
> >
> > From: Lutz Lengemann <lutz@lengemann.net>
> >
> > The zsh completion wrapper (__git_zsh_main) did not handle the global -C
> > option, so "git -C <path> <command> <TAB>" offered nothing and could not
> > complete a command's arguments.
> >
> > Three things are needed to make it work, all scoped to -C:
> >
> >   - Add -C to the _arguments specification, so completion no longer stops
> >     at it.
> >
> >   - Advance __git_cmd_idx past any leading "-C <path>" options. The index
> >     is hard-coded to 1, i.e. the command is assumed to be the first
> >     argument; with -C present the command sits two words later for each
> >     -C, so the bash helpers otherwise look at the wrong word and produce
> >     nothing.
> >
> >   - Collect the -C paths into __git_C_args, as __git_main does. The bash
> >     helpers run git to resolve aliases and list refs; without the -C
> >     paths they run in the current directory, so completion fails whenever
> >     the cwd is not the target repository or the command is an alias.
> >
> > With these, "git -C <path> <command> <TAB>" completes the command, its
> > options and its arguments, including outside the repository, through
> > aliases, and with repeated -C options.
> >
> > Signed-off-by: Lutz Lengemann <lutz@lengemann.net>
> > ---
> >     completion: zsh: support completion after "git -C "
> >
> >     This patch is intentionally scoped to -C, but the underlying problem is
> >     more general. The zsh wrapper hard-codes __git_cmd_idx=1, i.e. it
> >     assumes the command is always the first argument. That assumption breaks
> >     argument completion after any global option that precedes the command,
> >     not just -C — e.g. --git-dir, --work-tree, --namespace, -c, and
> >     -p/--paginate. After those, git <opt> <command> <TAB> currently
> >     completes the command name but not its arguments.
> >
> >     The same approach generalizes cleanly: instead of skipping only leading
> >     -C options, walk all leading global options and their arguments to
> >     locate the command and its true index (mirroring the option scan in
> >     __git_main in git-completion.bash), while collecting -C into
> >     __git_C_args and --git-dir into __git_dir as today.
> >
> >     I kept this revision narrow for reviewability and because git -C is the
> >     case where I miss the completion, but I'm happy to extend it to cover
> >     the other global options in a follow-up (or fold it into this patch) if
> >     that's preferred.
>
> See Junio's review for whether we should expand in this patch or a follow-up.
>
> In reply to Junio:
>
> > [the new handling only knows about -C]
> > Doesn't it want to do something similar to what __git_main in
> > git-completion.bash does at the beginning, namely, this part?
>
> Yeah, we probably do want to skip over -c, etc. (I see some support for
> --bare and --git-dir, but not skipping over it.) Still, this patch makes
> things no worse in that regard, and improves the situation for -C
> AFAICT.
>
> In reply to Lutz:
>
> > +        local -a __git_C_args
> > +        local -i i=2
> > +
> > +        while [[ ${orig_words[i]} == -C ]]; do
> > +            __git_C_args+=(-C ${orig_words[i+1]})
> > +            (( __git_cmd_idx += 2 ))
> > +            (( i += 2 ))
> > +        done
>
> I don't see either of these 2 local variables used anywhere else…
>
> …well, except the Bash completion helpers, I suppose. But we mark these
> local, so how do they propagate to the other functions?
>
> Still, I was able to try this out with the somewhat hacky
>
>     zsh # new shell :)
>     # absolute path important
>     autoload -Uz $PWD/contrib/completion/git-completion.zsh
>     compdef git-completion.zsh git
>
>     git -C <tab>
>
> and it does prioritize directories there (though I still get a listing
> of files afterwards, so the screen is taken up by that gigantic listing
> in git.git, for example).
>
> By the way, I've realized that "git -<tab>" has the same problem (a
> giant list of files after the other option completions), and worse has
> some _funky_ output!
>
>     git -<tab> # without patch
>     (option)
>     --bare
>     --exec-path
>     --git-dir
>     --help
>     --html-path
>     --info-path
>     --man-path
>     --namespace
>     --no-pager
>     --no-replace-objects
>     --paginate
>     --version
>     --work-tree
>
>     -p
>
>     # treat the repository as a bare repository
>     # path to where your core git programs are installed
>     # set the path to the repository
>     # prints the synopsis and a list of the most commonly used commands
>     # print the path where gits HTML documentation is installed
>     # print the path where the Info files are installed
>     # print the manpath (see `man(1)`) for the man pages
>     # set the git namespace
>     # do not pipe git output into a pager
>     # do not use replacement refs to replace git objects
>     # pipe all output into less
>     # prints the git suite version
>     # set the path to the working tree
>     [ed: the above block repeats twice more before the (file) listing below]
>     (file)
>     […]
>
> Here's the output of _complete_help (^Xh by default) in both situations,
> in case that helps to understand either the extra files listing (1) in
> the example further back or the issue with single letter options (2)
> just mentioned:
>
> 1: tags in context :completion::complete:git::
>     option-C-1     (_arguments __git_zsh_main _git git-completion.zsh)
>     use-compctl    (_default _git git-completion.zsh)
>     globbed-files  (_files _default _git git-completion.zsh)
> tags in context :completion::complete:git:option-C-1:
>     directories    (_directories _arguments __git_zsh_main _git
> git-completion.zsh)
>     globbed-files  (_files _directories _arguments __git_zsh_main _git
> git-completion.zsh)
>     all-files      (_files _directories _arguments __git_zsh_main _git
> git-completion.zsh)
>
> 2: tags in context :completion::complete:git::
>     argument-1 options  (_arguments __git_zsh_main _git)
>     use-compctl         (_default _git)
>     globbed-files       (_files _default _git)
> tags in context :completion::complete:git:argument-1:
>     common-commands alias-commands all-commands  (__git_zsh_main _git)
>     common-commands                              (__git_zsh_cmd_common
> __git_zsh_main _git)
>     alias-commands                               (__git_zsh_cmd_alias
> __git_zsh_main _git)
>     all-commands                                 (__git_zsh_cmd_all
> __git_zsh_main _git)
> tags in context :completion::complete:git:options:
>     options  (_arguments __git_zsh_main _git)
>
> > +        '*-C[run as if git was started in <path>]: :_directories' \
>
> We should probably note in the log message that the _directories
> completion will not account for previous -C; that is, after typing
>
>     git -C dir -C <tab>
>
> we will complete directories in ".", not "dir". That's probably a
> reasonable limitation for now, but I think we could do _slightly_ better
> by using a state "->dir" or something, accumulating the current prefix,
> and passing that to _directories as a prefix with -W (see _path_files in
> zshcompsys, which _directories delegates to via _files, IIUC).
>
> --
> D. Ben Knoble

Any progress here? I just found my local copy of this patch and was
briefly surprised to see it hadn't graduated anywhere (until I
realized conversation had stalled at this point).

-- 
D. Ben Knoble

^ permalink raw reply

* Re: local mistake - need help in recover
From: D. Ben Knoble @ 2026-07-14 22:27 UTC (permalink / raw)
  To: Kishore N.S; +Cc: git
In-Reply-To: <MA0PR01MB9857F8757F67BD2F8FF358CF97F92@MA0PR01MB9857.INDPRD01.PROD.OUTLOOK.COM>

> Le 14 juil. 2026 à 00:06, N.S Kishore <n.s.kishore@cctech.co.in> a écrit :
> 
> Hi Team,
> Need help to recover files from local mistake.
> Regards,
> Kishore N S.

> After merge (and/or related operations in the same session), staged files disappeared from the index

I could only slightly reproduce the issue reported:

    git init foo && cd foo
    echo a >a && git add a && git commit -ma
    echo a >>a && git commit -am aa
    git switch -c b HEAD~
    echo b >b && git add b && git commit -mb
    echo b >>b && git add b
    echo b >>b

From here, « git merge main » refuses (« Your local changes to the
following files would be overwritten by merge »). With autostash mode,
the merge succeeds, but the staged line of b is now unstaged. The
working tree contents were not lost, though.

In retrospect, I didn’t try with new files on the side branch.

Test performed with Git 2.53.0 and repeated with
2.55.0.rc0.738.g0c8ab3ebcc.dirty.

^ permalink raw reply

* Re: [PATCH v2] show-branch: convert object.flags to commit-slab with uint64_t
From: Jeff King @ 2026-07-14 22:00 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Gatla Vishweshwar Reddy, git
In-Reply-To: <xmqqcxwps3ro.fsf@gitster.g>

On Tue, Jul 14, 2026 at 01:41:15PM -0700, Junio C Hamano wrote:

> This limitation is precisely where the concept of using a commit
> slab shines.  However, to truly take advantage of a commit slab, the
> slab stride must be variable.  If the tool is handling more than 80
> branches, for example, each commit requires a `uint64_t[2]` array
> allocation (since a single `uint64_t` provides only 64 bits, while
> `uint64_t[2]` can store up to 128 bits).

Yep. Going back to the last time this topic came up, I'll just point at:

  https://lore.kernel.org/git/20250225011757.GA752084@coredump.intra.peff.net/

which references one of the earliest commit-slab series. Especially the
part that adds arbitrary-sized bitset support, which could be useful
here (patch 4 adds the bitset, patch 6 shows how it is used).

-Peff

^ permalink raw reply

* Re: [PATCH 0/4] send-pack: introduce a `no-ref-delta` capability
From: Taylor Blau @ 2026-07-14 21:58 UTC (permalink / raw)
  To: Jeff King; +Cc: git, Junio C Hamano
In-Reply-To: <20260714074506.GD4058320@coredump.intra.peff.net>

On Tue, Jul 14, 2026 at 03:45:06AM -0400, Jeff King wrote:
> On Sun, Jul 12, 2026 at 06:11:47PM -0700, Taylor Blau wrote:
>
> > Some 'receive-pack' implementations may wish to retain the incoming pack
> > without first building an object ID index, in which case requiring delta
> > bases to appear earlier in the same pack makes them easier to locate.
>
> This explanation puzzles me. OK, I can see why you might want to take in
> the incoming pack and then sit on it for a bit. But surely you are not
> going to update refs without seeing what's in the pack, right? Otherwise
> any pushing client can corrupt your repo.
>
> And the only way to know what's in the pack is to index it. At which
> point resolving REF_DELTAs is the least of your worries there.
>
> So I have the feeling that there's some ulterior motive, or that this is
> part of a larger system, but I don't quite understand what it is. And so
> it's hard to say whether this is a sensible approach.

The implementation motivating this is write-through in the sense that it
first parses and spools the incoming pack, then replays those exact
bytes together with the same ref commands to an upstream receive-pack.

The packfile contents and pending transaction may be staged before that
upstream request finishes, but no local ref update is published unless
the upstream accepts the push. So the usual receive-pack connectivity
checks still happen before the update becomes visible locally.

(Apologies for all of the hand-waving here, BTW. I'm trying to describe
the system in generic terms to make clear my motivations here, but I am
somewhat limited in what I can discuss.)

In retrospect, I don't think the cover letter distinguishes this well.
The pack that we receive over the wire is stored byte-for-byte as an
immutable artifact, and the per-object physical index is derived
asynchronously. That indexer is designed to operate in a single pass
forward over the pack.

(Supporting REF_DELTA there during the indexing process is possible in
theory, but requires keeping an OID lookup around, delaying resolution,
taking another pass, or rewriting the retained pack. This design avoids
all of those.)

> > Bitmap pack reuse is different, since it copies entries directly from
> > an existing pack. Under `--no-ref-delta`, it must inspect candidate
> > objects individually, omit `REF_DELTA` entries from direct pack reuse,
> > and leave them to the normal object-writing path.
>
> Hmm. We wouldn't normally expect verbatim pack-reuse to kick in, since
> this is about the client sending to the server. But OK, we certainly
> need to make sure that path remains correct.

That is an edge case rather than part of the motivation. It is only
there so that `pack-objects --no-ref-delta` means what it says even
if/when it performs verbatim pack-reuse.

> >  - The final patch advertises and consumes the new `no-ref-delta`
> >    capability.
>
> What about thin packs? They'll result in REF_DELTAs on the server once
> the pack is completed/indexed. I guess we have the "no-thin" capability,
> but I don't think our receive-pack implementation support sending it. I
> also wouldn't be terribly surprised if not every client implementation
> supports it (it was added in 2013 I think to support libgit2). But I
> guess that is also true of your new no-ref-delta; only updated clients
> will respect it.
>
> What will/should a server do when they get a ref delta anyway? That
> again goes back to the question of: why don't we want ref deltas?

The implementation in question already advertises 'no-thin',

A sender honoring `no-ref-delta` cannot send a thin pack in the first
place, since an external base must be encoded as REF_DELTA. `send-pack`
may still invoke `pack-objects` with both `--thin` and `--no-ref-delta`,
but the latter causes it to skip excluded bases.

Older clients may ignore `no-ref-delta` and still get rejected. That is
already the receiver's behavior. The capability just lets updated
clients avoid sending a pack which will be rejected.

If a REF_DELTA arrives anyway, the receiver rejects the pack before
publishing the ref.

Thanks,
Taylor

^ permalink raw reply

* Re: [PATCH] strbuf: avoid redundant reset in strbuf_getwholeline()
From: Jeff King @ 2026-07-14 21:49 UTC (permalink / raw)
  To: René Scharfe; +Cc: Git List
In-Reply-To: <d4ffe7fb-f782-4f06-9e3b-f72729d1e225@web.de>

On Tue, Jul 14, 2026 at 10:45:59AM +0200, René Scharfe wrote:

> The HAVE_GETDELIM variant of strbuf_getwholeline() calls strbuf_reset()
> on the strbuf before handing it over to getdelim(3).  This is
> unnecessary:
> 
>   - getdelim(3) doesn't care whether the old buffer contents is
>     NUL-terminated and has no access to ->len,
>   - on success getdelim(3) NUL-terminates the buffer and we set ->len,
>   - on error we either call strbuf_init() or strbuf_reset().
> 
> Remove the superfluous preparatory call.

Good catch. In the original version of strbuf_getwholeline() we were
missing that reset on error, which is why this was included. I think
it became redundant in b70904306f (strbuf_getwholeline: NUL-terminate
getdelim buffer on error, 2016-03-05).

-Peff

^ permalink raw reply

* Re: [PATCH 2/2] fetch-pack: accept "pack" output for packfile URIs
From: Jeff King @ 2026-07-14 21:47 UTC (permalink / raw)
  To: Ted Nyman
  Cc: git, Junio C Hamano, Taylor Blau, Patrick Steinhardt,
	Karthik Nayak, brian m. carlson,
	Ævar Arnfjörð Bjarmason
In-Reply-To: <alaCQKXKcWr723Ij@com-76773>

On Tue, Jul 14, 2026 at 11:38:56AM -0700, Ted Nyman wrote:

> > I also think this would all be much nicer with a strbuf (which would
> > let us get rid of the magic numbers), but that is a slightly larger
> > refactor:
> 
> Using a strbuf makes sense. One wrinkle, I think, is that with
> transfer.fsckobjects enabled, index-pack can emit dangling .gitmodules
> OIDs after the initial pack/keep line, which parse_gitmodules_oids()
> still needs to read from cmd.out. Would strbuf_getwholeline_fd() be a
> better fit here, so we don't consume those with strbuf_read()?

Ah, yeah, I didn't think about whether it might have more output. I
_think_ it actually works just fine with more output because the
memcmp() is limited to the hash algo's hex_sz. For the same reason what
I posted works even though it has the trailing newline.

It is a bit subtle, though. Using getwholeline_fd would work (though you
still have the trailing newline subtlety). Or maybe just using
strbuf_setlen() to cut off the output (ironically it is probably more
efficient to read the whole thing in and then chomp it, since
getwholeline_fd will read() one char at a time).

The "cleanest" thing is perhaps xfdopen() followed by strbuf_getline(),
but maybe that's overkill.

I'd be happy with any of the solutions. Or even just keeping the magic
numbers but maybe with a comment explaining what the heck "6" means.

> I'll also fix the --index-pack-args documentation while rerolling.

Great, thanks.

-Peff

^ permalink raw reply

* Re: [PATCH 0/5] tempfile: stop using the_repository
From: Junio C Hamano @ 2026-07-14 20:45 UTC (permalink / raw)
  To: René Scharfe; +Cc: git
In-Reply-To: <20260714175956.54601-1-l.s.r@web.de>

René Scharfe <l.s.r@web.de> writes:

> create_tempfile_mode() and create_tempfile() use the_repository
> internally to call adjust_shared_perm().  Expose that dependency and
> push it out to their callers.
>
> Patch 5 is a bonus; it converts lockfile users that already work with
> other repositories.
>
>   tempfile: add repo_create_tempfile{,_mode}()
>   refs/packed: use repo_create_tempfile()
>   lockfile: add repo_hold_lock_file_for_update{,_timeout}{,_mode}()
>   tempfile: stop using the_repository
>   use repo_hold_lock_file_for_update{,_mode,_timeout}() with custom repos



Will queue.  If I have a chance I may revisit the topic a bit
deeper, but nothing stood out as glaringly wrong to my cursory
reading so far.

Thanks.

>
>  apply.c                   | 10 ++++++----
>  builtin/difftool.c        |  2 +-
>  builtin/gc.c              |  2 +-
>  builtin/history.c         |  2 +-
>  builtin/sparse-checkout.c |  3 ++-
>  bundle.c                  |  4 ++--
>  commit-graph.c            |  9 +++++----
>  config.c                  |  4 ++--
>  lockfile.c                | 30 ++++++++++++++++++++++--------
>  lockfile.h                | 31 +++++++++++++++++++++++++++++++
>  loose.c                   |  6 ++++--
>  midx-write.c              |  7 ++++---
>  odb/source-files.c        |  3 ++-
>  refs/files-backend.c      | 10 ++++++----
>  refs/packed-backend.c     |  9 ++++-----
>  refs/packed-backend.h     |  2 +-
>  repack-midx.c             |  3 ++-
>  repository.c              |  2 +-
>  rerere.c                  |  6 +++---
>  tempfile.c                |  7 +++----
>  tempfile.h                | 10 +++++++---
>  21 files changed, 110 insertions(+), 52 deletions(-)

^ permalink raw reply

* Re: [PATCH v2] show-branch: convert object.flags to commit-slab with uint64_t
From: Junio C Hamano @ 2026-07-14 20:41 UTC (permalink / raw)
  To: Gatla Vishweshwar Reddy; +Cc: git
In-Reply-To: <20260714200237.70509-1-gatlavishweshwarreddy26@gmail.com>

Gatla Vishweshwar Reddy <gatlavishweshwarreddy26@gmail.com> writes:

> show-branch uses commit->object.flags to store per-commit data:
> the UNINTERESTING bit and per-branch reachability bits. Using the
> shared object.flags field for this purpose is fragile as it
> conflicts with other users of the same field, and limits the
> number of branches that can be shown to MAX_REVS (27).

The command was written with the understanding that it would not
allow other parts of the system to touch these per-object flag
bits.  Therefore, fragility is not a relevant issue.  The primary
problem with this design is that the flags word has only a fixed
number of available bits, meaning it cannot process hundreds of
branches simultaneously.

This limitation is precisely where the concept of using a commit
slab shines.  However, to truly take advantage of a commit slab, the
slab stride must be variable.  If the tool is handling more than 80
branches, for example, each commit requires a `uint64_t[2]` array
allocation (since a single `uint64_t` provides only 64 bits, while
`uint64_t[2]` can store up to 128 bits).

> Convert this usage to a dedicated commit-slab using uint64_t as
> the element type. This is the canonical way to associate per-commit
> data in Git without polluting the shared object flags. Using
> uint64_t instead of unsigned int lifts the MAX_REVS limitation
> from 27 to 62 branches, as suggested in prior review discussions.

I do not understand the reference to 62.  As I previously noted,
storing a fixed uint64_t[1] instead of variable-length uint64_t[n]
in each slab entry fails to realize the full potential of using
commit slabs.  Furthermore, we should be able to utilize all 64 bits
of a uint64_t word.  There is no need to pollute this dedicated,
one-bit-per-branch slab with the UNINTERESTING bit, which is used
for the command's revision walking.  Revision walking can continue
using the UNINTERESTING bit in the standard object.flags instead.

> @@ -511,18 +523,20 @@ static int rev_is_head(const char *head, const char *name)
>
>  static int show_merge_base(const struct commit_list *seen, int num_rev)
>  {
> -	int all_mask = ((1u << (REV_SHIFT + num_rev)) - 1);
> -	int all_revs = all_mask & ~((1u << REV_SHIFT) - 1);
> +	uint64_t all_mask = ((UINT64_C(1) << (REV_SHIFT + num_rev)) - 1);
> +	uint64_t all_revs = all_mask & ~((UINT64_C(1) << REV_SHIFT) - 1);
>  	int exit_status = 1;
>
>  	for (const struct commit_list *s = seen; s; s = s->next) {
>  		struct commit *commit = s->item;
> -		int flags = commit->object.flags & all_mask;
> +		uint64_t flags = get_rev_flags(commit) & all_mask;
>  		if (!(flags & UNINTERESTING) &&
>  		    ((flags & all_revs) == all_revs)) {
>  			puts(oid_to_hex(&commit->object.oid));
>  			exit_status = 0;
> -			commit->object.flags |= UNINTERESTING;
> +
> +or_rev_flags(commit, UNINTERESTING);
> +
>  		}
>  	}

What's this funny indentation?

> @@ -607,9 +621,9 @@ static int omit_in_dense(struct commit *commit, struct commit **rev, int n)
>  	for (i = 0; i < n; i++)
>  		if (rev[i] == commit)
>  			return 0;
> -	flag = commit->object.flags;
> +	flag = get_rev_flags(commit);

Has the definition of local variable "flag" in omit_in_dense() been
updated to u64?  If it is still "int", then this would not work
well on platforms whose "int" is still i32.

>  	for (i = count = 0; i < n; i++) {
> -		if (flag & (1u << (i + REV_SHIFT)))
> +		if (flag & (UINT64_C(1) << (i + REV_SHIFT)))
>  			count++;
>  	}
>  	if (count == 1)

^ permalink raw reply

* Persistent shallow + fake-linearizing a whole mainline
From: Richard Fine @ 2026-07-14 20:04 UTC (permalink / raw)
  To: git

Hi,

The repository at my company uses a standard branch-and-pull-request
model for developers to make changes. Pull requests are integrated as
2-parent merge commits. I'm trying to find ways to optimise working
with the repository, particularly by reducing the Git database size to
make git operations faster. I see two clear opportunities, though I'm
struggling to get engineering alignment on each:

* Squash-merging. If we switched to squash-merging pull requests into
our mainline branch, developers wouldn't have to carry the added load
of the individual commits from the branches used to construct the PR.
However, two objections arise: first, landing a sequence of stacked
PRs becomes painful because Git can no longer accurately identify
merge-bases; and second, in-branch history is sometimes useful for
code archaeology. Suggestions that the in-branch history would still
be available in our source-of-truth repository are met with complaints
that looking at a second repo for history is inconvenient :)

* Shallow cloning. Setting a shallow boundary with something like
--shallow-since="3 months ago" gives us an intuitive way to trade off
repository size/speed against history availability. The problem with
this is that we sometimes have PRs which initially branched off old
revisions - earlier than our shallow-since point - and then get landed
with merge commits. When people pull those merge commits, git follows
the second parent of the merge, pulls the history of the branch, and
ends up bypassing the 'firebreak' revisions defined in `.git/shallow`,
pulling large amounts of history. People can avoid this by specifying
the --shallow options when running `git fetch`, but very often, this
is not something they are running manually: UI tools are running it
for them, or they run `git remote update`, or an AI agent is doing it
for them, etc. Suggestions that we should block people from landing
PRs with branches based on ancient revisions, and that people should
instead rebase the work on a newer revision, are met with the
objection that if the branch is based on that old of a revision it's
typically because it's a long-running branch which accumulated a lot
of work, and rebasing that work on a more recent revision of mainline
is painful.

I've not yet given up trying to get my colleagues to change their
workflows (and I welcome advice on how others approach these
engineering-culture problems). In the meantime, I have a couple of
thoughts for possible Git improvements that might help, which I
figured I'd raise here.

* The biggest issue with the shallow clone solution is the possibility
that someone fetches one of these 'based on ancient history' merges
without passing --shallow-since, causing Git to end up pulling huge
amounts of history. What if one could set the shallow options
persistently? For example, a "remote.origin.shallowsince" in the
.git/config. If set, it would make fetches from that remote behave as
if --shallow-since was specified on the command-line, regardless of
how the fetch was triggered.

* This is more complicated, but... I did wonder if there is some way
to use .git/shallow (or something similar, like replace refs) to make
Git pretend that the merge commits on our mainline are actually linear
commits (i.e. pretend they only have their first parent). Then when a
developer actually wants to delve into the commits that made up a
specific branch, they could make Git stop pretending for that specific
merge commit, and Git would then fetch the commits needed to fill in
the missing second parent and its ancestors. When they're done, they
flip it back to being a fake linear commit, and the branch commits
would no longer be reachable, eventually being cleaned up by `git gc`.
I think I could probably write a script to convert an existing
repository into this 'fake linear mainline' mode, but I'm not sure how
I'd then make incremental fetches of the mainline continue to keep up
the masquerade without pulling all the commits first and then running
the script. I'd like to avoid pulling the extra commits if possible.

What do you think? I could probably take on at least the first idea if
there is interest in it.

- Richard

^ permalink raw reply

* [PATCH v2] show-branch: convert object.flags to commit-slab with uint64_t
From: Gatla Vishweshwar Reddy @ 2026-07-14 20:01 UTC (permalink / raw)
  To: git; +Cc: Gatla Vishweshwar Reddy
In-Reply-To: <20260714183028.67857-1-gatlavishweshwarreddy26@gmail.com>

show-branch uses commit->object.flags to store per-commit data:
the UNINTERESTING bit and per-branch reachability bits. Using the
shared object.flags field for this purpose is fragile as it
conflicts with other users of the same field, and limits the
number of branches that can be shown to MAX_REVS (27).

Convert this usage to a dedicated commit-slab using uint64_t as
the element type. This is the canonical way to associate per-commit
data in Git without polluting the shared object flags. Using
uint64_t instead of unsigned int lifts the MAX_REVS limitation
from 27 to 62 branches, as suggested in prior review discussions.

Add helper functions get_rev_flags() and or_rev_flags() to
encapsulate slab access cleanly. Update all bit operations to use
UINT64_C(1) instead of 1u to ensure correct 64-bit shifts.
Initialize and clear the slab in cmd_show_branch() to avoid
memory leaks.

Signed-off-by: Gatla Vishweshwar Reddy <gatlavishweshwarreddy26@gmail.com>
---

Changes in v2:

- Use uint64_t instead of unsigned int for the slab element type.
  This lifts MAX_REVS from 27 to 62 branches since uint64_t provides
  64 bits instead of the 32 bits available in unsigned int.
- Update all bit shift operations from 1u to UINT64_C(1) to ensure
  correct 64-bit shifts without undefined behavior.
- Update printf format specifiers from %d to %zu for MAX_REVS since
  sizeof() expressions produce size_t, not int.

I noticed the prior RFC by Meet Soni (Feb 2025, Message-ID:
<20250217055024.3978-1-meetsoni3017@gmail.com>) which Junio C Hamano
and Jeff King reviewed. That patch did the basic conversion but did
not lift the MAX_REVS limitation. This v2 addresses Junio's feedback
where he suggested "using a slab whose element is still a bag of bits
that is wider than object.flags word is the most straight-forward way
to lift MAX_REVS limitation." We use uint64_t as that wider element.

 builtin/show-branch.c | 106 ++++++++++++++++++++++++------------------
 1 file changed, 61 insertions(+), 45 deletions(-)

diff --git a/builtin/show-branch.c b/builtin/show-branch.c
index f02831b085..625e456411 100644
--- a/builtin/show-branch.c
+++ b/builtin/show-branch.c
@@ -34,15 +34,13 @@ static enum git_colorbool showbranch_use_color = GIT_COLOR_UNKNOWN;

 static struct strvec default_args = STRVEC_INIT;

-/*
- * TODO: convert this use of commit->object.flags to commit-slab
- * instead to store a pointer to ref name directly. Then use the same
- * UNINTERESTING definition from revision.h here.
- */
 #define UNINTERESTING	01

+static uint64_t get_rev_flags(struct commit *commit);
+static void or_rev_flags(struct commit *commit, uint64_t flags);
+
 #define REV_SHIFT	 2
-#define MAX_REVS	(FLAG_BITS - REV_SHIFT) /* should not exceed bits_per_int - REV_SHIFT */
+#define MAX_REVS	(sizeof(uint64_t) * 8 - REV_SHIFT)

 #define DEFAULT_REFLOG	4

@@ -64,7 +62,7 @@ static struct commit *interesting(struct prio_queue *queue)
 {
 	for (size_t i = 0; i < queue->nr; i++) {
 		struct commit *commit = queue->array[i].data;
-		if (commit->object.flags & UNINTERESTING)
+		if (get_rev_flags(commit) & UNINTERESTING)
 			continue;
 		return commit;
 	}
@@ -79,11 +77,25 @@ struct commit_name {
 define_commit_slab(commit_name_slab, struct commit_name *);
 static struct commit_name_slab name_slab;

+define_commit_slab(commit_rev_flags, uint64_t);
+static struct commit_rev_flags rev_flags_slab;
+
 static struct commit_name *commit_to_name(struct commit *commit)
 {
 	return *commit_name_slab_at(&name_slab, commit);
 }

+static uint64_t get_rev_flags(struct commit *commit)
+{
+	uint64_t *f = commit_rev_flags_peek(&rev_flags_slab, commit);
+	return f ? *f : 0;
+}
+
+static void or_rev_flags(struct commit *commit, uint64_t flags)
+{
+	*commit_rev_flags_at(&rev_flags_slab, commit) |= flags;
+}
+

 /* Name the commit as nth generation ancestor of head_name;
  * we count only the first-parent relationship for naming purposes.
@@ -215,7 +227,7 @@ static void name_commits(struct commit_list *list,

 static int mark_seen(struct commit *commit, struct commit_list **seen_p)
 {
-	if (!commit->object.flags) {
+	if (!get_rev_flags(commit)) {
 		commit_list_insert(commit, seen_p);
 		return 1;
 	}
@@ -226,15 +238,15 @@ static void join_revs(struct prio_queue *queue,
 		      struct commit_list **seen_p,
 		      int num_rev, int extra)
 {
-	int all_mask = ((1u << (REV_SHIFT + num_rev)) - 1);
-	int all_revs = all_mask & ~((1u << REV_SHIFT) - 1);
+	uint64_t all_mask = ((UINT64_C(1) << (REV_SHIFT + num_rev)) - 1);
+	uint64_t all_revs = all_mask & ~((UINT64_C(1) << REV_SHIFT) - 1);

 	while (queue->nr) {
 		struct commit_list *parents;
 		int still_interesting = !!interesting(queue);
 		struct commit *commit = prio_queue_peek(queue);
 		bool get_pending = true;
-		int flags = commit->object.flags & all_mask;
+		uint64_t flags = get_rev_flags(commit) & all_mask;

 		if (!still_interesting && extra <= 0)
 			break;
@@ -246,14 +258,14 @@ static void join_revs(struct prio_queue *queue,

 		while (parents) {
 			struct commit *p = parents->item;
-			int this_flag = p->object.flags;
+			uint64_t this_flag = get_rev_flags(p);
 			parents = parents->next;
 			if ((this_flag & flags) == flags)
 				continue;
 			repo_parse_commit(the_repository, p);
 			if (mark_seen(p, seen_p) && !still_interesting)
 				extra--;
-			p->object.flags |= flags;
+			or_rev_flags(p, flags);
 			if (get_pending)
 				prio_queue_replace(queue, p);
 			else
@@ -278,8 +290,8 @@ static void join_revs(struct prio_queue *queue,
 			struct commit *c = s->item;
 			struct commit_list *parents;

-			if (((c->object.flags & all_revs) != all_revs) &&
-			    !(c->object.flags & UNINTERESTING))
+			if (((get_rev_flags(c) & all_revs) != all_revs) &&
+			    !(get_rev_flags(c) & UNINTERESTING))
 				continue;

 			/* The current commit is either a merge base or
@@ -292,8 +304,8 @@ static void join_revs(struct prio_queue *queue,
 			while (parents) {
 				struct commit *p = parents->item;
 				parents = parents->next;
-				if (!(p->object.flags & UNINTERESTING)) {
-					p->object.flags |= UNINTERESTING;
+				if (!(get_rev_flags(p) & UNINTERESTING)) {
+					or_rev_flags(p, UNINTERESTING);
 					changed = 1;
 				}
 			}
@@ -410,8 +422,8 @@ static int append_ref(const char *refname, const struct object_id *oid,
 				return 0;
 	}
 	if (MAX_REVS <= ref_name_cnt) {
-		warning(Q_("ignoring %s; cannot handle more than %d ref",
-			   "ignoring %s; cannot handle more than %d refs",
+		warning(Q_("ignoring %s; cannot handle more than %zu ref",
+			   "ignoring %s; cannot handle more than %zu refs",
 			   MAX_REVS), refname, MAX_REVS);
 		return 0;
 	}
@@ -511,18 +523,20 @@ static int rev_is_head(const char *head, const char *name)

 static int show_merge_base(const struct commit_list *seen, int num_rev)
 {
-	int all_mask = ((1u << (REV_SHIFT + num_rev)) - 1);
-	int all_revs = all_mask & ~((1u << REV_SHIFT) - 1);
+	uint64_t all_mask = ((UINT64_C(1) << (REV_SHIFT + num_rev)) - 1);
+	uint64_t all_revs = all_mask & ~((UINT64_C(1) << REV_SHIFT) - 1);
 	int exit_status = 1;

 	for (const struct commit_list *s = seen; s; s = s->next) {
 		struct commit *commit = s->item;
-		int flags = commit->object.flags & all_mask;
+		uint64_t flags = get_rev_flags(commit) & all_mask;
 		if (!(flags & UNINTERESTING) &&
 		    ((flags & all_revs) == all_revs)) {
 			puts(oid_to_hex(&commit->object.oid));
 			exit_status = 0;
-			commit->object.flags |= UNINTERESTING;
+
+or_rev_flags(commit, UNINTERESTING);
+
 		}
 	}
 	return exit_status;
@@ -530,17 +544,17 @@ static int show_merge_base(const struct commit_list *seen, int num_rev)

 static int show_independent(struct commit **rev,
 			    int num_rev,
-			    unsigned int *rev_mask)
+			    uint64_t *rev_mask)
 {
 	int i;

 	for (i = 0; i < num_rev; i++) {
 		struct commit *commit = rev[i];
-		unsigned int flag = rev_mask[i];
+		uint64_t flag = rev_mask[i];

-		if (commit->object.flags == flag)
+		if (get_rev_flags(commit) == flag)
 			puts(oid_to_hex(&commit->object.oid));
-		commit->object.flags |= UNINTERESTING;
+		or_rev_flags(commit, UNINTERESTING);
 	}
 	return 0;
 }
@@ -607,9 +621,9 @@ static int omit_in_dense(struct commit *commit, struct commit **rev, int n)
 	for (i = 0; i < n; i++)
 		if (rev[i] == commit)
 			return 0;
-	flag = commit->object.flags;
+	flag = get_rev_flags(commit);
 	for (i = count = 0; i < n; i++) {
-		if (flag & (1u << (i + REV_SHIFT)))
+		if (flag & (UINT64_C(1) << (i + REV_SHIFT)))
 			count++;
 	}
 	if (count == 1)
@@ -648,10 +662,10 @@ int cmd_show_branch(int ac,
 	char *reflog_msg[MAX_REVS] = {0};
 	struct commit_list *seen = NULL;
 	struct prio_queue queue = { compare_commits_by_commit_date };
-	unsigned int rev_mask[MAX_REVS];
+	uint64_t rev_mask[MAX_REVS];
 	int num_rev, i, extra = 0;
 	int all_heads = 0, all_remotes = 0;
-	int all_mask, all_revs;
+	uint64_t all_mask, all_revs;
 	enum rev_sort_order sort_order = REV_SORT_IN_GRAPH_ORDER;
 	char *head;
 	struct object_id head_oid;
@@ -714,6 +728,7 @@ int cmd_show_branch(int ac,
 	int ret;

 	init_commit_name_slab(&name_slab);
+	init_commit_rev_flags(&rev_flags_slab);

 	repo_config(the_repository, git_show_branch_config, NULL);

@@ -759,7 +774,7 @@ int cmd_show_branch(int ac,
 		struct object_id oid;
 		char *ref;
 		int base = 0;
-		unsigned int flags = 0;
+		uint64_t flags = 0;

 		if (ac == 0) {
 			static const char *fake_av[2];
@@ -779,8 +794,8 @@ int cmd_show_branch(int ac,
 			die(_("--reflog option needs one branch name"));

 		if (MAX_REVS < reflog)
-			die(Q_("only %d entry can be shown at one time.",
-			       "only %d entries can be shown at one time.",
+			die(Q_("only %zu entry can be shown at one time.",
+			       "only %zu entries can be shown at one time.",
 			       MAX_REVS), MAX_REVS);
 		if (!repo_dwim_ref(the_repository, *av, strlen(*av), &oid,
 				   &ref, 0))
@@ -870,11 +885,11 @@ int cmd_show_branch(int ac,

 	for (num_rev = 0; ref_name[num_rev]; num_rev++) {
 		struct object_id revkey;
-		unsigned int flag = 1u << (num_rev + REV_SHIFT);
+		uint64_t flag = UINT64_C(1) << (num_rev + REV_SHIFT);

 		if (MAX_REVS <= num_rev)
-			die(Q_("cannot handle more than %d rev.",
-			       "cannot handle more than %d revs.",
+			die(Q_("cannot handle more than %zu rev.",
+			       "cannot handle more than %zu revs.",
 			       MAX_REVS), MAX_REVS);
 		if (repo_get_oid(the_repository, ref_name[num_rev], &revkey))
 			die(_("'%s' is not a valid ref."), ref_name[num_rev]);
@@ -889,13 +904,13 @@ int cmd_show_branch(int ac,
 		 * and so on.  REV_SHIFT bits from bit 0 are used for
 		 * internal bookkeeping.
 		 */
-		commit->object.flags |= flag;
-		if (commit->object.flags == flag)
+		or_rev_flags(commit, flag);
+		if (get_rev_flags(commit) == flag)
 			prio_queue_put(&queue, commit);
 		rev[num_rev] = commit;
 	}
 	for (i = 0; i < num_rev; i++)
-		rev_mask[i] = rev[i]->object.flags;
+		rev_mask[i] = get_rev_flags(rev[i]);

 	if (0 <= extra)
 		join_revs(&queue, &seen, num_rev, extra);
@@ -958,12 +973,12 @@ int cmd_show_branch(int ac,
 	if (!sha1_name && !no_name)
 		name_commits(seen, rev, ref_name, num_rev);

-	all_mask = ((1u << (REV_SHIFT + num_rev)) - 1);
-	all_revs = all_mask & ~((1u << REV_SHIFT) - 1);
+	all_mask = ((UINT64_C(1) << (REV_SHIFT + num_rev)) - 1);
+	all_revs = all_mask & ~((UINT64_C(1) << REV_SHIFT) - 1);

 	for (struct commit_list *l = seen; l; l = l->next) {
 		struct commit *commit = l->item;
-		int this_flag = commit->object.flags;
+		uint64_t this_flag = get_rev_flags(commit);
 		int is_merge_point = ((this_flag & all_revs) == all_revs);

 		shown_merge_point |= is_merge_point;
@@ -973,14 +988,14 @@ int cmd_show_branch(int ac,
 					  commit->parents->next);
 			if (topics &&
 			    !is_merge_point &&
-			    (this_flag & (1u << REV_SHIFT)))
+			    (this_flag & (UINT64_C(1) << REV_SHIFT)))
 				continue;
 			if (!sparse && is_merge &&
 			    omit_in_dense(commit, rev, num_rev))
 				continue;
 			for (i = 0; i < num_rev; i++) {
 				int mark;
-				if (!(this_flag & (1u << (i + REV_SHIFT))))
+				if (!(this_flag & (UINT64_C(1) << (i + REV_SHIFT))))
 					mark = ' ';
 				else if (is_merge)
 					mark = '-';
@@ -1010,6 +1025,7 @@ int cmd_show_branch(int ac,
 		free(reflog_msg[i]);
 	commit_list_free(seen);
 	clear_prio_queue(&queue);
+	clear_commit_rev_flags(&rev_flags_slab);
 	free(args_copy);
 	free(head);
 	return ret;
--
2.54.0


^ permalink raw reply related

* [ANNOUNCE] Git for Windows 2.55.0(3)
From: Johannes Schindelin @ 2026-07-14 19:44 UTC (permalink / raw)
  To: git, git-packagers

Dear Git users,

I hereby announce that Git for Windows 2.55.0(3) is available from:

    https://gitforwindows.org/

Changes since Git for Windows v2.55.0(2) (July 2nd 2026):

New Features

  * Comes with Git Credential Manager v2.9.0.

Bug Fixes

  * Fixes heap overflows in the credential helper wincred, see
    GHSA-rxqw-wxqg-g7hw for full details.

Git-2.55.0.3-64-bit.exe | af12577d0fdff74243a5988197aa49b957d5044edc17004f6ddf0768996f1dca
Git-2.55.0.3-arm64.exe | e3d7f5a2214f214f0a93cf0d8915dab236a0e91c7de6de70a7dbde9a61c794db
PortableGit-2.55.0.3-64-bit.7z.exe | ab00566336b5472120f9a52d34f2e79c5406535792acb0548001ffd0bd090e5d
PortableGit-2.55.0.3-arm64.7z.exe | 3bf26b94d9399b16a890776e468334f501742861576cbcdea2d9134643c374bd
MinGit-2.55.0.3-64-bit.zip | f48e2d2dc74a24454adc6d8fd0ac25bf9c2386f19cfb06202b9465aaad4f9f05
MinGit-2.55.0.3-arm64.zip | f7748965d5068e81ad93ca1923650db6742d6e22332b1ae7567a841c59f6bde5
MinGit-2.55.0.3-32-bit.zip | 352380d06caa45e569a3b3967b6d1d6c605d564c29f37ef059b59e657a522ef4
MinGit-2.55.0.3-busybox-64-bit.zip | cbb2ade2bf690b62f0d692ec64733cb26c6b4ea294b0b9752a705446f011b41f
MinGit-2.55.0.3-busybox-32-bit.zip | 88a703c92b8af980d6bbbdeb3b4a531c6d615879ec8c16ddac16cd5d3dbabd49
Git-2.55.0.3-64-bit.tar.bz2 | 4ee071816e424f928f493c4b42e5486d05344a371665c82f1802ebcecaa1d19a
Git-2.55.0.3-arm64.tar.bz2 | ff753aa49b9baeafda33470128ee799b19e48b06736d3c555585bc926dc13b2d

Ciao,
Johannes

^ permalink raw reply

* Re: [PATCH GSoC v17 10/13] transport: add client support for object-info
From: Pablo Sabater @ 2026-07-14 19:33 UTC (permalink / raw)
  To: Junio C Hamano, Pablo Sabater
  Cc: chandrapratap3519, chriscool, eric.peijian, git, jltobler,
	karthik.188, peff, toon, Calvin Wan, Jonathan Tan
In-Reply-To: <xmqqik6htpv4.fsf@gitster.g>

On Tue Jul 14, 2026 at 7:58 PM CEST, Junio C Hamano wrote:
> Pablo Sabater <pabloosabaterr@gmail.com> writes:
>
>> +	for (size_t i = 0; packet_reader_read(reader) == PACKET_READ_NORMAL && i < args->oids->nr; i++) {
>
> An overly long line.  Format it like this, perhaps?
>
> 	for (size_t i = 0;
> 	     packet_reader_read(reader) == PACKET_READ_NORMAL && i < args->oids->nr;
> 	     i++) {
>
> or even:
>
> 	for (size_t i = 0;
> 	     packet_reader_read(reader) == PACKET_READ_NORMAL &&
> 	     i < args->oids->nr;
> 	     i++) {
>

Will wrap that line, thanks.

>
>> +		struct string_list object_info_values = STRING_LIST_INIT_DUP;
>> +
>> +		string_list_split(&object_info_values, reader->line, " ", -1);
>> +		if (size_index >= 0) {
>> +			if (!strcmp(object_info_values.items[1 + size_index].string, "")) {
>> +				FREE_AND_NULL(object_info_data[i].sizep);
>> +				string_list_clear(&object_info_values, 0);
>> +				continue;
>> +			}
>> +
>> +			if (parse_object_size(object_info_values.items[1 + size_index].string,
>> +					      object_info_data[i].sizep))
>> +				die("object-info: ref %s has invalid size %s",
>> +				    object_info_values.items[0].string,
>> +				    object_info_values.items[1 + size_index].string);
>> +		}
>> +
>> +		string_list_clear(&object_info_values, 0);
>
> Is this not trusting the other side too much?
>
> If the other end returns fewer values than expected (e.g., if a
> buggy or malicious server returns only "<oid>" without a trailing
> space for an unrecognized object, or if we request multiple
> attributes in the future and the server returns fewer values than
> expected), string_list_split may return a list with fewer elements
> than size_index + 1.  Accessing object_info_values.items[size_index
> + 1] will then result in an out-of-bounds read/crash.

I will add a check for a malformed response from the server so "<oid>" is
considered corrupted, similar to the size values a few lines below.

A subsequent commit in this series (13) adds a filter that drops
attributes requested by the client but that the server doesn't support so
we should expect full return of the attributes asked or "<oid> SP".

I'll add a guard just in case in a future what we expect changes.

>
> By the way, from a stylistic standpoint, "size_index + 1" reads a
> bit more naturally than the "1 + size_index" used in the current
> patch.


Will change it.

Thanks for the feedback,
Pablo.


^ permalink raw reply

* Re: [PATCH v8 0/5] history: add squash subcommand to fold a range
From: Junio C Hamano @ 2026-07-14 18:41 UTC (permalink / raw)
  To: Ben Knoble
  Cc: Matt Hunter, Harald Nordgren via GitGitGadget, git, Phillip Wood,
	Patrick Steinhardt, Harald Nordgren
In-Reply-To: <0ECE2A94-0537-42E0-A525-FA16184D7735@gmail.com>

Ben Knoble <ben.knoble@gmail.com> writes:

>> Thanks for the work on this topic!
>
> Ditto! I suspect that using a combination of « git history squash
> » and « git replay » to emulate « git rebase » in non-interactive
> autosquash mode will be much faster, too, due to the differences
> in implementation. If that proves to be the case and we can safely
> do so with feature compatibility, I wonder if it will be worth
> making the non-interactive autosquash rebase actually delegate
> through a history squash + replay.

Yes, that would be an ideal future, and these efforts move us in
that direction.

> I’m sure there’s a few instances that couldn’t be done (for
> example when the special! commits cross the current range and
> upstream; that is, a fixup! for an upstream commit or some such
> oddity;; there are also conflicts to consider), but in the cases
> it can be it ought to be a performance win.

Since you assume "we can safely do so with feature compatibility"
above, once we are finished, there will, by definition, be no
such "special" commits that the combination cannot handle.  By
the time that happens, we will have replaced the internals of
"rebase [-i]" with a new implementation that does not need to
touch the working tree.

That would indeed be an exciting future.

^ permalink raw reply

* Re: [PATCH 2/2] fetch-pack: accept "pack" output for packfile URIs
From: Ted Nyman @ 2026-07-14 18:38 UTC (permalink / raw)
  To: Jeff King
  Cc: git, Junio C Hamano, Taylor Blau, Patrick Steinhardt,
	Karthik Nayak, brian m. carlson,
	Ævar Arnfjörð Bjarmason
In-Reply-To: <20260714071231.GD2516582@coredump.intra.peff.net>

> I also think this would all be much nicer with a strbuf (which would
> let us get rid of the magic numbers), but that is a slightly larger
> refactor:

Using a strbuf makes sense. One wrinkle, I think, is that with
transfer.fsckobjects enabled, index-pack can emit dangling .gitmodules
OIDs after the initial pack/keep line, which parse_gitmodules_oids()
still needs to read from cmd.out. Would strbuf_getwholeline_fd() be a
better fit here, so we don't consume those with strbuf_read()?

I'll also fix the --index-pack-args documentation while rerolling.

Thanks,
Ted

^ permalink raw reply

* git-last-modified(1) slower than git-log(1)?
From: Gusted @ 2026-07-14 18:33 UTC (permalink / raw)
  To: git, Toon Claes

Hi,

I'm working at switching Forgejo's implementation of getting the last
modified commits in a directory to git-last-modified(1). I'd expected
equal or better performance than the current implementation, but have
not yet been able to get this and I'm a bit puzzled as to why.

The current implementation of Forgejo (inherited from Gitea) works
roughly like this:
1. Run `git log --name-status -c --format=commit%x00%H %P%x00" --parents
--no-renames -t -z $OID -- :(literal)some/path`, the output of this is
quite complex and possible outputs more information than necessary.
2. The output of this is piped to some code to a parser and reconstructs
what commit ID last modified each file in the directory.
3. Via `git cat-file --batch` get each unique commits information.

With git-last-modified(1) (-z --show-trees --max-depth=0) this replaces
step 1-2, but is slower. I've isolated the degraded performance to the
fact that git-last-changed(1) takes more time to finish. So from my
perspective it does not seem worth it to replace the current
implementation with git-last-modified(1), and I would like to know if
I'm missing something here or if git-last-modified(1) possibly could see
a speedup?

The repository I'm currently using to evaluate the performance is
https://codeberg.org/ziglang/zig

Reproduction steps:
1. `git clone https://codeberg.org/ziglang/zig $(mktemp -d)`
2. cd to tmp directory.
3. `git commit-graph write --changed-paths`. As git-last-modified(1)
makes good use of the bloom filters.
4. `hyperfine 'git last-modified -z -t --max-depth=0
80d06578ac66bce3aa0a21e9610cdb782b9a0593 -- doc/langref/' 'git log
--name-status -c "--format=commit%x00%H %P%x00" --parents --no-renames
-t -z 80d06578ac66bce3aa0a21e9610cdb782b9a0593 -- ":(literal)doc/langref"'`

With as output:
Benchmark 1: git last-modified -z -t --max-depth=0
80d06578ac66bce3aa0a21e9610cdb782b9a0593 -- doc/langref/
 Time (mean ± σ): 66.5 ms ± 0.6 ms [User: 60.6 ms, System: 5.2 ms]
 Range (min … max): 65.3 ms … 67.7 ms 44 runs

Benchmark 2: git log --name-status -c "--format=commit%x00%H %P%x00"
--parents --no-renames -t -z 80d06578ac66bce3aa0a21e9610cdb782b9a0593 --
":(literal)doc/langref"
 Time (mean ± σ): 26.2 ms ± 1.0 ms [User: 17.3 ms, System: 8.4 ms]
 Range (min … max): 24.3 ms … 30.1 ms 110 runs

Summary
 git log --name-status -c "--format=commit%x00%H %P%x00" --parents
--no-renames -t -z 80d06578ac66bce3aa0a21e9610cdb782b9a0593 --
":(literal)doc/langref" ran
 2.54 ± 0.10 times faster than git last-modified -z -t --max-depth=0
80d06578ac66bce3aa0a21e9610cdb782b9a0593 -- doc/langref/

Kind Regards
Gusted

^ permalink raw reply

* Re: [PATCH GSoC v17 00/13] cat-file: add remote-object-info to batch-command
From: Junio C Hamano @ 2026-07-14 18:33 UTC (permalink / raw)
  To: Pablo Sabater
  Cc: chandrapratap3519, chriscool, eric.peijian, git, jltobler,
	karthik.188, peff, toon
In-Reply-To: <20260714-ps-eric-work-rebase-v17-0-afabfc83260e@gmail.com>

Pablo Sabater <pabloosabaterr@gmail.com> writes:

> This patch series is a continuation of Eric Ju's
> (eric.peijian@gmail.com) and Calvin Wan's (calvinwan@google.com) patch
> series [1] and [2] respectively.

Yuck.  I thought we had this marked as "Will merge to 'next'?" for
some time and this morning I pushed out a merge to 'next' of v16.
I'll revert the merge and replace.

^ permalink raw reply

* Re: [PATCH 1/2] http: use unique tempfiles for packfile URI downloads
From: Ted Nyman @ 2026-07-14 18:31 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Jeff King, git, Taylor Blau, Patrick Steinhardt, Karthik Nayak,
	brian m. carlson, Ævar Arnfjörð Bjarmason
In-Reply-To: <xmqqcxwptpb0.fsf@gitster.g>

> I share that sentiment. I am not entirely convinced by Ted's
> response, since a major goal of the packfile URI feature, as I
> understand it, is to allow the use of resumable protocols for
> large transfers.

Agreed. I was too quick to dismiss the loss of resumption.

I'll take another look at preserving the predictable partial pack while
preventing concurrent writers, including the handoff and stale-file
cases Peff raised. Dumb HTTP has the same underlying concurrency issue,
so I'll keep that path in mind as well before sending a reroll.

Thanks,
Ted

^ permalink raw reply

* [PATCH] show-branch: convert object.flags usage to a commit-slab
From: Gatla Vishweshwar Reddy @ 2026-07-14 18:30 UTC (permalink / raw)
  To: git; +Cc: Gatla Vishweshwar Reddy

show-branch uses commit->object.flags to store two kinds of
per-commit data: the UNINTERESTING bit to mark commits that are
ancestors of all given revisions, and per-branch reachability
bits (one bit per branch, starting at REV_SHIFT) to track which
branches can reach each commit.

Using the shared object.flags field for this purpose is fragile.
The field is shared across the entire Git codebase and other
subsystems use it for their own bookkeeping. Storing show-branch
specific data there risks conflicts with other users of the same
field.

Convert this usage to a dedicated commit-slab named
commit_rev_flags, which is the canonical way to associate
per-commit data in Git without polluting the shared object flags.
Add helper functions get_rev_flags() and or_rev_flags() to
encapsulate slab access cleanly, and initialize and clear the
slab in cmd_show_branch() to avoid memory leaks.

Signed-off-by: Gatla Vishweshwar Reddy <gatlavishweshwarreddy26@gmail.com>
---
 builtin/show-branch.c | 62 +++++++++++++++++++++++++++----------------
 1 file changed, 39 insertions(+), 23 deletions(-)

diff --git a/builtin/show-branch.c b/builtin/show-branch.c
index f02831b085..ad3a85fafa 100644
--- a/builtin/show-branch.c
+++ b/builtin/show-branch.c
@@ -34,13 +34,11 @@ static enum git_colorbool showbranch_use_color = GIT_COLOR_UNKNOWN;
 
 static struct strvec default_args = STRVEC_INIT;
 
-/*
- * TODO: convert this use of commit->object.flags to commit-slab
- * instead to store a pointer to ref name directly. Then use the same
- * UNINTERESTING definition from revision.h here.
- */
 #define UNINTERESTING	01
 
+static unsigned int get_rev_flags(struct commit *commit);
+static void or_rev_flags(struct commit *commit, unsigned int flags);
+
 #define REV_SHIFT	 2
 #define MAX_REVS	(FLAG_BITS - REV_SHIFT) /* should not exceed bits_per_int - REV_SHIFT */
 
@@ -64,7 +62,7 @@ static struct commit *interesting(struct prio_queue *queue)
 {
 	for (size_t i = 0; i < queue->nr; i++) {
 		struct commit *commit = queue->array[i].data;
-		if (commit->object.flags & UNINTERESTING)
+		if (get_rev_flags(commit) & UNINTERESTING)
 			continue;
 		return commit;
 	}
@@ -79,11 +77,25 @@ struct commit_name {
 define_commit_slab(commit_name_slab, struct commit_name *);
 static struct commit_name_slab name_slab;
 
+define_commit_slab(commit_rev_flags, unsigned int);
+static struct commit_rev_flags rev_flags_slab;
+
 static struct commit_name *commit_to_name(struct commit *commit)
 {
 	return *commit_name_slab_at(&name_slab, commit);
 }
 
+static unsigned int get_rev_flags(struct commit *commit)
+{
+	unsigned int *f = commit_rev_flags_peek(&rev_flags_slab, commit);
+	return f ? *f : 0;
+}
+
+static void or_rev_flags(struct commit *commit, unsigned int flags)
+{
+	*commit_rev_flags_at(&rev_flags_slab, commit) |= flags;
+}
+
 
 /* Name the commit as nth generation ancestor of head_name;
  * we count only the first-parent relationship for naming purposes.
@@ -215,7 +227,7 @@ static void name_commits(struct commit_list *list,
 
 static int mark_seen(struct commit *commit, struct commit_list **seen_p)
 {
-	if (!commit->object.flags) {
+	if (!get_rev_flags(commit)) {
 		commit_list_insert(commit, seen_p);
 		return 1;
 	}
@@ -234,7 +246,7 @@ static void join_revs(struct prio_queue *queue,
 		int still_interesting = !!interesting(queue);
 		struct commit *commit = prio_queue_peek(queue);
 		bool get_pending = true;
-		int flags = commit->object.flags & all_mask;
+		int flags = get_rev_flags(commit) & all_mask;
 
 		if (!still_interesting && extra <= 0)
 			break;
@@ -246,14 +258,14 @@ static void join_revs(struct prio_queue *queue,
 
 		while (parents) {
 			struct commit *p = parents->item;
-			int this_flag = p->object.flags;
+			int this_flag = get_rev_flags(p);
 			parents = parents->next;
 			if ((this_flag & flags) == flags)
 				continue;
 			repo_parse_commit(the_repository, p);
 			if (mark_seen(p, seen_p) && !still_interesting)
 				extra--;
-			p->object.flags |= flags;
+			or_rev_flags(p, flags);
 			if (get_pending)
 				prio_queue_replace(queue, p);
 			else
@@ -278,8 +290,8 @@ static void join_revs(struct prio_queue *queue,
 			struct commit *c = s->item;
 			struct commit_list *parents;
 
-			if (((c->object.flags & all_revs) != all_revs) &&
-			    !(c->object.flags & UNINTERESTING))
+			if (((get_rev_flags(c) & all_revs) != all_revs) &&
+			    !(get_rev_flags(c) & UNINTERESTING))
 				continue;
 
 			/* The current commit is either a merge base or
@@ -292,8 +304,8 @@ static void join_revs(struct prio_queue *queue,
 			while (parents) {
 				struct commit *p = parents->item;
 				parents = parents->next;
-				if (!(p->object.flags & UNINTERESTING)) {
-					p->object.flags |= UNINTERESTING;
+				if (!(get_rev_flags(p) & UNINTERESTING)) {
+					or_rev_flags(p, UNINTERESTING);
 					changed = 1;
 				}
 			}
@@ -517,12 +529,14 @@ static int show_merge_base(const struct commit_list *seen, int num_rev)
 
 	for (const struct commit_list *s = seen; s; s = s->next) {
 		struct commit *commit = s->item;
-		int flags = commit->object.flags & all_mask;
+		int flags = get_rev_flags(commit) & all_mask;
 		if (!(flags & UNINTERESTING) &&
 		    ((flags & all_revs) == all_revs)) {
 			puts(oid_to_hex(&commit->object.oid));
 			exit_status = 0;
-			commit->object.flags |= UNINTERESTING;
+
+or_rev_flags(commit, UNINTERESTING);
+
 		}
 	}
 	return exit_status;
@@ -538,9 +552,9 @@ static int show_independent(struct commit **rev,
 		struct commit *commit = rev[i];
 		unsigned int flag = rev_mask[i];
 
-		if (commit->object.flags == flag)
+		if (get_rev_flags(commit) == flag)
 			puts(oid_to_hex(&commit->object.oid));
-		commit->object.flags |= UNINTERESTING;
+		or_rev_flags(commit, UNINTERESTING);
 	}
 	return 0;
 }
@@ -607,7 +621,7 @@ static int omit_in_dense(struct commit *commit, struct commit **rev, int n)
 	for (i = 0; i < n; i++)
 		if (rev[i] == commit)
 			return 0;
-	flag = commit->object.flags;
+	flag = get_rev_flags(commit);
 	for (i = count = 0; i < n; i++) {
 		if (flag & (1u << (i + REV_SHIFT)))
 			count++;
@@ -714,6 +728,7 @@ int cmd_show_branch(int ac,
 	int ret;
 
 	init_commit_name_slab(&name_slab);
+	init_commit_rev_flags(&rev_flags_slab);
 
 	repo_config(the_repository, git_show_branch_config, NULL);
 
@@ -889,13 +904,13 @@ int cmd_show_branch(int ac,
 		 * and so on.  REV_SHIFT bits from bit 0 are used for
 		 * internal bookkeeping.
 		 */
-		commit->object.flags |= flag;
-		if (commit->object.flags == flag)
+		or_rev_flags(commit, flag);
+		if (get_rev_flags(commit) == flag)
 			prio_queue_put(&queue, commit);
 		rev[num_rev] = commit;
 	}
 	for (i = 0; i < num_rev; i++)
-		rev_mask[i] = rev[i]->object.flags;
+		rev_mask[i] = get_rev_flags(rev[i]);
 
 	if (0 <= extra)
 		join_revs(&queue, &seen, num_rev, extra);
@@ -963,7 +978,7 @@ int cmd_show_branch(int ac,
 
 	for (struct commit_list *l = seen; l; l = l->next) {
 		struct commit *commit = l->item;
-		int this_flag = commit->object.flags;
+		int this_flag = get_rev_flags(commit);
 		int is_merge_point = ((this_flag & all_revs) == all_revs);
 
 		shown_merge_point |= is_merge_point;
@@ -1010,6 +1025,7 @@ int cmd_show_branch(int ac,
 		free(reflog_msg[i]);
 	commit_list_free(seen);
 	clear_prio_queue(&queue);
+	clear_commit_rev_flags(&rev_flags_slab);
 	free(args_copy);
 	free(head);
 	return ret;
-- 
2.54.0


^ permalink raw reply related

* [PATCH v19 7/7] branch: add --dry-run for --delete-merged
From: Harald Nordgren via GitGitGadget @ 2026-07-14 18:24 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Johannes Sixt, Phillip Wood,
	Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2285.v19.git.git.1784053493.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

"git branch --dry-run --delete-merged ..." prints one line per ref that
would be deleted without modifying refs or branch configuration.

--dry-run is only meaningful together with --delete-merged and is
rejected otherwise.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 Documentation/git-branch.adoc |  8 +++++-
 builtin/branch.c              | 54 +++++++++++++++++++++++------------
 t/t3200-branch.sh             | 35 ++++++++++++++++++++++-
 3 files changed, 77 insertions(+), 20 deletions(-)

diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index ffb39811ab..633031f248 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -25,7 +25,7 @@ git branch (-m|-M) [<old-branch>] <new-branch>
 git branch (-c|-C) [<old-branch>] <new-branch>
 git branch (-d|-D) [-r] <branch-name>...
 git branch --edit-description [<branch-name>]
-git branch (--delete-merged <branch>)... [<pattern>...]
+git branch [--dry-run] (--delete-merged <branch>)... [<pattern>...]
 
 DESCRIPTION
 -----------
@@ -233,6 +233,12 @@ kept, so a branch is never deleted out from under one stacked on top
 of it. If that kept branch in turn tracks a branch that is being
 deleted, its now-stale upstream configuration is cleared.
 
+`--dry-run`::
+	With `--delete-merged`, print which branches would be
+	deleted and exit without touching any ref.  Useful for
+	sanity-checking a wide pattern like `'origin/*'` before
+	committing to the deletion.
+
 `-v`::
 `-vv`::
 `--verbose`::
diff --git a/builtin/branch.c b/builtin/branch.c
index 61f414b3c7..117af854a0 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -199,6 +199,7 @@ enum delete_branch_flags {
 	DELETE_BRANCH_QUIET = (1 << 1),
 	DELETE_BRANCH_SKIP_UNMERGED = (1 << 2),
 	DELETE_BRANCH_NO_HEAD_FALLBACK = (1 << 3),
+	DELETE_BRANCH_DRY_RUN = (1 << 4),
 };
 
 static int check_branch_commit(const char *branchname, const char *refname,
@@ -340,13 +341,20 @@ static int delete_branches(int argc, const char **argv, int kinds,
 		free(target);
 	}
 
-	if (refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
+	if (!(flags & DELETE_BRANCH_DRY_RUN) &&
+	    refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
 		ret = 1;
 
 	for_each_string_list_item(item, &refs_to_delete) {
 		char *describe_ref = item->util;
 		char *name = item->string;
-		if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
+		if (flags & DELETE_BRANCH_DRY_RUN) {
+			if (!(flags & DELETE_BRANCH_QUIET))
+				printf(remote_branch
+					? _("Would delete remote-tracking branch %s (was %s).\n")
+					: _("Would delete branch %s (was %s).\n"),
+					name + branch_name_pos, describe_ref);
+		} else if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
 			char *refname = name + branch_name_pos;
 			if (!(flags & DELETE_BRANCH_QUIET))
 				printf(remote_branch
@@ -736,7 +744,8 @@ static int spare_stacked_base(const struct reference *ref, void *cb_data)
  * base is itself merged, so when its own upstream is also going away
  * (no surviving branch tracks it), clear the base's now-stale upstream.
  */
-static void spare_stacked_bases(struct ref_store *refs, struct strset *deletable)
+static void spare_stacked_bases(struct ref_store *refs, struct strset *deletable,
+				unsigned int flags)
 {
 	struct strset spared = STRSET_INIT;
 	struct spare_data data = { .deletable = deletable, .spared = &spared };
@@ -746,21 +755,23 @@ static void spare_stacked_bases(struct ref_store *refs, struct strset *deletable
 
 	refs_for_each_branch_ref(refs, spare_stacked_base, &data);
 
-	strset_for_each_entry(&spared, &iter, entry) {
-		struct branch *branch = branch_get(entry->key);
-		const char *upstream = branch_get_upstream(branch, NULL);
-		const char *up_short;
+	if (!(flags & DELETE_BRANCH_DRY_RUN)) {
+		strset_for_each_entry(&spared, &iter, entry) {
+			struct branch *branch = branch_get(entry->key);
+			const char *upstream = branch_get_upstream(branch, NULL);
+			const char *up_short;
 
-		if (!upstream || !skip_prefix(upstream, "refs/heads/", &up_short) ||
-		    !strset_contains(deletable, up_short))
-			continue;
+			if (!upstream || !skip_prefix(upstream, "refs/heads/", &up_short) ||
+			    !strset_contains(deletable, up_short))
+				continue;
 
-		strbuf_reset(&key);
-		strbuf_addf(&key, "branch.%s.merge", branch->name);
-		repo_config_set_gently(the_repository, key.buf, NULL);
-		strbuf_reset(&key);
-		strbuf_addf(&key, "branch.%s.remote", branch->name);
-		repo_config_set_gently(the_repository, key.buf, NULL);
+			strbuf_reset(&key);
+			strbuf_addf(&key, "branch.%s.merge", branch->name);
+			repo_config_set_gently(the_repository, key.buf, NULL);
+			strbuf_reset(&key);
+			strbuf_addf(&key, "branch.%s.remote", branch->name);
+			repo_config_set_gently(the_repository, key.buf, NULL);
+		}
 	}
 
 	strbuf_release(&key);
@@ -843,7 +854,7 @@ static int delete_merged_branches(const struct strvec *upstreams,
 		strset_add(&deletable, short_name);
 	}
 
-	spare_stacked_bases(refs, &deletable);
+	spare_stacked_bases(refs, &deletable, flags);
 
 	strset_for_each_entry(&deletable, &iter, entry)
 		strvec_push(&to_delete, entry->key);
@@ -905,6 +916,7 @@ int cmd_branch(int argc,
 	int delete = 0, rename = 0, copy = 0, list = 0,
 	    unset_upstream = 0, show_current = 0, edit_description = 0;
 	struct strvec delete_merged = STRVEC_INIT;
+	int dry_run = 0;
 	const char *new_upstream = NULL;
 	int noncreate_actions = 0;
 	/* possible options */
@@ -961,6 +973,8 @@ int cmd_branch(int argc,
 		OPT_CALLBACK_F(0, "delete-merged", &delete_merged, N_("branch"),
 			N_("delete merged branches whose upstream matches <branch> (repeatable)"),
 			PARSE_OPT_NONEG, parse_opt_strvec),
+		OPT_BOOL(0, "dry-run", &dry_run,
+			N_("with --delete-merged, only print which branches would be deleted")),
 		OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE),
 		OPT_MERGED(&filter, N_("print only branches that are merged")),
 		OPT_NO_MERGED(&filter, N_("print only branches that are not merged")),
@@ -1023,6 +1037,9 @@ int cmd_branch(int argc,
 	if (noncreate_actions > 1)
 		usage_with_options(builtin_branch_usage, options);
 
+	if (dry_run && !delete_merged.nr)
+		die(_("--dry-run requires --delete-merged"));
+
 	if (recurse_submodules_explicit) {
 		if (!submodule_propagate_branches)
 			die(_("branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled"));
@@ -1063,7 +1080,8 @@ int cmd_branch(int argc,
 		goto out;
 	} else if (delete_merged.nr) {
 		ret = delete_merged_branches(&delete_merged, argv,
-					     quiet ? DELETE_BRANCH_QUIET : 0);
+					     (quiet ? DELETE_BRANCH_QUIET : 0) |
+					     (dry_run ? DELETE_BRANCH_DRY_RUN : 0));
 		goto out;
 	} else if (show_current) {
 		print_current_branch_name();
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 54292bfbdf..c055bc8287 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -1900,6 +1900,19 @@ test_expect_success '--delete-merged deletes only selected merged branches' '
 		git checkout -b tracks-other other/main --track &&
 		sha=$(git rev-parse --short merged) &&
 
+		git branch --dry-run --delete-merged origin/next merged >actual 2>&1 &&
+		echo "Would delete branch merged (was $sha)." >expect &&
+		test_cmp expect actual &&
+		git rev-parse --verify refs/heads/merged &&
+
+		check_branches <<-\EOF &&
+		also-merged
+		main
+		merged
+		tracks-other
+		unmerged
+		EOF
+
 		git branch --delete-merged origin/next merged >actual 2>&1 &&
 		echo "Deleted branch merged (was $sha)." >expect &&
 		test_cmp expect actual &&
@@ -1948,9 +1961,12 @@ test_expect_success '--delete-merged keeps the upstream of a surviving branch' '
 		git checkout -b topic feature --track &&
 		git commit --allow-empty -m "topic work" &&
 
-		git branch --delete-merged origin/next 2>err &&
+		git branch --dry-run --delete-merged origin/next >out &&
+		test_grep ! "feature" out &&
 
+		git branch --delete-merged origin/next 2>err &&
 		test_must_be_empty err &&
+
 		check_branches <<-\EOF &&
 		feature
 		main
@@ -1978,6 +1994,18 @@ test_expect_success '--delete-merged clears the deleted upstream of a spared bra
 		git checkout -b tip mid --track &&
 		git commit --allow-empty -m "tip work" &&
 
+		git branch --dry-run --delete-merged origin/next \
+			--delete-merged lower &&
+
+		git config --local --get-regexp "branch\\.(mid|tip)\\.(merge|remote)" >actual &&
+		cat >expect <<-\EOF &&
+		branch.mid.remote .
+		branch.mid.merge refs/heads/lower
+		branch.tip.remote .
+		branch.tip.merge refs/heads/mid
+		EOF
+		test_cmp expect actual &&
+
 		git branch --delete-merged origin/next \
 			--delete-merged lower &&
 
@@ -2036,4 +2064,9 @@ test_expect_success "branch -d still deletes a deleteMerged=false branch" '
 	)
 '
 
+test_expect_success '--dry-run without --delete-merged is rejected' '
+	test_must_fail git -C forked branch --dry-run 2>err &&
+	test_grep "requires --delete-merged" err
+'
+
 test_done
-- 
gitgitgadget

^ permalink raw reply related

* [PATCH v19 6/7] branch: add branch.<name>.deleteMerged opt-out
From: Harald Nordgren via GitGitGadget @ 2026-07-14 18:24 UTC (permalink / raw)
  To: git
  Cc: Kristoffer Haugsbakk, Johannes Sixt, Phillip Wood,
	Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2285.v19.git.git.1784053493.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

Setting branch.<name>.deleteMerged=false exempts that branch from
"git branch --delete-merged", which is useful for a topic you want
to keep developing after an early round of it has been merged
upstream. Unless --quiet is given, each skip is reported so the
user knows why their topic was kept.

Explicit deletion with "git branch -d" still uses the normal merge
check and ignores this setting.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 Documentation/config/branch.adoc |  7 +++++++
 Documentation/git-branch.adoc    |  5 +++--
 builtin/branch.c                 | 14 +++++++++++++
 t/t3200-branch.sh                | 36 ++++++++++++++++++++++++++++++++
 4 files changed, 60 insertions(+), 2 deletions(-)

diff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc
index a4db9fa5c8..d8483acb4f 100644
--- a/Documentation/config/branch.adoc
+++ b/Documentation/config/branch.adoc
@@ -102,3 +102,10 @@ for details).
 	`git branch --edit-description`. Branch description is
 	automatically added to the `format-patch` cover letter or
 	`request-pull` summary.
+
+`branch.<name>.deleteMerged`::
+	If set to `false`, branch _<name>_ is exempt from
+	`git branch --delete-merged`.  Useful for a topic branch you
+	intend to develop further after an initial round has been
+	merged upstream.  Defaults to true.  Explicit deletion via
+	`git branch -d` is unaffected.
diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index cee3904cfd..ffb39811ab 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -216,11 +216,12 @@ A branch is not deleted when:
 +
 --
 * its configured upstream ref no longer exists,
-* it is checked out in any worktree, or
+* it is checked out in any worktree,
 * pushing it by name to the remote configured by
   `branch.<name>.remote` would update its upstream, so it cannot be
   distinguished from a branch that just looks "fully merged" right
-  after a pull.
+  after a pull, or
+* `branch.<name>.deleteMerged` is set to `false`.
 --
 +
 A branch whose work has not yet been merged into its upstream is
diff --git a/builtin/branch.c b/builtin/branch.c
index 8ce8840fa7..61f414b3c7 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -791,6 +791,7 @@ static int delete_merged_branches(const struct strvec *upstreams,
 	struct ref_array candidates = { 0 };
 	struct strset deletable = STRSET_INIT;
 	struct strvec to_delete = STRVEC_INIT;
+	struct strbuf key = STRBUF_INIT;
 	struct hashmap_iter iter;
 	struct strmap_entry *entry;
 	size_t i;
@@ -810,6 +811,7 @@ static int delete_merged_branches(const struct strvec *upstreams,
 		const char *short_name;
 		struct branch *branch;
 		const char *upstream;
+		int opt_out;
 
 		if (!skip_prefix(full_name, "refs/heads/", &short_name))
 			BUG("filter returned non-branch ref '%s'", full_name);
@@ -827,6 +829,17 @@ static int delete_merged_branches(const struct strvec *upstreams,
 					FILTER_REFS_BRANCHES, DELETE_BRANCH_SKIP_UNMERGED))
 			continue;
 
+		strbuf_reset(&key);
+		strbuf_addf(&key, "branch.%s.deletemerged", short_name);
+		if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
+		    !opt_out) {
+			if (!(flags & DELETE_BRANCH_QUIET))
+				fprintf(stderr,
+					_("Skipping '%s' (branch.%s.deleteMerged is false)\n"),
+					short_name, short_name);
+			continue;
+		}
+
 		strset_add(&deletable, short_name);
 	}
 
@@ -842,6 +855,7 @@ static int delete_merged_branches(const struct strvec *upstreams,
 				      DELETE_BRANCH_NO_HEAD_FALLBACK |
 				      flags);
 
+	strbuf_release(&key);
 	strvec_clear(&to_delete);
 	strset_clear(&deletable);
 	ref_array_clear(&candidates);
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index fa8a60c9e7..54292bfbdf 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -2000,4 +2000,40 @@ test_expect_success '--delete-merged requires a value' '
 	test_must_fail git -C forked branch --delete-merged 2>err &&
 	test_grep "requires a value" err
 '
+
+test_expect_success '--delete-merged honours branch.<name>.deleteMerged=false' '
+	setup_repo_for_delete_merged &&
+	create_merged_branch deleted &&
+	create_merged_branch kept &&
+	(
+		cd repo &&
+		git config branch.kept.deleteMerged false &&
+		git checkout --detach &&
+
+		git branch --delete-merged origin/next 2>err &&
+
+		test_grep "Skipping .kept." err &&
+		check_branches <<-\EOF
+		kept
+		main
+		EOF
+	)
+'
+
+test_expect_success "branch -d still deletes a deleteMerged=false branch" '
+	setup_repo_for_delete_merged &&
+	create_merged_branch kept &&
+	(
+		cd repo &&
+		git config branch.kept.deleteMerged false &&
+		git checkout --detach &&
+
+		git branch -d kept &&
+
+		check_branches <<-\EOF
+		main
+		EOF
+	)
+'
+
 test_done
-- 
gitgitgadget


^ permalink raw reply related


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