All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>,
	Johannes Sixt <j6t@kdbg.org>,
	Phillip Wood <phillip.wood123@gmail.com>,
	Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v19 0/7] branch: delete-merged
Date: Tue, 14 Jul 2026 18:24:46 +0000	[thread overview]
Message-ID: <pull.2285.v19.git.git.1784053493.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2285.v18.git.git.1782338106.gitgitgadget@gmail.com>

Delete branches that have already been merged on upstream.

Changes in v19:

 * Fix bug where dry-run would still remove config, added test coverage.
 * Redesigned --delete-merged as a repeatable upstream selector with
   optional positional patterns limiting deletion scope.
 * Protect same-name upstream branches independently of push-default
   configuration.
 * Simplified flags handling where local caching became complicated when
   mutating values.
 * Clarified assertions in tests.

Changes in v18:

 * Instead of keeping the whole chain of upstream branches, keep only the
   ones an unmerged branch still needs. When a kept (merged) branch in turn
   tracks a branch that is being deleted, clear its now-stale upstream
   config.
 * Rework spare_stacked_bases() to record the kept bases and, in a second
   pass, clear the upstream of any whose own base is going away. Build the
   to-delete list with strset_for_each_entry() instead of re-walking the
   candidate array.

Changes in v17:

 * Keep a merged branch when another surviving branch still tracks it as its
   upstream, so --delete-merged no longer deletes a branch out from under
   one stacked on top of it.
 * Move the --dry-run and branch.<name>.deleteMerged opt-out fully into
   their own commits.

Changes in v16:

 * Convert delete_merged_branches() to take an unsigned int flags argument
   instead of separate quiet/dry_run booleans, matching delete_branches()
 * Reuse the strbuf across the skip-config loop (strbuf_reset per iteration,
   single strbuf_release after) instead of allocating and freeing it each
   time
 * Rewrite the --delete-merged tests as integration tests: branches that
   land commits upstream, with deletion and the checked-out, upstream-gone,
   and push-equals-upstream safety cases exercised together in one run and
   output asserted via test_cmp
 * Collapse the many per-aspect test repos into a single reused repo set up
   by a setup_repo_for_delete_merged helper, and rename helpers off the old
   pm_/prune naming
 * Nest single-repo setup sequences in ( cd ... ) subshells instead of
   prefixing every command with -C

Changes in v15:

 * Renamed --prune-merged to --delete-merged throughout. Not necessarily
   final, but something to advance the discussion.
 * --delete-merged now silently skips not-yet-merged branches instead of
   warning.
 * Initialized the delete_branches() flag locals where declared. Only force
   stays deferred.
 * delete_branches()/check_branch_commit() doc and code cleanups: redundant
   branch NULL checks dropped, ref_array candidates = { 0 }, a BUG() for the
   unreachable non-branch ref, and reworked --delete-merged doc wording.
 * Broadened the --forked tests (local commits for realism, remote add -f,
   --forked coverage), renamed the misleading trunk fixture, and replaced
   the misnamed detached branch with git checkout --detach.

Changes in v14:

 * Fixed a git branch -d -r regression (broke t5404/t5505/t5514): the
   remotes path set a local force but not the DELETE_BRANCH_FORCE bit that
   check_branch_commit() reads, so it wrongly ran the merge check.
 * Made flags the single source of truth in delete_branches() so the bit and
   the derived locals can't disagree.
 * Works locally, but GitHub CI has problems that are there for other
   branches too, hopefully not related
   (https://github.com/git/git/pull/2285).

Changes in v13:

 * Reworked --forked into a real ref-filter applied in apply_ref_filter()
   instead of a post-pass, so non-matching branches are never allocated.
 * Match exact --forked patterns on full refnames (only globs use the
   abbreviated upstream), and dropped the old helper machinery, forward
   declaration, and string_list in favor of a strvec.
 * Replaced the boolean parameters of
   delete_branches()/check_branch_commit() with a single unsigned int flags.
 * --prune-merged now collects candidates via filter_refs() rather than its
   own branch walk.
 * --prune-merged now takes its patterns as positional arguments (e.g. git
   branch --prune-merged origin/main 'feature*') instead of repeating the
   option.

Changes in v12:

 * Reworked --forked from a standalone action into a --list-mode filter.
 * Switched --forked and --prune-merged to repeatable OPT_STRING_LIST
   options.
 * Dropped the bare-remote-name resolution for --forked, the argument is now
   a ref or a glob.

Changes in v11:

 * The flags now take a branch, not a remote. --forked and --prune-merged
   accept a literal upstream short name like origin/main or a wildmatch
   pattern like origin/. The old --all-remotes flag is gone, since origin/
   covers that case.
 * The prune guard now compares @{push} against @{upstream}. A branch is
   spared when these are equal. That is the trunk like case, such as local
   main tracking and pushing to origin/main, where "fully merged to
   upstream" cannot be told apart from "just pulled". Only branches that
   push somewhere other than their upstream, typically fork based topics,
   are candidates. The earlier /HEAD by name guard that the reviewer
   rejected is gone.
 * New --dry-run for --prune-merged.

Changes in v10:

 * --forked / --prune-merged now take a branch glob instead of a remote name
   — origin, origin/*, origin/release-- all work. This replaces the
   remote-only form and subsumes the old --all-remotes flag, which has been
   dropped.
 * New --dry-run for --prune-merged.

Changes in v9:

 * --force no longer has special meaning with --prune-merged; reachability
   is always enforced. Use git branch -D to delete an unmerged branch.
   Matches how git branch's other read/safe actions treat --force.
 * Synopsis drops [-f]; "not fully merged" hint points at git branch -D.
 * Dropped the --prune-merged --force tests.

Changes in v8:

 * Delete only when the branch's work is actually reachable from its
   upstream
 * Skip branches whose upstream is gone (even with --force)
 * Simplified the internal safety flag to live in one place

Changes in v7:

 * --prune-merged now checks if a branch is merged into its own upstream
   first. If the upstream is gone, it checks against the remote's default
   branch instead. If neither exists, the branch is refused (use --force to
   delete anyway).

Changes in v6:

 * --prune-merged now measures merged-ness against the remote's default
   branch instead of the candidate's upstream — so the decision no longer
   depends on which branch happens to be checked out locally.
 * delete_branches() / check_branch_commit() gained a per-candidate override
   that lets a caller substitute a different "what counts as merged"
   reference (or skip the check). branch -d callers pass NULL and keep their
   existing semantics.
 * prune_merged_branches() resolves each candidate's push-remote HEAD and
   threads it through, so --prune-merged --all-remotes measures each
   candidate against its own remote rather than a single global reference.

Changes in v5:

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

Changes in v4:

 * Resolve each remote's HEAD and collect the targets into a
   protected_default_refs set in collect_forked_set.
 * In prune_merged_branches, skip a candidate when its upstream is a
   protected default ref and the local branch name matches the default
   branch's leaf name (so a local main tracking origin/main is spared, but a
   renamed trunk tracking origin/main is not).
 * Also skip when the candidate's push ref points at a protected default
   ref, so a topic branch configured to push to origin/main is never pruned.
 * Tests: spare the local default branch; only protect by matching leaf name
   (not by upstream alone); spare a branch whose push ref is the remote
   default.

Changes in v3:

 * s/remote-tracking refs/remote-tracking branches/g

Changes in v2:

 * The whole feature moved out of git fetch and into git branch. git fetch
   --prune-merged now just calls git branch --prune-merged after fetching.
 * The fetch.pruneLocalBranches and remote..pruneLocalBranches config
   options are gone, replaced by per-branch opt-out via branch..pruneMerged.
 * New git branch --forked lists local branches whose upstream lives on the
   given remote (read-only building block).
 * New git branch --prune-merged deletes those branches, but only if their
   tip is reachable from the upstream tracking ref; --force skips that
   safety check.
 * New git branch --all-remotes lets --forked/--prune-merged operate across
   every configured remote at once.
 * The currently checked-out branch in any worktree is always preserved.
 * branch..pruneMerged=false lets you exempt a branch (e.g. a long-running
   topic branch) even with --force; doesn't affect explicit git branch -d.
 * delete_branches() got a warn_only mode so bulk deletion prints a one-line
   warning per skipped branch instead of the noisy four-line hint that git
   branch -d shows.
 * New section in git-branch docs; git-fetch docs trimmed to just mention
   --prune-merged.
 * New tests in t3200-branch.sh for the new branch flags; t5510-fetch.sh
   shrunk since most logic moved.

Harald Nordgren (7):
  branch: add --forked filter for --list mode
  branch: convert delete_branches() to a flags argument
  branch: let delete_branches skip unmerged branches on bulk refusal
  branch: prepare delete_branches for a bulk caller
  branch: add --delete-merged <branch>
  branch: add branch.<name>.deleteMerged opt-out
  branch: add --dry-run for --delete-merged

 Documentation/config/branch.adoc |   7 +
 Documentation/git-branch.adoc    |  50 ++++-
 builtin/branch.c                 | 284 ++++++++++++++++++++++---
 ref-filter.c                     |  70 ++++++
 ref-filter.h                     |  10 +
 t/t3200-branch.sh                | 352 +++++++++++++++++++++++++++++++
 6 files changed, 742 insertions(+), 31 deletions(-)


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

Range-diff vs v18:

 1:  3e29ff17bd ! 1:  562648132d branch: add --forked filter for --list mode
     @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
      +	(
      +		cd forked &&
      +		git remote add -f other ../forked-other &&
     -+		git remote set-head origin one &&
      +		git branch local-base &&
      +		git branch --track local-one origin/one &&
      +		git branch --track local-two origin/two &&
     @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
      +		git checkout local-one &&
      +		test_commit --no-tag local-one-work local-one.t &&
      +		git checkout local-foreign &&
     -+		test_commit --no-tag local-foreign-work local-foreign.t &&
     -+		git checkout --detach
     ++		test_commit --no-tag local-foreign-work local-foreign.t
      +	)
      +'
      +
     @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
      +'
      +
      +test_expect_success '--forked composes with --no-merged' '
     -+	test_when_finished "git -C forked checkout --detach" &&
     -+	git -C forked checkout local-one &&
     -+	test_commit -C forked local-only &&
      +	git -C forked branch --forked "origin/*" --no-merged origin/one \
      +		--format="%(refname:short)" >actual &&
      +	echo local-one >expect &&
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success '--forked rejects unknown branch/pattern' '
     -+	test_must_fail git -C forked branch --forked nope 2>err &&
     -+	test_grep "not a valid branch or pattern" err
     -+'
     -+
     -+test_expect_success '--forked requires a value' '
     -+	test_must_fail git -C forked branch --forked 2>err &&
     -+	test_grep "requires a value" err
     -+'
     -+
      +test_expect_success '--forked <remote> uses the branch <remote>/HEAD points at' '
      +	git -C forked branch --forked origin --format="%(refname:short)" >actual &&
     -+	echo local-one >expect &&
     ++	echo main >expect &&
      +	test_cmp expect actual
      +'
      +
     @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
      +	EOF
      +	test_cmp expect actual
      +'
     ++
     ++test_expect_success '--forked rejects unknown branch/pattern' '
     ++	test_must_fail git -C forked branch --forked nope 2>err &&
     ++	test_grep "not a valid branch or pattern" err
     ++'
     ++
     ++test_expect_success '--forked requires a value' '
     ++	test_must_fail git -C forked branch --forked 2>err &&
     ++	test_grep "requires a value" err
     ++'
      +
       test_done
 2:  cdd4fea4a7 ! 2:  c7ebd9344c branch: convert delete_branches() to a flags argument
     @@ Metadata
       ## Commit message ##
          branch: convert delete_branches() to a flags argument
      
     -    delete_branches() and check_branch_commit() take a pair of int
     -    booleans (force and quiet) that the next commits would grow further.
     -    Replace them with a single "unsigned int flags" argument and an
     -    enum, splitting the bits back into named bool locals so the body
     -    keeps reading the same named values.
     +    delete_branches() takes separate force and quiet parameters, while
     +    check_branch_commit() takes force. The next commits would grow this
     +    collection further. Replace them with a single unsigned flags argument
     +    and an enum.
     +
     +    Test the FORCE and QUIET bits directly from flags at each use site so
     +    that mutating or forwarding flags cannot leave cached values stale.
      
          No change in behavior.
      
     @@ builtin/branch.c: static int branch_merged(int kind, const char *name,
      -			       int kinds, int force)
      +			       int kinds, unsigned int flags)
       {
     -+	bool force = flags & DELETE_BRANCH_FORCE;
       	struct commit *rev = lookup_commit_reference(the_repository, oid);
     - 	if (!force && !rev) {
     +-	if (!force && !rev) {
     ++	if (!(flags & DELETE_BRANCH_FORCE) && !rev) {
       		error(_("couldn't look up commit object for '%s'"), refname);
     + 		return -1;
     + 	}
     +-	if (!force && !branch_merged(kinds, branchname, rev, head_rev)) {
     ++	if (!(flags & DELETE_BRANCH_FORCE) &&
     ++	    !branch_merged(kinds, branchname, rev, head_rev)) {
     + 		error(_("the branch '%s' is not fully merged"), branchname);
     + 		advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
     + 				  _("If you are sure you want to delete it, "
      @@ builtin/branch.c: static void delete_branch_config(const char *branchname)
       	strbuf_release(&buf);
       }
     @@ builtin/branch.c: static void delete_branch_config(const char *branchname)
       {
       	struct commit *head_rev = NULL;
       	struct object_id oid;
     -@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int force, int kinds,
     - 	int i;
     - 	int ret = 0;
     - 	int remote_branch = 0;
     -+	bool force;
     -+	bool quiet = flags & DELETE_BRANCH_QUIET;
     - 	struct strbuf bname = STRBUF_INIT;
     - 	enum interpret_branch_kind allowed_interpret;
     - 	struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
      @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int force, int kinds,
       		remote_branch = 1;
       		allowed_interpret = INTERPRET_BRANCH_REMOTE;
     @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
       	}
       	branch_name_pos = strcspn(fmt, "%");
       
     -+	force = flags & DELETE_BRANCH_FORCE;
     -+
     - 	if (!force)
     +-	if (!force)
     ++	if (!(flags & DELETE_BRANCH_FORCE))
       		head_rev = lookup_commit_reference(the_repository, &head_oid);
       
       	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
     @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
       				    : repo_find_unique_abbrev(the_repository, &oid, DEFAULT_ABBREV));
       
       	next:
     +@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int force, int kinds,
     + 		char *name = item->string;
     + 		if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
     + 			char *refname = name + branch_name_pos;
     +-			if (!quiet)
     ++			if (!(flags & DELETE_BRANCH_QUIET))
     + 				printf(remote_branch
     + 					? _("Deleted remote-tracking branch %s (was %s).\n")
     + 					: _("Deleted branch %s (was %s).\n"),
      @@ builtin/branch.c: int cmd_branch(int argc,
       	if (delete) {
       		if (!argc)
 3:  a0fd5b4a6c ! 3:  0c4f3358e3 branch: let delete_branches skip unmerged branches on bulk refusal
     @@ builtin/branch.c: static int branch_merged(int kind, const char *name,
       
       static int check_branch_commit(const char *branchname, const char *refname,
      @@ builtin/branch.c: static int check_branch_commit(const char *branchname, const char *refname,
     - 			       int kinds, unsigned int flags)
     - {
     - 	bool force = flags & DELETE_BRANCH_FORCE;
     -+	bool skip_unmerged = flags & DELETE_BRANCH_SKIP_UNMERGED;
     - 	struct commit *rev = lookup_commit_reference(the_repository, oid);
     - 	if (!force && !rev) {
     - 		error(_("couldn't look up commit object for '%s'"), refname);
     - 		return -1;
       	}
     - 	if (!force && !branch_merged(kinds, branchname, rev, head_rev)) {
     + 	if (!(flags & DELETE_BRANCH_FORCE) &&
     + 	    !branch_merged(kinds, branchname, rev, head_rev)) {
      -		error(_("the branch '%s' is not fully merged"), branchname);
      -		advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
      -				  _("If you are sure you want to delete it, "
      -				  "run 'git branch -D %s'"), branchname);
     -+		if (!skip_unmerged) {
     ++		if (!(flags & DELETE_BRANCH_SKIP_UNMERGED)) {
      +			error(_("the branch '%s' is not fully merged"),
      +			      branchname);
      +			advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
     @@ builtin/branch.c: static int check_branch_commit(const char *branchname, const c
       		return -1;
       	}
       	return 0;
     -@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int kinds,
     - 	int remote_branch = 0;
     - 	bool force;
     - 	bool quiet = flags & DELETE_BRANCH_QUIET;
     -+	bool skip_unmerged = flags & DELETE_BRANCH_SKIP_UNMERGED;
     - 	struct strbuf bname = STRBUF_INIT;
     - 	enum interpret_branch_kind allowed_interpret;
     - 	struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
      @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int kinds,
       		if (!(ref_flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
       		    check_branch_commit(bname.buf, name, &oid, head_rev, kinds,
       					flags)) {
      -			ret = 1;
     -+			if (!skip_unmerged)
     ++			if (!(flags & DELETE_BRANCH_SKIP_UNMERGED))
      +				ret = 1;
       			goto next;
       		}
 4:  a56d8fe93e ! 4:  64a202526a branch: prepare delete_branches for a bulk caller
     @@ Metadata
       ## Commit message ##
          branch: prepare delete_branches for a bulk caller
      
     -    Teach delete_branches() two new modes for the upcoming
     -    --delete-merged: one that asks only whether a branch is merged into
     -    its upstream, without falling back to HEAD when there is no
     -    upstream, and one that rehearses the deletions without removing any
     -    ref. Existing callers keep their current behavior.
     +    Teach delete_branches() a new mode for the upcoming --delete-merged
     +    caller that checks whether a branch is merged into its upstream without
     +    falling back to HEAD when there is no upstream. Existing callers keep
     +    their current behavior.
      
          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
      
     @@ builtin/branch.c: enum delete_branch_flags {
       
       static int check_branch_commit(const char *branchname, const char *refname,
      @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int kinds,
     - 	bool force;
     - 	bool quiet = flags & DELETE_BRANCH_QUIET;
     - 	bool skip_unmerged = flags & DELETE_BRANCH_SKIP_UNMERGED;
     -+	bool no_head_fallback = flags & DELETE_BRANCH_NO_HEAD_FALLBACK;
     - 	struct strbuf bname = STRBUF_INIT;
     - 	enum interpret_branch_kind allowed_interpret;
     - 	struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
     -@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int kinds,
     - 
     - 	force = flags & DELETE_BRANCH_FORCE;
     + 	}
     + 	branch_name_pos = strcspn(fmt, "%");
       
     --	if (!force)
     -+	if (!force && !no_head_fallback)
     +-	if (!(flags & DELETE_BRANCH_FORCE))
     ++	if (!(flags & DELETE_BRANCH_FORCE) &&
     ++	    !(flags & DELETE_BRANCH_NO_HEAD_FALLBACK))
       		head_rev = lookup_commit_reference(the_repository, &head_oid);
       
       	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
 5:  a84c555d99 ! 5:  a6caa5b397 branch: add --delete-merged <branch>
     @@ Metadata
       ## Commit message ##
          branch: add --delete-merged <branch>
      
     -            git branch --delete-merged <branch>...
     +        git branch (--delete-merged <branch>)... [<pattern>...]
      
     -    deletes the local branches that "--forked <branch>" would list,
     -    keeping only those whose tip is reachable from their configured
     -    upstream. The work has already landed on the upstream they track,
     -    so the local copy is no longer needed.
     +    deletes local branches matching the optional patterns when their
     +    configured upstream matches one of the --delete-merged arguments and
     +    their tip is reachable from that upstream. The work has already landed
     +    on the upstream they track, so the local copy is no longer needed.
     +
     +    The option can be repeated to widen the upstream match. Keeping the
     +    candidate patterns as positional arguments lets users bound the set of
     +    local branches that may be deleted independently of the upstream
     +    selection.
      
          A branch is not deleted when:
      
            * it is checked out in any worktree
     -      * its upstream remote-tracking branch no longer exists, since a
     -        missing upstream is not by itself a sign of integration
     -      * its push destination equals its upstream (<branch>@{push} is
     -        the same as <branch>@{upstream}), such as a local "main" that
     -        tracks and pushes to "origin/main". Right after a pull it just
     -        looks "fully merged", so it is kept. Only branches that push
     -        somewhere other than their upstream, typically topics in a fork
     -        workflow, are candidates.
     +      * its configured upstream ref no longer exists, since a missing
     +        upstream is not by itself a sign of integration
     +      * pushing it by name to the remote configured by
     +        branch.<name>.remote would update its upstream, as determined by
     +        mapping the branch ref through that remote's fetch refspec. For
     +        example, a local "main" that tracks "origin/main" is kept even when
     +        remote.pushDefault names a fork. Right after a pull it merely looks
     +        fully merged.
      
          A branch whose work is not yet merged into its upstream is silently
          skipped, so one unmerged topic does not abort the whole sweep.
     @@ Documentation/git-branch.adoc: git branch (-m|-M) [<old-branch>] <new-branch>
       git branch (-c|-C) [<old-branch>] <new-branch>
       git branch (-d|-D) [-r] <branch-name>...
       git branch --edit-description [<branch-name>]
     -+git branch --delete-merged <branch>...
     ++git branch (--delete-merged <branch>)... [<pattern>...]
       
       DESCRIPTION
       -----------
     @@ Documentation/git-branch.adoc: This option is only applicable in non-verbose mod
       	Print the name of the current branch. In detached `HEAD` state,
       	nothing is printed.
       
     -+`--delete-merged <branch>...`::
     -+	Delete the local branches that `--forked` would list for the
     -+	given _<branch>_ arguments, but only those whose tip is
     -+	reachable from their configured upstream. In other words, the
     -+	work on the branch has already landed on the upstream it
     -+	tracks, so the local copy is no longer needed. Several
     -+	_<branch>_ patterns may be given, e.g. `git branch
     -+	--delete-merged origin/main 'feature*'`.
     ++`--delete-merged <branch>`::
     ++	Delete local branches whose configured upstream matches
     ++	_<branch>_, but only when their tip is reachable from that
     ++	upstream. In other words, the work on the branch has already
     ++	landed on the upstream it tracks, so the local copy is no longer
     ++	needed. The option can be repeated to widen the upstream match.
     ++	Optional _<pattern>_ arguments limit which local branches are
     ++	considered, e.g. `git branch --delete-merged 'origin/*'
     ++	'topic-*'`.
      ++
      +A branch is not deleted when:
      ++
      +--
     -+* its upstream remote-tracking branch no longer exists,
     ++* its configured upstream ref no longer exists,
      +* it is checked out in any worktree, or
     -+* its push destination (`<branch>@{push}`) equals its upstream
     -+  (`<branch>@{upstream}`), so it cannot be distinguished from a
     -+  branch that just looks "fully merged" right after a pull.
     ++* pushing it by name to the remote configured by
     ++  `branch.<name>.remote` would update its upstream, so it cannot be
     ++  distinguished from a branch that just looks "fully merged" right
     ++  after a pull.
      +--
      ++
      +A branch whose work has not yet been merged into its upstream is
     @@ builtin/branch.c: static const char * const builtin_branch_usage[] = {
       	N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
       	N_("git branch [<options>] [-r | -a] [--points-at]"),
       	N_("git branch [<options>] [-r | -a] [--format]"),
     -+	N_("git branch [<options>] --delete-merged <branch>..."),
     ++	N_("git branch [<options>] (--delete-merged <branch>)... [<pattern>...]"),
       	NULL
       };
       
     @@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const ch
      +	strset_clear(&spared);
      +}
      +
     -+static int delete_merged_branches(int argc, const char **argv,
     -+				 unsigned int flags)
     ++static int branch_pushes_to_upstream(struct branch *branch,
     ++				     const char *upstream)
     ++{
     ++	struct remote *remote = remote_get(remote_for_branch(branch, NULL));
     ++	char *tracking = NULL;
     ++	int ret = 0;
     ++
     ++	if (remote)
     ++		tracking = apply_refspecs(&remote->fetch, branch->refname);
     ++	if (tracking && !strcmp(tracking, upstream))
     ++		ret = 1;
     ++
     ++	free(tracking);
     ++	return ret;
     ++}
     ++
     ++static int delete_merged_branches(const struct strvec *upstreams,
     ++				 const char **argv, unsigned int flags)
      +{
      +	struct ref_store *refs = get_main_ref_store(the_repository);
      +	struct ref_filter filter = REF_FILTER_INIT;
     @@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const ch
      +	struct strvec to_delete = STRVEC_INIT;
      +	struct hashmap_iter iter;
      +	struct strmap_entry *entry;
     -+	int i, ret = 0;
     ++	size_t i;
     ++	int ret = 0;
      +
     -+	if (!argc)
     -+		die(_("--delete-merged requires at least one <branch>"));
     -+
     -+	for (i = 0; i < argc; i++)
     -+		if (ref_filter_forked_add(&filter, argv[i]) < 0)
     -+			die(_("'%s' is not a valid branch or pattern"), argv[i]);
     ++	for (i = 0; i < upstreams->nr; i++)
     ++		if (ref_filter_forked_add(&filter, upstreams->v[i]) < 0)
     ++			die(_("'%s' is not a valid branch or pattern"),
     ++			    upstreams->v[i]);
      +
      +	filter.kind = FILTER_REFS_BRANCHES;
     ++	filter.name_patterns = argv;
      +	filter_refs(&candidates, &filter, filter.kind);
      +
     -+	for (i = 0; i < candidates.nr; i++) {
     ++	for (i = 0; i < (size_t)candidates.nr; i++) {
      +		const char *full_name = candidates.items[i]->refname;
      +		const char *short_name;
      +		struct branch *branch;
     -+		const char *upstream, *push;
     ++		const char *upstream;
      +
      +		if (!skip_prefix(full_name, "refs/heads/", &short_name))
      +			BUG("filter returned non-branch ref '%s'", full_name);
     @@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const ch
      +		upstream = branch_get_upstream(branch, NULL);
      +		if (!upstream || !refs_ref_exists(refs, upstream))
      +			continue;
     -+		push = branch_get_push(branch, NULL);
     -+		if (!push || !strcmp(push, upstream))
     ++		if (branch_pushes_to_upstream(branch, upstream))
      +			continue;
      +		if (check_branch_commit(short_name, short_name,
      +					&candidates.items[i]->objectname, NULL,
     @@ builtin/branch.c: int cmd_branch(int argc,
       	/* possible actions */
       	int delete = 0, rename = 0, copy = 0, list = 0,
       	    unset_upstream = 0, show_current = 0, edit_description = 0;
     -+	int delete_merged = 0;
     ++	struct strvec delete_merged = STRVEC_INIT;
       	const char *new_upstream = NULL;
       	int noncreate_actions = 0;
       	/* possible options */
     @@ builtin/branch.c: int cmd_branch(int argc,
       		OPT_BOOL(0, "create-reflog", &reflog, N_("create the branch's reflog")),
       		OPT_BOOL(0, "edit-description", &edit_description,
       			 N_("edit the description for the branch")),
     -+		OPT_BOOL(0, "delete-merged", &delete_merged,
     -+			N_("delete local branches whose upstream matches <branch> and are merged")),
     ++		OPT_CALLBACK_F(0, "delete-merged", &delete_merged, N_("branch"),
     ++			N_("delete merged branches whose upstream matches <branch> (repeatable)"),
     ++			PARSE_OPT_NONEG, parse_opt_strvec),
       		OPT__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")),
     @@ builtin/branch.c: int cmd_branch(int argc,
       
       	if (!delete && !rename && !copy && !edit_description && !new_upstream &&
      -	    !show_current && !unset_upstream && argc == 0)
     -+	    !show_current && !unset_upstream && !delete_merged &&
     ++	    !show_current && !unset_upstream && !delete_merged.nr &&
      +	    argc == 0)
       		list = 1;
       
     @@ builtin/branch.c: int cmd_branch(int argc,
       	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
       			    !!show_current + !!list + !!edit_description +
      -			    !!unset_upstream;
     -+			    !!unset_upstream + !!delete_merged;
     ++			    !!unset_upstream + !!delete_merged.nr;
       	if (noncreate_actions > 1)
       		usage_with_options(builtin_branch_usage, options);
       
     @@ builtin/branch.c: int cmd_branch(int argc,
       				      (delete > 1 ? DELETE_BRANCH_FORCE : 0) |
       				      (quiet ? DELETE_BRANCH_QUIET : 0));
       		goto out;
     -+	} else if (delete_merged) {
     -+		ret = delete_merged_branches(argc, argv,
     ++	} else if (delete_merged.nr) {
     ++		ret = delete_merged_branches(&delete_merged, argv,
      +					     quiet ? DELETE_BRANCH_QUIET : 0);
      +		goto out;
       	} else if (show_current) {
       		print_current_branch_name();
       		ret = 0;
     +@@ builtin/branch.c: int cmd_branch(int argc,
     + 	ret = 0;
     + 
     + out:
     ++	strvec_clear(&delete_merged);
     + 	string_list_clear(&sorting_options, 0);
     + 	return ret;
     + }
      
       ## t/t3200-branch.sh ##
     -@@ t/t3200-branch.sh: test_expect_success '--forked narrows a <pattern> argument' '
     - 	test_cmp expect actual
     +@@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
     + 	test_grep "requires a value" err
       '
       
      +test_expect_success '--delete-merged: setup' '
     @@ t/t3200-branch.sh: test_expect_success '--forked narrows a <pattern> argument' '
      +		cd repo &&
      +		git remote add fork ../fork &&
      +		git remote add other ../other &&
     -+		git config remote.pushDefault fork &&
      +		git config push.default current &&
      +		git fetch other
      +	)
      +}
      +
     -+merged_branch () {
     ++create_merged_branch () {
      +	(
      +		cd repo &&
     -+		git checkout -b "$1" "$2" &&
     ++		git checkout -b "$1" origin/next --track &&
      +		git commit --allow-empty -m "$1 work" &&
     -+		git push origin "$1:next" &&
     -+		git fetch origin &&
     -+		git branch --set-upstream-to="$2" "$1"
     ++		git push origin "$1:next"
      +	)
      +}
      +
     -+test_expect_success '--delete-merged deletes merged branches and spares the rest' '
     -+	test_when_finished "rm -rf repo" &&
     -+	setup_repo_for_delete_merged &&
     -+	merged_branch merged origin/next &&
     -+	(
     -+		cd repo &&
     -+		git checkout -b unmerged origin/next &&
     -+		git commit --allow-empty -m "unmerged work" &&
     -+		git branch --set-upstream-to=origin/next unmerged &&
     -+		git checkout -b tracks-other other/main &&
     -+		git branch --set-upstream-to=other/main tracks-other &&
     -+		git checkout --detach
     -+	) &&
     -+	sha=$(git -C repo rev-parse --short merged) &&
     -+
     -+	git -C repo branch --delete-merged origin/next >actual 2>&1 &&
     -+
     -+	echo "Deleted branch merged (was $sha)." >expect &&
     -+	test_cmp expect actual &&
     -+	git -C repo for-each-ref --format="%(refname:short)" refs/heads/ >actual &&
     -+	cat >expect <<-\EOF &&
     -+	main
     -+	tracks-other
     -+	unmerged
     -+	EOF
     ++check_branches () {
     ++	git for-each-ref --format="%(refname:short)" refs/heads/ >actual &&
     ++	cat >expect &&
      +	test_cmp expect actual
     -+'
     ++}
      +
     -+test_expect_success '--delete-merged deletes merged branches and spares protected ones' '
     -+	test_when_finished "rm -rf repo" &&
     ++test_expect_success '--delete-merged keeps cloned main without a default push remote' '
      +	setup_repo_for_delete_merged &&
     -+	merged_branch on-next origin/next &&
     -+	merged_branch checked-out origin/next &&
     -+	merged_branch upstream-gone origin/next &&
      +	(
      +		cd repo &&
     -+		git checkout -b mainline main &&
     -+		git checkout -b on-local mainline &&
     -+		git branch --set-upstream-to=mainline on-local &&
     -+		git update-ref refs/remotes/origin/topic refs/remotes/origin/next &&
     -+		git branch --set-upstream-to=origin/topic upstream-gone &&
     -+		git update-ref -d refs/remotes/origin/topic &&
     -+		git branch --set-upstream-to=origin/main main &&
     -+		git config branch.main.pushRemote origin &&
     -+		git checkout -b tracks-other other/main &&
     -+		git branch --set-upstream-to=other/main tracks-other &&
     -+		git checkout checked-out
     -+	) &&
     ++		git checkout --detach &&
      +
     -+	git -C repo branch --delete-merged origin/next mainline &&
     ++		git branch --delete-merged */* &&
      +
     -+	git -C repo for-each-ref --format="%(refname:short)" refs/heads/ >actual &&
     -+	cat >expect <<-\EOF &&
     -+	checked-out
     -+	main
     -+	mainline
     -+	tracks-other
     -+	upstream-gone
     -+	EOF
     -+	test_cmp expect actual
     ++		check_branches <<-\EOF
     ++		main
     ++		EOF
     ++	)
      +'
      +
     -+test_expect_success '--delete-merged requires at least one <branch>' '
     -+	test_must_fail git -C forked branch --delete-merged 2>err &&
     -+	test_grep "requires at least one <branch>" err
     ++test_expect_success '--delete-merged deletes only selected merged branches' '
     ++	setup_repo_for_delete_merged &&
     ++	create_merged_branch also-merged &&
     ++	create_merged_branch merged &&
     ++	(
     ++		cd repo &&
     ++		git checkout -b unmerged origin/next --track &&
     ++		git commit --allow-empty -m "unmerged work" &&
     ++		git checkout -b tracks-other other/main --track &&
     ++		sha=$(git rev-parse --short merged) &&
     ++
     ++		git branch --delete-merged origin/next merged >actual 2>&1 &&
     ++		echo "Deleted branch merged (was $sha)." >expect &&
     ++		test_cmp expect actual &&
     ++
     ++		check_branches <<-\EOF
     ++		also-merged
     ++		main
     ++		tracks-other
     ++		unmerged
     ++		EOF
     ++	)
      +'
      +
     -+test_expect_success '--delete-merged keeps a branch that is an upstream' '
     -+	test_when_finished "rm -rf repo" &&
     ++test_expect_success '--delete-merged keeps main despite a different default push remote' '
      +	setup_repo_for_delete_merged &&
     -+	merged_branch feature origin/next &&
     ++	create_merged_branch on-next &&
     ++	create_merged_branch checked-out &&
     ++	create_merged_branch upstream-gone &&
      +	(
      +		cd repo &&
     -+		git checkout -b topic feature &&
     -+		git commit --allow-empty -m "topic work" &&
     -+		git branch --set-upstream-to=feature topic &&
     -+		git checkout --detach
     -+	) &&
     -+
     -+	git -C repo branch --dry-run --delete-merged origin/next >out &&
     -+	test_grep ! "feature" out &&
     -+
     -+	git -C repo branch --delete-merged origin/next 2>err &&
     -+
     -+	test_must_be_empty err &&
     -+	git -C repo rev-parse --verify refs/heads/feature &&
     -+	git -C repo rev-parse --verify refs/heads/topic &&
     -+	echo origin/next >expect &&
     -+	git -C repo rev-parse --abbrev-ref feature@{upstream} >actual &&
     -+	test_cmp expect actual &&
     -+	echo feature >expect &&
     -+	git -C repo rev-parse --abbrev-ref topic@{upstream} >actual &&
     -+	test_cmp expect actual
     ++		git config remote.pushDefault fork &&
     ++		git checkout -b local-to-delete main --track &&
     ++		git update-ref refs/remotes/origin/topic refs/remotes/origin/next &&
     ++		git branch --set-upstream-to=origin/topic upstream-gone &&
     ++		git update-ref -d refs/remotes/origin/topic &&
     ++		git checkout -b tracks-other other/main --track &&
     ++		git checkout checked-out &&
     ++
     ++		git branch --delete-merged origin/* \
     ++			--delete-merged main &&
     ++
     ++		check_branches <<-\EOF
     ++		checked-out
     ++		main
     ++		tracks-other
     ++		upstream-gone
     ++		EOF
     ++	)
      +'
      +
     -+test_expect_success '--delete-merged keeps a chain of upstreams of a kept branch' '
     -+	test_when_finished "rm -rf repo" &&
     ++test_expect_success '--delete-merged keeps the upstream of a surviving branch' '
      +	setup_repo_for_delete_merged &&
     ++	create_merged_branch feature &&
      +	(
      +		cd repo &&
     -+		git branch b3 origin/next &&
     -+		git branch --set-upstream-to=origin/next b3 &&
     -+		git branch b2 origin/next &&
     -+		git branch --set-upstream-to=b3 b2 &&
     -+		git checkout -b b1 b2 &&
     -+		git commit --allow-empty -m "b1 work" &&
     -+		git branch --set-upstream-to=b2 b1 &&
     -+		git checkout --detach
     -+	) &&
     -+
     -+	git -C repo branch --delete-merged origin/next &&
     ++		git checkout -b topic feature --track &&
     ++		git commit --allow-empty -m "topic work" &&
      +
     -+	git -C repo for-each-ref --format="%(refname:short)" refs/heads/ >actual &&
     -+	cat >expect <<-\EOF &&
     -+	b1
     -+	b2
     -+	b3
     -+	main
     -+	EOF
     -+	test_cmp expect actual
     ++		git branch --delete-merged origin/next 2>err &&
     ++
     ++		test_must_be_empty err &&
     ++		check_branches <<-\EOF &&
     ++		feature
     ++		main
     ++		topic
     ++		EOF
     ++
     ++		git config --local --get-regexp "branch\\.(feature|topic)\\.(merge|remote)" >actual &&
     ++		cat >expect <<-\EOF &&
     ++		branch.feature.remote origin
     ++		branch.feature.merge refs/heads/next
     ++		branch.topic.remote .
     ++		branch.topic.merge refs/heads/feature
     ++		EOF
     ++		test_cmp expect actual
     ++	)
      +'
      +
     -+test_expect_success '--delete-merged clears the upstream of a kept base whose own base is deleted' '
     -+	test_when_finished "rm -rf repo" &&
     ++test_expect_success '--delete-merged clears the deleted upstream of a spared branch' '
      +	setup_repo_for_delete_merged &&
      +	(
      +		cd repo &&
     -+		git branch lower origin/next &&
     -+		git branch --set-upstream-to=origin/next lower &&
     -+		git branch mid origin/next &&
     -+		git branch --set-upstream-to=lower mid &&
     -+		git checkout -b tip mid &&
     ++		git config remote.pushDefault fork &&
     ++		git branch lower origin/next --track &&
     ++		git branch mid lower --track &&
     ++		git checkout -b tip mid --track &&
      +		git commit --allow-empty -m "tip work" &&
     -+		git branch --set-upstream-to=mid tip &&
     -+		git checkout --detach
     -+	) &&
     -+
     -+	git -C repo branch --delete-merged origin/next lower &&
      +
     -+	test_must_fail git -C repo rev-parse --verify refs/heads/lower &&
     -+	git -C repo rev-parse --verify refs/heads/mid &&
     -+	test_must_fail git -C repo rev-parse mid@{upstream} &&
     -+	echo mid >expect &&
     -+	git -C repo rev-parse --abbrev-ref tip@{upstream} >actual &&
     -+	test_cmp expect actual
     ++		git branch --delete-merged origin/next \
     ++			--delete-merged lower &&
     ++
     ++		check_branches <<-\EOF &&
     ++		main
     ++		mid
     ++		tip
     ++		EOF
     ++
     ++		git config --local --get-regexp "branch\\.(mid|tip)\\.(merge|remote)" >actual &&
     ++		cat >expect <<-\EOF &&
     ++		branch.tip.remote .
     ++		branch.tip.merge refs/heads/mid
     ++		EOF
     ++		test_cmp expect actual
     ++	)
      +'
      +
     ++test_expect_success '--delete-merged requires a value' '
     ++	test_must_fail git -C forked branch --delete-merged 2>err &&
     ++	test_grep "requires a value" err
     ++'
       test_done
 6:  d52d717b70 ! 6:  734d27c908 branch: add branch.<name>.deleteMerged opt-out
     @@ Documentation/git-branch.adoc
      @@ Documentation/git-branch.adoc: A branch is not deleted when:
       +
       --
     - * its upstream remote-tracking branch no longer exists,
     + * its configured upstream ref no longer exists,
      -* it is checked out in any worktree, or
      +* it is checked out in any worktree,
     - * its push destination (`<branch>@{push}`) equals its upstream
     -   (`<branch>@{upstream}`), so it cannot be distinguished from a
     --  branch that just looks "fully merged" right after a pull.
     -+  branch that just looks "fully merged" right after a pull, or
     + * pushing it by name to the remote configured by
     +   `branch.<name>.remote` would update its upstream, so it cannot be
     +   distinguished from a branch that just looks "fully merged" right
     +-  after a pull.
     ++  after a pull, or
      +* `branch.<name>.deleteMerged` is set to `false`.
       --
       +
       A branch whose work has not yet been merged into its upstream is
      
       ## builtin/branch.c ##
     -@@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
     +@@ builtin/branch.c: static int delete_merged_branches(const struct strvec *upstreams,
       	struct ref_array candidates = { 0 };
       	struct strset deletable = STRSET_INIT;
       	struct strvec to_delete = STRVEC_INIT;
      +	struct strbuf key = STRBUF_INIT;
       	struct hashmap_iter iter;
       	struct strmap_entry *entry;
     -+	bool quiet = flags & DELETE_BRANCH_QUIET;
     - 	int i, ret = 0;
     - 
     - 	if (!argc)
     -@@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
     + 	size_t i;
     +@@ builtin/branch.c: static int delete_merged_branches(const struct strvec *upstreams,
       		const char *short_name;
       		struct branch *branch;
     - 		const char *upstream, *push;
     + 		const char *upstream;
      +		int opt_out;
       
       		if (!skip_prefix(full_name, "refs/heads/", &short_name))
       			BUG("filter returned non-branch ref '%s'", full_name);
     -@@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
     +@@ builtin/branch.c: static int delete_merged_branches(const struct strvec *upstreams,
       					FILTER_REFS_BRANCHES, DELETE_BRANCH_SKIP_UNMERGED))
       			continue;
       
     @@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
      +		strbuf_addf(&key, "branch.%s.deletemerged", short_name);
      +		if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
      +		    !opt_out) {
     -+			if (!quiet)
     ++			if (!(flags & DELETE_BRANCH_QUIET))
      +				fprintf(stderr,
      +					_("Skipping '%s' (branch.%s.deleteMerged is false)\n"),
      +					short_name, short_name);
     @@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
       		strset_add(&deletable, short_name);
       	}
       
     -@@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
     +@@ builtin/branch.c: static int delete_merged_branches(const struct strvec *upstreams,
       				      DELETE_BRANCH_NO_HEAD_FALLBACK |
       				      flags);
       
     @@ builtin/branch.c: static int delete_merged_branches(int argc, const char **argv,
       	ref_array_clear(&candidates);
      
       ## t/t3200-branch.sh ##
     -@@ t/t3200-branch.sh: test_expect_success '--delete-merged clears the upstream of a kept base whose ow
     - 	test_cmp expect actual
     +@@ t/t3200-branch.sh: test_expect_success '--delete-merged requires a value' '
     + 	test_must_fail git -C forked branch --delete-merged 2>err &&
     + 	test_grep "requires a value" err
       '
     - 
     ++
      +test_expect_success '--delete-merged honours branch.<name>.deleteMerged=false' '
     -+	test_when_finished "rm -rf repo" &&
      +	setup_repo_for_delete_merged &&
     -+	merged_branch deleted origin/next &&
     -+	merged_branch kept origin/next &&
     -+	git -C repo config branch.kept.deleteMerged false &&
     -+	git -C repo checkout --detach &&
     ++	create_merged_branch deleted &&
     ++	create_merged_branch kept &&
     ++	(
     ++		cd repo &&
     ++		git config branch.kept.deleteMerged false &&
     ++		git checkout --detach &&
      +
     -+	git -C repo branch --delete-merged origin/next 2>err &&
     ++		git branch --delete-merged origin/next 2>err &&
      +
     -+	test_grep "Skipping .kept." err &&
     -+	test_must_fail git -C repo rev-parse --verify refs/heads/deleted &&
     -+	git -C repo rev-parse --verify refs/heads/kept
     ++		test_grep "Skipping .kept." err &&
     ++		check_branches <<-\EOF
     ++		kept
     ++		main
     ++		EOF
     ++	)
      +'
      +
      +test_expect_success "branch -d still deletes a deleteMerged=false branch" '
     -+	test_when_finished "rm -rf repo" &&
      +	setup_repo_for_delete_merged &&
     -+	merged_branch kept origin/next &&
     -+	git -C repo config branch.kept.deleteMerged false &&
     -+	git -C repo checkout --detach &&
     ++	create_merged_branch kept &&
     ++	(
     ++		cd repo &&
     ++		git config branch.kept.deleteMerged false &&
     ++		git checkout --detach &&
     ++
     ++		git branch -d kept &&
      +
     -+	git -C repo branch -d kept &&
     -+	test_must_fail git -C repo rev-parse --verify refs/heads/kept
     ++		check_branches <<-\EOF
     ++		main
     ++		EOF
     ++	)
      +'
      +
       test_done
 7:  8d0323f4b3 ! 7:  7aa9d5db14 branch: add --dry-run for --delete-merged
     @@ Metadata
       ## Commit message ##
          branch: add --dry-run for --delete-merged
      
     -    With --dry-run, --delete-merged prints the local branches it would
     -    delete, one "Would delete branch <name>" line each, and exits
     -    without touching any ref. The same filtering applies, so the output
     -    is exactly the set that the real run would delete.
     +    "git branch --dry-run --delete-merged ..." prints one line per ref that
     +    would be deleted without modifying refs or branch configuration.
      
          --dry-run is only meaningful together with --delete-merged and is
          rejected otherwise.
     @@ Documentation/git-branch.adoc: git branch (-m|-M) [<old-branch>] <new-branch>
       git branch (-c|-C) [<old-branch>] <new-branch>
       git branch (-d|-D) [-r] <branch-name>...
       git branch --edit-description [<branch-name>]
     --git branch --delete-merged <branch>...
     -+git branch [--dry-run] --delete-merged <branch>...
     +-git branch (--delete-merged <branch>)... [<pattern>...]
     ++git branch [--dry-run] (--delete-merged <branch>)... [<pattern>...]
       
       DESCRIPTION
       -----------
     @@ builtin/branch.c: enum delete_branch_flags {
       };
       
       static int check_branch_commit(const char *branchname, const char *refname,
     -@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int kinds,
     - 	bool quiet = flags & DELETE_BRANCH_QUIET;
     - 	bool skip_unmerged = flags & DELETE_BRANCH_SKIP_UNMERGED;
     - 	bool no_head_fallback = flags & DELETE_BRANCH_NO_HEAD_FALLBACK;
     -+	bool dry_run = flags & DELETE_BRANCH_DRY_RUN;
     - 	struct strbuf bname = STRBUF_INIT;
     - 	enum interpret_branch_kind allowed_interpret;
     - 	struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
      @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int kinds,
       		free(target);
       	}
       
      -	if (refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
     -+	if (!dry_run &&
     ++	if (!(flags & DELETE_BRANCH_DRY_RUN) &&
      +	    refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
       		ret = 1;
       
     @@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int ki
       		char *describe_ref = item->util;
       		char *name = item->string;
      -		if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
     -+		if (dry_run) {
     -+			if (!quiet)
     ++		if (flags & DELETE_BRANCH_DRY_RUN) {
     ++			if (!(flags & DELETE_BRANCH_QUIET))
      +				printf(remote_branch
      +					? _("Would delete remote-tracking branch %s (was %s).\n")
      +					: _("Would delete branch %s (was %s).\n"),
      +					name + branch_name_pos, describe_ref);
      +		} else if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
       			char *refname = name + branch_name_pos;
     - 			if (!quiet)
     + 			if (!(flags & DELETE_BRANCH_QUIET))
       				printf(remote_branch
     +@@ builtin/branch.c: static int spare_stacked_base(const struct reference *ref, void *cb_data)
     +  * base is itself merged, so when its own upstream is also going away
     +  * (no surviving branch tracks it), clear the base's now-stale upstream.
     +  */
     +-static void spare_stacked_bases(struct ref_store *refs, struct strset *deletable)
     ++static void spare_stacked_bases(struct ref_store *refs, struct strset *deletable,
     ++				unsigned int flags)
     + {
     + 	struct strset spared = STRSET_INIT;
     + 	struct spare_data data = { .deletable = deletable, .spared = &spared };
     +@@ builtin/branch.c: static void spare_stacked_bases(struct ref_store *refs, struct strset *deletable
     + 
     + 	refs_for_each_branch_ref(refs, spare_stacked_base, &data);
     + 
     +-	strset_for_each_entry(&spared, &iter, entry) {
     +-		struct branch *branch = branch_get(entry->key);
     +-		const char *upstream = branch_get_upstream(branch, NULL);
     +-		const char *up_short;
     ++	if (!(flags & DELETE_BRANCH_DRY_RUN)) {
     ++		strset_for_each_entry(&spared, &iter, entry) {
     ++			struct branch *branch = branch_get(entry->key);
     ++			const char *upstream = branch_get_upstream(branch, NULL);
     ++			const char *up_short;
     + 
     +-		if (!upstream || !skip_prefix(upstream, "refs/heads/", &up_short) ||
     +-		    !strset_contains(deletable, up_short))
     +-			continue;
     ++			if (!upstream || !skip_prefix(upstream, "refs/heads/", &up_short) ||
     ++			    !strset_contains(deletable, up_short))
     ++				continue;
     + 
     +-		strbuf_reset(&key);
     +-		strbuf_addf(&key, "branch.%s.merge", branch->name);
     +-		repo_config_set_gently(the_repository, key.buf, NULL);
     +-		strbuf_reset(&key);
     +-		strbuf_addf(&key, "branch.%s.remote", branch->name);
     +-		repo_config_set_gently(the_repository, key.buf, NULL);
     ++			strbuf_reset(&key);
     ++			strbuf_addf(&key, "branch.%s.merge", branch->name);
     ++			repo_config_set_gently(the_repository, key.buf, NULL);
     ++			strbuf_reset(&key);
     ++			strbuf_addf(&key, "branch.%s.remote", branch->name);
     ++			repo_config_set_gently(the_repository, key.buf, NULL);
     ++		}
     + 	}
     + 
     + 	strbuf_release(&key);
     +@@ builtin/branch.c: static int delete_merged_branches(const struct strvec *upstreams,
     + 		strset_add(&deletable, short_name);
     + 	}
     + 
     +-	spare_stacked_bases(refs, &deletable);
     ++	spare_stacked_bases(refs, &deletable, flags);
     + 
     + 	strset_for_each_entry(&deletable, &iter, entry)
     + 		strvec_push(&to_delete, entry->key);
      @@ builtin/branch.c: int cmd_branch(int argc,
       	int delete = 0, rename = 0, copy = 0, list = 0,
       	    unset_upstream = 0, show_current = 0, edit_description = 0;
     - 	int delete_merged = 0;
     + 	struct strvec delete_merged = STRVEC_INIT;
      +	int dry_run = 0;
       	const char *new_upstream = NULL;
       	int noncreate_actions = 0;
       	/* possible options */
      @@ builtin/branch.c: int cmd_branch(int argc,
     - 			 N_("edit the description for the branch")),
     - 		OPT_BOOL(0, "delete-merged", &delete_merged,
     - 			N_("delete local branches whose upstream matches <branch> and are merged")),
     + 		OPT_CALLBACK_F(0, "delete-merged", &delete_merged, N_("branch"),
     + 			N_("delete merged branches whose upstream matches <branch> (repeatable)"),
     + 			PARSE_OPT_NONEG, parse_opt_strvec),
      +		OPT_BOOL(0, "dry-run", &dry_run,
      +			N_("with --delete-merged, only print which branches would be deleted")),
       		OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE),
     @@ builtin/branch.c: int cmd_branch(int argc,
       	if (noncreate_actions > 1)
       		usage_with_options(builtin_branch_usage, options);
       
     -+	if (dry_run && !delete_merged)
     ++	if (dry_run && !delete_merged.nr)
      +		die(_("--dry-run requires --delete-merged"));
      +
       	if (recurse_submodules_explicit) {
     @@ builtin/branch.c: int cmd_branch(int argc,
       			die(_("branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled"));
      @@ builtin/branch.c: int cmd_branch(int argc,
       		goto out;
     - 	} else if (delete_merged) {
     - 		ret = delete_merged_branches(argc, argv,
     + 	} else if (delete_merged.nr) {
     + 		ret = delete_merged_branches(&delete_merged, argv,
      -					     quiet ? DELETE_BRANCH_QUIET : 0);
      +					     (quiet ? DELETE_BRANCH_QUIET : 0) |
      +					     (dry_run ? DELETE_BRANCH_DRY_RUN : 0));
     @@ builtin/branch.c: int cmd_branch(int argc,
       		print_current_branch_name();
      
       ## t/t3200-branch.sh ##
     -@@ t/t3200-branch.sh: test_expect_success '--delete-merged deletes merged branches and spares the rest
     - 	) &&
     - 	sha=$(git -C repo rev-parse --short merged) &&
     - 
     --	git -C repo branch --delete-merged origin/next >actual 2>&1 &&
     -+	git -C repo branch --dry-run --delete-merged origin/next >actual 2>&1 &&
     -+	echo "Would delete branch merged (was $sha)." >expect &&
     -+	test_cmp expect actual &&
     -+	git -C repo rev-parse --verify refs/heads/merged &&
     - 
     -+	git -C repo branch --delete-merged origin/next >actual 2>&1 &&
     - 	echo "Deleted branch merged (was $sha)." >expect &&
     - 	test_cmp expect actual &&
     - 	git -C repo for-each-ref --format="%(refname:short)" refs/heads/ >actual &&
     +@@ t/t3200-branch.sh: test_expect_success '--delete-merged deletes only selected merged branches' '
     + 		git checkout -b tracks-other other/main --track &&
     + 		sha=$(git rev-parse --short merged) &&
     + 
     ++		git branch --dry-run --delete-merged origin/next merged >actual 2>&1 &&
     ++		echo "Would delete branch merged (was $sha)." >expect &&
     ++		test_cmp expect actual &&
     ++		git rev-parse --verify refs/heads/merged &&
     ++
     ++		check_branches <<-\EOF &&
     ++		also-merged
     ++		main
     ++		merged
     ++		tracks-other
     ++		unmerged
     ++		EOF
     ++
     + 		git branch --delete-merged origin/next merged >actual 2>&1 &&
     + 		echo "Deleted branch merged (was $sha)." >expect &&
     + 		test_cmp expect actual &&
     +@@ t/t3200-branch.sh: test_expect_success '--delete-merged keeps the upstream of a surviving branch' '
     + 		git checkout -b topic feature --track &&
     + 		git commit --allow-empty -m "topic work" &&
     + 
     +-		git branch --delete-merged origin/next 2>err &&
     ++		git branch --dry-run --delete-merged origin/next >out &&
     ++		test_grep ! "feature" out &&
     + 
     ++		git branch --delete-merged origin/next 2>err &&
     + 		test_must_be_empty err &&
     ++
     + 		check_branches <<-\EOF &&
     + 		feature
     + 		main
     +@@ t/t3200-branch.sh: test_expect_success '--delete-merged clears the deleted upstream of a spared bra
     + 		git checkout -b tip mid --track &&
     + 		git commit --allow-empty -m "tip work" &&
     + 
     ++		git branch --dry-run --delete-merged origin/next \
     ++			--delete-merged lower &&
     ++
     ++		git config --local --get-regexp "branch\\.(mid|tip)\\.(merge|remote)" >actual &&
     ++		cat >expect <<-\EOF &&
     ++		branch.mid.remote .
     ++		branch.mid.merge refs/heads/lower
     ++		branch.tip.remote .
     ++		branch.tip.merge refs/heads/mid
     ++		EOF
     ++		test_cmp expect actual &&
     ++
     + 		git branch --delete-merged origin/next \
     + 			--delete-merged lower &&
     + 
      @@ t/t3200-branch.sh: test_expect_success "branch -d still deletes a deleteMerged=false branch" '
     - 	test_must_fail git -C repo rev-parse --verify refs/heads/kept
     + 	)
       '
       
      +test_expect_success '--dry-run without --delete-merged is rejected' '

-- 
gitgitgadget

  parent reply	other threads:[~2026-07-14 18:24 UTC|newest]

Thread overview: 225+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-01 21:35 [PATCH] fetch: add fetch.pruneLocalBranches config Harald Nordgren via GitGitGadget
2026-05-03 22:39 ` Junio C Hamano
2026-05-04 18:28   ` [PATCH] checkout: add --autostash option for branch switching Harald Nordgren
2026-05-10  1:01     ` Junio C Hamano
2026-05-05  7:14   ` [PATCH] fetch: add fetch.pruneLocalBranches config Johannes Sixt
2026-05-04 18:27 ` [PATCH v2 0/6] fetch: add fetch.pruneBranches config Harald Nordgren via GitGitGadget
2026-05-04 18:27   ` [PATCH v2 1/6] branch: add --forked <remote> Harald Nordgren via GitGitGadget
2026-05-04 23:25     ` Kristoffer Haugsbakk
2026-05-04 18:27   ` [PATCH v2 2/6] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-05-04 18:27   ` [PATCH v2 3/6] branch: add --prune-merged <remote> Harald Nordgren via GitGitGadget
2026-05-04 18:27   ` [PATCH v2 4/6] fetch: add --prune-merged Harald Nordgren via GitGitGadget
2026-05-04 18:27   ` [PATCH v2 5/6] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-05-04 18:27   ` [PATCH v2 6/6] branch: add --all-remotes flag Harald Nordgren via GitGitGadget
2026-05-05  7:22   ` [PATCH v3 0/6] fetch: add fetch.pruneBranches config Harald Nordgren via GitGitGadget
2026-05-05  7:22     ` [PATCH v3 1/6] branch: add --forked <remote> Harald Nordgren via GitGitGadget
2026-05-05  7:22     ` [PATCH v3 2/6] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-05-05  7:22     ` [PATCH v3 3/6] branch: add --prune-merged <remote> Harald Nordgren via GitGitGadget
2026-05-05  7:22     ` [PATCH v3 4/6] fetch: add --prune-merged Harald Nordgren via GitGitGadget
2026-05-05  7:22     ` [PATCH v3 5/6] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-05-05  7:22     ` [PATCH v3 6/6] branch: add --all-remotes flag Harald Nordgren via GitGitGadget
2026-05-05 19:23     ` [PATCH v4 0/6] fetch: add fetch.pruneBranches config Harald Nordgren via GitGitGadget
2026-05-05 19:23       ` [PATCH v4 1/6] branch: add --forked <remote> Harald Nordgren via GitGitGadget
2026-05-05 19:23       ` [PATCH v4 2/6] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-05-05 19:23       ` [PATCH v4 3/6] branch: add --prune-merged <remote> Harald Nordgren via GitGitGadget
2026-05-05 19:23       ` [PATCH v4 4/6] fetch: add --prune-merged Harald Nordgren via GitGitGadget
2026-05-05 20:48         ` Johannes Sixt
2026-05-05 22:07           ` [PATCH] fetch: add fetch.pruneLocalBranches config Harald Nordgren
2026-05-11  2:59             ` Junio C Hamano
2026-05-11  6:56               ` Harald Nordgren
2026-05-05 19:23       ` [PATCH v4 5/6] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-05-05 19:23       ` [PATCH v4 6/6] branch: add --all-remotes flag Harald Nordgren via GitGitGadget
2026-05-07 20:14       ` [PATCH v4 0/6] fetch: add fetch.pruneBranches config Harald Nordgren
2026-05-11  6:58       ` [PATCH v5 0/5] branch: prune-merged Harald Nordgren via GitGitGadget
2026-05-11  6:58         ` [PATCH v5 1/5] branch: add --forked <remote> Harald Nordgren via GitGitGadget
2026-05-11  6:58         ` [PATCH v5 2/5] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-05-11  8:18           ` Junio C Hamano
2026-05-11  8:44             ` [PATCH] fetch: add fetch.pruneLocalBranches config Harald Nordgren
2026-05-11  6:58         ` [PATCH v5 3/5] branch: add --prune-merged <remote> Harald Nordgren via GitGitGadget
2026-05-11  6:58         ` [PATCH v5 4/5] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-05-11  6:58         ` [PATCH v5 5/5] branch: add --all-remotes flag Harald Nordgren via GitGitGadget
2026-05-11  9:44         ` [PATCH v6 0/5] branch: prune-merged Harald Nordgren via GitGitGadget
2026-05-11  9:44           ` [PATCH v6 1/5] branch: add --forked <remote> Harald Nordgren via GitGitGadget
2026-05-11  9:44           ` [PATCH v6 2/5] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-05-11  9:44           ` [PATCH v6 3/5] branch: add --prune-merged <remote> Harald Nordgren via GitGitGadget
2026-05-11  9:44           ` [PATCH v6 4/5] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-05-11  9:44           ` [PATCH v6 5/5] branch: add --all-remotes flag Harald Nordgren via GitGitGadget
2026-05-11 23:20           ` [PATCH v6 0/5] branch: prune-merged Junio C Hamano
2026-05-12  7:35             ` [PATCH] fetch: add fetch.pruneLocalBranches config Harald Nordgren
2026-05-12  8:23           ` [PATCH v7 0/5] branch: prune-merged Harald Nordgren via GitGitGadget
2026-05-12  8:23             ` [PATCH v7 1/5] branch: add --forked <remote> Harald Nordgren via GitGitGadget
2026-05-12  8:23             ` [PATCH v7 2/5] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-05-12  8:23             ` [PATCH v7 3/5] branch: add --prune-merged <remote> Harald Nordgren via GitGitGadget
2026-05-12 13:53               ` Junio C Hamano
2026-05-12 17:00                 ` [PATCH] fetch: add fetch.pruneLocalBranches config Harald Nordgren
2026-05-12  8:23             ` [PATCH v7 4/5] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-05-12  8:23             ` [PATCH v7 5/5] branch: add --all-remotes flag Harald Nordgren via GitGitGadget
2026-05-12 17:07             ` [PATCH v8 0/5] branch: prune-merged Harald Nordgren via GitGitGadget
2026-05-12 17:07               ` [PATCH v8 1/5] branch: add --forked <remote> Harald Nordgren via GitGitGadget
2026-05-12 17:07               ` [PATCH v8 2/5] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-05-12 17:07               ` [PATCH v8 3/5] branch: add --prune-merged <remote> Harald Nordgren via GitGitGadget
2026-05-12 17:07               ` [PATCH v8 4/5] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-05-12 17:07               ` [PATCH v8 5/5] branch: add --all-remotes flag Harald Nordgren via GitGitGadget
2026-05-13 13:46               ` [PATCH v8 0/5] branch: prune-merged Junio C Hamano
2026-05-13 18:57                 ` [PATCH] fetch: add fetch.pruneLocalBranches config Harald Nordgren
2026-05-13 19:34               ` [PATCH v9 0/5] branch: prune-merged Harald Nordgren via GitGitGadget
2026-05-13 19:34                 ` [PATCH v9 1/5] branch: add --forked <remote> Harald Nordgren via GitGitGadget
2026-05-13 19:34                 ` [PATCH v9 2/5] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-05-13 19:34                 ` [PATCH v9 3/5] branch: add --prune-merged <remote> Harald Nordgren via GitGitGadget
2026-05-18 15:27                   ` Phillip Wood
2026-05-21  9:46                     ` Phillip Wood
2026-05-21 19:16                       ` Harald Nordgren
2026-05-22  9:47                         ` Phillip Wood
2026-05-22 10:51                           ` Harald Nordgren
2026-05-21 12:37                     ` Harald Nordgren
2026-05-21 13:29                     ` Junio C Hamano
2026-05-13 19:34                 ` [PATCH v9 4/5] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-05-13 19:34                 ` [PATCH v9 5/5] branch: add --all-remotes flag Harald Nordgren via GitGitGadget
2026-05-18 15:27                   ` Phillip Wood
2026-05-18  8:14                 ` [PATCH v9 0/5] branch: prune-merged Harald Nordgren
2026-05-21 22:40                 ` [PATCH v10 0/4] " Harald Nordgren via GitGitGadget
2026-05-21 22:40                   ` [PATCH v10 1/4] branch: add --forked <branch> Harald Nordgren via GitGitGadget
2026-05-22  1:52                     ` Junio C Hamano
2026-05-22  6:18                     ` Johannes Sixt
2026-05-22  6:36                       ` Junio C Hamano
2026-05-22 10:49                         ` Harald Nordgren
2026-05-22 11:25                           ` Johannes Sixt
2026-05-21 22:40                   ` [PATCH v10 2/4] branch: add --prune-merged <branch> Harald Nordgren via GitGitGadget
2026-05-22  1:17                     ` Junio C Hamano
2026-05-22  2:51                     ` Junio C Hamano
2026-05-22  2:53                       ` Junio C Hamano
2026-05-22  7:59                         ` Harald Nordgren
2026-05-22 11:58                           ` Junio C Hamano
2026-05-22  2:52                     ` Junio C Hamano
2026-05-21 22:40                   ` [PATCH v10 3/4] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-05-21 22:40                   ` [PATCH v10 4/4] branch: add --dry-run for --prune-merged Harald Nordgren via GitGitGadget
2026-05-22 11:31                   ` [PATCH v11 0/6] branch: prune-merged Harald Nordgren via GitGitGadget
2026-05-22 11:31                     ` [PATCH v11 1/6] branch: add --forked <branch> Harald Nordgren via GitGitGadget
2026-05-22 11:31                     ` [PATCH v11 2/6] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-05-22 11:31                     ` [PATCH v11 3/6] branch: prepare delete_branches for a bulk caller Harald Nordgren via GitGitGadget
2026-05-22 11:31                     ` [PATCH v11 4/6] branch: add --prune-merged <branch> Harald Nordgren via GitGitGadget
2026-05-22 11:31                     ` [PATCH v11 5/6] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-05-22 11:31                     ` [PATCH v11 6/6] branch: add --dry-run for --prune-merged Harald Nordgren via GitGitGadget
2026-06-02 13:05                     ` [PATCH v11 0/6] branch: prune-merged Phillip Wood
2026-06-02 13:41                       ` Harald Nordgren
2026-06-03  9:04                     ` [PATCH v12 " Harald Nordgren via GitGitGadget
2026-06-03  9:04                       ` [PATCH v12 1/6] branch: add --forked filter for --list mode Harald Nordgren via GitGitGadget
2026-06-05 13:48                         ` Phillip Wood
2026-06-05 17:50                           ` Harald Nordgren
2026-06-03  9:04                       ` [PATCH v12 2/6] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-06-05 13:49                         ` Phillip Wood
2026-06-03  9:04                       ` [PATCH v12 3/6] branch: prepare delete_branches for a bulk caller Harald Nordgren via GitGitGadget
2026-06-05 13:49                         ` Phillip Wood
2026-06-03  9:04                       ` [PATCH v12 4/6] branch: add --prune-merged <branch> Harald Nordgren via GitGitGadget
2026-06-05 13:50                         ` Phillip Wood
2026-06-05 15:04                           ` Phillip Wood
2026-06-03  9:04                       ` [PATCH v12 5/6] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-06-03  9:04                       ` [PATCH v12 6/6] branch: add --dry-run for --prune-merged Harald Nordgren via GitGitGadget
2026-06-05 18:35                       ` [PATCH v13 0/6] branch: prune-merged Harald Nordgren via GitGitGadget
2026-06-05 18:35                         ` [PATCH v13 1/6] branch: add --forked filter for --list mode Harald Nordgren via GitGitGadget
2026-06-05 18:35                         ` [PATCH v13 2/6] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-06-08 23:56                           ` Junio C Hamano
2026-06-09  7:52                             ` Harald Nordgren
2026-06-09 12:38                               ` Junio C Hamano
2026-06-09 13:20                                 ` Harald Nordgren
2026-06-05 18:35                         ` [PATCH v13 3/6] branch: prepare delete_branches for a bulk caller Harald Nordgren via GitGitGadget
2026-06-05 18:35                         ` [PATCH v13 4/6] branch: add --prune-merged <branch> Harald Nordgren via GitGitGadget
2026-06-05 18:35                         ` [PATCH v13 5/6] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-06-05 18:35                         ` [PATCH v13 6/6] branch: add --dry-run for --prune-merged Harald Nordgren via GitGitGadget
2026-06-09 10:11                         ` [PATCH v14 0/6] branch: prune-merged Harald Nordgren via GitGitGadget
2026-06-09 10:11                           ` [PATCH v14 1/6] branch: add --forked filter for --list mode Harald Nordgren via GitGitGadget
2026-06-15  9:46                             ` Phillip Wood
2026-06-09 10:11                           ` [PATCH v14 2/6] branch: let delete_branches warn instead of error on bulk refusal Harald Nordgren via GitGitGadget
2026-06-09 13:21                             ` Phillip Wood
2026-06-09 10:11                           ` [PATCH v14 3/6] branch: prepare delete_branches for a bulk caller Harald Nordgren via GitGitGadget
2026-06-15  9:47                             ` Phillip Wood
2026-06-09 10:11                           ` [PATCH v14 4/6] branch: add --prune-merged <branch> Harald Nordgren via GitGitGadget
2026-06-15  9:46                             ` Phillip Wood
2026-06-16  9:59                             ` Phillip Wood
2026-06-16 19:15                               ` Harald Nordgren
2026-06-18 13:42                                 ` Phillip Wood
2026-06-18 16:08                                   ` Junio C Hamano
2026-06-19 13:13                                     ` Phillip Wood
2026-06-19 15:42                                       ` Junio C Hamano
2026-06-19 16:01                                         ` Junio C Hamano
2026-06-20  9:04                                           ` Harald Nordgren
2026-06-21 18:46                                             ` Harald Nordgren
2026-06-22  9:09                                               ` Phillip Wood
2026-06-22  9:28                                                 ` Phillip Wood
2026-06-22  9:37                                                   ` Harald Nordgren
2026-06-22  9:57                                                     ` Phillip Wood
2026-06-22 10:52                                                       ` Harald Nordgren
2026-06-22  9:25                                             ` Phillip Wood
2026-06-22  9:07                                           ` Phillip Wood
2026-06-22 12:26                                             ` Junio C Hamano
2026-06-09 10:11                           ` [PATCH v14 5/6] branch: add branch.<name>.pruneMerged opt-out Harald Nordgren via GitGitGadget
2026-06-16  9:57                             ` Phillip Wood
2026-06-09 10:11                           ` [PATCH v14 6/6] branch: add --dry-run for --prune-merged Harald Nordgren via GitGitGadget
2026-06-16  9:57                             ` Phillip Wood
2026-06-16 18:28                               ` Harald Nordgren
2026-06-15 16:47                           ` [PATCH v15 0/7] branch: delete-merged Harald Nordgren via GitGitGadget
2026-06-15 16:47                             ` [PATCH v15 1/7] branch: add --forked filter for --list mode Harald Nordgren via GitGitGadget
2026-06-15 16:47                             ` [PATCH v15 2/7] branch: convert delete_branches() to a flags argument Harald Nordgren via GitGitGadget
2026-06-15 16:47                             ` [PATCH v15 3/7] branch: let delete_branches skip unmerged branches on bulk refusal Harald Nordgren via GitGitGadget
2026-06-15 16:47                             ` [PATCH v15 4/7] branch: prepare delete_branches for a bulk caller Harald Nordgren via GitGitGadget
2026-06-15 16:47                             ` [PATCH v15 5/7] branch: add --delete-merged <branch> Harald Nordgren via GitGitGadget
2026-06-15 16:47                             ` [PATCH v15 6/7] branch: add branch.<name>.deleteMerged opt-out Harald Nordgren via GitGitGadget
2026-06-15 16:47                             ` [PATCH v15 7/7] branch: add --dry-run for --delete-merged Harald Nordgren via GitGitGadget
2026-06-17 10:01                             ` [PATCH v15 0/7] branch: delete-merged Phillip Wood
2026-06-17 11:17                               ` Harald Nordgren
2026-06-17 14:21                                 ` Phillip Wood
2026-06-17 19:11                                   ` Harald Nordgren
2026-06-18 13:48                                     ` Phillip Wood
2026-06-18 17:53                                       ` Harald Nordgren
2026-06-18 19:25                             ` [PATCH v16 " Harald Nordgren via GitGitGadget
2026-06-18 19:25                               ` [PATCH v16 1/7] branch: add --forked filter for --list mode Harald Nordgren via GitGitGadget
2026-06-18 19:25                               ` [PATCH v16 2/7] branch: convert delete_branches() to a flags argument Harald Nordgren via GitGitGadget
2026-06-18 19:25                               ` [PATCH v16 3/7] branch: let delete_branches skip unmerged branches on bulk refusal Harald Nordgren via GitGitGadget
2026-06-18 19:25                               ` [PATCH v16 4/7] branch: prepare delete_branches for a bulk caller Harald Nordgren via GitGitGadget
2026-06-18 19:25                               ` [PATCH v16 5/7] branch: add --delete-merged <branch> Harald Nordgren via GitGitGadget
2026-06-18 19:25                               ` [PATCH v16 6/7] branch: add branch.<name>.deleteMerged opt-out Harald Nordgren via GitGitGadget
2026-06-18 19:25                               ` [PATCH v16 7/7] branch: add --dry-run for --delete-merged Harald Nordgren via GitGitGadget
2026-06-22  7:29                               ` [PATCH v17 0/7] branch: delete-merged Harald Nordgren via GitGitGadget
2026-06-22  7:29                                 ` [PATCH v17 1/7] branch: add --forked filter for --list mode Harald Nordgren via GitGitGadget
2026-06-22  7:29                                 ` [PATCH v17 2/7] branch: convert delete_branches() to a flags argument Harald Nordgren via GitGitGadget
2026-06-22  7:29                                 ` [PATCH v17 3/7] branch: let delete_branches skip unmerged branches on bulk refusal Harald Nordgren via GitGitGadget
2026-06-22  7:29                                 ` [PATCH v17 4/7] branch: prepare delete_branches for a bulk caller Harald Nordgren via GitGitGadget
2026-06-22  7:29                                 ` [PATCH v17 5/7] branch: add --delete-merged <branch> Harald Nordgren via GitGitGadget
2026-06-22 15:36                                   ` Phillip Wood
2026-06-22  7:29                                 ` [PATCH v17 6/7] branch: add branch.<name>.deleteMerged opt-out Harald Nordgren via GitGitGadget
2026-06-22  7:29                                 ` [PATCH v17 7/7] branch: add --dry-run for --delete-merged Harald Nordgren via GitGitGadget
2026-06-24 21:54                                 ` [PATCH v18 0/7] branch: delete-merged Harald Nordgren via GitGitGadget
2026-06-24 21:55                                   ` [PATCH v18 1/7] branch: add --forked filter for --list mode Harald Nordgren via GitGitGadget
2026-07-10 15:18                                     ` Phillip Wood
2026-07-11 12:30                                       ` Harald Nordgren
2026-06-24 21:55                                   ` [PATCH v18 2/7] branch: convert delete_branches() to a flags argument Harald Nordgren via GitGitGadget
2026-07-10 15:18                                     ` Phillip Wood
2026-07-10 17:25                                       ` Harald Nordgren
2026-07-13 15:28                                         ` Phillip Wood
2026-06-24 21:55                                   ` [PATCH v18 3/7] branch: let delete_branches skip unmerged branches on bulk refusal Harald Nordgren via GitGitGadget
2026-07-10 15:18                                     ` Phillip Wood
2026-06-24 21:55                                   ` [PATCH v18 4/7] branch: prepare delete_branches for a bulk caller Harald Nordgren via GitGitGadget
2026-06-24 21:55                                   ` [PATCH v18 5/7] branch: add --delete-merged <branch> Harald Nordgren via GitGitGadget
2026-07-10 15:24                                     ` Phillip Wood
2026-07-11 13:08                                       ` Harald Nordgren
2026-07-11 19:36                                         ` Harald Nordgren
2026-07-13 15:39                                           ` Phillip Wood
2026-07-13 16:39                                             ` Phillip Wood
2026-07-13 18:17                                               ` Harald Nordgren
2026-07-14 13:01                                                 ` Phillip Wood
2026-07-14 17:00                                                   ` Harald Nordgren
2026-06-24 21:55                                   ` [PATCH v18 6/7] branch: add branch.<name>.deleteMerged opt-out Harald Nordgren via GitGitGadget
2026-07-13 15:27                                     ` Phillip Wood
2026-07-13 16:33                                       ` Harald Nordgren
2026-06-24 21:55                                   ` [PATCH v18 7/7] branch: add --dry-run for --delete-merged Harald Nordgren via GitGitGadget
2026-07-13 15:27                                     ` Phillip Wood
2026-07-13 16:23                                       ` Harald Nordgren
2026-07-10 15:26                                   ` [PATCH v18 0/7] branch: delete-merged Phillip Wood
2026-07-14 18:24                                   ` Harald Nordgren via GitGitGadget [this message]
2026-07-14 18:24                                     ` [PATCH v19 1/7] branch: add --forked filter for --list mode Harald Nordgren via GitGitGadget
2026-07-14 18:24                                     ` [PATCH v19 2/7] branch: convert delete_branches() to a flags argument Harald Nordgren via GitGitGadget
2026-07-14 18:24                                     ` [PATCH v19 3/7] branch: let delete_branches skip unmerged branches on bulk refusal Harald Nordgren via GitGitGadget
2026-07-14 18:24                                     ` [PATCH v19 4/7] branch: prepare delete_branches for a bulk caller Harald Nordgren via GitGitGadget
2026-07-14 18:24                                     ` [PATCH v19 5/7] branch: add --delete-merged <branch> Harald Nordgren via GitGitGadget
2026-07-14 18:24                                     ` [PATCH v19 6/7] branch: add branch.<name>.deleteMerged opt-out Harald Nordgren via GitGitGadget
2026-07-14 18:24                                     ` [PATCH v19 7/7] branch: add --dry-run for --delete-merged Harald Nordgren via GitGitGadget

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=pull.2285.v19.git.git.1784053493.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=haraldnordgren@gmail.com \
    --cc=j6t@kdbg.org \
    --cc=kristofferhaugsbakk@fastmail.com \
    --cc=phillip.wood123@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.