[PATCH v9 0/5] branch: prune-merged
COLD55d
From: Harald Nordgren via GitGitGadget <hidden>
Date: 2026-05-13 19:34:45
Revision v9 of 10 in this series.
Revisions (10)
* --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. 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 | 37 ++++ builtin/branch.c | 292 +++++++++++++++++++++++++++++-- t/t3200-branch.sh | 208 ++++++++++++++++++++++ 4 files changed, 528 insertions(+), 16 deletions(-) base-commit: 59ff4886a579f4bc91e976fe18590b9ae02c7a08 Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2285%2FHaraldNordgren%2Ffetch-prune-local-branches-v9 Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2285/HaraldNordgren/fetch-prune-local-branches-v9 Pull-Request: https://github.com/git/git/pull/2285 Range-diff vs v8: 1: 22fa8515df = 1: 9324b26091 branch: add --forked <remote> 2: b443f0f367 = 2: 2a13e5d4bc branch: let delete_branches warn instead of error on bulk refusal 3: 3032e9c39a ! 3: f87e96e99d branch: add --prune-merged <remote> @@ Documentation/git-branch.adoc: git branch (-c|-C) [<old-branch>] <new-branch> git branch (-d|-D) [-r] <branch-name>... git branch --edit-description [<branch-name>] git branch --forked <remote>... -+git branch [-f] --prune-merged <remote>... ++git branch --prune-merged <remote>... DESCRIPTION ----------- @@ Documentation/git-branch.adoc: Each _<remote>_ may be either the name of a confi ++ +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. ++integrated). 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: static int collect_forked_branch(const struct reference *ref, return 0; } -+static int prune_merged_branches(int argc, const char **argv, int force, -+ int quiet) ++static int prune_merged_branches(int argc, const char **argv, int quiet) +{ + struct string_list candidates = STRING_LIST_INIT_DUP; + struct string_list protected_default_refs = STRING_LIST_INIT_DUP; @@ builtin/branch.c: static int collect_forked_branch(const struct reference *ref, + + if (deletable.nr) + ret = delete_branches(deletable.nr, deletable.v, -+ 1, force, ++ 1, 0, + FILTER_REFS_BRANCHES, quiet, + 1, &n_not_merged); + + if (n_not_merged && !quiet) + fprintf(stderr, + Q_("Skipped %d branch that is not fully merged; " -+ "re-run with --force to delete it anyway.\n", ++ "delete it with 'git branch -D' if you are sure.\n", + "Skipped %d branches that are not fully merged; " -+ "re-run with --force to delete them anyway.\n", ++ "delete them with 'git branch -D' if you are sure.\n", + n_not_merged), + n_not_merged); + @@ builtin/branch.c: int cmd_branch(int argc, ret = list_forked_branches(argc, argv); goto out; + } else if (prune_merged) { -+ ret = prune_merged_branches(argc, argv, force, quiet); ++ ret = prune_merged_branches(argc, argv, quiet); + goto out; } else if (show_current) { print_current_branch_name(); @@ t/t3200-branch.sh: test_expect_success '--forked requires at least one <remote>' + 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 "git branch -D" err && + test_grep ! "If you are sure you want to delete it" err && + git -C pm-unmerged rev-parse --verify refs/heads/wip +' + -+test_expect_success '--prune-merged --force deletes branches regardless of reachability' ' -+ test_when_finished "rm -rf pm-force" && -+ git clone pm-upstream pm-force && -+ git -C pm-force checkout -b wip origin/wip && -+ git -C pm-force branch --set-upstream-to=origin/next wip && -+ test_commit -C pm-force local-only && -+ git -C pm-force checkout - && -+ -+ git -C pm-force branch --force --prune-merged origin && -+ -+ test_must_fail git -C pm-force rev-parse --verify refs/heads/wip -+' -+ +test_expect_success '--prune-merged skips branches whose upstream is gone' ' + test_when_finished "rm -rf pm-upstream-gone" && + git clone pm-upstream pm-upstream-gone && @@ t/t3200-branch.sh: test_expect_success '--forked requires at least one <remote>' + git -C pm-head checkout -b one one-commit && + git -C pm-head branch --set-upstream-to=origin/next one && + -+ git -C pm-head branch --force --prune-merged origin && ++ git -C pm-head branch --prune-merged origin && + + git -C pm-head rev-parse --verify refs/heads/one +' @@ t/t3200-branch.sh: test_expect_success '--forked requires at least one <remote>' + test_when_finished "rm -rf pm-default" && + git clone pm-upstream pm-default && + git -C pm-default checkout --detach && -+ git -C pm-default branch --force --prune-merged origin && ++ git -C pm-default branch --prune-merged origin && + git -C pm-default rev-parse --verify refs/heads/main +' + 4: dd33309344 ! 4: 19b6d94fa7 branch: add branch.<name>.pruneMerged opt-out @@ Documentation/config/branch.adoc: for details). + Explicit deletion via `git branch -d` is unaffected. ## Documentation/git-branch.adoc ## -@@ 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. +@@ Documentation/git-branch.adoc: against whatever the remote-tracking refs say locally. + A branch whose upstream no longer resolves locally is left alone + (its disappearance is not, on its own, evidence that the work was + integrated). The currently checked-out branch in any worktree is +-always preserved, as is the local branch that mirrors _<remote>_'s ++always preserved, 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 ## -@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv, int force, +@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv, int quiet) for_each_string_list_item(item, &candidates) { const char *short_name = item->string; struct strbuf full = STRBUF_INIT; @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv, continue; } strbuf_release(&full); -@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv, int force, +@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv, int quiet) upstream = branch ? branch_get_upstream(branch, NULL) : NULL; if (!upstream || !refs_ref_exists(get_main_ref_store(the_repository), @@ t/t3200-branch.sh: test_expect_success '--prune-merged protects only the default + test_grep "Skipping .one." err +' + -+test_expect_success '--prune-merged --force still honours pruneMerged=false' ' -+ test_when_finished "rm -rf pm-optout-force" && -+ git clone pm-upstream pm-optout-force && -+ git -C pm-optout-force checkout -b wip origin/wip && -+ git -C pm-optout-force branch --set-upstream-to=origin/next wip && -+ test_commit -C pm-optout-force local-only && -+ git -C pm-optout-force checkout - && -+ git -C pm-optout-force config branch.wip.pruneMerged false && -+ -+ git -C pm-optout-force branch --force --prune-merged origin && -+ -+ git -C pm-optout-force rev-parse --verify refs/heads/wip -+' -+ +test_expect_success 'branch -d still deletes a pruneMerged=false branch' ' + test_when_finished "rm -rf pm-optout-d" && + git clone pm-upstream pm-optout-d && 5: 6e81ed3147 ! 5: 6ae95d3f98 branch: add --all-remotes flag @@ Documentation/git-branch.adoc: git branch (-m|-M) [<old-branch>] <new-branch> git branch (-d|-D) [-r] <branch-name>... git branch --edit-description [<branch-name>] -git branch --forked <remote>... --git branch [-f] --prune-merged <remote>... +-git branch --prune-merged <remote>... +git branch --forked (<remote>... | --all-remotes) -+git branch [-f] --prune-merged (<remote>... | --all-remotes) ++git branch --prune-merged (<remote>... | --all-remotes) DESCRIPTION ----------- -@@ 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. +@@ Documentation/git-branch.adoc: 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 @@ builtin/branch.c: static int list_forked_branches(int argc, const char **argv) return 0; } --static int prune_merged_branches(int argc, const char **argv, int force, -- int quiet) +-static int prune_merged_branches(int argc, const char **argv, int quiet) +static int prune_merged_branches(int argc, const char **argv, -+ int all_remotes, int force, int quiet) ++ int all_remotes, int quiet) { struct string_list candidates = STRING_LIST_INIT_DUP; struct string_list protected_default_refs = STRING_LIST_INIT_DUP; -@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv, int force, +@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv, int quiet) int n_not_merged = 0; int ret = 0; @@ builtin/branch.c: int cmd_branch(int argc, + if (all_remotes && !forked && !prune_merged) + die(_("--all-remotes requires --forked or --prune-merged")); ++ + if (!delete && !rename && !copy && !edit_description && !new_upstream && !show_current && !unset_upstream && !forked && !prune_merged && @@ builtin/branch.c: int cmd_branch(int argc, + ret = list_forked_branches(argc, argv, all_remotes); goto out; } else if (prune_merged) { -- ret = prune_merged_branches(argc, argv, force, quiet); -+ ret = prune_merged_branches(argc, argv, all_remotes, force, quiet); +- ret = prune_merged_branches(argc, argv, quiet); ++ ret = prune_merged_branches(argc, argv, all_remotes, quiet); goto out; } else if (show_current) { print_current_branch_name(); -- gitgitgadget