Git development
 help / color / mirror / Atom feed
* Re: [PATCH 1/1] shallow: fix relative deepen on non-shallow repositories
From: Junio C Hamano @ 2026-05-11  8:30 UTC (permalink / raw)
  To: René Scharfe; +Cc: Samo Pogačnik, git, owen
In-Reply-To: <ac1aac76-17bc-469b-8dc1-d3a384f5c6af@web.de>

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

> Perhaps, but no warning has been given for deepening a non-shallow repo
> since the introduction of this option by cccf74e2da (fetch, upload-pack:
> --deepen=N extends shallow boundary by N commits, 2016-06-12).
>
> The best place for such a warning would be close to the user, in fetch,
> no?  And in its own patch.

Yeah, the lack of warning may or may not be considered a bug, but I
agree that it is totally orthogonal to the problem the patch is
trying to address.

Thanks.

^ permalink raw reply

* Re: [PATCH v3] doc: add caveat about turning off commit-graph
From: Oswald Buddenhagen @ 2026-05-11  8:29 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Kristoffer Haugsbakk, Derrick Stolee, git
In-Reply-To: <xmqq8q9qwxrr.fsf@gitster.g>

On Mon, May 11, 2026 at 10:16:56AM +0900, Junio C Hamano wrote:
>There will be lot of unapplied patches left on the mailing list 
>initially until the contributors adjust their behaviour,

>but in the long run it may be beneficial?  
>
no, it won't, because "the contributors" doesn't have a collective mind 
beyond the core group.

every bit of bureaucracy you add just leads to fewer successful (and 
subsequently attempted repeat) contributions.

if the scalability problems with making things contributor-friendly are 
too much for you, then rethink the process/tooling. i've already made my 
case for gerrit [1] ...

[1] https://lore.kernel.org/git/ZcA0NEb+lnjeZUBe@ugly/

^ permalink raw reply

* Re: [PATCH] ci: enable EXPENSIVE for contributor builds
From: Junio C Hamano @ 2026-05-11  8:29 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: Johannes Schindelin via GitGitGadget, git, Derrick Stolee,
	Torsten Bögershausen, Jeff King, Johannes Schindelin
In-Reply-To: <agF_0x0yq78J-RFk@pks.im>

Patrick Steinhardt <ps@pks.im> writes:

> So with this change we now run the tests for all "official" branches,
> and on pull requests. Which raises the question: are there any events
> that happen regularly that are excluded by this? Because if not I think
> it might be sensible to just enable this unconditionally, also because
> that would make jobs on GitLab CI run expensive tests, as well.

The simplicity certainly is tempting.

We could instead do the "let's enable only on linux-test-vars" kind
of "optimization", which is on the other side of the extreme, but
that is only valid if the kind of bugs that can be revealed only by
EXPENSIVE tests, which may not be caught by others, is expected to
be pretty much platform or configuration agnostic.  I somehow doubt
that it is the case.

In any case, I think spending on more machine cycles is certainly
cheaper than human resources for things like this.

Thanks.

^ permalink raw reply

* Re: [PATCH v5 2/5] branch: let delete_branches warn instead of error on bulk refusal
From: Junio C Hamano @ 2026-05-11  8:18 UTC (permalink / raw)
  To: Harald Nordgren via GitGitGadget
  Cc: git, Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren
In-Reply-To: <807c9f981fa05bd6e06228e54ddacb0a397a0f98.1778482708.git.gitgitgadget@gmail.com>

"Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> Add two new parameters to delete_branches() and the helper
> check_branch_commit():
>
> * warn_only switches the per-branch refusal from a hard error
>   ("error: the branch 'X' is not fully merged" plus a four-line
>   hint about 'git branch -D X') to a one-line warning, and
>   causes the function to skip those branches without setting its
>   exit code. Each refused branch is still skipped from deletion.
> * n_not_merged, when non-NULL, is incremented for each branch
>   refused on the not-merged path, so a bulk caller can summarize
>   rather than print per-branch advice.
>
> All existing call sites pass 0 / NULL and so are unaffected. Both
> parameters are wired up so a bulk-deletion caller can suppress
> the noise normally appropriate for a one-shot 'git branch -d'.

Existing call sites are about "branch -d <other>" that allows the
other branch to be deleted if it is part of HEAD or if it is part of
its tracking branch, but should "branch --prune-merged" pay
attention to what branch happens to be checked out the same way (not
a rherotical question to hint that I do not think it should---I do
not have a strong opinion on this either way)?

> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---
>  builtin/branch.c | 29 ++++++++++++++++++++---------
>  1 file changed, 20 insertions(+), 9 deletions(-)
>
> diff --git a/builtin/branch.c b/builtin/branch.c
> index b3289a8875..1941f8a9ad 100644
> --- a/builtin/branch.c
> +++ b/builtin/branch.c
> @@ -192,7 +192,8 @@ static int branch_merged(int kind, const char *name,
>  
>  static int check_branch_commit(const char *branchname, const char *refname,
>  			       const struct object_id *oid, struct commit *head_rev,
> -			       int kinds, int force)
> +			       int kinds, int force, int warn_only,
> +			       int *n_not_merged)
>  {
>  	struct commit *rev = lookup_commit_reference(the_repository, oid);
>  	if (!force && !rev) {
> @@ -200,10 +201,18 @@ static int check_branch_commit(const char *branchname, const char *refname,
>  		return -1;
>  	}
>  	if (!force && !branch_merged(kinds, branchname, rev, head_rev)) {
> -		error(_("the branch '%s' is not fully merged"), branchname);
> -		advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
> -				  _("If you are sure you want to delete it, "
> -				  "run 'git branch -D %s'"), branchname);
> +		if (warn_only) {
> +			warning(_("the branch '%s' is not fully merged"),
> +				branchname);
> +		} else {
> +			error(_("the branch '%s' is not fully merged"),
> +			      branchname);
> +			advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
> +					  _("If you are sure you want to delete it, "
> +					  "run 'git branch -D %s'"), branchname);
> +		}
> +		if (n_not_merged)
> +			(*n_not_merged)++;
>  		return -1;
>  	}
>  	return 0;
> @@ -219,7 +228,7 @@ static void delete_branch_config(const char *branchname)
>  }
>  
>  static int delete_branches(int argc, const char **argv, int force, int kinds,
> -			   int quiet)
> +			   int quiet, int warn_only, int *n_not_merged)
>  {
>  	struct commit *head_rev = NULL;
>  	struct object_id oid;
> @@ -309,8 +318,9 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
>  
>  		if (!(flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
>  		    check_branch_commit(bname.buf, name, &oid, head_rev, kinds,
> -					force)) {
> -			ret = 1;
> +					force, warn_only, n_not_merged)) {
> +			if (!warn_only)
> +				ret = 1;
>  			goto next;
>  		}
>  
> @@ -961,7 +971,8 @@ int cmd_branch(int argc,
>  	if (delete) {
>  		if (!argc)
>  			die(_("branch name required"));
> -		ret = delete_branches(argc, argv, delete > 1, filter.kind, quiet);
> +		ret = delete_branches(argc, argv, delete > 1, filter.kind,
> +				      quiet, 0, NULL);
>  		goto out;
>  	} else if (forked) {
>  		ret = list_forked_branches(argc, argv);

^ permalink raw reply

* Re: [PATCH] daemon: fix network address handling bugs
From: Patrick Steinhardt @ 2026-05-11  7:54 UTC (permalink / raw)
  To: Sebastien Tardif via GitGitGadget; +Cc: git, Sebastien Tardif
In-Reply-To: <pull.2299.git.git.1778291290159.gitgitgadget@gmail.com>

On Sat, May 09, 2026 at 01:48:10AM +0000, Sebastien Tardif via GitGitGadget wrote:
> From: Sebastien Tardif <sebtardif@ncf.ca>
> 
> Fix three related issues in daemon.c's network address handling:

This is a good indicator that this patch should be split up into three
patches, where each patch addresses one of the issues.

> diff --git a/daemon.c b/daemon.c
> index 0a7b1aae44..84a5e38f92 100644
> --- a/daemon.c
> +++ b/daemon.c
> @@ -674,10 +674,17 @@ static void lookup_hostname(struct hostinfo *hi)
>  
>  		gai = getaddrinfo(hi->hostname.buf, NULL, &hints, &ai);
>  		if (!gai) {
> -			struct sockaddr_in *sin_addr = (void *)ai->ai_addr;
> -
> -			inet_ntop(AF_INET, &sin_addr->sin_addr,
> -				  addrbuf, sizeof(addrbuf));
> +			if (ai->ai_family == AF_INET) {
> +				struct sockaddr_in *sa =
> +					(struct sockaddr_in *)ai->ai_addr;
> +				inet_ntop(AF_INET, &sa->sin_addr,
> +					  addrbuf, sizeof(addrbuf));
> +			} else if (ai->ai_family == AF_INET6) {
> +				struct sockaddr_in6 *sa6 =
> +					(struct sockaddr_in6 *)ai->ai_addr;
> +				inet_ntop(AF_INET6, &sa6->sin6_addr,
> +					  addrbuf, sizeof(addrbuf));
> +			}
>  			strbuf_addstr(&hi->ip_address, addrbuf);
>  
>  			if (ai->ai_canonname)

We could deduplicate the logic by assigning the address pointer to a
local field first:

    gai = getaddrinfo(hi->hostname.buf, NULL, &hints, &ai);
    if (!gai) {
        struct sockaddr_in *sin_addr = (void *)ai->ai_addr;
        void *addr;

        if (ai->ai_family == AF_INET) {
            struct sockaddr_in *sa = ai->ai_addr;
            addr = sa->sin_addr;
        } else if (ai->ai_family == AF_INET6) {
            struct sockaddr_in6 *sa6 = ai->ai_addr;
            addr = sa->sin6_addr;
        } else {
            die("unexpected address info family");
        }

        inet_ntop(ai->ai_family, addr, addrbuf, sizeof(addrbuf));
        strbuf_addstr(&hi->ip_address, addrbuf);

> @@ -742,7 +749,7 @@ static int execute(void)
>  	struct strvec env = STRVEC_INIT;
>  
>  	if (addr)
> -		loginfo("Connection from %s:%s", addr, port);
> +		loginfo("Connection from %s:%s", addr, port ? port : "?");

Hm. It shouldn't ever happen that either of these is unset as far as I
know. But it's weird indeed that we check for one of them to exist, but
not for the other.

> @@ -936,7 +943,7 @@ struct socketlist {
>  	size_t alloc;
>  };
>  
> -static const char *ip2str(int family, struct sockaddr *sin, socklen_t len)
> +static const char *ip2str(int family, struct sockaddr *sin)
>  {
>  #ifdef NO_IPV6
>  	static char ip[INET_ADDRSTRLEN];
> @@ -947,11 +954,11 @@ static const char *ip2str(int family, struct sockaddr *sin, socklen_t len)
>  	switch (family) {
>  #ifndef NO_IPV6
>  	case AF_INET6:
> -		inet_ntop(family, &((struct sockaddr_in6*)sin)->sin6_addr, ip, len);
> +		inet_ntop(family, &((struct sockaddr_in6*)sin)->sin6_addr, ip, sizeof(ip));
>  		break;
>  #endif
>  	case AF_INET:
> -		inet_ntop(family, &((struct sockaddr_in*)sin)->sin_addr, ip, len);
> +		inet_ntop(family, &((struct sockaddr_in*)sin)->sin_addr, ip, sizeof(ip));
>  		break;
>  	default:
>  		xsnprintf(ip, sizeof(ip), "<unknown>");

Right, the last parameter of inet_ntop(3p) declares the size of the
output buffer, not of the input address.

Thanks!

Patrick

^ permalink raw reply

* Re: [PATCH 1/1] shallow: fix relative deepen on non-shallow repositories
From: René Scharfe @ 2026-05-11  7:45 UTC (permalink / raw)
  To: Junio C Hamano, Samo Pogačnik; +Cc: git, owen
In-Reply-To: <xmqqzf26x0vi.fsf@gitster.g>

On 5/11/26 2:09 AM, Junio C Hamano wrote:
> 
> We obviously should not truncate when asked to "deepen" (i.e., the
> user asked to get more history, not reset the number of commits we
> have to a specific depth), and making the operation in this
> situation a no-op may be a good first step, but should we just do so
> silently, instead of giving a warning/diagnosis?
Perhaps, but no warning has been given for deepening a non-shallow repo
since the introduction of this option by cccf74e2da (fetch, upload-pack:
--deepen=N extends shallow boundary by N commits, 2016-06-12).

The best place for such a warning would be close to the user, in fetch,
no?  And in its own patch.

René


^ permalink raw reply

* Re: [PATCH] config: retry acquiring config.lock for 100ms
From: Patrick Steinhardt @ 2026-05-11  7:33 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Joerg Thalheim, git
In-Reply-To: <xmqqcxz2vfpa.fsf@gitster.g>

On Mon, May 11, 2026 at 11:32:33AM +0900, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> >> This bites in practice when running `git worktree add -b` concurrently
> >> against the same repository. Each invocation makes several writes to
> >> ".git/config" to set up branch tracking, and tooling that creates
> >> worktrees in parallel sees intermittent failures. Worse, `git worktree
> >> add` does not propagate the failed config write to its exit code: the
> >> worktree is created and the command exits 0, but tracking
> >> configuration is silently dropped.
> >
> > This very much sounds like a bug that is worth fixing independently.
> >
> >> The lock is held only for the duration of rewriting a small file, so
> >> retrying for 100 ms papers over any realistic contention while still
> >> failing fast if a stale lock has been left behind by a crashed
> >> process. This mirrors what we already do for individual reference
> >> locks (4ff0f01cb7 (refs: retry acquiring reference locks for 100ms,
> >> 2017-08-21)).
> >
> > Famous last words :) Experience tells me that any timeout value that
> > isn't excessive will eventually be hit in some production system. Which
> > raises the question whether we want to make the timeout configurable,
> > similar to "core.filesRefLockTimeout" and "core.packedRefsTimeout".
> > ...
> > Honestly though, I'm not really sure what to make with this. We could
> > of course also add some validation that the configuration we want to set
> > hasn't been modified meanwhile. But that would now lead to a situation
> > where we have to update every single caller in our tree to make use of
> > the new mechanism, which would be a bunch of work.
> >
> > And adding the timeout doesn't really change the status quo, either. We
> > already have the case that we'll happily overwrite changes made by
> > concurrent processes. The only thing that changes is that we make it
> > more likely for concurrent changes to succeed.
> 
> We haven't heard any response to these points raised in the message
> I am responding to.  Should I still keep the patch in my tree,
> hoping that a responses may come some day?  I am tempted to discard
> the topic as it has been quite a while since we last looked at it.

Same here, let's discard it for now as it can be easily added back in
once a new version was posted or the feedback has been addressed.

Patrick

^ permalink raw reply

* Re: [PATCH v6] clone: add clone.<url>.defaultObjectFilter config
From: Patrick Steinhardt @ 2026-05-11  7:30 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Alan Braithwaite via GitGitGadget, git, christian.couder,
	jonathantanmy, me, Jeff King, brian m. carlson, Alan Braithwaite
In-Reply-To: <xmqq8q9qvffs.fsf@gitster.g>

On Mon, May 11, 2026 at 11:38:15AM +0900, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> > On Sun, Mar 15, 2026 at 05:37:02AM +0000, Alan Braithwaite via GitGitGadget wrote:
> >>  1:  fa1ea69bdb ! 1:  480453b2e7 clone: add clone.<url>.defaultObjectFilter config
> >>      @@ t/t5616-partial-clone.sh: test_expect_success 'after fetching descendants of non
> >>       +test_expect_success 'setup for clone.defaultObjectFilter tests' '
> >>       +	git init default-filter-src &&
> >>       +	echo "small" >default-filter-src/small.txt &&
> >>      -+	dd if=/dev/zero of=default-filter-src/large.bin bs=1024 count=100 2>/dev/null &&
> >>       +	git -C default-filter-src add . &&
> >>       +	git -C default-filter-src commit -m "initial" &&
> >>       +
> >
> > As Junio already pointed out, this change here is a bit puzzling. Not
> > that I think it's a problem, but one wonders why this existed in the
> > first place if it seemed to not be necessary.
> >> ...
> >> +	normalized_url = url_normalize(url, &config.url);
> >> +	if (!normalized_url) {
> >> +		urlmatch_config_release(&config);
> >> +		return NULL;
> >> +	}
> >
> > We haven't allocated anything, right? So in theory, we should be able to
> > return early without calling `urlmatch_config_release()`. This could be
> > stressed further by moving the error path earlier, so that it's the
> > first thing we do in the function.
> 
> 
> We haven't heard any response to these points raised in the message
> I am responding to.  Should I still keep the patch in my tree,
> hoping that a responses may come some day?  I am tempted to discard
> the topic as it has been quite a while since we last looked at it.

It's been a while indeed. I'd say we can discard it for now, as we can
easily add it back in at a later point in time once the next version is
posted.

Patrick

^ permalink raw reply

* Re: [PATCH v2] commit-reach: early exit paint_down_to_common for single merge-base
From: Patrick Steinhardt @ 2026-05-11  7:22 UTC (permalink / raw)
  To: Kristofer Karlsson via GitGitGadget; +Cc: git, Kristofer Karlsson
In-Reply-To: <pull.2109.v2.git.1778480348118.gitgitgadget@gmail.com>

On Mon, May 11, 2026 at 06:19:08AM +0000, Kristofer Karlsson via GitGitGadget wrote:
> diff --git a/commit-reach.c b/commit-reach.c
> index d3a9b3ed6f..b4ca00bb7e 100644
> --- a/commit-reach.c
> +++ b/commit-reach.c
> @@ -165,7 +175,7 @@ static int merge_bases_many(struct repository *r,
>  				     oid_to_hex(&twos[i]->object.oid));
>  	}
>  
> -	if (paint_down_to_common(r, one, n, twos, 0, 0, &list)) {
> +	if (paint_down_to_common(r, one, n, twos, 0, 0, find_all, &list)) {
>  		commit_list_free(list);
>  		return -1;
>  	}

Callsites like this are quite hard to read now with these boolean flags.
Would it be preferable to instead use flags?

    enum paint_down_to_common_flags {
        PAINT_DOWN_TO_COMMON_IGNORE_MISSING_COMMITS = (1 << 0),
        PAINT_DOWN_TO_COMMON_FIND_ALL = (1 << 1),
    };

It's more verbose of course, but that's kind of the point.

Only weirdness is that we don't only accept these flags in
`paint_down_to_common()`, but also in other functions that pass those
flags down.

Patrick

^ permalink raw reply

* Re: [PATCH] ci: enable EXPENSIVE for contributor builds
From: Patrick Steinhardt @ 2026-05-11  7:05 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Johannes Schindelin via GitGitGadget, git, Derrick Stolee,
	Torsten Bögershausen, Jeff King, Johannes Schindelin
In-Reply-To: <xmqqjyta9630.fsf@gitster.g>

On Mon, May 11, 2026 at 08:51:15AM +0900, Junio C Hamano wrote:
> diff --git a/ci/lib.sh b/ci/lib.sh
> index a671994bdf..4ca3ecef2c 100755
> --- a/ci/lib.sh
> +++ b/ci/lib.sh
> @@ -314,11 +314,13 @@ export DEFAULT_TEST_TARGET=prove
>  export GIT_TEST_CLONE_2GB=true
>  export SKIP_DASHED_BUILT_INS=YesPlease
>  
> -# Enable expensive tests on push builds to integration branches, but
> -# not on PR builds where the extra time is not justified for every
> -# iteration.
> +# In order to give maximum test coverage to contributor builds,
> +# preferrably even before the changes consume public review bandwidth,
> +# enable "expensive" tests for PR events.
> +# In order to catch bugs introduced at integration time by mismerges,
> +# enable the long tests for pushes to the integration branches as well.
>  case "$GITHUB_EVENT_NAME,$CI_BRANCH" in
> -push,*next*|push,*master*|push,*main*|push,*maint*)
> +pull_request,*|push,*next*|push,*master*|push,*main*|push,*maint*)
>  	export GIT_TEST_LONG=YesPlease
>  	;;
>  esac

So with this change we now run the tests for all "official" branches,
and on pull requests. Which raises the question: are there any events
that happen regularly that are excluded by this? Because if not I think
it might be sensible to just enable this unconditionally, also because
that would make jobs on GitLab CI run expensive tests, as well.

Patrick

^ permalink raw reply

* [PATCH v5 5/5] branch: add --all-remotes flag
From: Harald Nordgren via GitGitGadget @ 2026-05-11  6:58 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2285.v5.git.git.1778482708.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

Combined with --forked or --prune-merged, --all-remotes acts on
every configured remote, in addition to any explicit <remote>
arguments. Used alone, it errors out.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 Documentation/git-branch.adoc |  9 ++++++--
 builtin/branch.c              | 41 +++++++++++++++++++++++++----------
 t/t3200-branch.sh             | 40 ++++++++++++++++++++++++++++++++++
 3 files changed, 76 insertions(+), 14 deletions(-)

diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index 9d4944d17e..5c5b91d9b6 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -24,8 +24,8 @@ 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 --forked <remote>...
-git branch [-f] --prune-merged <remote>...
+git branch --forked (<remote>... | --all-remotes)
+git branch [-f] --prune-merged (<remote>... | --all-remotes)
 
 DESCRIPTION
 -----------
@@ -226,6 +226,11 @@ With `--force` (or `-f`), delete them regardless. The currently
 checked-out branch in any worktree is always preserved, as is
 any branch with `branch.<name>.pruneMerged` set to `false`.
 
+`--all-remotes`::
+	With `--forked` or `--prune-merged`, act on every
+	configured remote in addition to any explicit _<remote>_
+	arguments.
+
 `-v`::
 `-vv`::
 `--verbose`::
diff --git a/builtin/branch.c b/builtin/branch.c
index 07d867373f..37ea75ecc3 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -685,6 +685,13 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int
 	free_worktrees(worktrees);
 }
 
+static int collect_remote_name(struct remote *remote, void *cb_data)
+{
+	struct string_list *remote_names = cb_data;
+	string_list_insert(remote_names, remote->name);
+	return 0;
+}
+
 static void parse_forked_args(int argc, const char **argv,
 			      struct string_list *remote_names,
 			      struct string_list *tracking_refs)
@@ -774,7 +781,7 @@ static void collect_default_branch_refs(const struct string_list *remote_names,
 	}
 }
 
-static void collect_forked_set(int argc, const char **argv,
+static void collect_forked_set(int argc, const char **argv, int all_remotes,
 			       struct string_list *protected_default_refs,
 			       struct string_list *out)
 {
@@ -787,6 +794,8 @@ static void collect_forked_set(int argc, const char **argv,
 	};
 
 	parse_forked_args(argc, argv, &remote_names, &tracking_refs);
+	if (all_remotes)
+		for_each_remote(collect_remote_name, &remote_names);
 
 	refs_for_each_branch_ref(get_main_ref_store(the_repository),
 				 collect_forked_branch, &cb);
@@ -800,15 +809,15 @@ static void collect_forked_set(int argc, const char **argv,
 	string_list_clear(&tracking_refs, 0);
 }
 
-static int list_forked_branches(int argc, const char **argv)
+static int list_forked_branches(int argc, const char **argv, int all_remotes)
 {
 	struct string_list out = STRING_LIST_INIT_DUP;
 	struct string_list_item *item;
 
-	if (!argc)
-		die(_("--forked requires at least one <remote>"));
+	if (!argc && !all_remotes)
+		die(_("--forked requires at least one <remote> or --all-remotes"));
 
-	collect_forked_set(argc, argv, NULL, &out);
+	collect_forked_set(argc, argv, all_remotes, NULL, &out);
 	for_each_string_list_item(item, &out)
 		puts(item->string);
 
@@ -816,8 +825,8 @@ static int list_forked_branches(int argc, const char **argv)
 	return 0;
 }
 
-static int prune_merged_branches(int argc, const char **argv, int force,
-				 int quiet)
+static int prune_merged_branches(int argc, const char **argv,
+				 int all_remotes, int force, int quiet)
 {
 	struct string_list candidates = STRING_LIST_INIT_DUP;
 	struct string_list protected_default_refs = STRING_LIST_INIT_DUP;
@@ -826,10 +835,11 @@ static int prune_merged_branches(int argc, const char **argv, int force,
 	int n_not_merged = 0;
 	int ret = 0;
 
-	if (!argc)
-		die(_("--prune-merged requires at least one <remote>"));
+	if (!argc && !all_remotes)
+		die(_("--prune-merged requires at least one <remote> or --all-remotes"));
 
-	collect_forked_set(argc, argv, &protected_default_refs, &candidates);
+	collect_forked_set(argc, argv, all_remotes, &protected_default_refs,
+			   &candidates);
 
 	for_each_string_list_item(item, &candidates) {
 		const char *short_name = item->string;
@@ -952,6 +962,7 @@ int cmd_branch(int argc,
 	    unset_upstream = 0, show_current = 0, edit_description = 0;
 	int forked = 0;
 	int prune_merged = 0;
+	int all_remotes = 0;
 	const char *new_upstream = NULL;
 	int noncreate_actions = 0;
 	/* possible options */
@@ -1009,6 +1020,9 @@ int cmd_branch(int argc,
 			N_("list local branches forked from the given <remote>s")),
 		OPT_BOOL(0, "prune-merged", &prune_merged,
 			N_("delete local branches forked from the given <remote>s that are merged into their upstream")),
+		OPT_BOOL_F(0, "all-remotes", &all_remotes,
+			N_("with --forked or --prune-merged, act on every configured remote"),
+			PARSE_OPT_NONEG),
 		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")),
@@ -1052,6 +1066,9 @@ int cmd_branch(int argc,
 	argc = parse_options(argc, argv, prefix, options, builtin_branch_usage,
 			     0);
 
+	if (all_remotes && !forked && !prune_merged)
+		die(_("--all-remotes requires --forked or --prune-merged"));
+
 	if (!delete && !rename && !copy && !edit_description && !new_upstream &&
 	    !show_current && !unset_upstream && !forked && !prune_merged &&
 	    argc == 0)
@@ -1105,10 +1122,10 @@ int cmd_branch(int argc,
 				      quiet, 0, NULL);
 		goto out;
 	} else if (forked) {
-		ret = list_forked_branches(argc, argv);
+		ret = list_forked_branches(argc, argv, all_remotes);
 		goto out;
 	} else if (prune_merged) {
-		ret = prune_merged_branches(argc, argv, force, quiet);
+		ret = prune_merged_branches(argc, argv, all_remotes, force, quiet);
 		goto out;
 	} else if (show_current) {
 		print_current_branch_name();
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index f9aca90f4d..3809bfe0ad 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -1771,6 +1771,27 @@ test_expect_success '--forked requires at least one <remote>' '
 	test_grep "at least one <remote>" err
 '
 
+test_expect_success '--forked --all-remotes covers every configured remote' '
+	git -C forked branch --forked --all-remotes >actual &&
+	cat >expect <<-\EOF &&
+	local-foreign
+	local-one
+	local-two
+	main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success '--forked --all-remotes still validates explicit <remote>' '
+	test_must_fail git -C forked branch --forked nope --all-remotes 2>err &&
+	test_grep "neither a configured remote nor a remote-tracking branch" err
+'
+
+test_expect_success '--all-remotes alone is rejected' '
+	test_must_fail git -C forked branch --all-remotes 2>err &&
+	test_grep "requires --forked or --prune-merged" err
+'
+
 test_expect_success '--prune-merged: setup' '
 	test_create_repo pm-upstream &&
 	test_commit -C pm-upstream base &&
@@ -1924,4 +1945,23 @@ test_expect_success 'branch -d still deletes a pruneMerged=false branch' '
 	test_must_fail git -C pm-optout-d rev-parse --verify refs/heads/one
 '
 
+test_expect_success '--prune-merged --all-remotes covers every configured remote' '
+	test_when_finished "rm -rf pm-allremotes" &&
+	git clone pm-upstream pm-allremotes &&
+	test_create_repo pm-other &&
+	test_commit -C pm-other other-base &&
+	git -C pm-other branch foreign other-base &&
+	git -C pm-allremotes remote add other ../pm-other &&
+	git -C pm-allremotes fetch other &&
+	git -C pm-allremotes branch one --track origin/one &&
+	git -C pm-allremotes branch foreign --track other/foreign &&
+
+	git -C pm-allremotes update-ref -d refs/remotes/origin/one &&
+	git -C pm-allremotes update-ref -d refs/remotes/other/foreign &&
+	git -C pm-allremotes branch --force --prune-merged --all-remotes &&
+
+	test_must_fail git -C pm-allremotes rev-parse --verify refs/heads/one &&
+	test_must_fail git -C pm-allremotes rev-parse --verify refs/heads/foreign
+'
+
 test_done
-- 
gitgitgadget

^ permalink raw reply related

* [PATCH v5 4/5] branch: add branch.<name>.pruneMerged opt-out
From: Harald Nordgren via GitGitGadget @ 2026-05-11  6:58 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2285.v5.git.git.1778482708.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

Setting branch.<name>.pruneMerged=false exempts that branch from
--prune-merged (and from fetch --prune-merged), even with --force.
Useful for keeping a topic branch around between rounds.

Explicit deletion via 'git branch -d' is unaffected.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 Documentation/config/branch.adoc |  7 ++++++
 Documentation/git-branch.adoc    | 17 +++++++-------
 builtin/branch.c                 | 31 +++++++++++++++++++++----
 t/t3200-branch.sh                | 40 ++++++++++++++++++++++++++++++++
 4 files changed, 82 insertions(+), 13 deletions(-)

diff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc
index a4db9fa5c8..4662ef35c1 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>.pruneMerged`::
+	If set to `false`, branch _<name>_ is exempt from
+	`git branch --prune-merged`.
+	Useful for topic branches 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 80b20a55eb..9d4944d17e 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -216,16 +216,15 @@ Each _<remote>_ may be either the name of a configured remote
 	Delete the local branches that `--forked` would list for
 	the same _<remote>_ arguments, but only when the branch's
 	push destination remote-tracking branch (the branch `git push`
-	would update; see `branch_get_push` semantics) no longer
-	resolves locally. In other words: the branch was pushed
-	under some name on _<remote>_, and that name has since
-	been pruned upstream.
+	would update) no longer resolves locally. In other words:
+	the branch was pushed under some name on _<remote>_, and
+	that name has since been pruned upstream.
 +
-By default, the local tip must also be reachable from the
-upstream remote-tracking branch (see `--no-merged`); branches with
-unpushed commits are refused. With `--force` (or `-f`), delete
-them regardless. The currently checked-out branch in any worktree
-is always preserved.
+The local tip must also be reachable from the upstream
+remote-tracking branch; branches with unpushed commits are refused.
+With `--force` (or `-f`), delete them regardless. The currently
+checked-out branch in any worktree is always preserved, as is
+any branch with `branch.<name>.pruneMerged` set to `false`.
 
 `-v`::
 `-vv`::
diff --git a/builtin/branch.c b/builtin/branch.c
index f2ca7b64d3..07d867373f 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -834,13 +834,16 @@ static int prune_merged_branches(int argc, const char **argv, int force,
 	for_each_string_list_item(item, &candidates) {
 		const char *short_name = item->string;
 		struct strbuf full = STRBUF_INIT;
+		struct strbuf key = STRBUF_INIT;
 		struct branch *branch;
 		const char *push_ref;
 		const char *upstream;
+		int opt_out = 0;
 
 		strbuf_addf(&full, "refs/heads/%s", short_name);
 		if (branch_checked_out(full.buf)) {
 			strbuf_release(&full);
+			strbuf_release(&key);
 			continue;
 		}
 		strbuf_release(&full);
@@ -850,18 +853,38 @@ static int prune_merged_branches(int argc, const char **argv, int force,
 		if (upstream &&
 		    string_list_has_string(&protected_default_refs, upstream)) {
 			const char *leaf = strrchr(upstream, '/');
-			if (leaf && !strcmp(leaf + 1, short_name))
+			if (leaf && !strcmp(leaf + 1, short_name)) {
+				strbuf_release(&key);
 				continue;
+			}
 		}
 
 		push_ref = branch ? branch_get_push(branch, NULL) : NULL;
-		if (!push_ref)
+		if (!push_ref) {
+			strbuf_release(&key);
 			continue;
+		}
 		if (refs_ref_exists(get_main_ref_store(the_repository),
-				    push_ref))
+				    push_ref)) {
+			strbuf_release(&key);
+			continue;
+		}
+		if (string_list_has_string(&protected_default_refs, push_ref)) {
+			strbuf_release(&key);
 			continue;
-		if (string_list_has_string(&protected_default_refs, push_ref))
+		}
+
+		strbuf_addf(&key, "branch.%s.prunemerged", short_name);
+		if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
+		    !opt_out) {
+			if (!quiet)
+				fprintf(stderr, _("Skipping '%s' "
+						  "(branch.%s.pruneMerged is false)\n"),
+					short_name, short_name);
+			strbuf_release(&key);
 			continue;
+		}
+		strbuf_release(&key);
 
 		strvec_push(&deletable, short_name);
 	}
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index b41f8343b3..f9aca90f4d 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -1884,4 +1884,44 @@ test_expect_success '--prune-merged spares branches whose push ref is the defaul
 	git -C pm-pushdefault rev-parse --verify refs/heads/topic
 '
 
+test_expect_success '--prune-merged honours branch.<name>.pruneMerged=false' '
+	test_when_finished "rm -rf pm-optout" &&
+	git clone pm-upstream pm-optout &&
+	git -C pm-optout branch one --track origin/one &&
+	git -C pm-optout branch two --track origin/two &&
+	git -C pm-optout config branch.one.pruneMerged false &&
+
+	git -C pm-optout update-ref -d refs/remotes/origin/one &&
+	git -C pm-optout update-ref -d refs/remotes/origin/two &&
+	git -C pm-optout branch --prune-merged origin 2>err &&
+
+	git -C pm-optout rev-parse --verify refs/heads/one &&
+	test_must_fail git -C pm-optout rev-parse --verify refs/heads/two &&
+	test_grep "Skipping .one." err
+'
+
+test_expect_success '--prune-merged --force still honours pruneMerged=false' '
+	test_when_finished "rm -rf pm-optout-force" &&
+	git clone pm-upstream pm-optout-force &&
+	git -C pm-optout-force checkout -b one --track origin/one &&
+	test_commit -C pm-optout-force unpushed &&
+	git -C pm-optout-force checkout - &&
+	git -C pm-optout-force config branch.one.pruneMerged false &&
+
+	git -C pm-optout-force update-ref -d refs/remotes/origin/one &&
+	git -C pm-optout-force branch --force --prune-merged origin &&
+
+	git -C pm-optout-force rev-parse --verify refs/heads/one
+'
+
+test_expect_success 'branch -d still deletes a pruneMerged=false branch' '
+	test_when_finished "rm -rf pm-optout-d" &&
+	git clone pm-upstream pm-optout-d &&
+	git -C pm-optout-d branch one --track origin/one &&
+	git -C pm-optout-d config branch.one.pruneMerged false &&
+
+	git -C pm-optout-d branch -d one &&
+	test_must_fail git -C pm-optout-d rev-parse --verify refs/heads/one
+'
+
 test_done
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v5 3/5] branch: add --prune-merged <remote>
From: Harald Nordgren via GitGitGadget @ 2026-05-11  6:58 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2285.v5.git.git.1778482708.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

Delete the local branches that --forked <remote> would list,
refusing any whose tip is not reachable from its upstream
remote-tracking branch. With --force, delete unconditionally.
The currently checked-out branch in any worktree is always
preserved.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 Documentation/git-branch.adoc |  16 ++++
 builtin/branch.c              | 134 +++++++++++++++++++++++++++++++---
 t/t3200-branch.sh             | 113 ++++++++++++++++++++++++++++
 3 files changed, 251 insertions(+), 12 deletions(-)

diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index 5773104cd3..80b20a55eb 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -25,6 +25,7 @@ git branch (-c|-C) [<old-branch>] <new-branch>
 git branch (-d|-D) [-r] <branch-name>...
 git branch --edit-description [<branch-name>]
 git branch --forked <remote>...
+git branch [-f] --prune-merged <remote>...
 
 DESCRIPTION
 -----------
@@ -211,6 +212,21 @@ Each _<remote>_ may be either the name of a configured remote
 `refs/remotes/origin/*` ref) or a specific remote-tracking branch
 (e.g. `origin/master`). Multiple _<remote>_ arguments are unioned.
 
+`--prune-merged`::
+	Delete the local branches that `--forked` would list for
+	the same _<remote>_ arguments, but only when the branch's
+	push destination remote-tracking branch (the branch `git push`
+	would update; see `branch_get_push` semantics) no longer
+	resolves locally. In other words: the branch was pushed
+	under some name on _<remote>_, and that name has since
+	been pruned upstream.
++
+By default, the local tip must also be reachable from the
+upstream remote-tracking branch (see `--no-merged`); branches with
+unpushed commits are refused. With `--force` (or `-f`), delete
+them regardless. The currently checked-out branch in any worktree
+is always preserved.
+
 `-v`::
 `-vv`::
 `--verbose`::
diff --git a/builtin/branch.c b/builtin/branch.c
index 1941f8a9ad..f2ca7b64d3 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -21,6 +21,7 @@
 #include "branch.h"
 #include "path.h"
 #include "string-list.h"
+#include "strvec.h"
 #include "column.h"
 #include "utf8.h"
 #include "ref-filter.h"
@@ -753,36 +754,138 @@ static int collect_forked_branch(const struct reference *ref, void *cb_data)
 	return 0;
 }
 
-static int list_forked_branches(int argc, const char **argv)
+static void collect_default_branch_refs(const struct string_list *remote_names,
+					struct string_list *out)
+{
+	struct ref_store *refs = get_main_ref_store(the_repository);
+	struct string_list_item *item;
+
+	for_each_string_list_item(item, remote_names) {
+		struct strbuf head = STRBUF_INIT;
+		const char *target;
+
+		strbuf_addf(&head, "refs/remotes/%s/HEAD", item->string);
+		target = refs_resolve_ref_unsafe(refs, head.buf,
+						 RESOLVE_REF_NO_RECURSE,
+						 NULL, NULL);
+		if (target && starts_with(target, "refs/remotes/"))
+			string_list_insert(out, target);
+		strbuf_release(&head);
+	}
+}
+
+static void collect_forked_set(int argc, const char **argv,
+			       struct string_list *protected_default_refs,
+			       struct string_list *out)
 {
 	struct string_list remote_names = STRING_LIST_INIT_NODUP;
 	struct string_list tracking_refs = STRING_LIST_INIT_DUP;
-	struct string_list out = STRING_LIST_INIT_DUP;
-	struct string_list_item *item;
 	struct forked_cb cb = {
 		.remote_names = &remote_names,
 		.tracking_refs = &tracking_refs,
-		.out = &out,
+		.out = out,
 	};
 
-	if (!argc)
-		die(_("--forked requires at least one <remote>"));
-
 	parse_forked_args(argc, argv, &remote_names, &tracking_refs);
 
 	refs_for_each_branch_ref(get_main_ref_store(the_repository),
 				 collect_forked_branch, &cb);
 
-	string_list_sort(&out);
-	for_each_string_list_item(item, &out)
-		puts(item->string);
+	string_list_sort(out);
+
+	if (protected_default_refs)
+		collect_default_branch_refs(&remote_names, protected_default_refs);
 
 	string_list_clear(&remote_names, 0);
 	string_list_clear(&tracking_refs, 0);
+}
+
+static int list_forked_branches(int argc, const char **argv)
+{
+	struct string_list out = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+
+	if (!argc)
+		die(_("--forked requires at least one <remote>"));
+
+	collect_forked_set(argc, argv, NULL, &out);
+	for_each_string_list_item(item, &out)
+		puts(item->string);
+
 	string_list_clear(&out, 0);
 	return 0;
 }
 
+static int prune_merged_branches(int argc, const char **argv, int force,
+				 int quiet)
+{
+	struct string_list candidates = STRING_LIST_INIT_DUP;
+	struct string_list protected_default_refs = STRING_LIST_INIT_DUP;
+	struct strvec deletable = STRVEC_INIT;
+	struct string_list_item *item;
+	int n_not_merged = 0;
+	int ret = 0;
+
+	if (!argc)
+		die(_("--prune-merged requires at least one <remote>"));
+
+	collect_forked_set(argc, argv, &protected_default_refs, &candidates);
+
+	for_each_string_list_item(item, &candidates) {
+		const char *short_name = item->string;
+		struct strbuf full = STRBUF_INIT;
+		struct branch *branch;
+		const char *push_ref;
+		const char *upstream;
+
+		strbuf_addf(&full, "refs/heads/%s", short_name);
+		if (branch_checked_out(full.buf)) {
+			strbuf_release(&full);
+			continue;
+		}
+		strbuf_release(&full);
+
+		branch = branch_get(short_name);
+		upstream = branch ? branch_get_upstream(branch, NULL) : NULL;
+		if (upstream &&
+		    string_list_has_string(&protected_default_refs, upstream)) {
+			const char *leaf = strrchr(upstream, '/');
+			if (leaf && !strcmp(leaf + 1, short_name))
+				continue;
+		}
+
+		push_ref = branch ? branch_get_push(branch, NULL) : NULL;
+		if (!push_ref)
+			continue;
+		if (refs_ref_exists(get_main_ref_store(the_repository),
+				    push_ref))
+			continue;
+		if (string_list_has_string(&protected_default_refs, push_ref))
+			continue;
+
+		strvec_push(&deletable, short_name);
+	}
+
+	if (deletable.nr)
+		ret = delete_branches(deletable.nr, deletable.v, force,
+				      FILTER_REFS_BRANCHES, quiet,
+				      1, &n_not_merged);
+
+	if (n_not_merged && !quiet)
+		fprintf(stderr,
+			Q_("Skipped %d branch that is not fully merged; "
+			   "re-run with --force to delete it anyway.\n",
+			   "Skipped %d branches that are not fully merged; "
+			   "re-run with --force to delete them anyway.\n",
+			   n_not_merged),
+			n_not_merged);
+
+	strvec_clear(&deletable);
+	string_list_clear(&candidates, 0);
+	string_list_clear(&protected_default_refs, 0);
+	return ret;
+}
+
 static GIT_PATH_FUNC(edit_description, "EDIT_DESCRIPTION")
 
 static int edit_branch_description(const char *branch_name)
@@ -825,6 +928,7 @@ int cmd_branch(int argc,
 	int delete = 0, rename = 0, copy = 0, list = 0,
 	    unset_upstream = 0, show_current = 0, edit_description = 0;
 	int forked = 0;
+	int prune_merged = 0;
 	const char *new_upstream = NULL;
 	int noncreate_actions = 0;
 	/* possible options */
@@ -880,6 +984,8 @@ int cmd_branch(int argc,
 			 N_("edit the description for the branch")),
 		OPT_BOOL(0, "forked", &forked,
 			N_("list local branches forked from the given <remote>s")),
+		OPT_BOOL(0, "prune-merged", &prune_merged,
+			N_("delete local branches forked from the given <remote>s that are merged into their upstream")),
 		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")),
@@ -924,7 +1030,8 @@ int cmd_branch(int argc,
 			     0);
 
 	if (!delete && !rename && !copy && !edit_description && !new_upstream &&
-	    !show_current && !unset_upstream && !forked && argc == 0)
+	    !show_current && !unset_upstream && !forked && !prune_merged &&
+	    argc == 0)
 		list = 1;
 
 	if (filter.with_commit || filter.no_commit ||
@@ -933,7 +1040,7 @@ int cmd_branch(int argc,
 
 	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
 			    !!show_current + !!list + !!edit_description +
-			    !!unset_upstream + !!forked;
+			    !!unset_upstream + !!forked + !!prune_merged;
 	if (noncreate_actions > 1)
 		usage_with_options(builtin_branch_usage, options);
 
@@ -977,6 +1084,9 @@ int cmd_branch(int argc,
 	} else if (forked) {
 		ret = list_forked_branches(argc, argv);
 		goto out;
+	} else if (prune_merged) {
+		ret = prune_merged_branches(argc, argv, force, quiet);
+		goto out;
 	} else if (show_current) {
 		print_current_branch_name();
 		ret = 0;
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 24a3ec44ee..b41f8343b3 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -1771,4 +1771,117 @@ test_expect_success '--forked requires at least one <remote>' '
 	test_grep "at least one <remote>" err
 '
 
+test_expect_success '--prune-merged: setup' '
+	test_create_repo pm-upstream &&
+	test_commit -C pm-upstream base &&
+	git -C pm-upstream branch one base &&
+	git -C pm-upstream branch two base
+'
+
+test_expect_success '--prune-merged deletes branches whose push ref is gone' '
+	test_when_finished "rm -rf pm-clean" &&
+	git clone pm-upstream pm-clean &&
+	git -C pm-clean branch one --track origin/one &&
+	git -C pm-clean branch two --track origin/two &&
+
+	git -C pm-clean update-ref -d refs/remotes/origin/one &&
+	git -C pm-clean branch --prune-merged origin &&
+
+	test_must_fail git -C pm-clean rev-parse --verify refs/heads/one &&
+	git -C pm-clean rev-parse --verify refs/heads/two
+'
+
+test_expect_success '--prune-merged spares in-flight branches whose push ref still exists' '
+	test_when_finished "rm -rf pm-inflight" &&
+	git clone pm-upstream pm-inflight &&
+	git -C pm-inflight branch one --track origin/one &&
+
+	git -C pm-inflight branch --prune-merged origin &&
+
+	git -C pm-inflight rev-parse --verify refs/heads/one
+'
+
+test_expect_success '--prune-merged skips branches with unpushed commits' '
+	test_when_finished "rm -rf pm-unmerged" &&
+	git clone pm-upstream pm-unmerged &&
+	git -C pm-unmerged checkout -b one --track origin/one &&
+	test_commit -C pm-unmerged unpushed &&
+	git -C pm-unmerged checkout - &&
+
+	git -C pm-unmerged update-ref -d refs/remotes/origin/one &&
+	git -C pm-unmerged branch --prune-merged origin 2>err &&
+	test_grep "not fully merged" err &&
+	test_grep "Skipped 1 branch" err &&
+	test_grep "re-run with --force" err &&
+	test_grep ! "If you are sure you want to delete it" err &&
+	git -C pm-unmerged rev-parse --verify refs/heads/one
+'
+
+test_expect_success '--prune-merged --force deletes branches with unpushed commits' '
+	test_when_finished "rm -rf pm-force" &&
+	git clone pm-upstream pm-force &&
+	git -C pm-force checkout -b one --track origin/one &&
+	test_commit -C pm-force unpushed &&
+	git -C pm-force checkout - &&
+
+	git -C pm-force update-ref -d refs/remotes/origin/one &&
+	git -C pm-force branch --force --prune-merged origin &&
+
+	test_must_fail git -C pm-force rev-parse --verify refs/heads/one
+'
+
+test_expect_success '--prune-merged never deletes the checked-out branch' '
+	test_when_finished "rm -rf pm-head" &&
+	git clone pm-upstream pm-head &&
+	git -C pm-head checkout -b one --track origin/one &&
+
+	git -C pm-head update-ref -d refs/remotes/origin/one &&
+	git -C pm-head branch --force --prune-merged origin &&
+
+	git -C pm-head rev-parse --verify refs/heads/one
+'
+
+test_expect_success '--prune-merged deletes when push ref differs from upstream' '
+	test_when_finished "rm -rf pm-pushdiff" &&
+	git clone pm-upstream pm-pushdiff &&
+	git -C pm-pushdiff config push.default current &&
+	git -C pm-pushdiff branch --track topic-a origin/one &&
+
+	git -C pm-pushdiff branch --force --prune-merged origin &&
+
+	test_must_fail git -C pm-pushdiff rev-parse --verify refs/heads/topic-a
+'
+
+test_expect_success '--prune-merged spares the local default branch' '
+	test_when_finished "rm -rf pm-default" &&
+	git clone pm-upstream pm-default &&
+	git -C pm-default config push.default current &&
+	git -C pm-default checkout --detach &&
+	git -C pm-default branch --prune-merged origin &&
+	git -C pm-default rev-parse --verify refs/heads/main
+'
+
+test_expect_success '--prune-merged protects only the default branch by name, not by upstream' '
+	test_when_finished "rm -rf pm-default-alias" &&
+	git clone pm-upstream pm-default-alias &&
+	git -C pm-default-alias config push.default current &&
+	git -C pm-default-alias branch --track trunk origin/main &&
+	git -C pm-default-alias checkout --detach &&
+	git -C pm-default-alias branch --force --prune-merged origin &&
+	git -C pm-default-alias rev-parse --verify refs/heads/main &&
+	test_must_fail git -C pm-default-alias rev-parse --verify refs/heads/trunk
+'
+
+test_expect_success '--prune-merged spares branches whose push ref is the default branch' '
+	test_when_finished "rm -rf pm-pushdefault" &&
+	git clone pm-upstream pm-pushdefault &&
+	git -C pm-pushdefault branch --track topic origin/one &&
+	git -C pm-pushdefault config --add remote.origin.push refs/heads/topic:refs/heads/main &&
+	git -C pm-pushdefault update-ref -d refs/remotes/origin/one &&
+	git -C pm-pushdefault update-ref -d refs/remotes/origin/main &&
+	git -C pm-pushdefault checkout --detach &&
+	git -C pm-pushdefault branch --prune-merged origin &&
+	git -C pm-pushdefault rev-parse --verify refs/heads/topic
+'
+
 test_done
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v5 2/5] branch: let delete_branches warn instead of error on bulk refusal
From: Harald Nordgren via GitGitGadget @ 2026-05-11  6:58 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2285.v5.git.git.1778482708.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

Add two new parameters to delete_branches() and the helper
check_branch_commit():

* warn_only switches the per-branch refusal from a hard error
  ("error: the branch 'X' is not fully merged" plus a four-line
  hint about 'git branch -D X') to a one-line warning, and
  causes the function to skip those branches without setting its
  exit code. Each refused branch is still skipped from deletion.
* n_not_merged, when non-NULL, is incremented for each branch
  refused on the not-merged path, so a bulk caller can summarize
  rather than print per-branch advice.

All existing call sites pass 0 / NULL and so are unaffected. Both
parameters are wired up so a bulk-deletion caller can suppress
the noise normally appropriate for a one-shot 'git branch -d'.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 builtin/branch.c | 29 ++++++++++++++++++++---------
 1 file changed, 20 insertions(+), 9 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index b3289a8875..1941f8a9ad 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -192,7 +192,8 @@ static int branch_merged(int kind, const char *name,
 
 static int check_branch_commit(const char *branchname, const char *refname,
 			       const struct object_id *oid, struct commit *head_rev,
-			       int kinds, int force)
+			       int kinds, int force, int warn_only,
+			       int *n_not_merged)
 {
 	struct commit *rev = lookup_commit_reference(the_repository, oid);
 	if (!force && !rev) {
@@ -200,10 +201,18 @@ static int check_branch_commit(const char *branchname, const char *refname,
 		return -1;
 	}
 	if (!force && !branch_merged(kinds, branchname, rev, head_rev)) {
-		error(_("the branch '%s' is not fully merged"), branchname);
-		advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
-				  _("If you are sure you want to delete it, "
-				  "run 'git branch -D %s'"), branchname);
+		if (warn_only) {
+			warning(_("the branch '%s' is not fully merged"),
+				branchname);
+		} else {
+			error(_("the branch '%s' is not fully merged"),
+			      branchname);
+			advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
+					  _("If you are sure you want to delete it, "
+					  "run 'git branch -D %s'"), branchname);
+		}
+		if (n_not_merged)
+			(*n_not_merged)++;
 		return -1;
 	}
 	return 0;
@@ -219,7 +228,7 @@ static void delete_branch_config(const char *branchname)
 }
 
 static int delete_branches(int argc, const char **argv, int force, int kinds,
-			   int quiet)
+			   int quiet, int warn_only, int *n_not_merged)
 {
 	struct commit *head_rev = NULL;
 	struct object_id oid;
@@ -309,8 +318,9 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
 
 		if (!(flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
 		    check_branch_commit(bname.buf, name, &oid, head_rev, kinds,
-					force)) {
-			ret = 1;
+					force, warn_only, n_not_merged)) {
+			if (!warn_only)
+				ret = 1;
 			goto next;
 		}
 
@@ -961,7 +971,8 @@ int cmd_branch(int argc,
 	if (delete) {
 		if (!argc)
 			die(_("branch name required"));
-		ret = delete_branches(argc, argv, delete > 1, filter.kind, quiet);
+		ret = delete_branches(argc, argv, delete > 1, filter.kind,
+				      quiet, 0, NULL);
 		goto out;
 	} else if (forked) {
 		ret = list_forked_branches(argc, argv);
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v5 1/5] branch: add --forked <remote>
From: Harald Nordgren via GitGitGadget @ 2026-05-11  6:58 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2285.v5.git.git.1778482708.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

List local branches whose configured upstream falls within any of
the given <remote> arguments. <remote> may be either a configured
remote name (matching all of its remote-tracking branches) or a
single remote-tracking branch. Multiple <remote> arguments are
unioned.

This is the building block for --prune-merged, which deletes the
listed branches.

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

diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index c0afddc424..5773104cd3 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -24,6 +24,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 --forked <remote>...
 
 DESCRIPTION
 -----------
@@ -199,6 +200,17 @@ This option is only applicable in non-verbose mode.
 	Print the name of the current branch. In detached `HEAD` state,
 	nothing is printed.
 
+`--forked`::
+	List local branches that fork from any of the given _<remote>_
+	arguments, that is, those whose configured upstream
+	(`branch.<name>.merge`) is one of those remotes' remote-tracking
+	branches.
++
+Each _<remote>_ may be either the name of a configured remote
+(e.g. `origin`, meaning any branch tracking a
+`refs/remotes/origin/*` ref) or a specific remote-tracking branch
+(e.g. `origin/master`). Multiple _<remote>_ arguments are unioned.
+
 `-v`::
 `-vv`::
 `--verbose`::
diff --git a/builtin/branch.c b/builtin/branch.c
index 1572a4f9ef..b3289a8875 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -38,6 +38,7 @@ static const char * const builtin_branch_usage[] = {
 	N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
 	N_("git branch [<options>] [-r | -a] [--points-at]"),
 	N_("git branch [<options>] [-r | -a] [--format]"),
+	N_("git branch [<options>] --forked <remote>..."),
 	NULL
 };
 
@@ -673,6 +674,105 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int
 	free_worktrees(worktrees);
 }
 
+static void parse_forked_args(int argc, const char **argv,
+			      struct string_list *remote_names,
+			      struct string_list *tracking_refs)
+{
+	int i;
+
+	for (i = 0; i < argc; i++) {
+		const char *arg = argv[i];
+		struct remote *remote;
+		struct object_id oid;
+		char *full_ref = NULL;
+
+		remote = remote_get(arg);
+		if (remote && remote_is_configured(remote, 0)) {
+			string_list_insert(remote_names, remote->name);
+			continue;
+		}
+
+		if (repo_dwim_ref(the_repository, arg, strlen(arg), &oid,
+				  &full_ref, 0) == 1 &&
+		    starts_with(full_ref, "refs/remotes/")) {
+			string_list_insert(tracking_refs, full_ref);
+			free(full_ref);
+			continue;
+		}
+		free(full_ref);
+
+		die(_("'%s' is neither a configured remote nor a "
+		      "remote-tracking branch"), arg);
+	}
+}
+
+static int branch_is_forked(const char *short_name,
+			    const struct string_list *remote_names,
+			    const struct string_list *tracking_refs)
+{
+	struct branch *branch = branch_get(short_name);
+	const char *upstream;
+
+	if (!branch || !branch->remote_name)
+		return 0;
+
+	if (string_list_has_string(remote_names, branch->remote_name))
+		return 1;
+
+	upstream = branch_get_upstream(branch, NULL);
+	if (upstream && string_list_has_string(tracking_refs, upstream))
+		return 1;
+
+	return 0;
+}
+
+struct forked_cb {
+	const struct string_list *remote_names;
+	const struct string_list *tracking_refs;
+	struct string_list *out;
+};
+
+static int collect_forked_branch(const struct reference *ref, void *cb_data)
+{
+	struct forked_cb *cb = cb_data;
+
+	if (ref->flags & REF_ISSYMREF)
+		return 0;
+	if (branch_is_forked(ref->name, cb->remote_names, cb->tracking_refs))
+		string_list_append(cb->out, ref->name);
+	return 0;
+}
+
+static int list_forked_branches(int argc, const char **argv)
+{
+	struct string_list remote_names = STRING_LIST_INIT_NODUP;
+	struct string_list tracking_refs = STRING_LIST_INIT_DUP;
+	struct string_list out = STRING_LIST_INIT_DUP;
+	struct string_list_item *item;
+	struct forked_cb cb = {
+		.remote_names = &remote_names,
+		.tracking_refs = &tracking_refs,
+		.out = &out,
+	};
+
+	if (!argc)
+		die(_("--forked requires at least one <remote>"));
+
+	parse_forked_args(argc, argv, &remote_names, &tracking_refs);
+
+	refs_for_each_branch_ref(get_main_ref_store(the_repository),
+				 collect_forked_branch, &cb);
+
+	string_list_sort(&out);
+	for_each_string_list_item(item, &out)
+		puts(item->string);
+
+	string_list_clear(&remote_names, 0);
+	string_list_clear(&tracking_refs, 0);
+	string_list_clear(&out, 0);
+	return 0;
+}
+
 static GIT_PATH_FUNC(edit_description, "EDIT_DESCRIPTION")
 
 static int edit_branch_description(const char *branch_name)
@@ -714,6 +814,7 @@ int cmd_branch(int argc,
 	/* possible actions */
 	int delete = 0, rename = 0, copy = 0, list = 0,
 	    unset_upstream = 0, show_current = 0, edit_description = 0;
+	int forked = 0;
 	const char *new_upstream = NULL;
 	int noncreate_actions = 0;
 	/* possible options */
@@ -767,6 +868,8 @@ int cmd_branch(int argc,
 		OPT_BOOL(0, "create-reflog", &reflog, N_("create the branch's reflog")),
 		OPT_BOOL(0, "edit-description", &edit_description,
 			 N_("edit the description for the branch")),
+		OPT_BOOL(0, "forked", &forked,
+			N_("list local branches forked from the given <remote>s")),
 		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")),
@@ -811,7 +914,7 @@ int cmd_branch(int argc,
 			     0);
 
 	if (!delete && !rename && !copy && !edit_description && !new_upstream &&
-	    !show_current && !unset_upstream && argc == 0)
+	    !show_current && !unset_upstream && !forked && argc == 0)
 		list = 1;
 
 	if (filter.with_commit || filter.no_commit ||
@@ -820,7 +923,7 @@ int cmd_branch(int argc,
 
 	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
 			    !!show_current + !!list + !!edit_description +
-			    !!unset_upstream;
+			    !!unset_upstream + !!forked;
 	if (noncreate_actions > 1)
 		usage_with_options(builtin_branch_usage, options);
 
@@ -860,6 +963,9 @@ int cmd_branch(int argc,
 			die(_("branch name required"));
 		ret = delete_branches(argc, argv, delete > 1, filter.kind, quiet);
 		goto out;
+	} else if (forked) {
+		ret = list_forked_branches(argc, argv);
+		goto out;
 	} else if (show_current) {
 		print_current_branch_name();
 		ret = 0;
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index e7829c2c4b..24a3ec44ee 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -1717,4 +1717,58 @@ test_expect_success 'errors if given a bad branch name' '
 	test_cmp expect actual
 '
 
+test_expect_success '--forked: setup' '
+	test_create_repo forked-upstream &&
+	test_commit -C forked-upstream base &&
+	git -C forked-upstream branch one base &&
+	git -C forked-upstream branch two base &&
+
+	test_create_repo forked-other &&
+	test_commit -C forked-other other-base &&
+	git -C forked-other branch foreign other-base &&
+
+	git clone forked-upstream forked &&
+	git -C forked remote add other ../forked-other &&
+	git -C forked fetch other &&
+	git -C forked branch --track local-one origin/one &&
+	git -C forked branch --track local-two origin/two &&
+	git -C forked branch --track local-foreign other/foreign &&
+	git -C forked branch detached
+'
+
+test_expect_success '--forked <remote-name> lists branches tracking that remote' '
+	git -C forked branch --forked origin >actual &&
+	cat >expect <<-\EOF &&
+	local-one
+	local-two
+	main
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success '--forked <remote-tracking-branch> lists only matching branches' '
+	git -C forked branch --forked origin/one >actual &&
+	echo local-one >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success '--forked unions multiple <remote> arguments' '
+	git -C forked branch --forked origin/one other >actual &&
+	cat >expect <<-\EOF &&
+	local-foreign
+	local-one
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success '--forked rejects unknown remote/ref' '
+	test_must_fail git -C forked branch --forked nope 2>err &&
+	test_grep "neither a configured remote nor a remote-tracking branch" err
+'
+
+test_expect_success '--forked requires at least one <remote>' '
+	test_must_fail git -C forked branch --forked 2>err &&
+	test_grep "at least one <remote>" err
+'
+
 test_done
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v5 0/5] branch: prune-merged
From: Harald Nordgren via GitGitGadget @ 2026-05-11  6:58 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren
In-Reply-To: <pull.2285.v4.git.git.1778009038.gitgitgadget@gmail.com>

Drop commit 'fetch: add --prune-merged'

Harald Nordgren (5):
  branch: add --forked <remote>
  branch: let delete_branches warn instead of error on bulk refusal
  branch: add --prune-merged <remote>
  branch: add branch.<name>.pruneMerged opt-out
  branch: add --all-remotes flag

 Documentation/config/branch.adoc |   7 +
 Documentation/git-branch.adoc    |  32 ++++
 builtin/branch.c                 | 289 +++++++++++++++++++++++++++++--
 t/t3200-branch.sh                | 247 ++++++++++++++++++++++++++
 4 files changed, 564 insertions(+), 11 deletions(-)


base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2285%2FHaraldNordgren%2Ffetch-prune-local-branches-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2285/HaraldNordgren/fetch-prune-local-branches-v5
Pull-Request: https://github.com/git/git/pull/2285

Range-diff vs v4:

 1:  77e67d4b8b = 1:  77e67d4b8b branch: add --forked <remote>
 2:  807c9f981f = 2:  807c9f981f branch: let delete_branches warn instead of error on bulk refusal
 3:  77beb620d7 = 3:  77beb620d7 branch: add --prune-merged <remote>
 4:  98cfdb87d2 < -:  ---------- fetch: add --prune-merged
 5:  c645526bb5 ! 4:  cf69fb5767 branch: add branch.<name>.pruneMerged opt-out
     @@ Documentation/config/branch.adoc: for details).
      +
      +`branch.<name>.pruneMerged`::
      +	If set to `false`, branch _<name>_ is exempt from
     -+	`git branch --prune-merged` (and `git fetch --prune-merged`).
     ++	`git branch --prune-merged`.
      +	Useful for topic branches you intend to develop further after
      +	an initial round has been merged upstream. Defaults to true.
      +	Explicit deletion via `git branch -d` is unaffected.
 6:  690242d89b = 5:  f2cee8c79b branch: add --all-remotes flag

-- 
gitgitgadget

^ permalink raw reply

* [PATCH] fetch: add fetch.pruneLocalBranches config
From: Harald Nordgren @ 2026-05-11  6:56 UTC (permalink / raw)
  To: gitster; +Cc: git, gitgitgadget, haraldnordgren, j6t, kristofferhaugsbakk
In-Reply-To: <xmqqh5oetzw3.fsf@gitster.g>

>>> I have some sympathy for the desire to clean up unnecessary local
>>> branches, but I don't like the concept that `git fetch` modifies local
>>> branches, not even as an opt-in. Deleting local branches should be `git
>>> branch`'s task exclusively (at the porcelain level).
>>
>> Yeah, maybe that's a good point.
>
> I think the latest iteration was sent after the above exchange, yet
> it seems to have changes to builtin/fetch.c to cause `git fetch` to
> modify local branches still.  Will we have another update that is
> hopefully final to excise that part, or are we OK to allow `fetch`
> to modify the local state as an opt-in now?

Done! (I didn't know if we wanted to do this yet, or we still just
discussion it, but now I deleted it.)


Harald

^ permalink raw reply

* Re: unexpected auto-maintenance, was Re: git hogs the CPU, RAM and storage despite its config
From: Patrick Steinhardt @ 2026-05-11  6:42 UTC (permalink / raw)
  To: Taylor Blau; +Cc: Derrick Stolee, Jeff King, jean-christophe manciot, git
In-Reply-To: <agDjyAXxkOIso8FU@nand.local>

On Sun, May 10, 2026 at 04:00:08PM -0400, Taylor Blau wrote:
> On Sun, May 10, 2026 at 12:08:14PM -0400, Derrick Stolee wrote:
> > > That's not too terrible to write, and I would feel OK about putting it
> > > in a 2.54.1 release soon-ish provided that others think it is reasonable.
> >
> > I'd rather revert the geometric maintenance default for a point
> > release and let something more complicated like this percolate
> > until the next release window.
> >
> > > Simply reverting 452b12c2e0 (builtin/maintenance: use "geometric"
> > > strategy by default, 2026-02-24) feels somewhat unsatisfying, since it
> > > is merely making the bug less likely rather than eliminating it
> > > entirely.
> >
> > It does limit the behavior to those who previously chose to opt-in to
> > this behavior, and likely that's a low number or else we'd have more
> > bug reports.
> 
> I agree with what you're saying. In an ideal world we would be able to
> apply a fix on top that would prevent this bug from occurring, but if
> that's not trivial to do then we shouldn't rush it in the 2.54.1 window.
> 
> I think that not having this bug whatsoever (as opposed to simply
> reverting 452b12c2e0) would be preferable, but as you note we haven't
> seen any bug reports in a pre-452b12c2e0 world, so perhaps the risk is
> low enough that we could merely revert 452b12c2e0.

I think having an actual fix for the locking logic in git-maintenance(1)
would be preferable though, as the issue isn't merely contained to Git
2.54. With that release we are of course much more likely to hit the
issue as the "geometric" strategy is the default now. But even before
this release it was possible to configure that strategy, and if the user
had opted in to it they might've hit the same bug.

> > For me to be convinced that this forward fix is the right direction,
> > I'd need to see a test that proves the detached process will clean up
> > the locks on a normal process end and an early exit.
> 
> Yeah, I agree. Testing this is a little tricky, but I played around
> with it for a while and came up with the following:
> 
> --- 8< ---
> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
> index 4700beacc18..77bfa537385 100755
> --- a/t/t7900-maintenance.sh
> +++ b/t/t7900-maintenance.sh
> @@ -1460,6 +1460,56 @@ test_expect_success '--detach causes maintenance to run in background' '
>  	)
>  '
> 
> +test_expect_success PIPE '--detach holds maintenance lock until daemonized child exits' '
> +	test_when_finished "rm -rf repo" &&
> +	git init repo &&
> +	(
> +		cd repo &&
> +
> +		mkfifo fifo &&
> +
> +		git config maintenance.auto false &&
> +		git config core.lockfilepid true &&
> +
> +		git remote add origin /does/not/exist &&
> +		git config set remote.origin.uploadpack \
> +			"echo \$PPID >child && cat \"$(pwd)/fifo\"" &&
> +
> +		# Start a detached prefetch maintenance task. Note that
> +		# we are backgrounding git-maintenance here in order to
> +		# determine its PID to validate that the lockfile was
> +		# created by the parent.
> +		{ git maintenance run --task=prefetch --detach & } &&
> +		parent="$!" &&
> +
> +		# Open fifo for writing, which will block until the
> +		# upload-pack helper opens it for reading. Once exec
> +		# returns, we know that the daemonized child is alive
> +		# and pinned.
> +		exec 8>fifo &&
> +
> +		test_path_is_file .git/objects/maintenance.lock &&
> +		test_path_is_file .git/objects/maintenance~pid.lock &&
> +
> +		# Verify that the maintenance.lock still exists, and
> +		# that it was created by the parent process, not the
> +		# child.
> +		echo "pid $parent" >expect &&
> +		test_cmp expect .git/objects/maintenance~pid.lock &&
> +
> +		# Close the write end of the FIFO, causing our upload-pack
> +		# helper to quit. Wait until the grandparent (from the
> +		# perspective of our upload-pack helper, the daemonized
> +		# git-maintenance child)
> +		exec 8>&- &&
> +		gpid="$(ps -o ppid= -p $(cat child) | tr -d " ")" &&
> +		test -n $gpid && wait $gpid &&
> +
> +		test_path_is_missing .git/objects/maintenance.lock &&
> +		test_path_is_missing .git/objects/maintenance~pid.lock
> +	)
> +'
> +
>  test_expect_success 'repacking loose objects is quiet' '
>  	test_when_finished "rm -rf repo" &&
>  	git init repo &&
> --- >8 ---
> 
> I'm not sure how portable it is, though. I think that 'ps -o ppid=' is
> OK, since 'start_git_in_background()' uses it, but I'm not sure if $PPID
> is available on Windows.

Yeah, this test seems to work on my system, too. But it's another
Windows machine indeed. I'll start a CI pipeline to check whether it
works on Windows, as well.

In any case, I also agree with Stolee that it may have unintended
consequences to unconditionally reassign tempfiles to the child process
when daemonizing. It is okay for all current callers, but I'm not sure
whether future callers would expect this behaviour.

An alternative would be to explicitly reassign the lockfile's ownership
in git-maintenance(1). Something like the below patch.

Thanks!

Patrick

diff --git a/builtin/gc.c b/builtin/gc.c
index 3a71e314c9..09cb92ac97 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1810,10 +1810,32 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,
 				   TASK_PHASE_FOREGROUND))
 			result = 1;
 
-	/* Failure to daemonize is ok, we'll continue in foreground. */
 	if (opts->detach > 0) {
+		pid_t child_pid;
+
 		trace2_region_enter("maintenance", "detach", the_repository);
-		daemonize();
+
+		child_pid = daemonize_without_exit();
+		if (!child_pid) {
+			/*
+			 * We're in the child process, so we take ownership of
+			 * the lockfile.
+			 */
+			lock_file_reassign_owner(&lk, getpid());
+		} else if (child_pid > 0) {
+			/*
+			 * We're in the parent process, so we assign ownership
+			 * of the lockfile to the child and then exit immediately.
+			 */
+			lock_file_reassign_owner(&lk, child_pid);
+			exit(0);
+		} else {
+			/*
+			 * Failure to daemonize is ok, we'll continue in
+			 * foreground.
+			 */
+		}
+
 		trace2_region_leave("maintenance", "detach", the_repository);
 	}
 
diff --git a/lockfile.c b/lockfile.c
index 7add2f136a..96aab3c885 100644
--- a/lockfile.c
+++ b/lockfile.c
@@ -356,3 +356,12 @@ int rollback_lock_file(struct lock_file *lk)
 	delete_tempfile(&lk->pid_tempfile);
 	return delete_tempfile(&lk->tempfile);
 }
+
+void lock_file_reassign_owner(struct lock_file *lk, pid_t owner)
+{
+	if (!is_lock_file_locked(lk))
+		BUG("cannot reassign ownership of unlocked lockfile");
+	lk->tempfile->owner = owner;
+	if (lk->pid_tempfile)
+		lk->pid_tempfile->owner = owner;
+}
diff --git a/lockfile.h b/lockfile.h
index e7233f28de..0b10b624fa 100644
--- a/lockfile.h
+++ b/lockfile.h
@@ -341,4 +341,14 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)
  */
 int rollback_lock_file(struct lock_file *lk);
 
+/*
+ * Reassign ownership of the lockfile to a different process.
+ *
+ * This is intended for use after `fork(2)`-ing. The parent transfers ownership
+ * to the daemonized child so that its atexit handler does not unlink the lock
+ * that should outlive it, and the child claims the inherited tempfiles so that
+ * they are cleaned up when the daemon exits.
+ */
+void lock_file_reassign_owner(struct lock_file *lk, pid_t owner);
+
 #endif /* LOCKFILE_H */
diff --git a/setup.c b/setup.c
index 7ec4427368..d651e11d4b 100644
--- a/setup.c
+++ b/setup.c
@@ -2156,20 +2156,18 @@ void sanitize_stdfds(void)
 		close(fd);
 }
 
-int daemonize(void)
+pid_t daemonize_without_exit(void)
 {
 #ifdef NO_POSIX_GOODIES
 	errno = ENOSYS;
 	return -1;
 #else
-	switch (fork()) {
-		case 0:
-			break;
-		case -1:
-			die_errno(_("fork failed"));
-		default:
-			exit(0);
-	}
+	pid_t pid = fork();
+	if (pid < 0)
+		return error_errno(_("fork failed"));
+	if (pid > 0)
+		return pid;
+
 	if (setsid() == -1)
 		die_errno(_("setsid failed"));
 	close(0);
@@ -2180,6 +2178,21 @@ int daemonize(void)
 #endif
 }
 
+int daemonize(void)
+{
+#ifdef NO_POSIX_GOODIES
+	errno = ENOSYS;
+	return -1;
+#else
+	pid_t pid = daemonize_without_exit();
+	if (pid < 0)
+		die(NULL);
+	if (pid > 0)
+		exit(0);
+	return 0;
+#endif
+}
+
 struct template_dir_cb_data {
 	char *path;
 	int initialized;
diff --git a/setup.h b/setup.h
index 80bc6e5f07..10f8b377f2 100644
--- a/setup.h
+++ b/setup.h
@@ -150,6 +150,7 @@ int path_inside_repo(const char *prefix, const char *path);
 
 void sanitize_stdfds(void);
 int daemonize(void);
+int daemonize_without_exit(void);
 
 /*
  * GIT_REPO_VERSION is the version we write by default. The

^ permalink raw reply related

* [PATCH v2] commit-reach: early exit paint_down_to_common for single merge-base
From: Kristofer Karlsson via GitGitGadget @ 2026-05-11  6:19 UTC (permalink / raw)
  To: git; +Cc: Kristofer Karlsson, Kristofer Karlsson
In-Reply-To: <pull.2109.git.1778252837132.gitgitgadget@gmail.com>

From: Kristofer Karlsson <krka@spotify.com>

Commits not in the commit-graph get GENERATION_NUMBER_INFINITY and
sort to the top of the priority queue.  After those, commits with
finite generation numbers are popped in non-increasing order.
When find_all is false the first doubly-painted commit with a
finite generation is therefore a best merge-base: no commit still
in the queue can be a descendant of it.  Skip the expensive STALE
drain in this case.

Add find_all parameter to repo_get_merge_bases_many_dirty() and
thread it through to paint_down_to_common().  git merge-base
(without --all) passes show_all=0, triggering the early exit.

On a 2.2M-commit merge-heavy monorepo with commit-graph:

  HEAD vs ~500:   5,229ms -> 24ms
  HEAD vs ~1000:  4,214ms -> 39ms
  HEAD vs ~5000:  3,799ms -> 46ms
  HEAD vs ~10000: 3,827ms -> 61ms

Signed-off-by: Kristofer Karlsson <krka@spotify.com>
---
    [RFC] commit-reach: skip STALE drain when only one merge-base needed
    
    Context for what this is all about.
    
    I am working with a very large git monorepo and have been investigating
    performance issues. After some digging I ended up looking more deeply
    into git merge-base. I saw it had an --all parameter but the default is
    to only return a single merge-base. Looking through the code and adding
    debug timing, I realized that although the total time to compute the
    merge-base was high, a very small amount of time was spent finding the
    initial merge-base value that was later returned.
    
    The optimization is actually quite dramatic in a large repo - runtime
    went down from 5000ms to 50ms, so it's roughly a 100x optimization. This
    comes from an exploding frontier of STALE commits to drain.
    
    Thus, my idea is simply to return early from the function once we know
    what will be returned. This only works if we find a candidate that we
    know will not be pruned later - but fortunately if we have a commit
    graph with generations we will visit commits in order such that it will
    actually not be pruned.
    
    CC: Derrick Stolee stolee@gmail.com
    
    Changes since v1 (thanks Junio for the review):
    
     * Dropped the has_gens variable entirely. If a commit has a finite
       generation then it is in the commit-graph, and so are all its
       ancestors — no additional check is needed to know the queue ordering
       is sound. Without a commit-graph every commit gets INFINITY and the
       guard never fires. This also avoids the misleading interaction with
       callers that pass non-zero min_generation without having generation
       data.
    
     * Simplified the early exit guard from three conditions to two:
       !find_all && generation < GENERATION_NUMBER_INFINITY.
    
     * Fixed multi-line comment style per CodingGuidelines.
    
     * Replaced "dominate" with concrete reasoning about queue ordering.
    
     * Did not extract a helper function: after the simplifications above
       the inner block is four lines and reads naturally inline. The right
       boundary for a helper is not obvious (it could absorb just the result
       marking, or also the RESULT flag check, or also the PARENT1|PARENT2
       test) and each level requires more local state passed by pointer.
       Happy to extract one if preferred.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2109%2Fspkrka%2Fmerge-base-early-exit-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2109/spkrka/merge-base-early-exit-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2109

Range-diff vs v1:

 1:  54cdf9bfd9 ! 1:  f7b5c267f3 commit-reach: early exit paint_down_to_common for single merge-base
     @@ Metadata
       ## Commit message ##
          commit-reach: early exit paint_down_to_common for single merge-base
      
     -    When find_all is false and generation numbers are available, the
     -    priority queue pops in non-increasing generation order.  The first
     -    doubly-painted commit is a valid best merge-base; no later commit
     -    can dominate it.  Skip the expensive STALE drain in this case.
     -
     -    The early exit is guarded by three conditions: find_all must be
     -    false, the commit-graph must provide generation numbers, and the
     -    merge-base commit itself must have a finite generation (not
     -    GENERATION_NUMBER_INFINITY from being outside the commit-graph).
     +    Commits not in the commit-graph get GENERATION_NUMBER_INFINITY and
     +    sort to the top of the priority queue.  After those, commits with
     +    finite generation numbers are popped in non-increasing order.
     +    When find_all is false the first doubly-painted commit with a
     +    finite generation is therefore a best merge-base: no commit still
     +    in the queue can be a descendant of it.  Skip the expensive STALE
     +    drain in this case.
      
          Add find_all parameter to repo_get_merge_bases_many_dirty() and
          thread it through to paint_down_to_common().  git merge-base
     @@ commit-reach.c: static int paint_down_to_common(struct repository *r,
       				struct commit_list **result)
       {
       	struct prio_queue queue = { compare_commits_by_gen_then_commit_date };
     - 	int i;
     -+	int has_gens = min_generation || corrected_commit_dates_enabled(r);
     - 	timestamp_t last_gen = GENERATION_NUMBER_INFINITY;
     - 	struct commit_list **tail = result;
     - 
     --	if (!min_generation && !corrected_commit_dates_enabled(r))
     -+	if (!has_gens)
     - 		queue.compare = compare_commits_by_commit_date;
     - 
     - 	one->object.flags |= PARENT1;
      @@ commit-reach.c: static int paint_down_to_common(struct repository *r,
       			if (!(commit->object.flags & RESULT)) {
       				commit->object.flags |= RESULT;
       				tail = commit_list_append(commit, tail);
     -+				/* Generation-ordered queue: no later
     -+				 * commit can dominate this one. */
     -+				if (!find_all && has_gens &&
     ++				/*
     ++				 * The queue is generation-ordered; no
     ++				 * remaining common ancestor can be a
     ++				 * descendant of this one.
     ++				 */
     ++				if (!find_all &&
      +				    generation < GENERATION_NUMBER_INFINITY)
      +					break;
       			}
     @@ commit-reach.h: int repo_get_merge_bases_many(struct repository *r,
       			      struct commit **twos,
       			      struct commit_list **result);
      -/* To be used only when object flags after this call no longer matter */
     -+/* To be used only when object flags after this call no longer matter.
     ++/*
     ++ * To be used only when object flags after this call no longer matter.
      + * When find_all is false and generation numbers are available, returns
     -+ * after finding the first merge-base, skipping the STALE drain. */
     ++ * after finding the first merge-base, skipping the STALE drain.
     ++ */
       int repo_get_merge_bases_many_dirty(struct repository *r,
       				    struct commit *one, size_t n,
       				    struct commit **twos,


 builtin/merge-base.c  |   3 +-
 commit-reach.c        |  26 ++++++---
 commit-reach.h        |   7 ++-
 t/t6010-merge-base.sh | 119 ++++++++++++++++++++++++++++++++++++++++++
 t/t6600-test-reach.sh |  40 ++++++++++++++
 5 files changed, 186 insertions(+), 9 deletions(-)

diff --git a/builtin/merge-base.c b/builtin/merge-base.c
index c7ee97fa6a..6b9d42f596 100644
--- a/builtin/merge-base.c
+++ b/builtin/merge-base.c
@@ -14,7 +14,8 @@ static int show_merge_base(struct commit **rev, size_t rev_nr, int show_all)
 	struct commit_list *result = NULL, *r;
 
 	if (repo_get_merge_bases_many_dirty(the_repository, rev[0],
-					    rev_nr - 1, rev + 1, &result) < 0) {
+					    rev_nr - 1, rev + 1,
+					    show_all, &result) < 0) {
 		commit_list_free(result);
 		return -1;
 	}
diff --git a/commit-reach.c b/commit-reach.c
index d3a9b3ed6f..b4ca00bb7e 100644
--- a/commit-reach.c
+++ b/commit-reach.c
@@ -55,6 +55,7 @@ static int paint_down_to_common(struct repository *r,
 				struct commit **twos,
 				timestamp_t min_generation,
 				int ignore_missing_commits,
+				int find_all,
 				struct commit_list **result)
 {
 	struct prio_queue queue = { compare_commits_by_gen_then_commit_date };
@@ -97,6 +98,14 @@ static int paint_down_to_common(struct repository *r,
 			if (!(commit->object.flags & RESULT)) {
 				commit->object.flags |= RESULT;
 				tail = commit_list_append(commit, tail);
+				/*
+				 * The queue is generation-ordered; no
+				 * remaining common ancestor can be a
+				 * descendant of this one.
+				 */
+				if (!find_all &&
+				    generation < GENERATION_NUMBER_INFINITY)
+					break;
 			}
 			/* Mark parents of a found merge stale */
 			flags |= STALE;
@@ -136,6 +145,7 @@ static int paint_down_to_common(struct repository *r,
 static int merge_bases_many(struct repository *r,
 			    struct commit *one, int n,
 			    struct commit **twos,
+			    int find_all,
 			    struct commit_list **result)
 {
 	struct commit_list *list = NULL, **tail = result;
@@ -165,7 +175,7 @@ static int merge_bases_many(struct repository *r,
 				     oid_to_hex(&twos[i]->object.oid));
 	}
 
-	if (paint_down_to_common(r, one, n, twos, 0, 0, &list)) {
+	if (paint_down_to_common(r, one, n, twos, 0, 0, find_all, &list)) {
 		commit_list_free(list);
 		return -1;
 	}
@@ -246,7 +256,7 @@ static int remove_redundant_no_gen(struct repository *r,
 				min_generation = curr_generation;
 		}
 		if (paint_down_to_common(r, array[i], filled,
-					 work, min_generation, 0, &common)) {
+					 work, min_generation, 0, 1, &common)) {
 			clear_commit_marks(array[i], all_flags);
 			clear_commit_marks_many(filled, work, all_flags);
 			commit_list_free(common);
@@ -425,6 +435,7 @@ static int get_merge_bases_many_0(struct repository *r,
 				  size_t n,
 				  struct commit **twos,
 				  int cleanup,
+				  int find_all,
 				  struct commit_list **result)
 {
 	struct commit_list *list, **tail = result;
@@ -432,7 +443,7 @@ static int get_merge_bases_many_0(struct repository *r,
 	size_t cnt, i;
 	int ret;
 
-	if (merge_bases_many(r, one, n, twos, result) < 0)
+	if (merge_bases_many(r, one, n, twos, find_all, result) < 0)
 		return -1;
 	for (i = 0; i < n; i++) {
 		if (one == twos[i])
@@ -475,16 +486,17 @@ int repo_get_merge_bases_many(struct repository *r,
 			      struct commit **twos,
 			      struct commit_list **result)
 {
-	return get_merge_bases_many_0(r, one, n, twos, 1, result);
+	return get_merge_bases_many_0(r, one, n, twos, 1, 1, result);
 }
 
 int repo_get_merge_bases_many_dirty(struct repository *r,
 				    struct commit *one,
 				    size_t n,
 				    struct commit **twos,
+				    int find_all,
 				    struct commit_list **result)
 {
-	return get_merge_bases_many_0(r, one, n, twos, 0, result);
+	return get_merge_bases_many_0(r, one, n, twos, 0, find_all, result);
 }
 
 int repo_get_merge_bases(struct repository *r,
@@ -492,7 +504,7 @@ int repo_get_merge_bases(struct repository *r,
 			 struct commit *two,
 			 struct commit_list **result)
 {
-	return get_merge_bases_many_0(r, one, 1, &two, 1, result);
+	return get_merge_bases_many_0(r, one, 1, &two, 1, 1, result);
 }
 
 /*
@@ -555,7 +567,7 @@ int repo_in_merge_bases_many(struct repository *r, struct commit *commit,
 
 	if (paint_down_to_common(r, commit,
 				 nr_reference, reference,
-				 generation, ignore_missing_commits, &bases))
+				 generation, ignore_missing_commits, 1, &bases))
 		ret = -1;
 	else if (commit->object.flags & PARENT2)
 		ret = 1;
diff --git a/commit-reach.h b/commit-reach.h
index 6012402dfc..c3b570a5cc 100644
--- a/commit-reach.h
+++ b/commit-reach.h
@@ -17,10 +17,15 @@ int repo_get_merge_bases_many(struct repository *r,
 			      struct commit *one, size_t n,
 			      struct commit **twos,
 			      struct commit_list **result);
-/* To be used only when object flags after this call no longer matter */
+/*
+ * To be used only when object flags after this call no longer matter.
+ * When find_all is false and generation numbers are available, returns
+ * after finding the first merge-base, skipping the STALE drain.
+ */
 int repo_get_merge_bases_many_dirty(struct repository *r,
 				    struct commit *one, size_t n,
 				    struct commit **twos,
+				    int find_all,
 				    struct commit_list **result);
 
 int get_octopus_merge_bases(struct commit_list *in, struct commit_list **result);
diff --git a/t/t6010-merge-base.sh b/t/t6010-merge-base.sh
index 44c726ea39..f6c85d4f53 100755
--- a/t/t6010-merge-base.sh
+++ b/t/t6010-merge-base.sh
@@ -305,4 +305,123 @@ test_expect_success 'merge-base --octopus --all for complex tree' '
 	test_cmp expected actual
 '
 
+# The following tests verify that "git merge-base" (without --all)
+# returns the same result with and without a commit-graph.
+# This exercises the early-exit optimisation in paint_down_to_common
+# that skips the STALE drain when generation numbers are available.
+
+test_expect_success 'setup for commit-graph tests' '
+	git init graph-repo &&
+	(
+		cd graph-repo &&
+
+		# Build a forked DAG:
+		#
+		#     L1---L2  (left)
+		#    /
+		#   S
+		#    \
+		#     R1---R2  (right)
+		#
+		test_commit GS &&
+		git checkout -b left &&
+		test_commit L1 &&
+		test_commit L2 &&
+		git checkout GS &&
+		git checkout -b right &&
+		test_commit GR1 &&
+		test_commit GR2
+	)
+'
+
+test_expect_success 'merge-base without commit-graph' '
+	(
+		cd graph-repo &&
+		rm -f .git/objects/info/commit-graph &&
+		git merge-base left right >actual &&
+		git rev-parse GS >expected &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'merge-base with commit-graph' '
+	(
+		cd graph-repo &&
+		git commit-graph write --reachable &&
+		git merge-base left right >actual &&
+		git rev-parse GS >expected &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'merge-base --all with commit-graph' '
+	(
+		cd graph-repo &&
+		git merge-base --all left right >actual &&
+		git rev-parse GS >expected &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'merge-base agrees with --all for single result' '
+	(
+		cd graph-repo &&
+		git commit-graph write --reachable &&
+		git merge-base left right >actual.single &&
+		git merge-base --all left right >actual.all &&
+		test_cmp actual.all actual.single
+	)
+'
+
+test_expect_success 'setup for deep chain commit-graph test' '
+	git init deep-repo &&
+	(
+		cd deep-repo &&
+
+		# Build a deep forked DAG:
+		#
+		#   L1--L2--...--L20  (left)
+		#  /
+		# S
+		#  \
+		#   R1--R2--...--R20  (right)
+		#
+		test_commit DS &&
+		git checkout -b left &&
+		for i in $(test_seq 1 20)
+		do
+			test_commit DL$i || return 1
+		done &&
+		git checkout DS &&
+		git checkout -b right &&
+		for i in $(test_seq 1 20)
+		do
+			test_commit DR$i || return 1
+		done
+	)
+'
+
+test_expect_success 'deep chain: merge-base matches with and without commit-graph' '
+	(
+		cd deep-repo &&
+		rm -f .git/objects/info/commit-graph &&
+		git merge-base left right >actual.no-graph &&
+		git rev-parse DS >expected &&
+		test_cmp expected actual.no-graph &&
+		git commit-graph write --reachable &&
+		git merge-base left right >actual.graph &&
+		test_cmp expected actual.graph
+	)
+'
+
+test_expect_success 'deep chain: --all and non---all agree with commit-graph' '
+	(
+		cd deep-repo &&
+		git commit-graph write --reachable &&
+		git merge-base left right >actual.single &&
+		git merge-base --all left right >actual.all &&
+		test_cmp actual.all actual.single
+	)
+'
+
 test_done
diff --git a/t/t6600-test-reach.sh b/t/t6600-test-reach.sh
index dc0421ed2f..51c23b7683 100755
--- a/t/t6600-test-reach.sh
+++ b/t/t6600-test-reach.sh
@@ -882,4 +882,44 @@ test_expect_success 'rev-list --maximal-only matches merge-base --independent' '
 	test_cmp expect.sorted actual.sorted
 '
 
+# The following tests verify the early-exit optimisation in
+# paint_down_to_common when merge-base is invoked without --all.
+# Each test checks all four commit-graph configurations.
+
+merge_base_all_modes () {
+	test_when_finished rm -rf .git/objects/info/commit-graph &&
+	git merge-base "$@" >actual &&
+	test_cmp expect actual &&
+	cp commit-graph-full .git/objects/info/commit-graph &&
+	git merge-base "$@" >actual &&
+	test_cmp expect actual &&
+	cp commit-graph-half .git/objects/info/commit-graph &&
+	git merge-base "$@" >actual &&
+	test_cmp expect actual &&
+	cp commit-graph-no-gdat .git/objects/info/commit-graph &&
+	git merge-base "$@" >actual &&
+	test_cmp expect actual
+}
+
+test_expect_success 'merge-base without --all (unique base)' '
+	git rev-parse commit-5-3 >expect &&
+	merge_base_all_modes commit-5-7 commit-8-3
+'
+
+test_expect_success 'merge-base without --all is one of --all results' '
+	test_when_finished rm -rf .git/objects/info/commit-graph &&
+
+	cp commit-graph-full .git/objects/info/commit-graph &&
+	git merge-base --all commit-5-7 commit-4-8 commit-6-6 commit-8-3 >all &&
+	git merge-base commit-5-7 commit-4-8 commit-6-6 commit-8-3 >single &&
+	test_line_count = 1 single &&
+	grep -F -f single all &&
+
+	cp commit-graph-half .git/objects/info/commit-graph &&
+	git merge-base --all commit-5-7 commit-4-8 commit-6-6 commit-8-3 >all &&
+	git merge-base commit-5-7 commit-4-8 commit-6-6 commit-8-3 >single &&
+	test_line_count = 1 single &&
+	grep -F -f single all
+'
+
 test_done

base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH] build: tolerate use of _Generic from glibc 2.43 with Clang
From: Junio C Hamano @ 2026-05-11  6:10 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: Shardul Natu, git
In-Reply-To: <agFtGM0H4S87ZxwR@pks.im>

Patrick Steinhardt <ps@pks.im> writes:

> On Mon, May 11, 2026 at 02:20:09PM +0900, Junio C Hamano wrote:
>> Junio C Hamano <gitster@pobox.com> writes:
>> 
>> > Wouldn't the approach you took on the meson side to pass
>> > "-Wno-c11-extensions" be yet another alternative?
>> 
>> In other words, I would imagine something like this patch that uses
>> the same strategy on both sides may be easier to reason about.
>
> I was going back and forth on this myself. I simply wasn't sure whether
> it even buys us anything anymore if we have both "-std=gnu99" _and_
> "-Wno-c11-extensions". But maybe this combination at least also detects
> the use of newer (C23) extensions?

We shouldn't be the only project hit by the unconditional use of
_Generic in glibc 2.43 headers, should we?  I was hoping that this
would be fixed upstream, and that anything we do locally is merely
a workaround of a tentative nature.

> In any case, I'm also happy with the patch you posted. Thanks!

Thanks for a quick response.

You may have guessed correctly that I want to fast-track this topic
down to 'next' and 'master' soonish to salvage CI jobs running for
them.

^ permalink raw reply

* Re: What's cooking in git.git (May 2026, #02)
From: Junio C Hamano @ 2026-05-11  5:51 UTC (permalink / raw)
  To: git
  Cc: brian m. carlson, Phillip Wood, Andreas Schwab, Ondrej Pohorelsky,
	Patrick Steinhardt, Jeff King, D. Ben Knoble, Johannes Schindelin
In-Reply-To: <xmqq4iketzh0.fsf@gitster.g>

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

[cc: list taken from <pull.1853.v4.git.1770113882.gitgitgadget@gmail.com>]

> * jc/neuter-sideband-fixup (2026-03-05) 6 commits
>   (merged to 'next' on 2026-03-13 at 5a4098b0cd)
>  + sideband: drop 'default' configuration
>  + sideband: offer to configure sanitizing on a per-URL basis
>  + sideband: add options to allow more control sequences to be passed through
>  + sideband: do allow ANSI color sequences by default
>  + sideband: introduce an "escape hatch" to allow control characters
>  + sideband: mask control characters
>  (this branch is used by jc/neuter-sideband-post-3.0.)
>
>  Try to resurrect and reboot a stalled "avoid sending risky escape
>  sequences taken from sideband to the terminal" topic by Dscho.  The
>  plan is to keep it in 'next' long enough to see if anybody screams
>  with the "everything dropped except for ANSI color escape sequences"
>  default.

This topic has cooked sufficiently long in 'next'.  I'd push it to
'master' so that it now would have enough exposure time before the
next release.

But I'd do so with a bit of twist.

I plan to hold off the final step.  That step allows to pass
anything until Git 3.0.  That way, those who work with the version
in 'master' will see how things would look like with these strict
checks that allowlist only a few types of selected control sequences
by default.  With luck, we might not hear any complaints from
anybody, in which case we may not have to apply the last step at all.

Of course, if there are huge complaints, then we may have to
reconsider the approach to allow only the selected sequences and
instead blacklist known bad/risky sequences.  In such a case, we may
have to revert the merge first before we regroup, but it is more
than likely that we won't have to do so---after all, the topic
(without the final "loosen rules for now" step) has been used by
those who are on 'next' for quite a while already.  Knock wood...

^ permalink raw reply

* Re: [PATCH] build: tolerate use of _Generic from glibc 2.43 with Clang
From: Patrick Steinhardt @ 2026-05-11  5:46 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Shardul Natu, git
In-Reply-To: <xmqqqzniset2.fsf@gitster.g>

On Mon, May 11, 2026 at 02:20:09PM +0900, Junio C Hamano wrote:
> Junio C Hamano <gitster@pobox.com> writes:
> 
> > Wouldn't the approach you took on the meson side to pass
> > "-Wno-c11-extensions" be yet another alternative?
> 
> In other words, I would imagine something like this patch that uses
> the same strategy on both sides may be easier to reason about.

I was going back and forth on this myself. I simply wasn't sure whether
it even buys us anything anymore if we have both "-std=gnu99" _and_
"-Wno-c11-extensions". But maybe this combination at least also detects
the use of newer (C23) extensions?

In any case, I'm also happy with the patch you posted. Thanks!

Patrick

^ permalink raw reply

* Re: [PATCH] build: tolerate use of _Generic from glibc 2.43 with Clang
From: Junio C Hamano @ 2026-05-11  5:20 UTC (permalink / raw)
  To: Patrick Steinhardt, Shardul Natu; +Cc: git
In-Reply-To: <xmqqzf26sk80.fsf@gitster.g>

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

> Wouldn't the approach you took on the meson side to pass
> "-Wno-c11-extensions" be yet another alternative?

In other words, I would imagine something like this patch that uses
the same strategy on both sides may be easier to reason about.

----- >8 -----
From: Patrick Steinhardt <ps@pks.im>
Subject: [PATCH] build: tolerate use of _Generic from glibc 2.43 with Clang

When building with `make DEVELOPER=1` we explicitly pass "-std=gnu99" to
the compiler so that we don't start leaning on features exposed by more
recent versions of the C standard. Unfortunately though, glibc 2.43
started to use type-generic expressions. This works alright with GCC,
but when compiling with Clang this leads to errors:

  $ make DEVELOPER=1 CC=clang
  CC daemon.o
  In file included from daemon.c:3:
  ./git-compat-util.h:344:11: error: '_Generic' is a C11 extension [-Werror,-Wc11-extensions]
    344 |         return !!strchr(path, '/');
        |                  ^
  /usr/include/string.h:265:3: note: expanded from macro 'strchr'
    265 |   __glibc_const_generic (S, const char *, strchr (S, C))
        |   ^
  /usr/include/x86_64-linux-gnu/sys/cdefs.h:838:3: note: expanded from macro '__glibc_const_generic'
    838 |   _Generic (0 ? (PTR) : (void *) 1,                     \
        |   ^

In theory, the `__glibc_const_generic` macro does have feature gating:

  #if !defined __cplusplus \
      && (__GNUC_PREREQ (4, 9) \
          || __glibc_has_extension (c_generic_selections) \
          || (!defined __GNUC__ && defined __STDC_VERSION__ \
              && __STDC_VERSION__ >= 201112L))
  # define __HAVE_GENERIC_SELECTION 1
  #else
  # define __HAVE_GENERIC_SELECTION 0
  #endif

But this feature gating isn't effective because `_has_extension()` will
always evaluate to true as C generics _are_ available as a language
extension to GNU C99 when using Clang. This would have been different if
`_has_feature()` was used instead, in which case it would have properly
evaluated to `false`.

GCC has a workaround to squelch this warning from standard system
headers, but because clang fails due to [-Werror,-Wc11-extensions],
as it lacks the corresponding workaround.

For both meson and Makefile, pass -Wno-c11-extensions when we are
building with clang.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Shardul Natu <snatu@google.com>
[jc: replaced Makefile side with Shardul's approach]
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 config.mak.dev | 7 +++++++
 meson.build    | 6 ++++++
 2 files changed, 13 insertions(+)

diff --git a/config.mak.dev b/config.mak.dev
index e86b6e1b34..794b1c9627 100644
--- a/config.mak.dev
+++ b/config.mak.dev
@@ -98,6 +98,13 @@ endif
 endif
 endif
 
+# glibc 2.43 headers unconditionally use _Generic even when we ask the
+# compiler to stick to -std=gnu99 and unlike GCC, clang lacks a
+# workaround to squelch warnings from system headers.
+ifneq ($(filter clang1,$(COMPILER_FEATURES)),)     # if we are using clang
+DEVELOPER_CFLAGS += -Wno-c11-extensions
+endif
+
 # https://bugzilla.redhat.com/show_bug.cgi?id=2075786
 ifneq ($(filter gcc12,$(COMPILER_FEATURES)),)
 DEVELOPER_CFLAGS += -Wno-error=stringop-overread
diff --git a/meson.build b/meson.build
index dd52efd1c8..536bd2679c 100644
--- a/meson.build
+++ b/meson.build
@@ -854,6 +854,12 @@ if get_option('warning_level') in ['2','3', 'everything'] and compiler.get_argum
       libgit_c_args += cflag
     endif
   endforeach
+
+  # Clang generates warnings when compiling glibc 2.43 because of the use of
+  # _Generic.
+  if compiler.get_id() == 'clang'
+    libgit_c_args += '-Wno-c11-extensions'
+  endif
 endif
 
 if get_option('breaking_changes')
-- 
2.54.0-170-g88022b8681


^ permalink raw reply related

* Re: [PATCH] build: tolerate use of _Generic from glibc 2.43 with Clang
From: Junio C Hamano @ 2026-05-11  3:23 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git
In-Reply-To: <20260505-b4-pks-ci-tolerate-glibc-generic-v1-1-5786386fe512@pks.im>

Patrick Steinhardt <ps@pks.im> writes:

> Instead, work around the issue by not using -std=gnu99 with Clang when
> using the Makefile and by disabling warnings about C11 extensions when
> using Meson. This isn't ideal, but we at least retain the ability to
> detect the (mis-)use of features from newer standards with GCC.
>
> An alternative to this might be to simply bump the required C standard
> to C11, which is 15 years old by now and should have support on most
> platforms out there. But some more esoteric platforms may not have it.

Wouldn't the approach you took on the meson side to pass
"-Wno-c11-extensions" be yet another alternative?  I think that is
what the other proposal (which was only for Makefile world and not
for meson world) did, even though it may not have been a great
implemenation to help only those who use config.mak.dev

   <pull.2291.git.git.1778120192298.gitgitgadget@gmail.com>

We would need a patch to apply at lesat on v2.54.0 but possibly
older tracks if we plan to keep them also buildable.

Thanks.

^ permalink raw reply

* What's cooking in git.git (May 2026, #02)
From: Junio C Hamano @ 2026-05-11  3:08 UTC (permalink / raw)
  To: git

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

The first batch of topics marked for graduation for quite a while
since 2.54-rc2 have all been merged to 'master'.

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

With maint, master, next, seen, todo:

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

With all the integration branches and topics broken out:

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

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

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

Release tarballs are available at:

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

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

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

 Hook scripts defined via the configuration system can now be
 configured to run in parallel.
 source: <20260410090608.75283-1-adrian.ratiu@collabora.com>


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

 Rust support is enabled by default (but still allows opting out) in
 some future version of Git.
 source: <20260409224434.1861422-1-sandals@crustytoothpaste.net>


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

 Promisor remote handling has been refactored and fixed in
 preparation for auto-configuration of advertised remotes.
 source: <20260407115243.358642-1-christian.couder@gmail.com>


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

 The check that implements the logic to see if an in-core cache-tree
 is fully ready to write out a tree object was broken, which has
 been corrected.
 source: <20260406192711.68870-1-davidlin@stripe.com>


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

 Doc mark-up updates.
 source: <pull.2077.git.1775322767.gitgitgadget@gmail.com>


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

 Doc update.
 source: <xmqqzf3aofdj.fsf_-_@gitster.g>


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

 The test suite harness and many individual test scripts have been
 updated to work correctly when 'set -e' is in effect, which helps
 detect misspelled test commands.
 source: <20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im>


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

 The userdiff driver for the Scheme language has been extended to
 cover other Lisp dialects.
 source: <pull.2000.v4.git.1776220063.gitgitgadget@gmail.com>


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

 Code clean-up to use the right instance of a repository instance in
 calls inside refs subsystem.
 source: <20260404135914.61195-1-shreyanshpaliwalcmsmn@gmail.com>

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

* js/mingw-no-nedmalloc (2026-05-08) 3 commits
 - mingw: remove the vendored compat/nedmalloc/ subtree
 - mingw: drop the build-system plumbing for nedmalloc
 - mingw: stop using nedmalloc

 Stop using unmaintained custom allocator in Windows build which was
 the last user of the code.

 Will merge to 'next'.
 source: <pull.2104.v3.git.1778244661.gitgitgadget@gmail.com>


* sj/submodule-update-clone-config-fix (2026-05-09) 1 commit
 - submodule-config: fix reading submodule.fetchJobs

 The configuration variable submodule.fetchJobs was not read correctly,
 which has been corrected.

 Will merge to 'next'.
 source: <pull.2287.v4.git.git.1778385022964.gitgitgadget@gmail.com>


* aw/validate-proxy-url-scheme (2026-05-05) 1 commit
 - http: reject unsupported proxy URL schemes

 Misspelt proxy URL (e.g., httt://...) did not trigger any warning
 or failure, which has been corrected.

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


* hn/branch-prune-merged (2026-05-05) 6 commits
 - branch: add --all-remotes flag
 - branch: add branch.<name>.pruneMerged opt-out
 - fetch: add --prune-merged
 - branch: add --prune-merged <remote>
 - branch: let delete_branches warn instead of error on bulk refusal
 - branch: add --forked <remote>

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

 Expecting a reroll to remove "fetch" part?
 cf. <20260505220712.93952-1-haraldnordgren@gmail.com>
 source: <pull.2285.v4.git.git.1778009038.gitgitgadget@gmail.com>


* kh/doc-restore-double-underscores-fix (2026-05-05) 1 commit
 - doc: restore: remove double underscore

 Doc update.

 Will merge to 'next'.
 source: <double_underscore.670@msgid.xyz>


* mm/diff-U-takes-no-negative-values (2026-05-05) 3 commits
 - xdiff: guard against negative context lengths
 - diff: reject negative values for -U/--unified
 - diff: reject negative values for --inter-hunk-context

 The command line parser for "git diff" learned a few options take
 only non-negative integers.

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


* ps/clang-w-glibc-2.43-and-_Generic (2026-05-05) 1 commit
 - build: tolerate use of _Generic from glibc 2.43 with Clang

 Headers from glibc 2.43 when used with clang does not allow
 disabling C11 language features, causing build failures..

 Will merge to 'next'.
 source: <20260505-b4-pks-ci-tolerate-glibc-generic-v1-1-5786386fe512@pks.im>


* mm/git-url-parse (2026-05-01) 8 commits
 - t9904: add tests for the new url-parse builtin
 - doc: describe the url-parse builtin
 - builtin: create url-parse command
 - urlmatch: define url_parse function
 - url: return URL_SCHEME_UNKNOWN instead of dying
 - url: move scheme detection to URL header/source
 - url: move url_is_local_not_ssh to url.h
 - connect: rename enum protocol to url_scheme

 The internal URL parsing logic has been made accessible via a new
 subcommand "git url-parse".

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


* kh/doc-commit-graph (2026-05-07) 1 commit
 - doc: add caveat about turning off commit-graph

 Ramifications of turning off commit-graph has been documented a bit
 more clearly.
 source: <V3_caveat_commit-graph.6b6@msgid.xyz>


* dk/doc-exclude-is-shared-per-repo (2026-05-08) 1 commit
 - ignore: note info/exclude lives in GIT_COMMON_DIR, not GIT_DIR

 Document the fact that .git/info/exclude is shared across worktrees
 linked to the same repository.

 Will merge to 'next'?
 source: <d58b6e921d3005c6170fc6c47f175214acb3fa68.1778249267.git.ben.knoble+github@gmail.com>


* jc/t5551-fix-expensive (2026-05-07) 1 commit
 - t5551: "GIT_TEST_LONG=Yes make test" is broken

 Test fix.

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


* kk/paint-down-to-common-optim (2026-05-08) 1 commit
 - commit-reach: early exit paint_down_to_common for single merge-base

 "git merge-base" optimization.

 Comments?
 source: <pull.2109.git.1778252837132.gitgitgadget@gmail.com>


* st/daemon-sockaddr-fixes (2026-05-08) 1 commit
 - daemon: fix network address handling bugs

 Correct use of sockaddr API in "git daemon".

 Comments?
 source: <pull.2299.git.git.1778291290159.gitgitgadget@gmail.com>


* ag/rebase-update-refs-limit-to-branches (2026-05-10) 1 commit
 - rebase: ignore non-branch update-refs

 "git rebase --update-refs", when used with an rebase.instructionFormat
 with "%d" (describe) in it, tried to update local branch HEAD by
 mistake, which has been corrected.

 Will merge to 'next'.
 source: <20260510224111.64467-2-mail@abhinavg.net>


* jc/ci-enable-expensive (2026-05-10) 2 commits
 - ci: enable EXPENSIVE for contributor builds
 - Merge branch 'js/objects-larger-than-4gb-on-windows' into jc/ci-enable-expensive
 (this branch uses js/objects-larger-than-4gb-on-windows.)

 Enable expensive tests to catch topics that may cause breakages on
 integration branches closer to their origin in the contributor PR
 builds.

 Comments?
 source: <xmqqjyta9630.fsf@gitster.g>


* rs/sideband-clear-line-before-print (2026-05-10) 1 commit
 - sideband: clear full line when printing remote messages

 Tweak the way how sideband messages from remote are printed while
 we talk with a remote repository to avoid tickling terminal
 emulator glitches.

 Will merge to 'next'.
 source: <9826dabf-c9a6-4397-8ae6-a24f9c507f1b@web.de>

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

* jh/alias-i18n-fixes (2026-04-24) 1 commit
 - alias: restore support for simple dotted aliases

 Further update to the i18n alias support to avoid regressions.

 Will merge to 'next'?
 source: <20260424161707.1514255-1-jonatan@jontes.page>


* kn/refs-generic-helpers (2026-05-04) 9 commits
 - refs: use peeled tag values in reference backends
 - refs: add peeled object ID to the `ref_update` struct
 - refs: move object parsing to the generic layer
 - update-ref: handle rejections while adding updates
 - update-ref: move `print_rejected_refs()` up
 - refs: return `ref_transaction_error` from `ref_transaction_update()`
 - refs: extract out reflog config to generic layer
 - refs: introduce `ref_store_init_options`
 - refs: remove unused typedef 'ref_transaction_commit_fn'

 Refactor service routines in the ref subsystem backends.

 Will merge to 'next'?
 source: <20260504-refs-move-to-generic-layer-v4-0-936ac2f0b1a3@gmail.com>


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

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


* ps/history-fixup (2026-04-26) 3 commits
 - builtin/history: introduce "fixup" subcommand
 - builtin/history: generalize function to commit trees
 - replay: allow callers to control what happens with empty commits

 "git history" learned "fixup" command.

 Comments?
 source: <20260427-b4-pks-history-fixup-v3-0-cb908f06264b@pks.im>


* rs/grep-column-only-match-fix (2026-04-24) 1 commit
 - grep: fix --column --only-match for 2nd and later matches

 "git grep" update.

 Will merge to 'next'.
 source: <9bd69678-f04b-41d2-ad74-a386820d34c8@web.de>


* sb/unpack-index-pack-buffer-resize (2026-04-28) 1 commit
 - index-pack, unpack-objects: increase input buffer from 4 KiB to 128 KiB

 Use a larger buffer size in the code paths to ingest pack stream.

 Will merge to 'next'?
 source: <pull.2282.v4.git.git.1777387660841.gitgitgadget@gmail.com>


* bc/sign-commit-with-custom-encoding (2026-04-27) 2 commits
 - commit: sign commit after mutating buffer
 - commit: name UTF-8 function appropriately

 Signing commit with custom encoding was passing the data to be
 signed at a wrong stage in the pipeline, which has been corrected.

 Will merge to 'next'?
 source: <20260427221834.1824543-1-sandals@crustytoothpaste.net>


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

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

 Comments?
 cf. <875x4yoys5.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set>
 source: <20260427124108.3524129-1-christian.couder@gmail.com>


* js/maintenance-fix-deadlock-on-win10 (2026-05-07) 2 commits
 - maintenance(geometric): do release the `.idx` files before repacking
 - mingw: optionally use legacy (non-POSIX) delete semantics

 To help Windows 10 installations, avoid removing files whose
 contents are still mmap()'ed.

 Will merge to 'next'.
 source: <pull.2103.v2.git.1778158273.gitgitgadget@gmail.com>


* js/objects-larger-than-4gb-on-windows (2026-05-08) 11 commits
 - ci: run expensive tests on push builds to integration branches
 - t5608: mark >4GB tests as EXPENSIVE
 - test-tool synthesize: add precomputed SHA-256 pack for 4 GiB + 1
 - test-tool synthesize: precompute pack for 4 GiB + 1
 - test-tool synthesize: use the unsafe hash for speed
 - t5608: add regression test for >4GB object clone
 - test-tool: add a helper to synthesize large packfiles
 - delta, packfile: use size_t for delta header sizes
 - odb, packfile: use size_t for streaming object sizes
 - git-zlib: handle data streams larger than 4GB
 - index-pack, unpack-objects: use size_t for object size
 (this branch is used by jc/ci-enable-expensive.)

 Update code paths that assumed "unsigned long" was long enough for
 "size_t".

 Will merge to 'next'.
 source: <pull.2102.v3.git.1778228209.gitgitgadget@gmail.com>


* js/t5564-socks-use-short-path (2026-04-29) 1 commit
  (merged to 'next' on 2026-05-11 at fbb23860ca)
 + t5564: use a short path for the SOCKS proxy socket

 Avoid hitting the pathname limit for socks proxy socket during the
 test..

 Will merge to 'master'.
 source: <pull.2100.v2.git.1777450974159.gitgitgadget@gmail.com>


* kh/doc-log-decorate-list (2026-04-27) 2 commits
 - doc: log: use the same delimiter in description list
 - doc: log: fix --decorate description list

 Doc update.

 Comments?
 source: <CV_doc_log_--decorate_list.626@msgid.xyz>


* za/t2000-modernise-more (2026-04-29) 1 commit
 - t2000: consolidate second scenario into a single test block

 Test update.

 Comments?
 source: <20260429103607.406339-1-zakariyahali100@gmail.com>


* hn/checkout-track-fetch (2026-05-08) 1 commit
 - checkout: extend --track with a "fetch" mode to refresh start-point

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

 Comments?
 cf. <f23eb128-958f-475f-911b-eac4f6daddff@gmail.com>
 source: <pull.2281.v7.git.git.1778280727849.gitgitgadget@gmail.com>


* mf/revision-max-count-oldest (2026-05-09) 2 commits
 - SQUASH??? tac is not portable
 - revision.c: implement --max-count-oldest

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

 Comments?
 source: <2f71a00b035e25b971641b77a6fa7626f1e2459c.1777578676.git.mroik@delayed.space>


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

 Code clean-up.

 Comments?
 source: <pull.2094.git.1777349126.gitgitgadget@gmail.com>


* pw/rename-to-get-current-worktree (2026-05-01) 1 commit
 - worktree: rename get_worktree_from_repository()

 Code clean-up.

 Will merge to 'next'.
 source: <bd48396137f8d1352d11b3bd2dca2848f24a347d.1777648798.git.phillip.wood@dunelm.org.uk>


* ds/path-walk-filters (2026-05-04) 11 commits
 - path-walk: support `combine` filter
 - path-walk: support `object:type` filter
 - path-walk: support `tree:0` filter
 - pack-objects: support sparse:oid filter with path-walk
 - path-walk: add pl_sparse_trees to control tree pruning
 - path-walk: support blob size limit filter
 - backfill: die on incompatible filter options
 - path-walk: support blobless filter
 - t/perf: add pack-objects filter and path-walk benchmark
 - pack-objects: pass --objects with --path-walk
 - Merge branch 'en/backfill-fixes-and-edges' into ds/path-walk-filters
 (this branch uses en/backfill-fixes-and-edges.)

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

 Expecting a reroll?
 cf. <07b36bd8-376b-4a98-a735-0c0f75452c24@gmail.com>
 source: <pull.2101.v2.git.1777926079.gitgitgadget@gmail.com>


* en/ort-cached-rename-with-trivial-resolution (2026-04-20) 1 commit
  (merged to 'next' on 2026-05-11 at 9fe24668d9)
 + merge-ort: handle cached rename & trivial resolution interaction better

 "ort" merge backend improvements.

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


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

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

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


* jk/revert-aa-reap-transport-child-processes (2026-04-22) 1 commit
  (merged to 'next' on 2026-05-11 at b41904c1d3)
 + Revert "transport-helper, connect: use clean_on_exit to reap children on abnormal exit"

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

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


* js/ci-github-actions-update (2026-04-30) 6 commits
  (merged to 'next' on 2026-05-11 at ba87f84054)
 + l10n: bump mshick/add-pr-comment from v2 to v3
 + ci: bump git-for-windows/setup-git-for-windows-sdk from v1 to v2
 + ci: bump actions/checkout from v5 to v6
 + ci: bump actions/github-script from v8 to v9
 + ci: bump actions/{upload,download}-artifact to v7 and v8
 + ci: bump microsoft/setup-msbuild from v2 to v3

 Update various GitHub Actions versions.

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


* mf/format-patch-cover-letter-format-docfix (2026-04-22) 1 commit
  (merged to 'next' on 2026-05-11 at 3cca9cc117)
 + Fix docs for format.commitListFormat

 Docfix.

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


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

 Test fix.

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


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

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

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


* ss/t7004-unhide-git-failures (2026-04-20) 3 commits
  (merged to 'next' on 2026-05-11 at 9ee9feacb7)
 + t7004: avoid subshells to capture git exit codes
 + t7004: dynamically grab expected state in tests
 + t7004: drop hardcoded tag count for state verification

 Test clean-up.

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


* tb/pseudo-merge-bugfixes (2026-04-21) 9 commits
 - pack-bitmap: prevent pattern leak on pseudo-merge re-assignment
 - Documentation: fix broken `sampleRate` in gitpacking(7)
 - pack-bitmap: reject pseudo-merge "sampleRate" of 0
 - pack-bitmap: parse commits in `find_pseudo_merge_group_for_ref()`
 - pack-bitmap: fix pseudo-merge lookup for shared commits
 - pack-bitmap: fix inverted binary search in `pseudo_merge_at()`
 - pack-bitmap-write: sort pseudo-merge commit lookup table in pack order
 - t5333: demonstrate various pseudo-merge bugs
 - t/helper: add 'test-tool bitmap write' subcommand

 Fixes many bugs in pseudo-merge code.

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


* ds/fetch-negotiation-options (2026-04-22) 7 commits
 - send-pack: pass negotiation config in push
 - remote: add remote.*.negotiationInclude config
 - fetch: add --negotiation-include option for negotiation
 - remote: add remote.*.negotiationRestrict config
 - transport: rename negotiation_tips
 - fetch: add --negotiation-restrict option
 - t5516: fix test order flakiness

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

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


* en/backfill-fixes-and-edges (2026-04-15) 3 commits
  (merged to 'next' on 2026-05-11 at baccbdf26d)
 + backfill: default to grabbing edge blobs too
 + backfill: document acceptance of revision-range in more standard manner
 + backfill: reject rev-list arguments that do not make sense
 (this branch is used by ds/path-walk-filters.)

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

 Will merge to 'master'.
 cf. <6e95b82a-19e3-460e-86f7-f899c2df261d@gmail.com>
 source: <pull.2088.git.1776297482.gitgitgadget@gmail.com>


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

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

 Will merge to 'next' and then to 'master'.
 source: <e0f236767f81ea60f90749d1bc00ab78081efd0e.1777546472.git.gitgitgadget@gmail.com>
 source: <pull.2087.git.1776331259.gitgitgadget@gmail.com>


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

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

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


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

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

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


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

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

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


* js/adjust-tests-to-explicitly-access-bare-repo (2026-04-26) 8 commits
 - safe.bareRepository: default to "explicit" with WITH_BREAKING_CHANGES
 - status tests: filter `.gitconfig` from status output
 - ls-files tests: filter `.gitconfig` from `--others` output
 - t5601: restore `.gitconfig` after includeIf test
 - t1305: use `--git-dir=.` for bare repo in include cycle test
 - t1300: remove global config settings injected by test-lib.sh
 - t7900: do not let `$HOME/.gitconfig` interfere with XDG tests
 - test-lib: allow bare repository access when breaking changes are enabled

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

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


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

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

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


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

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

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


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

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

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


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

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

 Comments?
 source: <cover.1776384902.git.lorenzo.pegorari2002@gmail.com>


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

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

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


* jt/odb-transaction-write (2026-04-02) 7 commits
 - odb/transaction: make `write_object_stream()` pluggable
 - object-file: generalize packfile writes to use odb_write_stream
 - object-file: avoid fd seekback by checking object size upfront
 - object-file: remove flags from transaction packfile writes
 - odb: update `struct odb_write_stream` read() callback
 - odb/transaction: use pluggable `begin_transaction()`
 - odb: split `struct odb_transaction` into separate header
 (this branch is used by ps/odb-in-memory.)

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

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


* sa/cat-file-batch-mailmap-switch (2026-04-15) 1 commit
 - cat-file: add mailmap subcommand to --batch-command

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

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


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

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

 Will merge to 'next'?
 source: <cover.1777507303.git.me@ttaylorr.com>


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

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

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


* ps/setup-wo-the-repository (2026-04-20) 18 commits
 - setup: stop using `the_repository` in `init_db()`
 - setup: stop using `the_repository` in `create_reference_database()`
 - setup: stop using `the_repository` in `initialize_repository_version()`
 - setup: stop using `the_repository` in `check_repository_format()`
 - setup: stop using `the_repository` in `upgrade_repository_format()`
 - setup: stop using `the_repository` in `setup_git_directory()`
 - setup: stop using `the_repository` in `setup_git_directory_gently()`
 - setup: stop using `the_repository` in `setup_git_env()`
 - setup: stop using `the_repository` in `set_git_work_tree()`
 - setup: stop using `the_repository` in `setup_work_tree()`
 - setup: stop using `the_repository` in `enter_repo()`
 - setup: stop using `the_repository` in `verify_non_filename()`
 - setup: stop using `the_repository` in `verify_filename()`
 - setup: stop using `the_repository` in `path_inside_repo()`
 - setup: stop using `the_repository` in `prefix_path()`
 - setup: stop using `the_repository` in `is_inside_git_dir()`
 - setup: stop using `the_repository` in `is_inside_worktree()`
 - setup: replace use of `the_repository` in static functions

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

 Needs review.
 source: <20260420-pks-setup-wo-the-repository-v1-0-f4a81c4988e8@pks.im>


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

 Documentation updates.

 Needs review.
 source: <V2_CV_doc_int-tr_key_format.613@msgid.xyz>


* ps/graph-lane-limit (2026-03-27) 3 commits
 - graph: add truncation mark to capped lanes
 - graph: add --graph-lane-limit option
 - graph: limit the graph width to a hard-coded max

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

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


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

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

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


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

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

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


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

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

 Will merge to 'next'.
 source: <pull.2234.v16.git.git.1777401552.gitgitgadget@gmail.com>


* kh/name-rev-custom-format (2026-05-07) 5 commits
 - format-rev: introduce builtin for on-demand pretty formatting
 - name-rev: make dedicated --annotate-stdin --name-only test
 - name-rev: factor code for sharing with a new command
 - name-rev: run clang-format before factoring code
 - name-rev: wrap both blocks in braces

 A new builtin "git format-rev" is introduced for pretty formatting
 one revision expression per line or commit object names found in
 running text.

 Will merge to 'next'.
 source: <V4_CV_format-rev.6aa@msgid.xyz>


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

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

 Expecting a reroll.
 source: <SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM>


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

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

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


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

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

 Will keep in 'next' a bit longer than usual.
 source: <20260305233452.3727126-1-gitster@pobox.com>


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

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

 On hold, until jc/neuter-sideband-fixup cooks long enough in 'next'.
 (this branch uses jc/neuter-sideband-fixup.)
 source: <20260305233452.3727126-8-gitster@pobox.com>


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

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

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


* pt/promisor-lazy-fetch-no-recurse (2026-03-11) 1 commit
 - promisor-remote: prevent lazy-fetch recursion in child fetch

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

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


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

 The fsmonitor daemon has been implemented for Linux.

 Will merge to 'next'?
 source: <pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com>


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

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

 Will merge to 'next'?
 source: <cover.1777903579.git.phillip.wood@dunelm.org.uk>


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

 Preparation of the xdiff/ codebase to work with Rust.

 Will merge to 'next'.
 source: <pull.2156.v6.git.git.1777500495.gitgitgadget@gmail.com>

^ 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