Git development
 help / color / mirror / Atom feed
* Re: [PATCH] http: handle absolute-path alternates from server root
From: Junio C Hamano @ 2026-05-13  1:10 UTC (permalink / raw)
  To: Jeff King; +Cc: git, slonkazoid
In-Reply-To: <20260512162619.GA69813@coredump.intra.peff.net>

Jeff King <peff@peff.net> writes:

>           ... Probably in a way that makes it totally invalid, but
>           if you were very unlucky you could turn something like:
>
>              http://victim.com.evil.domain:8000
>
>           into:
>
>             http://victim.com
>
> 	  Which looks like the start of a redirect attack, except that
> 	  the attacker could just have written "http://victim.com" in
> 	  the first place! Either way we feed it to
> 	  is_alternate_allowed(), which is where we check redirect and
> 	  protocol rules.

Yuck.  I know I am the guilty party who introduced the dumb HTTP
walker but I wish we could kill it off after all these years. I did
not even recall that we supported the alternate object store in the
"protocol" until I saw this patch X-<.

> I think we can just treat this like a regular bug.

Absolutely.  Thanks.

^ permalink raw reply

* Re: [PATCH] pretty: drop strbuf pre-sizing from add_rfc2047()
From: Junio C Hamano @ 2026-05-13  1:03 UTC (permalink / raw)
  To: Jeff King; +Cc: git, Luke Martin
In-Reply-To: <20260512162022.GA69669@coredump.intra.peff.net>

Jeff King <peff@peff.net> writes:

> At the top of add_rfc2047() we do this:
>
>   strbuf_grow(sb, len * 3 + strlen(encoding) + 100);
>
> where "len" is the size of the header (like an author name) we are about
> to encode into the buffer. This pre-sizing is purely an optimization; we
> use strbuf_addf() and friends to actually write into the buffer, and
> they will grow the buffer as necessary.
> ...
> Is the optimization helping? I don't think so. Even for a gigantic case
> like the 1.4GB author name, I couldn't measure any slowdown when
> removing it. And most input will be much smaller, and added to a running
> strbuf containing the rest of the email-header output. We can just rely
> on strbuf's usual amortized-linear growth.
>
> So deleting the line seems like the best way to go. It eliminates the
> integer overflow and makes the code a tiny bit simpler.

Very nice.

Someday we may want to go through the output from

    $ git grep -e 'strbuf_grow(' \*.c

and remove this ineffective presizing.  I think any call to
strbuf_grow() that is immediately followed by a call to
strbuf_addX() is suspect, like in the following illustration (there
are others in these files).

diff --git c/apply.c w/apply.c
index 3de4aa4d2e..b0f146276e 100644
--- c/apply.c
+++ w/apply.c
@@ -3305,7 +3305,6 @@ static int apply_fragments(struct apply_state *state, struct image *img, struct
 static int read_blob_object(struct strbuf *buf, const struct object_id *oid, unsigned mode)
 {
 	if (S_ISGITLINK(mode)) {
-		strbuf_grow(buf, 100);
 		strbuf_addf(buf, "Subproject commit %s\n", oid_to_hex(oid));
 	} else {
 		enum object_type type;
diff --git c/archive.c w/archive.c
index fcd474c682..47b8725f0a 100644
--- c/archive.c
+++ w/archive.c
@@ -164,7 +164,6 @@ static int write_archive_entry(const struct object_id *oid, const char *base,
 
 	args->convert = 0;
 	strbuf_reset(&path);
-	strbuf_grow(&path, PATH_MAX);
 	strbuf_add(&path, args->base, args->baselen);
 	strbuf_add(&path, base, baselen);
 	strbuf_addstr(&path, filename);






^ permalink raw reply related

* [PATCH] remote: qualify "git pull" advice for non-upstream branches
From: Harald Nordgren via GitGitGadget @ 2026-05-12 22:11 UTC (permalink / raw)
  To: git; +Cc: Harald Nordgren, Harald Nordgren

From: Harald Nordgren <haraldnordgren@gmail.com>

When "git status" reports the local branch is behind or has diverged
from the push branch, the advice suggested a bare "git pull". That
follows the upstream, which may live on a different remote, so emit
"git pull <remote> <branch>" instead.

Also enable the pull and divergence advice for push-branch
comparisons; they were previously only set for the upstream.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
    remote: qualify "git pull" advice for non-upstream branches

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2301%2FHaraldNordgren%2Fstatus-pull-advice-qualified-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2301/HaraldNordgren/status-pull-advice-qualified-v1
Pull-Request: https://github.com/git/git/pull/2301

 remote.c                 | 47 +++++++++++++++++++++++++++++++---------
 t/t6040-tracking-info.sh | 41 +++++++++++++++++++++++++++++++++++
 2 files changed, 78 insertions(+), 10 deletions(-)

diff --git a/remote.c b/remote.c
index a664cd166a..d1e09079cb 100644
--- a/remote.c
+++ b/remote.c
@@ -2267,6 +2267,8 @@ static void format_branch_comparison(struct strbuf *sb,
 				     bool up_to_date,
 				     int ours, int theirs,
 				     const char *branch_name,
+				     const char *push_remote_name,
+				     const char *push_branch_name,
 				     enum ahead_behind_flags abf,
 				     unsigned flags)
 {
@@ -2302,9 +2304,15 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "and can be fast-forwarded.\n",
 			   theirs),
 			branch_name, theirs);
-		if (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git pull\" to update your local branch)\n"));
+		if (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS)) {
+			if (push_remote_name && push_branch_name)
+				strbuf_addf(sb,
+					_("  (use \"git pull %s %s\" to update your local branch)\n"),
+					push_remote_name, push_branch_name);
+			else
+				strbuf_addstr(sb,
+					_("  (use \"git pull\" to update your local branch)\n"));
+		}
 	} else {
 		strbuf_addf(sb,
 			Q_("Your branch and '%s' have diverged,\n"
@@ -2315,9 +2323,15 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "respectively.\n",
 			   ours + theirs),
 			branch_name, ours, theirs);
-		if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
+		if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS)) {
+			if (push_remote_name && push_branch_name)
+				strbuf_addf(sb,
+					_("  (use \"git pull %s %s\" if you want to integrate the remote branch with yours)\n"),
+					push_remote_name, push_branch_name);
+			else
+				strbuf_addstr(sb,
+					_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
+		}
 	}
 }
 
@@ -2355,6 +2369,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 		int ours, theirs, cmp;
 		int is_upstream, is_push;
 		unsigned flags = 0;
+		const char *push_remote_name = NULL;
+		const char *push_branch_name = NULL;
 
 		full_ref = resolve_compare_branch(branch,
 						  branches.items[i].string);
@@ -2396,13 +2412,24 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 		if (reported)
 			strbuf_addstr(sb, "\n");
 
-		if (is_upstream)
+		if (is_upstream || is_push) {
 			flags |= ENABLE_ADVICE_PULL;
-		if (is_push)
+			if (show_divergence_advice)
+				flags |= ENABLE_ADVICE_DIVERGENCE;
+		}
+		if (is_push) {
 			flags |= ENABLE_ADVICE_PUSH;
-		if (show_divergence_advice && is_upstream)
-			flags |= ENABLE_ADVICE_DIVERGENCE;
+			push_remote_name = pushremote_for_branch(branch, NULL);
+			if (push_remote_name &&
+			    skip_prefix(full_ref, "refs/remotes/", &push_branch_name) &&
+			    skip_prefix(push_branch_name, push_remote_name, &push_branch_name) &&
+			    *push_branch_name == '/')
+				push_branch_name++;
+			else
+				push_remote_name = NULL;
+		}
 		format_branch_comparison(sb, !cmp, ours, theirs, short_ref,
+					 push_remote_name, push_branch_name,
 					 abf, flags);
 		reported = 1;
 
diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh
index 0242b5bf7a..3199063762 100755
--- a/t/t6040-tracking-info.sh
+++ b/t/t6040-tracking-info.sh
@@ -529,6 +529,7 @@ test_expect_success 'status.compareBranches with diverged push branch' '
 
 	Your branch and ${SQ}origin/feature8${SQ} have diverged,
 	and have 1 and 1 different commits each, respectively.
+	  (use "git pull origin feature8" if you want to integrate the remote branch with yours)
 
 	nothing to commit, working tree clean
 	EOF
@@ -646,4 +647,44 @@ test_expect_success 'status.compareBranches with remapped push and upstream remo
 	test_cmp expect actual
 '
 
+test_expect_success 'status.compareBranches with behind push branch suggests qualified pull' '
+	test_config -C test push.default current &&
+	test_config -C test remote.pushDefault origin &&
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
+	git -C test checkout -b feature13 upstream/main &&
+	(cd test && advance work13) &&
+	git -C test push origin &&
+	git -C test reset --hard HEAD^ &&
+	git -C test status >actual &&
+	cat >expect <<-EOF &&
+	On branch feature13
+	Your branch is up to date with ${SQ}upstream/main${SQ}.
+
+	Your branch is behind ${SQ}origin/feature13${SQ} by 1 commit, and can be fast-forwarded.
+	  (use "git pull origin feature13" to update your local branch)
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches with remapped push and behind push branch' '
+	test_config -C test remote.pushDefault origin &&
+	test_config -C test remote.origin.push refs/heads/feature14:refs/heads/remapped14 &&
+	test_config -C test status.compareBranches "@{push}" &&
+	git -C test checkout -b feature14 upstream/main &&
+	(cd test && advance work14) &&
+	git -C test push &&
+	git -C test reset --hard HEAD^ &&
+	git -C test status >actual &&
+	cat >expect <<-EOF &&
+	On branch feature14
+	Your branch is behind ${SQ}origin/remapped14${SQ} by 1 commit, and can be fast-forwarded.
+	  (use "git pull origin remapped14" to update your local branch)
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'
+
 test_done

base-commit: 29bd7ed5127255713c1ac2f43b7c6f257d7b4594
-- 
gitgitgadget

^ permalink raw reply related

* [PATCH v3] ignore: note info/exclude lives in GIT_COMMON_DIR, not GIT_DIR
From: D. Ben Knoble @ 2026-05-12 21:21 UTC (permalink / raw)
  To: git
  Cc: D. Ben Knoble, brian m . carlson, Patrick Steinhardt, Taylor Blau,
	Caleb White, Junio C Hamano, Elijah Newren, Andrew Berry,
	Jeff King, Derrick Stolee, Phillip Wood, Shreyansh Paliwal,
	Dan Drake, Alex Galvin
In-Reply-To: <d58b6e921d3005c6170fc6c47f175214acb3fa68.1778249267.git.ben.knoble+github@gmail.com>

gitignore(5) says that the per-repository ignore file is
$GIT_DIR/info/exclude, but in a worktree that is not the case:

    git rev-parse --git-path info/exclude
    /path/to/main/worktree/.git/info/exclude
    git rev-parse --git-common-dir
    /path/to/main/worktree/.git

We actually use $GIT_COMMON_DIR/info/exclude. Adjust the documentation
and some code comments to say so.

Signed-off-by: D. Ben Knoble <ben.knoble+github@gmail.com>
---

Notes (benknoble/commits):
    Changes in v3:
    
    Adjust more occurrences
    
    Link to v2: <d58b6e921d3005c6170fc6c47f175214acb3fa68.1778249267.git.ben.knoble+github@gmail.com>
    
    Changes in v2:
    
    Only adjust the documentation.
    
    brian points out that a more general extension would allow using more
    info/ files as "per-worktree," which I don't have the impetus to
    implement myself.
    
    Phillip and Junio asked for a concrete use case:
    
        A colleague is developing a tool for managing the "skill files" of
        various LLM tools (Claude, Windsurf, etc.). The files have
        requirements that make it hard to generically ignore them (e.g.,
        filenames and front-matter have to match), but different tasks
        (corresponding to worktrees) may want different active skills, so it
        is desirable to ignore the files. Think of this like node_modules.
    
        Unfortunately, since per-worktree ignores don't work, the current
        solution is to put a .gitignore file in the corresponding directory
        with the installed skills that ignores itself and the installed
        skills.
    
    Since overall reactions seem fairly negative (or require a more general
    extension, which I think is probably the right course but not simply
    implemented), I've opted to adjust the docs. They originally confused
    me, as I was surprised when my colleague reported that per-worktree
    ignores didn't work (the docs imply they should by use of $GIT_DIR).
    
    Link to v1: <e3ee0a11b566dd2cc605447c111ae4620bce0fe6.1777050300.git.ben.knoble+github@gmail.com>
    
    v1 notes:
    
    Discussed briefly at https://lore.kernel.org/git/CALnO6CCXmA+ATT7CuyWkU6P8qmLCCpMi5Ppr1c78s0heznpVyw@mail.gmail.com/T
    
    This is based on next (4f69b47b94 (Merge branch 'ps/test-set-e-clean'
    into next, 2026-04-23)) but cleanly applies to master (94f057755b (Git
    2.54, 2026-04-19)) and seen (50541634cb (Merge branch
    'js/parseopt-subcommand-autocorrection' into seen, 2026-04-23)).

 Documentation/git-ls-files.adoc    |  2 +-
 Documentation/git-svn.adoc         |  2 +-
 Documentation/gitformat-index.adoc |  4 ++--
 Documentation/gitignore.adoc       | 12 ++++++------
 dir.c                              |  4 ++--
 dir.h                              |  2 +-
 6 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/Documentation/git-ls-files.adoc b/Documentation/git-ls-files.adoc
index 58c529afbe..2b175388e1 100644
--- a/Documentation/git-ls-files.adoc
+++ b/Documentation/git-ls-files.adoc
@@ -331,7 +331,7 @@ can give `--exclude-per-directory=.gitignore`, and then specify:
   1. The file specified by the `core.excludesfile` configuration
      variable, if exists, or the `$XDG_CONFIG_HOME/git/ignore` file.
 
-  2. The `$GIT_DIR/info/exclude` file.
+  2. The `$GIT_COMMON_DIR/info/exclude` file.
 
 via the `--exclude-from=` option.
 
diff --git a/Documentation/git-svn.adoc b/Documentation/git-svn.adoc
index c26c12bab3..2a7fa60465 100644
--- a/Documentation/git-svn.adoc
+++ b/Documentation/git-svn.adoc
@@ -439,7 +439,7 @@ Any other arguments are passed directly to 'git log'
 'show-ignore'::
 	Recursively finds and lists the svn:ignore and svn:global-ignores
 	properties on directories. The output is suitable for appending to
-	the $GIT_DIR/info/exclude file.
+	the $GIT_COMMON_DIR/info/exclude file.
 
 'mkdirs'::
 	Attempts to recreate empty directories that core Git cannot track
diff --git a/Documentation/gitformat-index.adoc b/Documentation/gitformat-index.adoc
index 145cace1fe..f6a427cb49 100644
--- a/Documentation/gitformat-index.adoc
+++ b/Documentation/gitformat-index.adoc
@@ -291,14 +291,14 @@ Git index format
     sequence in variable width encoding. Each string describes the
     environment where the cache can be used.
 
-  - Stat data of $GIT_DIR/info/exclude. See "Index entry" section from
+  - Stat data of $GIT_COMMON_DIR/info/exclude. See "Index entry" section from
     ctime field until "file size".
 
   - Stat data of core.excludesFile
 
   - 32-bit dir_flags (see struct dir_struct)
 
-  - Hash of $GIT_DIR/info/exclude. A null hash means the file
+  - Hash of $GIT_COMMON_DIR/info/exclude. A null hash means the file
     does not exist.
 
   - Hash of core.excludesFile. A null hash means the file does
diff --git a/Documentation/gitignore.adoc b/Documentation/gitignore.adoc
index a3d24e5c34..7979e50f18 100644
--- a/Documentation/gitignore.adoc
+++ b/Documentation/gitignore.adoc
@@ -7,7 +7,7 @@ gitignore - Specifies intentionally untracked files to ignore
 
 SYNOPSIS
 --------
-$XDG_CONFIG_HOME/git/ignore, $GIT_DIR/info/exclude, .gitignore
+$XDG_CONFIG_HOME/git/ignore, $GIT_COMMON_DIR/info/exclude, .gitignore
 
 DESCRIPTION
 -----------
@@ -34,7 +34,7 @@ precedence, the last matching pattern decides the outcome):
    includes such `.gitignore` files in its repository, containing patterns for
    files generated as part of the project build.
 
- * Patterns read from `$GIT_DIR/info/exclude`.
+ * Patterns read from `$GIT_COMMON_DIR/info/exclude`.
 
  * Patterns read from the file specified by the configuration
    variable `core.excludesFile`.
