--- v7
+++ v11
@@ -1,221 +1,128 @@
From: Harald Nordgren <haraldnordgren@gmail.com>
-Combined with --forked or --prune-merged, --all-remotes acts on
-every configured remote, in addition to any explicit <remote>
-arguments. Used alone, it errors out.
+Setting branch.<name>.pruneMerged=false exempts that branch from
+"git branch --prune-merged". Useful for a topic branch you want
+to develop further after an initial round has been merged
+upstream.
+
+Unless --quiet is given, the skip is reported per branch so the
+user knows why their topic was preserved.
+
+Explicit deletion via "git branch -d" continues to consult the
+normal merge check and is not affected by this setting.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
- Documentation/git-branch.adoc | 9 ++++++--
- builtin/branch.c | 41 +++++++++++++++++++++++++----------
- t/t3200-branch.sh | 40 ++++++++++++++++++++++++++++++++++
- 3 files changed, 76 insertions(+), 14 deletions(-)
+ Documentation/config/branch.adoc | 7 +++++++
+ Documentation/git-branch.adoc | 5 +++--
+ builtin/branch.c | 14 ++++++++++++++
+ t/t3200-branch.sh | 30 ++++++++++++++++++++++++++++++
+ 4 files changed, 54 insertions(+), 2 deletions(-)
+diff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc
+index a4db9fa5c8..6c1b5bb9cd 100644
+--- a/Documentation/config/branch.adoc
++++ b/Documentation/config/branch.adoc
+@@ -102,3 +102,10 @@ for details).
+ `git branch --edit-description`. Branch description is
+ automatically added to the `format-patch` cover letter or
+ `request-pull` summary.
++
++`branch.<name>.pruneMerged`::
++ If set to `false`, branch _<name>_ is exempt from
++ `git branch --prune-merged`. Useful for a topic branch you
++ intend to develop further after an initial round has been
++ merged upstream. Defaults to true. Explicit deletion via
++ `git branch -d` is unaffected.
diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
-index 87a26da0cc..6fde8f642e 100644
+index c521b5f4ca..1bd28c4e37 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
-@@ -24,8 +24,8 @@ 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 --forked <remote>...
--git branch [-f] --prune-merged <remote>...
-+git branch --forked (<remote>... | --all-remotes)
-+git branch [-f] --prune-merged (<remote>... | --all-remotes)
-
- DESCRIPTION
- -----------
-@@ -227,6 +227,11 @@ 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`.
-
-+`--all-remotes`::
-+ With `--forked` or `--prune-merged`, act on every
-+ configured remote in addition to any explicit _<remote>_
-+ arguments.
-+
- `-v`::
- `-vv`::
- `--verbose`::
+@@ -226,9 +226,10 @@ the upstream refs refreshed.
+ +
+ A branch is left alone if any of the following holds:
+ its upstream no longer resolves locally; it is checked out in any
+-worktree; or its push destination (`<branch>@{push}`) equals its
++worktree; its push destination (`<branch>@{push}`) equals its
+ upstream (`<branch>@{upstream}`), so it cannot be distinguished
+-from a freshly pulled trunk that just looks "fully merged".
++from a freshly pulled trunk that just looks "fully merged"; or
++`branch.<name>.pruneMerged` is set to `false`.
+ +
+ Branches refused by the "fully merged" safety check are listed as
+ warnings and skipped; pass them to `git branch -D` explicitly if
diff --git a/builtin/branch.c b/builtin/branch.c
-index c48af54301..22c30164ca 100644
+index 1569f29573..187d5d1563 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
-@@ -715,6 +715,13 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int
- free_worktrees(worktrees);
- }
+@@ -875,7 +875,9 @@ static int prune_merged_branches(int argc, const char **argv, int quiet)
+ struct branch *branch = branch_get(short_name);
+ const char *upstream, *push;
+ struct strbuf full = STRBUF_INIT;
++ struct strbuf key = STRBUF_INIT;
+ int skip;
++ int opt_out;
-+static int collect_remote_name(struct remote *remote, void *cb_data)
-+{
-+ struct string_list *remote_names = cb_data;
-+ string_list_insert(remote_names, remote->name);
-+ return 0;
-+}
+ strbuf_addf(&full, "refs/heads/%s", short_name);
+ skip = !!branch_checked_out(full.buf);
+@@ -890,6 +892,18 @@ static int prune_merged_branches(int argc, const char **argv, int quiet)
+ if (!push || !strcmp(push, upstream))
+ continue;
+
++ strbuf_addf(&key, "branch.%s.prunemerged", short_name);
++ if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
++ !opt_out) {
++ if (!quiet)
++ fprintf(stderr,
++ _("Skipping '%s' (branch.%s.pruneMerged is false)\n"),
++ short_name, short_name);
++ strbuf_release(&key);
++ continue;
++ }
++ strbuf_release(&key);
+
- static void parse_forked_args(int argc, const char **argv,
- struct string_list *remote_names,
- struct string_list *tracking_refs)
-@@ -804,7 +811,7 @@ static void collect_default_branch_refs(const struct string_list *remote_names,
+ strvec_push(&deletable, short_name);
}
- }
--static void collect_forked_set(int argc, const char **argv,
-+static void collect_forked_set(int argc, const char **argv, int all_remotes,
- struct string_list *protected_default_refs,
- struct string_list *out)
- {
-@@ -817,6 +824,8 @@ static void collect_forked_set(int argc, const char **argv,
- };
-
- parse_forked_args(argc, argv, &remote_names, &tracking_refs);
-+ if (all_remotes)
-+ for_each_remote(collect_remote_name, &remote_names);
-
- refs_for_each_branch_ref(get_main_ref_store(the_repository),
- collect_forked_branch, &cb);
-@@ -830,15 +839,15 @@ static void collect_forked_set(int argc, const char **argv,
- string_list_clear(&tracking_refs, 0);
- }
-
--static int list_forked_branches(int argc, const char **argv)
-+static int list_forked_branches(int argc, const char **argv, int all_remotes)
- {
- struct string_list out = STRING_LIST_INIT_DUP;
- struct string_list_item *item;
-
-- if (!argc)
-- die(_("--forked requires at least one <remote>"));
-+ if (!argc && !all_remotes)
-+ die(_("--forked requires at least one <remote> or --all-remotes"));
-
-- collect_forked_set(argc, argv, NULL, &out);
-+ collect_forked_set(argc, argv, all_remotes, NULL, &out);
- for_each_string_list_item(item, &out)
- puts(item->string);
-
-@@ -846,8 +855,8 @@ 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 all_remotes, int force, int quiet)
- {
- struct string_list candidates = STRING_LIST_INIT_DUP;
- struct string_list protected_default_refs = STRING_LIST_INIT_DUP;
-@@ -856,10 +865,11 @@ static int prune_merged_branches(int argc, const char **argv, int force,
- int n_not_merged = 0;
- int ret = 0;
-
-- if (!argc)
-- die(_("--prune-merged requires at least one <remote>"));
-+ if (!argc && !all_remotes)
-+ die(_("--prune-merged requires at least one <remote> or --all-remotes"));
-
-- collect_forked_set(argc, argv, &protected_default_refs, &candidates);
-+ collect_forked_set(argc, argv, all_remotes, &protected_default_refs,
-+ &candidates);
-
- for_each_string_list_item(item, &candidates) {
- const char *short_name = item->string;
-@@ -983,6 +993,7 @@ int cmd_branch(int argc,
- unset_upstream = 0, show_current = 0, edit_description = 0;
- int forked = 0;
- int prune_merged = 0;
-+ int all_remotes = 0;
- const char *new_upstream = NULL;
- int noncreate_actions = 0;
- /* possible options */
-@@ -1040,6 +1051,9 @@ int cmd_branch(int argc,
- N_("list local branches forked from the given <remote>s")),
- OPT_BOOL(0, "prune-merged", &prune_merged,
- N_("delete local branches forked from the given <remote>s that are merged into their upstream")),
-+ OPT_BOOL_F(0, "all-remotes", &all_remotes,
-+ N_("with --forked or --prune-merged, act on every configured remote"),
-+ PARSE_OPT_NONEG),
- 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")),
-@@ -1083,6 +1097,9 @@ int cmd_branch(int argc,
- argc = parse_options(argc, argv, prefix, options, builtin_branch_usage,
- 0);
-
-+ 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 &&
- argc == 0)
-@@ -1136,10 +1153,10 @@ int cmd_branch(int argc,
- quiet, 0, NULL);
- goto out;
- } else if (forked) {
-- ret = list_forked_branches(argc, argv);
-+ 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);
- goto out;
- } else if (show_current) {
- print_current_branch_name();
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
-index 23b82615f5..4bd92fe430 100755
+index ad87946081..da7e174e09 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
-@@ -1771,6 +1771,27 @@ test_expect_success '--forked requires at least one <remote>' '
- test_grep "at least one <remote>" err
+@@ -1990,4 +1990,34 @@ test_expect_success '--prune-merged requires at least one <branch>' '
+ test_grep "at least one <branch>" err
'
-+test_expect_success '--forked --all-remotes covers every configured remote' '
-+ git -C forked branch --forked --all-remotes >actual &&
-+ cat >expect <<-\EOF &&
-+ local-foreign
-+ local-one
-+ local-two
-+ main
-+ EOF
-+ test_cmp expect actual
++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 remote add fork ../pm-fork &&
++ test_config -C pm-optout remote.pushDefault fork &&
++ test_config -C pm-optout push.default current &&
++ 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 &&
++ test_config -C pm-optout branch.one.pruneMerged false &&
++
++ git -C pm-optout branch --prune-merged "origin/*" 2>err &&
++
++ git -C pm-optout rev-parse --verify refs/heads/one &&
++ test_must_fail git -C pm-optout rev-parse --verify refs/heads/two &&
++ test_grep "Skipping .one." err
+'
+
-+test_expect_success '--forked --all-remotes still validates explicit <remote>' '
-+ test_must_fail git -C forked branch --forked nope --all-remotes 2>err &&
-+ test_grep "neither a configured remote nor a remote-tracking branch" err
-+'
++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 one-commit &&
++ git -C pm-optout-d branch --set-upstream-to=origin/next one &&
++ test_config -C pm-optout-d branch.one.pruneMerged false &&
+
-+test_expect_success '--all-remotes alone is rejected' '
-+ test_must_fail git -C forked branch --all-remotes 2>err &&
-+ test_grep "requires --forked or --prune-merged" err
-+'
-+
- test_expect_success '--prune-merged: setup' '
- test_create_repo pm-upstream &&
- test_commit -C pm-upstream base &&
-@@ -1957,4 +1978,23 @@ test_expect_success 'branch -d still deletes a pruneMerged=false branch' '
- test_must_fail git -C pm-optout-d rev-parse --verify refs/heads/one
- '
-
-+test_expect_success '--prune-merged --all-remotes covers every configured remote' '
-+ test_when_finished "rm -rf pm-allremotes" &&
-+ 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-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 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 &&
-+
-+ 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
++ git -C pm-optout-d branch -d one &&
++ test_must_fail git -C pm-optout-d rev-parse --verify refs/heads/one
+'
+
test_done
--
gitgitgadget
+