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
next prev 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