@@ -50,7 +50,7 @@ be used.
    specific to a particular repository but which do not need to be shared
    with other related repositories (e.g., auxiliary files that live inside
    the repository but are specific to one user's workflow) should go into
-   the `$GIT_DIR/info/exclude` file.
+   the `$GIT_COMMON_DIR/info/exclude` file.
 
  * Patterns which a user wants Git to
    ignore in all situations (e.g., backup or temporary files generated by
@@ -97,7 +97,7 @@ PATTERN FORMAT
    match at any level below the `.gitignore` level.
 
  - Patterns read from exclude sources that are outside the working tree,
-   such as $GIT_DIR/info/exclude and core.excludesFile, are treated as if
+   such as $GIT_COMMON_DIR/info/exclude and core.excludesFile, are treated as if
    they are specified at the root of the working tree, i.e. a leading "/"
    in such patterns anchors the match at the root of the repository.
 
@@ -146,8 +146,8 @@ CONFIGURATION
 
 The optional configuration variable `core.excludesFile` indicates a path to a
 file containing patterns of file names to exclude, similar to
-`$GIT_DIR/info/exclude`.  Patterns in the exclude file are used in addition to
-those in `$GIT_DIR/info/exclude`.
+`$GIT_COMMON_DIR/info/exclude`. Patterns in the exclude file are used in
+addition to those in `$GIT_COMMON_DIR/info/exclude`.
 
 NOTES
 -----
diff --git a/dir.c b/dir.c
index fcb8f6dd2a..33c81c256e 100644
--- a/dir.c
+++ b/dir.c
@@ -2985,7 +2985,7 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d
 		return NULL;
 
 	/*
-	 * We only support $GIT_DIR/info/exclude and core.excludesfile
+	 * We only support $GIT_COMMON_DIR/info/exclude and core.excludesfile
 	 * as the global ignore rule files. Any other additions
 	 * (e.g. from command line) invalidate the cache. This
 	 * condition also catches running setup_standard_excludes()
@@ -3078,7 +3078,7 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d
 		istate->cache_changed |= UNTRACKED_CHANGED;
 	}
 
-	/* Validate $GIT_DIR/info/exclude and core.excludesfile */
+	/* Validate $GIT_COMMON_DIR/info/exclude and core.excludesfile */
 	root = dir->untracked->root;
 	if (!oideq(&dir->internal.ss_info_exclude.oid,
 		   &dir->untracked->ss_info_exclude.oid)) {
diff --git a/dir.h b/dir.h
index 20d4a078d6..83e0f648a8 100644
--- a/dir.h
+++ b/dir.h
@@ -153,7 +153,7 @@ struct oid_stat {
  *   - The list of files and directories of the directory in question
  *   - The $GIT_DIR/index
  *   - dir_struct flags
- *   - The content of $GIT_DIR/info/exclude
+ *   - The content of $GIT_COMMON_DIR/info/exclude
  *   - The content of core.excludesfile
  *   - The content (or the lack) of .gitignore of all parent directories
  *     from $GIT_WORK_TREE

base-commit: 59709faab07346122d819453f4ad6f3ccdaf618e
-- 
2.54.0.564.ge3ee0a11b5.dirty


^ permalink raw reply related

* Re: [BUG] "git diff --word-diff" gives a diff while they are only space changes
From: Vincent Lefevre @ 2026-05-12 21:17 UTC (permalink / raw)
  To: Michael Montalbo; +Cc: git, j6t
In-Reply-To: <CAC2QwmKRyYfE+30Fh75gvAEmJjk8g-3k+G=RDiEJ-KGNExAEow@mail.gmail.com>

On 2026-05-12 13:56:19 -0700, Michael Montalbo wrote:
> Maybe something like this would be worth adding to the docs:
[...]
> +Word diff works by finding word-level changes within each hunk of
> +the line-level diff.  The line-level alignment determines which
> +changed lines are compared to each other, which can affect the
> +word-level output.
[...]

Yes, this would be useful.

-- 
Vincent Lefèvre <vincent@vinc17.net> - Web: <https://www.vinc17.net/>
100% accessible validated (X)HTML - Blog: <https://www.vinc17.net/blog/>
Work: CR INRIA - computer arithmetic / Pascaline project (LIP, ENS-Lyon)

^ permalink raw reply

* Re: [PATCH v2 1/2] builtin/maintenance: fix locking with "--detach"
From: Taylor Blau @ 2026-05-12 21:14 UTC (permalink / raw)
  To: Patrick Steinhardt
  Cc: git, Jean-Christophe Manciot, Mikael Magnusson, Jeff King,
	Derrick Stolee, Junio C Hamano
In-Reply-To: <20260512-pks-maintenance-fix-lock-with-detach-v2-1-dc6f2d284b6d@pks.im>

On Tue, May 12, 2026 at 10:30:30AM +0200, Patrick Steinhardt wrote:
> diff --git a/builtin/gc.c b/builtin/gc.c
> index 3a71e314c9..d866c19b92 100644
> --- a/builtin/gc.c
> +++ b/builtin/gc.c
> @@ -1810,10 +1810,33 @@ 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 drop ownership of
> +			 * the lockfile to prevent us from removing it upon
> +			 * exit.
> +			 */
> +			lock_file_reassign_owner(&lk, child_pid);
> +			exit(0);
> +		} else {
> +			/*
> +			 * Failure to daemonize is ok, we'll continue in
> +			 * foreground.
> +			 */
> +		}
> +

This works almost identically as the original implementation I shared[1]
original thread discussing this issue, but it takes a slightly different
approach.

Instead of doing this automatically as a part of daemonize(), this patch
only adjusts this singular call-site, and forces us to introduce a new
function daemonize_without_exit() in order to facilitate it.

I personally prefer the other approach, for many of the reasons that
Peff wrote about in [2, 3]. There are two questions in my mind that I
would be curious of your thoughts on:

 * Should we transfer ownership of all tempfiles, or a subset of them?

 * Should we perform that transfer automatically via daemonize(), or
   manually via its callers?

Judging from the other parts of the original thread, I think the answer
to the first question is an unambiguous "yes". The second question I
think there is less of a consensus on, but I'd like to advocate for
doing this automatically via daemonize().

To summarize some of my thoughts after reading [2], I think that because
daemonize() is about letting a process continue on in the background
rather than fork()-ing children and then continuing on ourselves, *not*
automatically handing those off feels like an accident waiting to
happen.

I can't think of a situation where someone would want to daemonize() but
not have the new process assume ownership of its locks and tempfiles. I
think an alternative approach here would be to document the behavior I'm
proposing above clearly in daemonize(). Should a caller arise in the
future that *doesn't* want to this behavior, then we could introduce a
function similar to your daemonize_without_exit() (maybe in this case it
would be daemonize_without_transfer(), since not exiting in a function
that is supposed to daemonize feels awkward).

I guess what I'm saying is that I feel that future daemonize() callers
are much more likely to want this behavior than not, and putting this in
daemonize() feels like the safer option between the two.

FWIW, I feel fairly strongly about ^ this, but I'm of course happy to
discuss more if you feel differently.

(If you do end up taking that approach, that makes the C changes
identical to the ones I shared in [1], so you are free to forge my
Co-authored-by and Signed-off-by lines if you want to take that approach
instead.)

> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
> index 4700beacc1..df0bbc1669 100755
> --- a/t/t7900-maintenance.sh
> +++ b/t/t7900-maintenance.sh
> @@ -1438,6 +1438,64 @@ test_expect_success '--no-detach causes maintenance to not 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 &&
> +
> +		git config maintenance.auto false &&
> +		git config core.lockfilepid true &&
> +
> +		git remote add origin /does/not/exist &&
> +		git config set remote.origin.uploadpack "cat fifo-uploadpack" &&
> +
> +		mkfifo fifo-uploadpack fifo-maint &&
> +
> +		# Open the maintenance FIFO, as otherwise spawning
> +		# git-maintenance(1) would block. Note that we need to open it
> +		# as read-write, as otherwise we would block here already.
> +		exec 9<>fifo-maint &&

Very nice. The fifo-maint idea is much more robust than what I was able
to come up with in my original sketch, and it is a nice compliment to
fifo-uploadpack.

The rest of the test is nearly identical to mine and looks good to me.

Thanks,
Taylor

[1]: https://lore.kernel.org/git/af+snTGFeoUUyfPU@nand.local/
[2]: https://lore.kernel.org/git/20260511200112.GA22912@coredump.intra.peff.net/
[3]: https://lore.kernel.org/git/20260511202258.GD22912@coredump.intra.peff.net/

^ permalink raw reply

* Re: [BUG] "git diff --word-diff" gives a diff while they are only space changes
From: Michael Montalbo @ 2026-05-12 20:56 UTC (permalink / raw)
  To: vincent; +Cc: git, j6t

On Sat, 9 May 2026 17:55:26 +0200, Vincent Lefevre wrote:
> For wdiff, it is just described as "display word differences between
> text files", and it does exactly that. For instance, if there are no
> differences in words, it shows no differences.
>
> For git with the --word-diff, there is actually no documentation,
> except the use of "changed words" and "word diff". No mention of
> line diff at all! So this is quite confusing.

Maybe something like this would be worth adding to the docs:

-- >8 --
diff --git a/Documentation/diff-options.adoc b/Documentation/diff-options.adoc
index 8a63b5e164..665473e61a 100644
--- a/Documentation/diff-options.adoc
+++ b/Documentation/diff-options.adoc
@@ -457,6 +457,11 @@ endif::git-diff[]
 +
 Note that despite the name of the first mode, color is used to
 highlight the changed parts in all modes if enabled.
++
+Word diff works by finding word-level changes within each hunk of
+the line-level diff.  The line-level alignment determines which
+changed lines are compared to each other, which can affect the
+word-level output.

 `--word-diff-regex=<regex>`::
        Use _<regex>_ to decide what a word is, instead of considering
-- >8 --

^ permalink raw reply related

* Re: [PATCH 3/4] ls-files: use strbuf_add_uint()
From: René Scharfe @ 2026-05-12 20:44 UTC (permalink / raw)
  To: Jeff King; +Cc: git
In-Reply-To: <20260512190105.GE70851@coredump.intra.peff.net>

On 5/12/26 9:01 PM, Jeff King wrote:
> On Tue, May 12, 2026 at 01:56:02PM +0200, René Scharfe wrote:
> 
>> Speed up printing of objectsize values by using the specialized function
>> strbuf_add_uint() as well as strbuf_insert() for padding instead of the
>> general-purpose function strbuf_addf().  Here are the numbers I get when
>> listing files in the Linux kernel repo:
>>
>> Benchmark 1: ./git_main -C ../linux ls-files --format='%(objectsize)'
>>   Time (mean ± σ):     257.3 ms ±   0.4 ms    [User: 197.4 ms, System: 56.7 ms]
>>   Range (min … max):   256.7 ms … 258.1 ms    11 runs
>>
>> Benchmark 2: ./git -C ../linux ls-files --format='%(objectsize)'
>>   Time (mean ± σ):     253.4 ms ±   0.3 ms    [User: 193.6 ms, System: 56.6 ms]
>>   Range (min … max):   253.0 ms … 253.8 ms    11 runs
> 
> OK, so here the improvement is less impressive than the previous commit.
> And the code is...
> 
>>  {
>> +	static const char padding[] = "       ";
>> +	size_t min_len = padded ? strlen(padding) : 0;
>> +	size_t orig_len = line->len;
>> +	size_t len;
>> +
>>  	if (type == OBJ_BLOB) {
>>  		unsigned long size;
>>  		if (odb_read_object_info(repo->objects, oid, &size) < 0)
>>  			die(_("could not get object info about '%s'"),
>>  			    oid_to_hex(oid));
>> -		if (padded)
>> -			strbuf_addf(line, "%7"PRIuMAX, (uintmax_t)size);
>> -		else
>> -			strbuf_addf(line, "%"PRIuMAX, (uintmax_t)size);
>> -	} else if (padded) {
>> -		strbuf_addf(line, "%7s", "-");
>> +		strbuf_add_uint(line, size);
>>  	} else {
>>  		strbuf_addstr(line, "-");
>>  	}
>> +	len = line->len - orig_len;
>> +	if (len < min_len)
>> +		strbuf_insert(line, orig_len, padding, min_len - len);
>>  }
> 
> ...also less nice. We are formatting into the strbuf, and then maybe
> memmove()-ing the result to accommodate padding. I wonder how much that
> affects the timing. It's extra shuffling, but memmove() etc is often
> surprisingly fast.

I gave my objectsize and objectsize:padded numbers; the difference was
1.2 ms, albeit with 1.0 ms noise in padded case.

> I wonder how bad it would be to handle the padding ahead of time.
> Obviously strbuf_add_uint() knows the size of the result right before it
> calls memcpy(), and it could insert the padding then. But adding a
> padding length parameter (let alone the space vs "0" decision) to that
> function feels kind of gross.
> 
> In the earlier patch I raised the notion of pre-computing the output
> length. If we had a helper to do that, it would be pretty easy to do:
> 
>   /* noop if third parameter is negative */
>   strbuf_pad(line, ' ', 7 - decimal_digits(size));
>   strbuf_add_uint(line, size);

I started with a struct numbuf for holding a number string and its
length, which helped avoid the memmove(3) call.  It's simple and
doesn't require a lot of code, but introducing that concept felt a bit
much for just two users.

> You could also imagine a world where we had some stateful formatting
> system, and you could say:
> 
>   strbuf_pad_next(line, ' ', 7);
>   strbuf_add_uint(line, size);
> 
> but somebody has to store that state between the calls, and I don't love
> the idea of bloating strbuf with it. So probably you have some
> "formatter" struct, and it operates on a strbuf. And now we have all of
> the OO boilerplate hassles like initializing and tearing down our
> formatter object. ;) So probably not worth it for this triviality.

Terrifying!

> Having it all in one string ("%7d") is nice and concise.

Yes, except the original code includes the 7 twice (again for the
"-" fallback).
> I have often wondered how hard it would be to implement our own
> vsnprintf(), and whether we could do better than the libc ones. It would
> be nice to be able to add shorthands for common types (instead of the
> unreadable PRIuMAX mess), as well as custom ones (e.g., hex oids).

C99 has %ju for uintmax_t and %zu for size_t.  Hmm, do we actually
still need to avoid them?  CodingGuidelines says "the C library used
by MinGW does not" support it.  82c36fa0a9 (submodule: hash the
submodule name for the gitdir path, 2026-01-12) just added a %zu,
and there are lots of them in compat/mimalloc/ in Git for Windows.

An extensible printf-like formatter would be nice indeed.  I wondered
about how something like that could be used to write structured output
like tar and zip archive entries in a terse way.  The thought faded,
I guess, when I found no compelling reason for that compactness that
would justify the required complexity of the mechanism.

René


^ permalink raw reply

* Re: [PATCH v15 00/13] fsmonitor: implement filesystem change listener for Linux
From: D. Ben Knoble @ 2026-05-12 20:36 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Paul Tarjan via GitGitGadget, git, Patrick Steinhardt,
	Paul Tarjan, Gábor SZEDER, Jeff King, Paul Tarjan
In-Reply-To: <xmqqa4u5nnxq.fsf@gitster.g>

On Tue, May 12, 2026 at 2:26 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Ben Knoble <ben.knoble@gmail.com> writes:
>
> >> Le 15 avr. 2026 à 09:27, Paul Tarjan via GitGitGadget <gitgitgadget@gmail.com> a écrit :
> >>
> >> This series implements the built-in fsmonitor daemon for Linux using the
> >> inotify API, bringing it to feature parity with the existing Windows and
> >> macOS implementations. It also fixes two memory leaks in the
> >> platform-independent daemon code and deduplicates the IPC and settings logic
> >> that is now shared between macOS and Linux.
> >
> > Troubleshooting a Gentoo build failure of next has me pretty
> > convinced this topic is in there already. Junio should probably
> > check my math, but I think that means we want to see fixes on top
> > of that base now (unless we are reverting this topic from next and
> > queuing a new version?).
> >
> > (The failure is a Gentoo-ism; we carry a patch that stops applying
> > with this series. Not anything this project needs to worry about.)
>
> So is there a verdict already, which this project may not need to
> worry about?  This has been kept out of 'next' after getting
> reverted but if the breakage was due to Gentoo-ism whose workaround
> does not need to get upstreamed, and if there are no other reasons to
> block the topic, I am inclined to mark the topic for 'next'.
>
> Thanks.

I think Gentoo folks know what needs done (which I will try to spend
some time on soon) to adjust the patches we carry. (That will not need
to get upstreamed, no.)

I would certainly not keep this out of the tree only on behalf of
Gentoo, unless there are other issues.

-- 
D. Ben Knoble

^ permalink raw reply

* Re: [PATCH v6] t2000: consolidate second scenario into a single test
From: Zakariyah Ali @ 2026-05-12 20:01 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git
In-Reply-To: <20260429103607.406339-1-zakariyahali100@gmail.com>

Dear Junio,

Thank you very much for your patient, guidance and feedback throughout the development of this patch series. It has been an invaluable learning experience for me.

While my initial goal with these contributions was to participate in GSoC internship, but I was unable to do so this time, however I have found the process of contributing to the Git ecosystem very rewarding. I am excited to stay involved and look forward to making more contributions in the future.

Also, I am a software engineer with over four years of experience in the field. I am currently seeking new opportunities, specifically entry-level or internship roles where I can continue to grow. If you happen to know of any openings or could offer any advice or assistance, I would be extremely grateful.

Thank you again for your time and for everything you do for the Git project.

Best regards,
Zakariyah Ali

^ permalink raw reply

* Re: [PATCH 1/4] strbuf: add strbuf_add_uint()
From: René Scharfe @ 2026-05-12 19:32 UTC (permalink / raw)
  To: Jeff King; +Cc: git
In-Reply-To: <20260512184238.GC70851@coredump.intra.peff.net>

On 5/12/26 8:42 PM, Jeff King wrote:
> On Tue, May 12, 2026 at 01:56:00PM +0200, René Scharfe wrote:
> 
>> Prepare the number string in a temporary buffer.  Make it big enough for
>> any unsigned integer value: A decimal digit can represent ln(10)/ln(2) ≈
>> 3.32 bits; dividing the number of bits of uintmax_t by 3.3 and rounding
>> up gives a sufficiently close conservative size estimate.
> 
> Cute. The naive obvious question here is: why not just grow the strbuf
> and format it there directly?
> 
> And the answer is that it's much easier to format numbers right-to-left,
> and then you know how many digits you need. ;)

Right.  Using the strbuf to format the number at the right end and
memmove()ing it in place is a valid approach.  I expected it to be
faster than using a buffer on the stack because there's a chance that
only one cache line needs to be touched.

> You can compute the number of digits needed up front, of course, but
> it's log-10. You might be able to do it quickly based on the size of the
> leading bit, but there are a lot of off-by-one gotchas.

That's the simplest approach and I expected the few necessary extra
divisions to be faster than using a buffer and having to copy the
result.

The three variants were close in my tests, the no-copy variant slightly
winning on Apple silicon, but losing slightly more on an AMD Ryzen
laptop CPU.  So I went with the solid choice of using an on-stack
buffer, same as in printf(3) (at least on BSD).  Buffering at the end of
the strbuf was not really faster; perhaps memmove(3) is just that much
slower than memcpy(3).

Perhaps an optimized decimal_width() could change the picture somewhat,
but I don't expect a big win.  On the other hand I just told you how
unreliable my expectations are, so there might be treasure after all. :)

René


^ permalink raw reply

* Re: [PATCH 3/4] ls-files: use strbuf_add_uint()
From: Jeff King @ 2026-05-12 19:01 UTC (permalink / raw)
  To: René Scharfe; +Cc: git
In-Reply-To: <20260512115603.80780-4-l.s.r@web.de>

On Tue, May 12, 2026 at 01:56:02PM +0200, René Scharfe wrote:

> Speed up printing of objectsize values by using the specialized function
> strbuf_add_uint() as well as strbuf_insert() for padding instead of the
> general-purpose function strbuf_addf().  Here are the numbers I get when
> listing files in the Linux kernel repo:
> 
> Benchmark 1: ./git_main -C ../linux ls-files --format='%(objectsize)'
>   Time (mean ± σ):     257.3 ms ±   0.4 ms    [User: 197.4 ms, System: 56.7 ms]
>   Range (min … max):   256.7 ms … 258.1 ms    11 runs
> 
> Benchmark 2: ./git -C ../linux ls-files --format='%(objectsize)'
>   Time (mean ± σ):     253.4 ms ±   0.3 ms    [User: 193.6 ms, System: 56.6 ms]
>   Range (min … max):   253.0 ms … 253.8 ms    11 runs

OK, so here the improvement is less impressive than the previous commit.
And the code is...

>  {
> +	static const char padding[] = "       ";
> +	size_t min_len = padded ? strlen(padding) : 0;
> +	size_t orig_len = line->len;
> +	size_t len;
> +
>  	if (type == OBJ_BLOB) {
>  		unsigned long size;
>  		if (odb_read_object_info(repo->objects, oid, &size) < 0)
>  			die(_("could not get object info about '%s'"),
>  			    oid_to_hex(oid));
> -		if (padded)
> -			strbuf_addf(line, "%7"PRIuMAX, (uintmax_t)size);
> -		else
> -			strbuf_addf(line, "%"PRIuMAX, (uintmax_t)size);
> -	} else if (padded) {
> -		strbuf_addf(line, "%7s", "-");
> +		strbuf_add_uint(line, size);
>  	} else {
>  		strbuf_addstr(line, "-");
>  	}
> +	len = line->len - orig_len;
> +	if (len < min_len)
> +		strbuf_insert(line, orig_len, padding, min_len - len);
>  }

...also less nice. We are formatting into the strbuf, and then maybe
memmove()-ing the result to accommodate padding. I wonder how much that
affects the timing. It's extra shuffling, but memmove() etc is often
surprisingly fast.

I wonder how bad it would be to handle the padding ahead of time.
Obviously strbuf_add_uint() knows the size of the result right before it
calls memcpy(), and it could insert the padding then. But adding a
padding length parameter (let alone the space vs "0" decision) to that
function feels kind of gross.

In the earlier patch I raised the notion of pre-computing the output
length. If we had a helper to do that, it would be pretty easy to do:

  /* noop if third parameter is negative */
  strbuf_pad(line, ' ', 7 - decimal_digits(size));
  strbuf_add_uint(line, size);

You could also imagine a world where we had some stateful formatting
system, and you could say:

  strbuf_pad_next(line, ' ', 7);
  strbuf_add_uint(line, size);

but somebody has to store that state between the calls, and I don't love
the idea of bloating strbuf with it. So probably you have some
"formatter" struct, and it operates on a strbuf. And now we have all of
the OO boilerplate hassles like initializing and tearing down our
formatter object. ;) So probably not worth it for this triviality.
Having it all in one string ("%7d") is nice and concise.

I have often wondered how hard it would be to implement our own
vsnprintf(), and whether we could do better than the libc ones. It would
be nice to be able to add shorthands for common types (instead of the
unreadable PRIuMAX mess), as well as custom ones (e.g., hex oids).

Probably also not worth the headache. ;)

-Peff

^ permalink raw reply

* Re: [PATCH 2/4] cat-file: use strbuf_add_uint()
From: Jeff King @ 2026-05-12 18:46 UTC (permalink / raw)
  To: René Scharfe; +Cc: git
In-Reply-To: <20260512115603.80780-3-l.s.r@web.de>

On Tue, May 12, 2026 at 01:56:01PM +0200, René Scharfe wrote:

