Git development
 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox