From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>,
Johannes Sixt <j6t@kdbg.org>,
Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v8 0/5] branch: prune-merged
Date: Tue, 12 May 2026 17:07:33 +0000 [thread overview]
Message-ID: <pull.2285.v8.git.git.1778605658.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2285.v7.git.git.1778574229.gitgitgadget@gmail.com>
* 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
Harald Nordgren (5):
branch: add --forked <remote>
branch: let delete_branches warn instead of error on bulk refusal
branch: add --prune-merged <remote>
branch: add branch.<name>.pruneMerged opt-out
branch: add --all-remotes flag
Documentation/config/branch.adoc | 7 +
Documentation/git-branch.adoc | 38 ++++
builtin/branch.c | 291 +++++++++++++++++++++++++++++--
t/t3200-branch.sh | 235 +++++++++++++++++++++++++
4 files changed, 555 insertions(+), 16 deletions(-)
base-commit: 29bd7ed5127255713c1ac2f43b7c6f257d7b4594
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2285%2FHaraldNordgren%2Ffetch-prune-local-branches-v8
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2285/HaraldNordgren/fetch-prune-local-branches-v8
Pull-Request: https://github.com/git/git/pull/2285
Range-diff vs v7:
1: 22fa8515df = 1: 22fa8515df branch: add --forked <remote>
2: b443f0f367 = 2: b443f0f367 branch: let delete_branches warn instead of error on bulk refusal
3: a245009893 ! 3: 3032e9c39a branch: add --prune-merged <remote>
@@ Metadata
## Commit message ##
branch: add --prune-merged <remote>
- Delete the local branches that --forked <remote> would list,
- refusing any whose tip is not reachable from the remote's default
- branch. With --force, delete unconditionally. The currently
- checked-out branch in any worktree is always preserved.
+ Delete the local branches that --forked <remote> would list, but
+ only those whose tip is reachable from their configured upstream
+ remote-tracking branch (branch.<name>.merge): the work has already
+ landed on the upstream it tracks, so the local copy is no longer
+ needed.
+
+ A branch whose upstream no longer resolves locally is left alone --
+ its disappearance is not, on its own, evidence that the work was
+ integrated. With --force, skip the reachability check and delete
+ every branch in the candidate set. The currently checked-out
+ branch in any worktree is always preserved, as is the local branch
+ that mirrors <remote>'s default branch.
+
+ Reachability is read from whatever the remote-tracking refs say
+ locally, so the natural workflow is
+
+ git fetch <remote>
+ git branch --prune-merged <remote>
+
+ with no implicit cleanup driven by fetch itself.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
@@ Documentation/git-branch.adoc: Each _<remote>_ may be either the name of a confi
+`--prune-merged`::
+ Delete the local branches that `--forked` would list for
-+ the same _<remote>_ arguments, but only when the branch's
-+ push destination remote-tracking branch (the branch `git push`
-+ would update; see `branch_get_push` semantics) no longer
-+ resolves locally. In other words: the branch was pushed
-+ under some name on _<remote>_, and that name has since
-+ been pruned upstream.
++ the same _<remote>_ arguments, but only those whose tip is
++ reachable from their configured upstream remote-tracking
++ branch (`branch.<name>.merge`). In other words: the work on
++ the branch has already landed on the upstream it tracks, so
++ the local copy is no longer needed.
+++
++Run `git fetch` first so the upstream remote-tracking branches
++reflect the current state of _<remote>_; reachability is checked
++against whatever the remote-tracking refs say locally.
++
-+As a safety check, branches with commits not yet integrated into
-+their upstream remote-tracking branch are refused; if the upstream
-+itself is gone, the remote's default branch is consulted instead.
-+With `--force` (or `-f`), delete refused branches regardless. The
-+currently checked-out branch in any worktree is always preserved.
++A branch whose upstream no longer resolves locally is left alone
++(its disappearance is not, on its own, evidence that the work was
++integrated). With `--force` (or `-f`), the reachability check is
++skipped and every branch in the candidate set is deleted. The
++currently checked-out branch in any worktree is always preserved,
++as is the local branch that mirrors _<remote>_'s default branch.
+
`-v`::
`-vv`::
@@ builtin/branch.c
#include "column.h"
#include "utf8.h"
#include "ref-filter.h"
-@@ builtin/branch.c: static const char *branch_get_color(enum color_branch ix)
- }
-
- static int branch_merged(int kind, const char *name,
-- struct commit *rev, struct commit *head_rev)
-+ struct commit *rev, struct commit *head_rev,
-+ int no_head_fallback)
- {
- /*
- * This checks whether the merge bases of branch and HEAD (or
-@@ builtin/branch.c: static int branch_merged(int kind, const char *name,
- &oid, NULL)) != NULL)
- reference_rev = lookup_commit_reference(the_repository,
- &oid);
-+
-+ if (!reference_rev && no_head_fallback && upstream &&
-+ starts_with(upstream, "refs/remotes/")) {
-+ const char *remote_name = upstream + strlen("refs/remotes/");
-+ const char *slash = strchr(remote_name, '/');
-+ if (slash) {
-+ struct strbuf head_ref = STRBUF_INIT;
-+ strbuf_add(&head_ref, "refs/remotes/", strlen("refs/remotes/"));
-+ strbuf_add(&head_ref, remote_name, slash - remote_name);
-+ strbuf_addstr(&head_ref, "/HEAD");
-+ if (refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
-+ head_ref.buf,
-+ RESOLVE_REF_READING,
-+ &oid, NULL))
-+ reference_rev = lookup_commit_reference(the_repository,
-+ &oid);
-+ strbuf_release(&head_ref);
-+ }
-+ }
- }
-- if (!reference_rev)
-+ if (!reference_rev) {
-+ if (no_head_fallback) {
-+ free(reference_name_to_free);
-+ return 0;
-+ }
- reference_rev = head_rev;
-+ }
-
- merged = reference_rev ? repo_in_merge_bases(the_repository, rev,
- reference_rev) : 0;
@@ builtin/branch.c: static int branch_merged(int kind, const char *name,
* any of the following code, but during the transition period,
* a gentle reminder is in order.
*/
- if (head_rev != reference_rev) {
-+ if (!no_head_fallback && head_rev != reference_rev) {
- int expect = head_rev ? repo_in_merge_bases(the_repository, rev, head_rev) : 0;
+- int expect = head_rev ? repo_in_merge_bases(the_repository, rev, head_rev) : 0;
++ if (head_rev && head_rev != reference_rev) {
++ int expect = repo_in_merge_bases(the_repository, rev, head_rev);
if (expect < 0)
exit(128);
-@@ builtin/branch.c: static int branch_merged(int kind, const char *name,
-
- static int check_branch_commit(const char *branchname, const char *refname,
- const struct object_id *oid, struct commit *head_rev,
-+ int no_head_fallback,
- int kinds, int force, int warn_only,
- int *n_not_merged)
- {
-@@ builtin/branch.c: static int check_branch_commit(const char *branchname, const char *refname,
- error(_("couldn't look up commit object for '%s'"), refname);
- return -1;
- }
-- if (!force && !branch_merged(kinds, branchname, rev, head_rev)) {
-+ if (!force && !branch_merged(kinds, branchname, rev, head_rev,
-+ no_head_fallback)) {
- if (warn_only) {
- warning(_("the branch '%s' is not fully merged"),
- branchname);
+ if (expect == merged)
@@ 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)
int quiet, int warn_only, int *n_not_merged)
{
struct commit *head_rev = NULL;
+@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int force, int kinds,
+ }
+ branch_name_pos = strcspn(fmt, "%");
+
+- if (!force)
++ if (!force && !no_head_fallback)
+ head_rev = lookup_commit_reference(the_repository, &head_oid);
+
+ for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int force, int kinds,
}
@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
- check_branch_commit(bname.buf, name, &oid, head_rev, kinds,
- force, warn_only, n_not_merged)) {
+ check_branch_commit(bname.buf, name, &oid, head_rev,
-+ no_head_fallback,
+ kinds, force, warn_only, n_not_merged)) {
if (!warn_only)
ret = 1;
@@ builtin/branch.c: static int collect_forked_branch(const struct reference *ref,
+ const char *short_name = item->string;
+ struct strbuf full = STRBUF_INIT;
+ struct branch *branch;
-+ const char *push_ref;
+ const char *upstream;
+
+ strbuf_addf(&full, "refs/heads/%s", short_name);
@@ builtin/branch.c: static int collect_forked_branch(const struct reference *ref,
+
+ branch = branch_get(short_name);
+ upstream = branch ? branch_get_upstream(branch, NULL) : NULL;
-+ if (upstream &&
-+ string_list_has_string(&protected_default_refs, upstream)) {
++ if (!upstream ||
++ !refs_ref_exists(get_main_ref_store(the_repository),
++ upstream))
++ continue;
++ if (string_list_has_string(&protected_default_refs, upstream)) {
+ const char *leaf = strrchr(upstream, '/');
+ if (leaf && !strcmp(leaf + 1, short_name))
+ continue;
+ }
+
-+ push_ref = branch ? branch_get_push(branch, NULL) : NULL;
-+ if (!push_ref)
-+ continue;
-+ if (refs_ref_exists(get_main_ref_store(the_repository),
-+ push_ref))
-+ continue;
-+ if (string_list_has_string(&protected_default_refs, push_ref))
-+ continue;
-+
+ strvec_push(&deletable, short_name);
+ }
+
@@ t/t3200-branch.sh: test_expect_success '--forked requires at least one <remote>'
+test_expect_success '--prune-merged: setup' '
+ test_create_repo pm-upstream &&
+ test_commit -C pm-upstream base &&
-+ git -C pm-upstream branch one base &&
-+ git -C pm-upstream branch two base
-+'
-+
-+test_expect_success '--prune-merged deletes branches whose push ref is gone' '
-+ test_when_finished "rm -rf pm-clean" &&
-+ git clone pm-upstream pm-clean &&
-+ git -C pm-clean branch one --track origin/one &&
-+ git -C pm-clean branch two --track origin/two &&
-+
-+ git -C pm-clean update-ref -d refs/remotes/origin/one &&
-+ git -C pm-clean branch --prune-merged origin &&
-+
-+ test_must_fail git -C pm-clean rev-parse --verify refs/heads/one &&
-+ git -C pm-clean rev-parse --verify refs/heads/two
++ git -C pm-upstream checkout -b next &&
++ test_commit -C pm-upstream one-commit &&
++ test_commit -C pm-upstream two-commit &&
++ git -C pm-upstream branch one HEAD~ &&
++ git -C pm-upstream branch two HEAD &&
++ git -C pm-upstream branch wip main &&
++ git -C pm-upstream checkout main
+'
+
-+test_expect_success '--prune-merged spares in-flight branches whose push ref still exists' '
-+ test_when_finished "rm -rf pm-inflight" &&
-+ git clone pm-upstream pm-inflight &&
-+ git -C pm-inflight branch one --track origin/one &&
++test_expect_success '--prune-merged deletes branches integrated into upstream' '
++ test_when_finished "rm -rf pm-merged" &&
++ git clone pm-upstream pm-merged &&
++ git -C pm-merged branch one one-commit &&
++ git -C pm-merged branch --set-upstream-to=origin/next one &&
++ git -C pm-merged branch two two-commit &&
++ git -C pm-merged branch --set-upstream-to=origin/next two &&
+
-+ git -C pm-inflight branch --prune-merged origin &&
++ git -C pm-merged branch --prune-merged origin &&
+
-+ git -C pm-inflight rev-parse --verify refs/heads/one
++ test_must_fail git -C pm-merged rev-parse --verify refs/heads/one &&
++ test_must_fail git -C pm-merged rev-parse --verify refs/heads/two
+'
+
-+test_expect_success '--prune-merged skips branches with unpushed commits' '
++test_expect_success '--prune-merged spares branches with un-integrated commits' '
+ test_when_finished "rm -rf pm-unmerged" &&
+ git clone pm-upstream pm-unmerged &&
-+ git -C pm-unmerged checkout -b one --track origin/one &&
-+ test_commit -C pm-unmerged unpushed &&
++ git -C pm-unmerged checkout -b wip origin/wip &&
++ git -C pm-unmerged branch --set-upstream-to=origin/next wip &&
++ test_commit -C pm-unmerged local-only &&
+ git -C pm-unmerged checkout - &&
+
-+ git -C pm-unmerged update-ref -d refs/remotes/origin/one &&
+ git -C pm-unmerged branch --prune-merged origin 2>err &&
+ test_grep "not fully merged" err &&
+ test_grep "Skipped 1 branch" err &&
+ test_grep "re-run with --force" err &&
+ test_grep ! "If you are sure you want to delete it" err &&
-+ git -C pm-unmerged rev-parse --verify refs/heads/one
++ git -C pm-unmerged rev-parse --verify refs/heads/wip
+'
+
-+test_expect_success '--prune-merged --force deletes branches with unpushed commits' '
++test_expect_success '--prune-merged --force deletes branches regardless of reachability' '
+ test_when_finished "rm -rf pm-force" &&
+ git clone pm-upstream pm-force &&
-+ git -C pm-force checkout -b one --track origin/one &&
-+ test_commit -C pm-force unpushed &&
++ git -C pm-force checkout -b wip origin/wip &&
++ git -C pm-force branch --set-upstream-to=origin/next wip &&
++ test_commit -C pm-force local-only &&
+ git -C pm-force checkout - &&
+
-+ git -C pm-force update-ref -d refs/remotes/origin/one &&
+ git -C pm-force branch --force --prune-merged origin &&
+
-+ test_must_fail git -C pm-force rev-parse --verify refs/heads/one
++ test_must_fail git -C pm-force rev-parse --verify refs/heads/wip
+'
+
-+test_expect_success '--prune-merged falls back to remote default branch when upstream is gone' '
-+ test_when_finished "rm -rf pm-fallback" &&
-+ git clone pm-upstream pm-fallback &&
-+ git -C pm-fallback branch one --track origin/one &&
-+ git -C pm-fallback update-ref -d refs/remotes/origin/one &&
-+ # Detach HEAD to an unrelated commit so the candidate is not
-+ # reachable from local HEAD. The upstream origin/one is now
-+ # gone; the merged-ness check should fall back to
-+ # refs/remotes/origin/HEAD, against which "one" is reachable.
-+ git -C pm-fallback commit --allow-empty -m unrelated &&
-+ git -C pm-fallback checkout --detach &&
-+ git -C pm-fallback reset --hard HEAD^ &&
-+
-+ git -C pm-fallback branch --prune-merged origin &&
-+
-+ test_must_fail git -C pm-fallback rev-parse --verify refs/heads/one
-+'
++test_expect_success '--prune-merged skips branches whose upstream is gone' '
++ test_when_finished "rm -rf pm-upstream-gone" &&
++ git clone pm-upstream pm-upstream-gone &&
++ git -C pm-upstream-gone branch one one-commit &&
++ git -C pm-upstream-gone branch --set-upstream-to=origin/next one &&
+
-+test_expect_success '--prune-merged refuses when upstream and remote default are both gone' '
-+ test_when_finished "rm -rf pm-both-gone" &&
-+ git clone pm-upstream pm-both-gone &&
-+ git -C pm-both-gone checkout -b one --track origin/one &&
-+ test_commit -C pm-both-gone unpushed &&
-+ git -C pm-both-gone checkout - &&
++ git -C pm-upstream-gone update-ref -d refs/remotes/origin/next &&
++ git -C pm-upstream-gone branch --prune-merged origin &&
+
-+ git -C pm-both-gone update-ref -d refs/remotes/origin/HEAD &&
-+ git -C pm-both-gone update-ref -d refs/remotes/origin/one &&
-+ git -C pm-both-gone branch --prune-merged origin 2>err &&
-+ test_grep "not fully merged" err &&
-+
-+ git -C pm-both-gone rev-parse --verify refs/heads/one
++ git -C pm-upstream-gone rev-parse --verify refs/heads/one
+'
+
+test_expect_success '--prune-merged never deletes the checked-out branch' '
+ test_when_finished "rm -rf pm-head" &&
+ git clone pm-upstream pm-head &&
-+ git -C pm-head checkout -b one --track origin/one &&
++ git -C pm-head checkout -b one one-commit &&
++ git -C pm-head branch --set-upstream-to=origin/next one &&
+
-+ git -C pm-head update-ref -d refs/remotes/origin/one &&
+ git -C pm-head branch --force --prune-merged origin &&
+
+ git -C pm-head rev-parse --verify refs/heads/one
+'
+
-+test_expect_success '--prune-merged deletes when push ref differs from upstream' '
-+ test_when_finished "rm -rf pm-pushdiff" &&
-+ git clone pm-upstream pm-pushdiff &&
-+ git -C pm-pushdiff config push.default current &&
-+ git -C pm-pushdiff branch --track topic-a origin/one &&
-+
-+ git -C pm-pushdiff branch --force --prune-merged origin &&
-+
-+ test_must_fail git -C pm-pushdiff rev-parse --verify refs/heads/topic-a
-+'
-+
+test_expect_success '--prune-merged spares the local default branch' '
+ test_when_finished "rm -rf pm-default" &&
+ git clone pm-upstream pm-default &&
-+ git -C pm-default config push.default current &&
+ git -C pm-default checkout --detach &&
-+ git -C pm-default branch --prune-merged origin &&
++ git -C pm-default branch --force --prune-merged origin &&
+ git -C pm-default rev-parse --verify refs/heads/main
+'
+
+test_expect_success '--prune-merged protects only the default branch by name, not by upstream' '
+ test_when_finished "rm -rf pm-default-alias" &&
+ git clone pm-upstream pm-default-alias &&
-+ git -C pm-default-alias config push.default current &&
+ git -C pm-default-alias branch --track trunk origin/main &&
+ git -C pm-default-alias checkout --detach &&
-+ git -C pm-default-alias branch --force --prune-merged origin &&
++ git -C pm-default-alias branch --prune-merged origin &&
+ git -C pm-default-alias rev-parse --verify refs/heads/main &&
+ test_must_fail git -C pm-default-alias rev-parse --verify refs/heads/trunk
+'
-+
-+test_expect_success '--prune-merged spares branches whose push ref is the default branch' '
-+ test_when_finished "rm -rf pm-pushdefault" &&
-+ git clone pm-upstream pm-pushdefault &&
-+ git -C pm-pushdefault branch --track topic origin/one &&
-+ git -C pm-pushdefault config --add remote.origin.push refs/heads/topic:refs/heads/main &&
-+ git -C pm-pushdefault update-ref -d refs/remotes/origin/one &&
-+ git -C pm-pushdefault update-ref -d refs/remotes/origin/main &&
-+ git -C pm-pushdefault checkout --detach &&
-+ git -C pm-pushdefault branch --prune-merged origin &&
-+ git -C pm-pushdefault rev-parse --verify refs/heads/topic
-+'
+
test_done
4: 2c3f751569 ! 4: dd33309344 branch: add branch.<name>.pruneMerged opt-out
@@ Commit message
branch: add branch.<name>.pruneMerged opt-out
Setting branch.<name>.pruneMerged=false exempts that branch from
- --prune-merged (and from fetch --prune-merged), even with --force.
- Useful for keeping a topic branch around between rounds.
+ --prune-merged, even with --force. Useful for keeping a topic
+ branch around between rounds.
Explicit deletion via 'git branch -d' is unaffected.
@@ Documentation/config/branch.adoc: for details).
+ Explicit deletion via `git branch -d` is unaffected.
## Documentation/git-branch.adoc ##
-@@ Documentation/git-branch.adoc: Each _<remote>_ may be either the name of a configured remote
- Delete the local branches that `--forked` would list for
- the same _<remote>_ arguments, but only when the branch's
- push destination remote-tracking branch (the branch `git push`
-- would update; see `branch_get_push` semantics) no longer
-- resolves locally. In other words: the branch was pushed
-- under some name on _<remote>_, and that name has since
-- been pruned upstream.
-+ would update) no longer resolves locally. In other words:
-+ the branch was pushed under some name on _<remote>_, and
-+ that name has since been pruned upstream.
- +
- As a safety check, branches with commits not yet integrated into
- their upstream remote-tracking branch are refused; if the upstream
- itself is gone, the remote's default branch is consulted instead.
- With `--force` (or `-f`), delete refused branches regardless. The
--currently checked-out branch in any worktree is always preserved.
-+currently checked-out branch in any worktree is always preserved,
-+as is any branch with `branch.<name>.pruneMerged` set to `false`.
+@@ Documentation/git-branch.adoc: A branch whose upstream no longer resolves locally is left alone
+ integrated). With `--force` (or `-f`), the reachability check is
+ skipped and every branch in the candidate set is deleted. The
+ currently checked-out branch in any worktree is always preserved,
+-as is the local branch that mirrors _<remote>_'s default branch.
++as is any branch with `branch.<name>.pruneMerged` set to `false`,
++and the local branch that mirrors _<remote>_'s default branch.
`-v`::
`-vv`::
@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
struct strbuf full = STRBUF_INIT;
+ struct strbuf key = STRBUF_INIT;
struct branch *branch;
- const char *push_ref;
const char *upstream;
+ int opt_out = 0;
@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
}
strbuf_release(&full);
@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv, int force,
- if (upstream &&
- string_list_has_string(&protected_default_refs, upstream)) {
+ upstream = branch ? branch_get_upstream(branch, NULL) : NULL;
+ if (!upstream ||
+ !refs_ref_exists(get_main_ref_store(the_repository),
+- upstream))
++ upstream)) {
++ strbuf_release(&key);
+ continue;
++ }
+ if (string_list_has_string(&protected_default_refs, upstream)) {
const char *leaf = strrchr(upstream, '/');
- if (leaf && !strcmp(leaf + 1, short_name))
+ if (leaf && !strcmp(leaf + 1, short_name)) {
+ strbuf_release(&key);
continue;
+ }
- }
-
- push_ref = branch ? branch_get_push(branch, NULL) : NULL;
-- if (!push_ref)
-+ if (!push_ref) {
-+ strbuf_release(&key);
- continue;
-+ }
- if (refs_ref_exists(get_main_ref_store(the_repository),
-- push_ref))
-+ push_ref)) {
-+ strbuf_release(&key);
-+ continue;
-+ }
-+ if (string_list_has_string(&protected_default_refs, push_ref)) {
-+ strbuf_release(&key);
- continue;
-- if (string_list_has_string(&protected_default_refs, push_ref))
+ }
+
+ strbuf_addf(&key, "branch.%s.prunemerged", short_name);
@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
+ "(branch.%s.pruneMerged is false)\n"),
+ short_name, short_name);
+ strbuf_release(&key);
- continue;
-+ }
++ continue;
+ }
+ strbuf_release(&key);
strvec_push(&deletable, short_name);
}
## t/t3200-branch.sh ##
-@@ t/t3200-branch.sh: test_expect_success '--prune-merged spares branches whose push ref is the defaul
- git -C pm-pushdefault rev-parse --verify refs/heads/topic
+@@ t/t3200-branch.sh: test_expect_success '--prune-merged protects only the default branch by name, no
+ test_must_fail git -C pm-default-alias rev-parse --verify refs/heads/trunk
'
+test_expect_success '--prune-merged honours branch.<name>.pruneMerged=false' '
+ test_when_finished "rm -rf pm-optout" &&
+ git clone pm-upstream pm-optout &&
-+ git -C pm-optout branch one --track origin/one &&
-+ git -C pm-optout branch two --track origin/two &&
++ git -C pm-optout branch one one-commit &&
++ git -C pm-optout branch --set-upstream-to=origin/next one &&
++ git -C pm-optout branch two two-commit &&
++ git -C pm-optout branch --set-upstream-to=origin/next two &&
+ git -C pm-optout config branch.one.pruneMerged false &&
+
-+ git -C pm-optout update-ref -d refs/remotes/origin/one &&
-+ git -C pm-optout update-ref -d refs/remotes/origin/two &&
+ git -C pm-optout branch --prune-merged origin 2>err &&
+
+ git -C pm-optout rev-parse --verify refs/heads/one &&
@@ t/t3200-branch.sh: test_expect_success '--prune-merged spares branches whose pus
+test_expect_success '--prune-merged --force still honours pruneMerged=false' '
+ test_when_finished "rm -rf pm-optout-force" &&
+ git clone pm-upstream pm-optout-force &&
-+ git -C pm-optout-force checkout -b one --track origin/one &&
-+ test_commit -C pm-optout-force unpushed &&
++ git -C pm-optout-force checkout -b wip origin/wip &&
++ git -C pm-optout-force branch --set-upstream-to=origin/next wip &&
++ test_commit -C pm-optout-force local-only &&
+ git -C pm-optout-force checkout - &&
-+ git -C pm-optout-force config branch.one.pruneMerged false &&
++ git -C pm-optout-force config branch.wip.pruneMerged false &&
+
-+ git -C pm-optout-force update-ref -d refs/remotes/origin/one &&
+ git -C pm-optout-force branch --force --prune-merged origin &&
+
-+ git -C pm-optout-force rev-parse --verify refs/heads/one
++ git -C pm-optout-force rev-parse --verify refs/heads/wip
+'
+
+test_expect_success 'branch -d still deletes a pruneMerged=false branch' '
+ test_when_finished "rm -rf pm-optout-d" &&
+ git clone pm-upstream pm-optout-d &&
-+ git -C pm-optout-d branch one --track origin/one &&
++ git -C pm-optout-d branch one one-commit &&
++ git -C pm-optout-d branch --set-upstream-to=origin/next one &&
+ git -C pm-optout-d config branch.one.pruneMerged false &&
+
+ git -C pm-optout-d branch -d one &&
5: f79707ce7c ! 5: 6e81ed3147 branch: add --all-remotes flag
@@ Documentation/git-branch.adoc: git branch (-m|-M) [<old-branch>] <new-branch>
DESCRIPTION
-----------
-@@ Documentation/git-branch.adoc: With `--force` (or `-f`), delete refused branches regardless. The
- currently checked-out branch in any worktree is always preserved,
- as is any branch with `branch.<name>.pruneMerged` set to `false`.
+@@ Documentation/git-branch.adoc: currently checked-out branch in any worktree is always preserved,
+ as is any branch with `branch.<name>.pruneMerged` set to `false`,
+ and the local branch that mirrors _<remote>_'s default branch.
+`--all-remotes`::
+ With `--forked` or `--prune-merged`, act on every
@@ t/t3200-branch.sh: test_expect_success 'branch -d still deletes a pruneMerged=fa
'
+test_expect_success '--prune-merged --all-remotes covers every configured remote' '
-+ test_when_finished "rm -rf pm-allremotes" &&
++ test_when_finished "rm -rf pm-allremotes pm-other" &&
+ git clone pm-upstream pm-allremotes &&
+ test_create_repo pm-other &&
+ test_commit -C pm-other other-base &&
-+ git -C pm-other branch foreign other-base &&
++ git -C pm-other checkout -b stable &&
++ test_commit -C pm-other foreign-commit &&
++ git -C pm-other branch foreign HEAD &&
++ git -C pm-other checkout main &&
++
+ git -C pm-allremotes remote add other ../pm-other &&
+ git -C pm-allremotes fetch other &&
-+ git -C pm-allremotes branch one --track origin/one &&
-+ git -C pm-allremotes branch foreign --track other/foreign &&
++ git -C pm-allremotes branch one one-commit &&
++ git -C pm-allremotes branch --set-upstream-to=origin/next one &&
++ git -C pm-allremotes branch foreign other/foreign &&
++ git -C pm-allremotes branch --set-upstream-to=other/stable foreign &&
+
-+ git -C pm-allremotes update-ref -d refs/remotes/origin/one &&
-+ git -C pm-allremotes update-ref -d refs/remotes/other/foreign &&
-+ git -C pm-allremotes branch --force --prune-merged --all-remotes &&
++ git -C pm-allremotes branch --prune-merged --all-remotes &&
+
+ test_must_fail git -C pm-allremotes rev-parse --verify refs/heads/one &&
+ test_must_fail git -C pm-allremotes rev-parse --verify refs/heads/foreign
--
gitgitgadget
next prev parent reply other threads:[~2026-05-12 17:07 UTC|newest]
Thread overview: 70+ 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 ` Harald Nordgren via GitGitGadget [this message]
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-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
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.v8.git.git.1778605658.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=git@vger.kernel.org \
--cc=haraldnordgren@gmail.com \
--cc=j6t@kdbg.org \
--cc=kristofferhaugsbakk@fastmail.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