> Benchmark 1: ./git_main cat-file --batch-all-objects --batch-check='%(objectsize)'
>   Time (mean ± σ):     751.7 ms ±   1.5 ms    [User: 733.5 ms, System: 17.1 ms]
>   Range (min … max):   750.5 ms … 755.0 ms    10 runs
> 
> Benchmark 2: ./git cat-file --batch-all-objects --batch-check='%(objectsize)'
>   Time (mean ± σ):     720.4 ms ±   0.4 ms    [User: 701.9 ms, System: 16.7 ms]
>   Range (min … max):   719.7 ms … 721.2 ms    10 runs
> 
> Summary
>   ./git cat-file --batch-all-objects --batch-check='%(objectsize)' ran
>     1.04 ± 0.00 times faster than ./git_main cat-file --batch-all-objects --batch-check='%(objectsize)'

Neat. It is sad that sprintf is so slow that this makes a difference. ;)

But it seems like an easy win, and I would argue the resulting code is
more readable here.

-Peff

^ permalink raw reply

* Re: [PATCH 1/4] strbuf: add strbuf_add_uint()
From: Jeff King @ 2026-05-12 18:42 UTC (permalink / raw)
  To: René Scharfe; +Cc: git
In-Reply-To: <20260512115603.80780-2-l.s.r@web.de>

On Tue, May 12, 2026 at 01:56:00PM +0200, René Scharfe wrote:

> Prepare the number string in a temporary buffer.  Make it big enough for
> any unsigned integer value: A decimal digit can represent ln(10)/ln(2) ≈
> 3.32 bits; dividing the number of bits of uintmax_t by 3.3 and rounding
> up gives a sufficiently close conservative size estimate.

Cute. The naive obvious question here is: why not just grow the strbuf
and format it there directly?

And the answer is that it's much easier to format numbers right-to-left,
and then you know how many digits you need. ;)

You can compute the number of digits needed up front, of course, but
it's log-10. You might be able to do it quickly based on the size of the
leading bit, but there are a lot of off-by-one gotchas.

So probably the extra memcpy() is not that big a deal.

-Peff

^ permalink raw reply

* Re: git clone fails when using --dissociate together with a reference repository that contains a commit-graph
From: Jeff King @ 2026-05-12 18:38 UTC (permalink / raw)
  To: Daniel Mach; +Cc: git
In-Reply-To: <6ae85515-9373-4c9e-90d2-5e4176590c5b@suse.com>

On Tue, May 12, 2026 at 09:35:49AM +0200, Daniel Mach wrote:

> I've stumbled upon a bug that the following command failed:
> 
> $ git clone <url> <dir> --reference <old-dir> --dissociate
> fatal: unable to parse commit <SHA>
> warning: Clone succeeded, but checkout failed.
> You can inspect what was checked out with 'git status'
> and retry with 'git restore --source=HEAD :/'
> 
> Omitting --dissociate fixed the error, but it wasn't clear to me what might
> be the root cause.

I think this is the same bug discussed here:

  https://lore.kernel.org/git/20260504095110.GA599780@coredump.intra.peff.net/

I haven't worked up a more polished patch yet because I was trying to
decide between the approach given there (to lazily fall back to manual
parsing) versus filling in all of the dependent tree fields when closing
the commit-graph. Which one is cheaper depends on the access patterns
(how many commits will actually be looked at post-close, versus how many
were ever loaded).

-Peff

^ permalink raw reply

* Re: [PATCH v2 1/3] t0006: add support for approxidate test date adjustment
From: Jeff King @ 2026-05-12 18:35 UTC (permalink / raw)
  To: Tuomas Ahola; +Cc: git
In-Reply-To: <20260512145430.13212-2-taahol@utu.fi>

On Tue, May 12, 2026 at 05:54:28PM +0300, Tuomas Ahola wrote:

> t0006 uses a hard-coded test date and provides no convenient
> way to override it temporarily.  Add an optional parameter to
> check_approxidate to adjust the time as needed, and demonstrate
> the feature with a new test.

Makes sense, but two small-ish comments:

> +check_approxidate 'January 5th yesterday' '2008-12-31 19:20:00' success +48

One, it sucks to have to say "success" here, but is awkward because now
we have two optional arguments. There's nobody passing "failure" right
now, so we could just drop support, though that might be annoying later
when somebody wants to add a failing test. But we could perhaps switch
to allowing:

  check_approxidate --failure 'January 5th yesterday' ...etc

which is fairly natural.

This is something we've run into in many different test scripts, and I
think the harness could do a better job of supporting this. Perhaps with
something like:

  test_expect() {
	if "$GIT_TEST_EXPECT" = "fail"
	then
		test_expect_failure "$@"
	else
		test_expect_success "$@"
  }

  test_fails() {
	# probably needs to be more careful of one-shot with functions
	GIT_TEST_EXPECT=fail "$@"
  }

  # this is a normal passing test snippet
  test_expect 'foo' 'git foo'

  # this one expects failure, but it can be toggled easily by
  # removing the leading "test_fails" wrapper. Not much better
  # than swapping out s/failure/success/ now. But...
  test_fails test_expect 'bar' 'git bar'

  # this one just does the right thing if the helper function
  # is using test_expect under the hood
  test_fails check_approxidate ...

I dunno. It is probably too big a rabbit hole to do before your series,
so mostly I'm just thinking out loud.

> +check_approxidate 'January 5th yesterday' '2008-12-31 19:20:00' success +48

The second thing is that "+48" is pretty opaque. It's a relative offset
to some arbitrary point. To some degree the script already suffers from
that (all of the tests are using some arbitrary point), but I think the
offset (without units!) adds a layer of indirection that makes it even
more confusing.

I wonder how hard it would be to just take an arbitrary time instead,
and then you could write:

  check_approxidate 'January 5th yesterday' '2008-12-31 19:20:00' '2009-08-28 12:00:00'

or whatever. There is a chicken-and-egg problem with testing our date
routines and using the date routines to parse out the starting point.
But I think for approxidate, we could be using the strict parser (tested
separately earlier via check_parse) to handle the base time.

-Peff

^ permalink raw reply

* [PATCH v2 4/4] parse-options: clarify what "negated" means for PARSE_OPT_NONEG
From: Michael Montalbo via GitGitGadget @ 2026-05-12 18:10 UTC (permalink / raw)
  To: git; +Cc: Michael Montalbo, Michael Montalbo
In-Reply-To: <pull.2105.v2.git.1778609423.gitgitgadget@gmail.com>

From: Michael Montalbo <mmontalbo@gmail.com>

The documentation says the flag prevents an option from being
"negated" without specifying what that means. Add a parenthetical
to clarify that it rejects the "--no-<option>" form.

Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
---
 parse-options.h | 1 +
 1 file changed, 1 insertion(+)

diff --git a/parse-options.h b/parse-options.h
index 706de9729f..0d1f738f8d 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -117,6 +117,7 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv,
  *   PARSE_OPT_OPTARG: says that the argument is optional (not for BOOLEANs)
  *   PARSE_OPT_NOARG: says that this option does not take an argument
  *   PARSE_OPT_NONEG: says that this option cannot be negated
+ *                   (i.e. rejects "--no-<option>")
  *   PARSE_OPT_HIDDEN: this option is skipped in the default usage, and
  *                     shown only in the full usage.
  *   PARSE_OPT_LASTARG_DEFAULT: says that this option will take the default
-- 
gitgitgadget

^ permalink raw reply related

* [PATCH v2 3/4] xdiff: guard against negative context lengths
From: Michael Montalbo via GitGitGadget @ 2026-05-12 18:10 UTC (permalink / raw)
  To: git; +Cc: Michael Montalbo, Michael Montalbo
In-Reply-To: <pull.2105.v2.git.1778609423.gitgitgadget@gmail.com>

From: Michael Montalbo <mmontalbo@gmail.com>

The xdemitconf_t fields ctxlen and interhunkctxlen are typed as long
(signed), but negative values are not meaningful for context line
counts. Unlike the diff_options fields changed in the previous two
commits, these cannot be converted to unsigned because the xdiff
arithmetic relies on signed subtraction:

    s1 = XDL_MAX(xch->i1 - xecfg->ctxlen, 0);

If ctxlen were unsigned long, the signed operand would be implicitly
converted to unsigned, and the subtraction would wrap to a large
positive value when i1 < ctxlen, defeating the XDL_MAX clamp. The
signed type is required for correct context-window calculations.

The previous two commits reject negative values at the parse layer
for --inter-hunk-context and -U/--unified, so negative values should
no longer reach xdiff in normal use. Add BUG() guards at the top of
xdl_get_hunk() as defense in depth to catch programming errors in
current or future callers that bypass option parsing.

xdl_get_hunk() is called by both xdl_emit_diff() and
xdl_call_hunk_func(), so a single guard covers all xdiff consumers.

Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
---
 xdiff/xemit.c | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/xdiff/xemit.c b/xdiff/xemit.c
index 04f7e9193b..7cd9cf0a44 100644
--- a/xdiff/xemit.c
+++ b/xdiff/xemit.c
@@ -46,12 +46,20 @@ static long saturating_add(long a, long b)
 xdchange_t *xdl_get_hunk(xdchange_t **xscr, xdemitconf_t const *xecfg)
 {
 	xdchange_t *xch, *xchp, *lxch;
-	long max_common = saturating_add(saturating_add(xecfg->ctxlen,
-							xecfg->ctxlen),
-					 xecfg->interhunkctxlen);
-	long max_ignorable = xecfg->ctxlen;
+	long max_common;
+	long max_ignorable;
 	long ignored = 0; /* number of ignored blank lines */
 
+	if (xecfg->ctxlen < 0)
+		BUG("negative context length: %ld", xecfg->ctxlen);
+	if (xecfg->interhunkctxlen < 0)
+		BUG("negative inter-hunk context length: %ld", xecfg->interhunkctxlen);
+
+	max_common = saturating_add(saturating_add(xecfg->ctxlen,
+						   xecfg->ctxlen),
+				    xecfg->interhunkctxlen);
+	max_ignorable = xecfg->ctxlen;
+
 	/* remove ignorable changes that are too far before other changes */
 	for (xchp = *xscr; xchp && xchp->ignore; xchp = xchp->next) {
 		xch = xchp->next;
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v2 2/4] diff: reject negative values for -U/--unified
From: Michael Montalbo via GitGitGadget @ 2026-05-12 18:10 UTC (permalink / raw)
  To: git; +Cc: Michael Montalbo, Michael Montalbo
In-Reply-To: <pull.2105.v2.git.1778609423.gitgitgadget@gmail.com>

From: Michael Montalbo <mmontalbo@gmail.com>

Passing a negative value to -U is silently accepted and produces
corrupt unified diff output with malformed hunk headers:

    $ git log -1 -p -U-500 -- GIT-VERSION-GEN | grep '^@@'
    @@ -503,999- +503,999- @@

Line 503 of a 106-line file, count "999-" is not a valid integer.

The config variable diff.context already rejects negative values, but
the command line callback diff_opt_unified() uses strtol() with no
range check.

Change the type of diff_options.context and its static default from
int to unsigned int, matching the change to interhunkcontext in the
previous commit. The type change requires reworking the callback and
config parsing to validate in a local variable before assigning to
the now-unsigned field.

Unlike --inter-hunk-context which could be converted to OPT_UNSIGNED,
-U needs OPT_CALLBACK_F for PARSE_OPT_OPTARG (bare -U with no value
enables patch output). Add a range check in the callback instead.

Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
---
 diff.c                  | 12 ++++++++----
 diff.h                  |  2 +-
 t/t4055-diff-context.sh |  5 +++++
 3 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/diff.c b/diff.c
index 5df28e49c5..1771b2c444 100644
--- a/diff.c
+++ b/diff.c
@@ -60,7 +60,7 @@ static int diff_suppress_blank_empty;
 static enum git_colorbool diff_use_color_default = GIT_COLOR_UNKNOWN;
 static int diff_color_moved_default;
 static int diff_color_moved_ws_default;
-static int diff_context_default = 3;
+static unsigned int diff_context_default = 3;
 static unsigned int diff_interhunk_context_default;
 static char *diff_word_regex_cfg;
 static struct external_diff external_diff_cfg;
@@ -382,9 +382,10 @@ int git_diff_ui_config(const char *var, const char *value,
 		return 0;
 	}
 	if (!strcmp(var, "diff.context")) {
-		diff_context_default = git_config_int(var, value, ctx->kvi);
-		if (diff_context_default < 0)
+		int val = git_config_int(var, value, ctx->kvi);
+		if (val < 0)
 			return -1;
+		diff_context_default = val;
 		return 0;
 	}
 	if (!strcmp(var, "diff.interhunkcontext")) {
@@ -5924,9 +5925,12 @@ static int diff_opt_unified(const struct option *opt,
 	BUG_ON_OPT_NEG(unset);
 
 	if (arg) {
-		options->context = strtol(arg, &s, 10);
+		long val = strtol(arg, &s, 10);
 		if (*s)
 			return error(_("%s expects a numerical value"), "--unified");
+		if (val < 0)
+			return error(_("%s expects a non-negative integer"), "--unified");
+		options->context = val;
 	}
 	enable_patch_output(&options->output_format);
 
diff --git a/diff.h b/diff.h
index 033d633db4..bb5cddaf34 100644
--- a/diff.h
+++ b/diff.h
@@ -294,7 +294,7 @@ struct diff_options {
 	enum git_colorbool use_color;
 
 	/* Number of context lines to generate in patch output. */
-	int context;
+	unsigned int context;
 
 	unsigned int interhunkcontext;
 
diff --git a/t/t4055-diff-context.sh b/t/t4055-diff-context.sh
index 1384a81957..b26f6eea7c 100755
--- a/t/t4055-diff-context.sh
+++ b/t/t4055-diff-context.sh
@@ -82,6 +82,11 @@ test_expect_success 'negative integer config parsing' '
 	test_grep "bad config variable" output
 '
 
+test_expect_success '-U-1 is rejected' '
+	test_must_fail git diff -U-1 2>err &&
+	test_grep "expects a non-negative integer" err
+'
+
 test_expect_success '-U0 is valid, so is diff.context=0' '
 	test_config diff.context 0 &&
 	git diff >output &&
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v2 1/4] diff: reject negative values for --inter-hunk-context
From: Michael Montalbo via GitGitGadget @ 2026-05-12 18:10 UTC (permalink / raw)
  To: git; +Cc: Michael Montalbo, Michael Montalbo
In-Reply-To: <pull.2105.v2.git.1778609423.gitgitgadget@gmail.com>

From: Michael Montalbo <mmontalbo@gmail.com>

Negative values for --inter-hunk-context produce structurally invalid
diff output with overlapping hunks:

    $ git log -1 -p -U3 --inter-hunk-context=-100 791aeddfa2 \
        -- git-compat-util.h | grep '^@@'
    @@ -110,6 +110,9 @@
    @@ -115,6 +118,9 @@
    @@ -116,6 +122,7 @@

Hunk 1 covers lines 110-115, hunk 2 starts at 115 (overlap), hunk 3
starts at 116 (overlaps both). The resulting patch cannot be applied.

The config variable diff.interHunkContext already rejects negative
values, but the command line option does not.

Change the type of diff_options.interhunkcontext and its static
default from int to unsigned int, and switch the option parser from
OPT_INTEGER_F to OPT_UNSIGNED. This rejects negative values at parse
time via git_parse_unsigned() and enforces the correct type at compile
time via BARF_UNLESS_UNSIGNED.

Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
---
 diff.c                             | 13 ++++++-------
 diff.h                             |  2 +-
 t/t4032-diff-inter-hunk-context.sh |  6 ++++++
 3 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/diff.c b/diff.c
index 397e38b41c..5df28e49c5 100644
--- a/diff.c
+++ b/diff.c
@@ -61,7 +61,7 @@ static enum git_colorbool diff_use_color_default = GIT_COLOR_UNKNOWN;
 static int diff_color_moved_default;
 static int diff_color_moved_ws_default;
 static int diff_context_default = 3;
-static int diff_interhunk_context_default;
+static unsigned int diff_interhunk_context_default;
 static char *diff_word_regex_cfg;
 static struct external_diff external_diff_cfg;
 static char *diff_order_file_cfg;
@@ -388,10 +388,10 @@ int git_diff_ui_config(const char *var, const char *value,
 		return 0;
 	}
 	if (!strcmp(var, "diff.interhunkcontext")) {
-		diff_interhunk_context_default = git_config_int(var, value,
-								ctx->kvi);
-		if (diff_interhunk_context_default < 0)
+		int val = git_config_int(var, value, ctx->kvi);
+		if (val < 0)
 			return -1;
+		diff_interhunk_context_default = val;
 		return 0;
 	}
 	if (!strcmp(var, "diff.renames")) {
@@ -6111,9 +6111,8 @@ struct option *add_diff_options(const struct option *opts,
 		OPT_CALLBACK_F(0, "default-prefix", options, NULL,
 			       N_("use default prefixes a/ and b/"),
 			       PARSE_OPT_NONEG | PARSE_OPT_NOARG, diff_opt_default_prefix),
-		OPT_INTEGER_F(0, "inter-hunk-context", &options->interhunkcontext,
-			      N_("show context between diff hunks up to the specified number of lines"),
-			      PARSE_OPT_NONEG),
+		OPT_UNSIGNED(0, "inter-hunk-context", &options->interhunkcontext,
+			     N_("show context between diff hunks up to the specified number of lines")),
 		OPT_CALLBACK_F(0, "output-indicator-new",
 			       &options->output_indicators[OUTPUT_INDICATOR_NEW],
 			       N_("<char>"),
diff --git a/diff.h b/diff.h
index 7eb84aadf4..033d633db4 100644
--- a/diff.h
+++ b/diff.h
@@ -296,7 +296,7 @@ struct diff_options {
 	/* Number of context lines to generate in patch output. */
 	int context;
 
-	int interhunkcontext;
+	unsigned int interhunkcontext;
 
 	/* Affects the way detection logic for complete rewrites, renames and
 	 * copies.
diff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh
index bada0cbd32..bec1676f8d 100755
--- a/t/t4032-diff-inter-hunk-context.sh
+++ b/t/t4032-diff-inter-hunk-context.sh
@@ -114,4 +114,10 @@ test_expect_success 'diff.interHunkContext invalid' '
 	test_must_fail git diff
 '
 
+test_expect_success '--inter-hunk-context rejects negative value' '
+	test_unconfig diff.interHunkContext &&
+	test_must_fail git diff --inter-hunk-context=-1 2>err &&
+	test_grep "expects a non-negative integer" err
+'
+
 test_done
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v2 0/4] diff: reject negative context values
From: Michael Montalbo via GitGitGadget @ 2026-05-12 18:10 UTC (permalink / raw)
  To: git; +Cc: Michael Montalbo
In-Reply-To: <pull.2105.git.1778022144.gitgitgadget@gmail.com>

Negative values for -U and --inter-hunk-context are silently accepted and
produce structurally invalid diff output.

Malformed hunk headers:

$ wc -l GIT-VERSION-GEN 106 $ git log -1 -p -U-500 -- GIT-VERSION-GEN | grep
'^@@' @@ -503,999- +503,999- @@

Line 503 of a 106-line file, count "999-" is not a valid integer.

Overlapping hunks that cannot be applied:

$ git log -1 -p -U3 --inter-hunk-context=100 791aeddfa2
-- git-compat-util.h | git apply --check --reverse (success)

$ git log -1 -p -U3 --inter-hunk-context=-100 791aeddfa2
-- git-compat-util.h | git apply --check --reverse error: patch failed:
git-compat-util.h:118 error: git-compat-util.h: patch does not apply

Both options were originally parsed via opt_arg() which gated on isdigit(),
making negative values impossible. When they were converted to OPT_INTEGER_F
/ OPT_CALLBACK in d473e2e0e8 (diff.c: convert -U|--unified, 2019-01-27) and
16ed6c97cc (diff-parseopt: convert --inter-hunk-context, 2019-03-24), the
implicit rejection was lost.

This series restores the original invariant with stronger guarantees:

1/4 diff: reject negative values for --inter-hunk-context Change type to
unsigned int, switch to OPT_UNSIGNED.

2/4 diff: reject negative values for -U/--unified Change type to unsigned
int, add range check in callback.

3/4 xdiff: guard against negative context lengths BUG() in xdl_get_hunk() as
defense in depth.

4/4 parse-options: clarify what "negated" means for PARSE_OPT_NONEG.

The config variables diff.context and diff.interHunkContext have always
rejected negative values. This series brings the CLI options in line.

Changes since v1:

Patch 1 and 4: Rewrote commit message to not imply NONEG was related to the
bug.

Patch 4: Trimmed to just clarify what "negated" means, without documenting
what PARSE_OPT_NONEG does not do.

Michael Montalbo (4):
  diff: reject negative values for --inter-hunk-context
  diff: reject negative values for -U/--unified
  xdiff: guard against negative context lengths
  parse-options: clarify what "negated" means for PARSE_OPT_NONEG

 diff.c                             | 25 ++++++++++++++-----------
 diff.h                             |  4 ++--
 parse-options.h                    |  1 +
 t/t4032-diff-inter-hunk-context.sh |  6 ++++++
 t/t4055-diff-context.sh            |  5 +++++
 xdiff/xemit.c                      | 16 ++++++++++++----
 6 files changed, 40 insertions(+), 17 deletions(-)


base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2105%2Fmmontalbo%2Fmm%2Freject-negative-interhunk-context-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2105/mmontalbo/mm/reject-negative-interhunk-context-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2105

Range-diff vs v1:

 1:  cca75eca0e ! 1:  f2ebb3a72b diff: reject negative values for --inter-hunk-context
     @@ Commit message
          starts at 116 (overlaps both). The resulting patch cannot be applied.
      
          The config variable diff.interHunkContext already rejects negative
     -    values, but the command line option does not. The option currently
     -    uses OPT_INTEGER_F with PARSE_OPT_NONEG, but PARSE_OPT_NONEG only
     -    prevents the "--no-inter-hunk-context" boolean negation form. It does
     -    not reject negative numeric arguments like "--inter-hunk-context=-1".
     +    values, but the command line option does not.
      
          Change the type of diff_options.interhunkcontext and its static
          default from int to unsigned int, and switch the option parser from
 2:  f0478d434c = 2:  fc3d2bc31e diff: reject negative values for -U/--unified
 3:  f9cfa0c55d = 3:  020ca774c0 xdiff: guard against negative context lengths
 4:  05ff821e6f ! 4:  3a656f8c0f parse-options: clarify PARSE_OPT_NONEG does not reject negative numbers
     @@ Metadata
      Author: Michael Montalbo <mmontalbo@gmail.com>
      
       ## Commit message ##
     -    parse-options: clarify PARSE_OPT_NONEG does not reject negative numbers
     +    parse-options: clarify what "negated" means for PARSE_OPT_NONEG
      
     -    The name "NONEG" can be misread as "no negative [values]" when it
     -    actually means "no [boolean] negation" (the --no-* form).
     -
     -    When --inter-hunk-context and -U/--unified were converted from a
     -    custom parser to OPT_INTEGER_F with PARSE_OPT_NONEG in d473e2e0e8
     -    and 16ed6c97cc, the implicit rejection of negative values (via
     -    isdigit() in the old opt_arg() parser) was silently lost. The
     -    previous commits in this series fix the resulting bugs.
     -
     -    Add a clarifying note to the flag documentation.
     +    The documentation says the flag prevents an option from being
     +    "negated" without specifying what that means. Add a parenthetical
     +    to clarify that it rejects the "--no-<option>" form.
      
          Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
      
       ## parse-options.h ##
      @@ parse-options.h: typedef int parse_opt_subcommand_fn(int argc, const char **argv,
     -  *   mask of parse_opt_option_flags.
        *   PARSE_OPT_OPTARG: says that the argument is optional (not for BOOLEANs)
        *   PARSE_OPT_NOARG: says that this option does not take an argument
     -- *   PARSE_OPT_NONEG: says that this option cannot be negated
     -+ *   PARSE_OPT_NONEG: says that this option cannot be negated (i.e.
     -+ *                   prevents --no-<option> boolean form). Does not reject
     -+ *                   negative numeric values like --option=-1. Use
     -+ *                   OPT_UNSIGNED for options that must be non-negative.
     +  *   PARSE_OPT_NONEG: says that this option cannot be negated
     ++ *                   (i.e. rejects "--no-<option>")
        *   PARSE_OPT_HIDDEN: this option is skipped in the default usage, and
        *                     shown only in the full usage.
        *   PARSE_OPT_LASTARG_DEFAULT: says that this option will take the default

-- 
gitgitgadget

^ permalink raw reply

* Re: [PATCH v3 6/7] remote: add remote.*.negotiationInclude config
From: Derrick Stolee @ 2026-05-12 17:55 UTC (permalink / raw)
  To: Matthew John Cheetham, Derrick Stolee via GitGitGadget, git; +Cc: gitster, ps
In-Reply-To: <VI0PR03MB11634F3D6B345482BD992FDC9C0392@VI0PR03MB11634.eurprd03.prod.outlook.com>

On 5/12/26 10:54 AM, Matthew John Cheetham wrote:
> On 2026-04-22 16:25, Derrick Stolee via GitGitGadget wrote:

> This patch is a mirror of patch 4 that added the remote config for
> negotiateRestrict. Some of the same comments apply here too:
> 
> - reusing `parse_transport_option()` vs inline resetting the list
> 
> - values could be commit SHAs as well as refs/globs

Will do. Thanks.

>> diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
>> index f1d889d03e..44de6d3c1f 100644
>> --- a/Documentation/config/remote.adoc
>> +++ b/Documentation/config/remote.adoc
>> @@ -126,6 +126,33 @@ values are not used.
>>   Blank values signal to ignore all previous values, allowing a reset of
>>   the list from broader config scenarios.
>> +remote.<name>.negotiationInclude::
>> +    When negotiating with this remote during `git fetch` and `git push`,
>> +    the client advertises a list of commits that exist locally.  In
>> +    repos with many references, this list of "haves" can be truncated.
>> +    Depending on data shape, dropping certain references may be
>> +    expensive.  This multi-valued config option specifies ref patterns
>> +    whose tips should always be sent as "have" commits during fetch
>> +    negotiation with this remote.
>> ++
>> +Each value is either an exact ref name (e.g. `refs/heads/release`) or a
>> +glob pattern (e.g. `refs/heads/release/*`).  The pattern syntax is the same
>> +as for `--negotiation-restrict`.
> 
> Should this say "..same as for `--negotiation-include`"?
> 
> This way each `remote.<name>.negotiationX` doc cross-references the
> corresponding `--negotiation-X` command line option.

Good find. The rest of the description uses *-include.

>> diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
>> index 4316f8d4ea..db73ed5379 100755
>> --- a/t/t5510-fetch.sh
>> +++ b/t/t5510-fetch.sh
>> @@ -1577,6 +1577,55 @@ test_expect_success '--negotiation-include avoids 
>> duplicates with negotiator' '
>>       test_line_count = 1 matches
>>   '
>> +test_expect_success 'remote.<name>.negotiationInclude used as default for -- 
>> negotiation-include' '
>> +    test_when_finished rm -f trace &&
>> +    setup_negotiation_tip server server 0 &&
>> +
>> +    # test the reset of the list on an empty value
>> +    git -C client config --add remote.origin.negotiationInclude refs/tags/ 
>> alpha_1 &&
>> +    git -C client config --add remote.origin.negotiationInclude "" &&
>> +    git -C client config --add remote.origin.negotiationInclude refs/tags/ 
>> beta_1 &&
>> +    GIT_TRACE_PACKET="$(pwd)/trace" git -C client fetch \
>> +        --negotiation-restrict=alpha_1 \
>> +        origin alpha_s beta_s &&
>> +
>> +    ALPHA_1=$(git -C client rev-parse alpha_1) &&
>> +    test_grep "fetch> have $ALPHA_1" trace &&
>> +    BETA_1=$(git -C client rev-parse beta_1) &&
>> +    test_grep "fetch> have $BETA_1" trace
>> +'
> 
> This test sets up the include list as [alpha_1, "", beta_1] which after
> the reset should become [beta_1], but the assertions in the test only
> check that alpha_1 (sent via the --negotiation-restrict option) and
> beta_1 (sent via the include) appear. If the reset of the list didn't
> work then the test still passes because alpha_1 is sent via the CLI
> option.

Good point. the negotiation-restrict option is making this less clear.
If I point the restrict option at alpha_2, then I think it exercises
things correctly.

Thanks,
-Stolee


^ permalink raw reply

* [PATCH v8 5/5] branch: add --all-remotes flag
From: Harald Nordgren via GitGitGadget @ 2026-05-12 17:07 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2285.v8.git.git.1778605658.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             | 44 +++++++++++++++++++++++++++++++++++
 3 files changed, 80 insertions(+), 14 deletions(-)

diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index 080cdc218a..bf59f4852d 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
 -----------
@@ -232,6 +232,11 @@ currently checked-out branch in any worktree is always preserved,
 as is any branch with `branch.<name>.pruneMerged` set to `false`,
 and the local branch that mirrors _<remote>_'s default branch.
 
+`--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 2969780210..081a1a1467 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -687,6 +687,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)
@@ -776,7 +783,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)
 {
@@ -789,6 +796,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);
@@ -802,15 +811,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);
 
@@ -818,8 +827,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;
@@ -828,10 +837,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;
@@ -944,6 +954,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 */
@@ -1001,6 +1012,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")),
@@ -1044,6 +1058,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)
@@ -1097,10 +1114,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 8e877862f5..e93e93654e 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 &&
@@ -1908,4 +1929,27 @@ 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 pm-other" &&
+	git clone pm-upstream pm-allremotes &&
+	test_create_repo pm-other &&
+	test_commit -C pm-other other-base &&
+	git -C pm-other checkout -b stable &&
+	test_commit -C pm-other foreign-commit &&
+	git -C pm-other branch foreign HEAD &&
+	git -C pm-other checkout main &&
+
+	git -C pm-allremotes remote add other ../pm-other &&
+	git -C pm-allremotes fetch other &&
+	git -C pm-allremotes branch one one-commit &&
+	git -C pm-allremotes branch --set-upstream-to=origin/next one &&
+	git -C pm-allremotes branch foreign other/foreign &&
+	git -C pm-allremotes branch --set-upstream-to=other/stable foreign &&
+
+	git -C pm-allremotes branch --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 v8 4/5] branch: add branch.<name>.pruneMerged opt-out
From: Harald Nordgren via GitGitGadget @ 2026-05-12 17:07 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2285.v8.git.git.1778605658.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

Setting branch.<name>.pruneMerged=false exempts that branch from
--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    |  3 ++-
 builtin/branch.c                 | 23 ++++++++++++++++--
 t/t3200-branch.sh                | 41 ++++++++++++++++++++++++++++++++
 4 files changed, 71 insertions(+), 3 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 c3f5150f03..080cdc218a 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -229,7 +229,8 @@ A branch whose upstream no longer resolves locally is left alone
 integrated). With `--force` (or `-f`), the reachability check is
 skipped and every branch in the candidate set is deleted. The
 currently checked-out branch in any worktree is always preserved,
-as is the local branch that mirrors _<remote>_'s default branch.
+as is any branch with `branch.<name>.pruneMerged` set to `false`,
+and the local branch that mirrors _<remote>_'s default branch.
 
 `-v`::
 `-vv`::
diff --git a/builtin/branch.c b/builtin/branch.c
index 50bf9774a8..2969780210 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -836,12 +836,15 @@ 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 *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,13 +853,29 @@ static int prune_merged_branches(int argc, const char **argv, int force,
 		upstream = branch ? branch_get_upstream(branch, NULL) : NULL;
 		if (!upstream ||
 		    !refs_ref_exists(get_main_ref_store(the_repository),
-				     upstream))
+				     upstream)) {
+			strbuf_release(&key);
 			continue;
+		}
 		if (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;
+			}
+		}
+
+		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 ca071338d3..8e877862f5 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -1867,4 +1867,45 @@ test_expect_success '--prune-merged protects only the default branch by name, no
 	test_must_fail git -C pm-default-alias rev-parse --verify refs/heads/trunk
 '
 
+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 one-commit &&
+	git -C pm-optout branch --set-upstream-to=origin/next one &&
+	git -C pm-optout branch two two-commit &&
+	git -C pm-optout branch --set-upstream-to=origin/next two &&
+	git -C pm-optout config branch.one.pruneMerged false &&
+
+	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 wip origin/wip &&
+	git -C pm-optout-force branch --set-upstream-to=origin/next wip &&
+	test_commit -C pm-optout-force local-only &&
+	git -C pm-optout-force checkout - &&
+	git -C pm-optout-force config branch.wip.pruneMerged false &&
+
+	git -C pm-optout-force branch --force --prune-merged origin &&
+
+	git -C pm-optout-force rev-parse --verify refs/heads/wip
+'
+
+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 one-commit &&
+	git -C pm-optout-d branch --set-upstream-to=origin/next 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 v8 3/5] branch: add --prune-merged <remote>
From: Harald Nordgren via GitGitGadget @ 2026-05-12 17:07 UTC (permalink / raw)
  To: git; +Cc: Kristoffer Haugsbakk, Johannes Sixt, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2285.v8.git.git.1778605658.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

Delete the local branches that --forked <remote> would list, but
only those whose tip is reachable from their configured upstream
remote-tracking branch (branch.<name>.merge): the work has already
landed on the upstream it tracks, so the local copy is no longer
needed.

A branch whose upstream no longer resolves locally is left alone --
its disappearance is not, on its own, evidence that the work was
integrated. With --force, skip the reachability check and delete
every branch in the candidate set. The currently checked-out
branch in any worktree is always preserved, as is the local branch
that mirrors <remote>'s default branch.

Reachability is read from whatever the remote-tracking refs say
locally, so the natural workflow is

	git fetch <remote>
	git branch --prune-merged <remote>

with no implicit cleanup driven by fetch itself.

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

diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index 5773104cd3..c3f5150f03 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,25 @@ 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 those whose tip is
+	reachable from their configured upstream remote-tracking
+	branch (`branch.<name>.merge`). In other words: the work on
+	the branch has already landed on the upstream it tracks, so
+	the local copy is no longer needed.
++
+Run `git fetch` first so the upstream remote-tracking branches
+reflect the current state of _<remote>_; reachability is checked
+against whatever the remote-tracking refs say locally.
++
+A branch whose upstream no longer resolves locally is left alone
+(its disappearance is not, on its own, evidence that the work was
+integrated). With `--force` (or `-f`), the reachability check is
+skipped and every branch in the candidate set is deleted. The
+currently checked-out branch in any worktree is always preserved,
+as is the local branch that mirrors _<remote>_'s default branch.
+
 `-v`::
 `-vv`::
 `--verbose`::
diff --git a/builtin/branch.c b/builtin/branch.c
index 1941f8a9ad..50bf9774a8 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"
@@ -171,8 +172,8 @@ static int branch_merged(int kind, const char *name,
 	 * any of the following code, but during the transition period,
 	 * a gentle reminder is in order.
 	 */
-	if (head_rev != reference_rev) {
-		int expect = head_rev ? repo_in_merge_bases(the_repository, rev, head_rev) : 0;
+	if (head_rev && head_rev != reference_rev) {
+		int expect = repo_in_merge_bases(the_repository, rev, head_rev);
 		if (expect < 0)
 			exit(128);
 		if (expect == merged)
@@ -227,7 +228,9 @@ static void delete_branch_config(const char *branchname)
 	strbuf_release(&buf);
 }
 
-static int delete_branches(int argc, const char **argv, int force, int kinds,
+static int delete_branches(int argc, const char **argv,
+			   int no_head_fallback,
+			   int force, int kinds,
 			   int quiet, int warn_only, int *n_not_merged)
 {
 	struct commit *head_rev = NULL;
@@ -262,7 +265,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
 	}
 	branch_name_pos = strcspn(fmt, "%");
 
-	if (!force)
+	if (!force && !no_head_fallback)
 		head_rev = lookup_commit_reference(the_repository, &head_oid);
 
 	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
@@ -317,8 +320,8 @@ 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, warn_only, n_not_merged)) {
+		    check_branch_commit(bname.buf, name, &oid, head_rev,
+					kinds, force, warn_only, n_not_merged)) {
 			if (!warn_only)
 				ret = 1;
 			goto next;
@@ -753,36 +756,132 @@ 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 *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 ||
+		    !refs_ref_exists(get_main_ref_store(the_repository),
+				     upstream))
+			continue;
+		if (string_list_has_string(&protected_default_refs, upstream)) {
+			const char *leaf = strrchr(upstream, '/');
+			if (leaf && !strcmp(leaf + 1, short_name))
+				continue;
+		}
+
+		strvec_push(&deletable, short_name);
+	}
+
+	if (deletable.nr)
+		ret = delete_branches(deletable.nr, deletable.v,
+				      1, 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 +924,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 +980,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 +1026,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 +1036,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);
 
@@ -971,12 +1074,15 @@ int cmd_branch(int argc,
 	if (delete) {
 		if (!argc)
 			die(_("branch name required"));
-		ret = delete_branches(argc, argv, delete > 1, filter.kind,
+		ret = delete_branches(argc, argv, 0, delete > 1, filter.kind,
 				      quiet, 0, NULL);
 		goto out;
 	} 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..ca071338d3 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -1771,4 +1771,100 @@ 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 checkout -b next &&
+	test_commit -C pm-upstream one-commit &&
+	test_commit -C pm-upstream two-commit &&
+	git -C pm-upstream branch one HEAD~ &&
+	git -C pm-upstream branch two HEAD &&
+	git -C pm-upstream branch wip main &&
+	git -C pm-upstream checkout main
+'
+
+test_expect_success '--prune-merged deletes branches integrated into upstream' '
+	test_when_finished "rm -rf pm-merged" &&
+	git clone pm-upstream pm-merged &&
+	git -C pm-merged branch one one-commit &&
+	git -C pm-merged branch --set-upstream-to=origin/next one &&
+	git -C pm-merged branch two two-commit &&
+	git -C pm-merged branch --set-upstream-to=origin/next two &&
+
+	git -C pm-merged branch --prune-merged origin &&
+
+	test_must_fail git -C pm-merged rev-parse --verify refs/heads/one &&
+	test_must_fail git -C pm-merged rev-parse --verify refs/heads/two
+'
+
+test_expect_success '--prune-merged spares branches with un-integrated commits' '
+	test_when_finished "rm -rf pm-unmerged" &&
+	git clone pm-upstream pm-unmerged &&
+	git -C pm-unmerged checkout -b wip origin/wip &&
+	git -C pm-unmerged branch --set-upstream-to=origin/next wip &&
+	test_commit -C pm-unmerged local-only &&
+	git -C pm-unmerged checkout - &&
+
+	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/wip
+'
+
+test_expect_success '--prune-merged --force deletes branches regardless of reachability' '
+	test_when_finished "rm -rf pm-force" &&
+	git clone pm-upstream pm-force &&
+	git -C pm-force checkout -b wip origin/wip &&
+	git -C pm-force branch --set-upstream-to=origin/next wip &&
+	test_commit -C pm-force local-only &&
+	git -C pm-force checkout - &&
+
+	git -C pm-force branch --force --prune-merged origin &&
+
+	test_must_fail git -C pm-force rev-parse --verify refs/heads/wip
+'
+
+test_expect_success '--prune-merged skips branches whose upstream is gone' '
+	test_when_finished "rm -rf pm-upstream-gone" &&
+	git clone pm-upstream pm-upstream-gone &&
+	git -C pm-upstream-gone branch one one-commit &&
+	git -C pm-upstream-gone branch --set-upstream-to=origin/next one &&
+
+	git -C pm-upstream-gone update-ref -d refs/remotes/origin/next &&
+	git -C pm-upstream-gone branch --prune-merged origin &&
+
+	git -C pm-upstream-gone 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 one-commit &&
+	git -C pm-head branch --set-upstream-to=origin/next 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 spares the local default branch' '
+	test_when_finished "rm -rf pm-default" &&
+	git clone pm-upstream pm-default &&
+	git -C pm-default checkout --detach &&
+	git -C pm-default branch --force --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 branch --track trunk origin/main &&
+	git -C pm-default-alias checkout --detach &&
+	git -C pm-default-alias branch --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_done
-- 
gitgitgadget


^ permalink raw reply related


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