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 v13 0/6] branch: prune-merged
Date: Fri, 05 Jun 2026 18:35:47 +0000 [thread overview]
Message-ID: <pull.2285.v13.git.git.1780684553.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2285.v12.git.git.1780477479.gitgitgadget@gmail.com>
* 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 <branch> patterns as positional arguments
(e.g. git branch --prune-merged origin/main 'feature*') instead of
repeating the option.
Harald Nordgren (6):
branch: add --forked filter for --list mode
branch: let delete_branches warn instead of error on bulk refusal
branch: prepare delete_branches for a bulk caller
branch: add --prune-merged <branch>
branch: add branch.<name>.pruneMerged opt-out
branch: add --dry-run for --prune-merged
Documentation/config/branch.adoc | 7 +
Documentation/git-branch.adoc | 41 +++-
builtin/branch.c | 182 ++++++++++++---
ref-filter.c | 70 ++++++
ref-filter.h | 10 +
t/t3200-branch.sh | 367 +++++++++++++++++++++++++++++++
6 files changed, 650 insertions(+), 27 deletions(-)
base-commit: 9ac3f193c05c2237e2b14ebaa1149e9fc8a1abe0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2285%2FHaraldNordgren%2Ffetch-prune-local-branches-v13
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2285/HaraldNordgren/fetch-prune-local-branches-v13
Pull-Request: https://github.com/git/git/pull/2285
Range-diff vs v12:
1: 8834c424fb ! 1: ccd07cff25 branch: add --forked filter for --list mode
@@ Metadata
## Commit message ##
branch: add --forked filter for --list mode
- Add a --forked option to "git branch" list mode that keeps only
+ Add a --forked option to "git branch" list mode that lists only
branches whose configured upstream matches <branch>. The argument
- can be a ref (e.g. "origin/main", "master") or a shell-style
- glob (e.g. "origin/*"). The option can be repeated to widen the
- filter.
+ can be a ref (e.g. "origin/main", "master") or a shell glob
+ (e.g. "origin/*"), and may be repeated to widen the filter.
- Because it is a filter on list mode, --forked composes with the
- existing list-mode filters, so
+ It is an ordinary list filter, so it combines with the others:
git branch --merged origin/main --forked 'origin/*'
- lists branches forked from origin that have already been
- integrated into origin/main, and --no-merged inverts the question.
+ lists branches forked from origin that are already merged into
+ origin/main, and --no-merged inverts the question.
This is the building block for --prune-merged, which deletes the
listed branches once they have landed on their upstream.
@@ Commit message
## Documentation/git-branch.adoc ##
@@ Documentation/git-branch.adoc: git branch [--color[=<when>] | --no-color] [--show-current]
+ [--column[=<options>] | --no-column] [--sort=<key>]
[--merged [<commit>]] [--no-merged [<commit>]]
[--contains [<commit>]] [--no-contains [<commit>]]
- [--points-at <object>] [--format=<format>]
+ [(--forked <branch>)...]
+ [--points-at <object>] [--format=<format>]
[(-r|--remotes) | (-a|--all)]
[--list] [<pattern>...]
- git branch [--track[=(direct|inherit)] | --no-track] [-f]
-@@ Documentation/git-branch.adoc: This option is only applicable in non-verbose mode.
- Print the name of the current branch. In detached `HEAD` state,
- nothing is printed.
+@@ Documentation/git-branch.adoc: merged into the named commit (i.e. the branches whose tip commits are
+ reachable from the named commit) will be listed. With `--no-merged` only
+ branches not merged into the named commit will be listed. If the _<commit>_
+ argument is missing it defaults to `HEAD` (i.e. the tip of the current
+-branch).
++branch). With `--forked`, only branches whose configured upstream matches
++the given branch or pattern will be listed.
+
+ The command's second form creates a new branch head named _<branch-name>_
+ which points to the current `HEAD`, or _<start-point>_ if given. As a
+@@ Documentation/git-branch.adoc: superproject's "origin/main", but tracks the submodule's "origin/main".
+ Only list branches whose tips are not reachable from
+ _<commit>_ (`HEAD` if not specified). Implies `--list`.
+`--forked <branch>`::
-+ List only branches whose configured upstream matches
++ Only list branches whose configured upstream matches
+ _<branch>_. The argument can be a ref (e.g. `origin/main`,
+ `master`) or a shell-style glob (e.g. `'origin/*'`). The
-+ option can be repeated to widen the filter.
++ option can be repeated to widen the filter. Implies `--list`.
+
- `-v`::
- `-vv`::
- `--verbose`::
+ `--points-at <object>`::
+ Only list branches of _<object>_.
+
## builtin/branch.c ##
@@
- #include "help.h"
- #include "advice.h"
#include "commit-reach.h"
-+#include "wildmatch.h"
static const char * const builtin_branch_usage[] = {
- N_("git branch [<options>] [-r | -a] [--merged] [--no-merged]"),
@@ builtin/branch.c
N_("git branch [<options>] [-f] [--recurse-submodules] <branch-name> [<start-point>]"),
N_("git branch [<options>] [-l] [<pattern>...]"),
N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
-@@ builtin/branch.c: static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
- return strbuf_detach(&fmt, NULL);
+@@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const char *newname, int
+ free_worktrees(worktrees);
}
-+static void filter_array_by_forked(struct ref_array *array,
-+ const struct string_list *upstreams);
++static int parse_opt_forked(const struct option *opt, const char *arg, int unset)
++{
++ struct ref_filter *filter = opt->value;
+
- static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sorting,
-- struct ref_format *format, struct string_list *output)
-+ struct ref_format *format, struct string_list *output,
-+ const struct string_list *forked_upstreams)
- {
- int i;
- struct ref_array array;
-@@ builtin/branch.c: static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
++ BUG_ON_OPT_NEG(unset);
++ if (ref_filter_forked_add(filter, arg) < 0)
++ die(_("'%s' is not a valid branch or pattern"), arg);
++ return 0;
++}
++
+ static GIT_PATH_FUNC(edit_description, "EDIT_DESCRIPTION")
- filter_refs(&array, filter, filter->kind);
+ static int edit_branch_description(const char *branch_name)
+@@ builtin/branch.c: int cmd_branch(int argc,
+ 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")),
++ OPT_CALLBACK_F(0, "forked", &filter, N_("branch"),
++ N_("print only branches whose upstream matches <branch> (repeatable)"),
++ PARSE_OPT_NONEG, parse_opt_forked),
+ OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")),
+ OPT_REF_SORT(&sorting_options),
+ OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"),
+@@ builtin/branch.c: int cmd_branch(int argc,
+ list = 1;
-+ if (forked_upstreams->nr)
-+ filter_array_by_forked(&array, forked_upstreams);
-+
- if (filter->verbose)
- maxwidth = calc_maxwidth(&array, strlen(remote_prefix));
+ if (filter.with_commit || filter.no_commit ||
+- filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
++ filter.reachable_from || filter.unreachable_from ||
++ filter.points_at.nr || filter.forked.nr)
+ list = 1;
-@@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const char *newname, int
- free_worktrees(worktrees);
+ noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
+
+ ## ref-filter.c ##
+@@ ref-filter.c: static int filter_exclude_match(struct ref_filter *filter, const char *refname)
+ return match_pattern(filter->exclude.v, refname, filter->ignore_case);
}
-+struct upstream_pattern {
-+ char *name;
-+ int is_wildcard;
-+};
-+
-+static void upstream_pattern_list_clear(struct upstream_pattern *items,
-+ size_t nr)
-+{
-+ size_t i;
-+ for (i = 0; i < nr; i++)
-+ free(items[i].name);
-+ free(items);
-+}
-+
+static const char *short_upstream_name(const char *full_ref)
+{
+ const char *short_name = full_ref;
@@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const c
+ return short_name;
+}
+
-+static int parse_one_forked_arg(const char *arg, struct upstream_pattern *out)
-+{
-+ struct object_id oid;
-+ char *full_ref = NULL;
-+
-+ if (has_glob_specials(arg)) {
-+ out->name = xstrdup(arg);
-+ out->is_wildcard = 1;
-+ return 0;
-+ }
-+
-+ if (repo_dwim_ref(the_repository, arg, strlen(arg), &oid,
-+ &full_ref, 0) == 1 &&
-+ (starts_with(full_ref, "refs/heads/") ||
-+ starts_with(full_ref, "refs/remotes/"))) {
-+ out->name = xstrdup(short_upstream_name(full_ref));
-+ out->is_wildcard = 0;
-+ free(full_ref);
-+ return 0;
-+ }
-+ free(full_ref);
-+ return -1;
-+}
-+
-+static void parse_forked_args(const struct string_list *args,
-+ struct upstream_pattern **patterns_out,
-+ size_t *nr_out)
-+{
-+ struct upstream_pattern *patterns;
-+ size_t i;
-+
-+ ALLOC_ARRAY(patterns, args->nr);
-+ for (i = 0; i < args->nr; i++) {
-+ const char *arg = args->items[i].string;
-+ if (parse_one_forked_arg(arg, &patterns[i]) < 0) {
-+ upstream_pattern_list_clear(patterns, i);
-+ die(_("'%s' is not a valid branch or pattern"), arg);
-+ }
-+ }
-+ *patterns_out = patterns;
-+ *nr_out = args->nr;
-+}
-+
-+static int upstream_matches(const char *short_upstream,
-+ const struct upstream_pattern *patterns,
-+ size_t nr)
-+{
-+ size_t i;
-+
-+ for (i = 0; i < nr; i++) {
-+ const struct upstream_pattern *p = &patterns[i];
-+ if (p->is_wildcard) {
-+ if (!wildmatch(p->name, short_upstream, WM_PATHNAME))
-+ return 1;
-+ } else if (!strcmp(p->name, short_upstream)) {
-+ return 1;
-+ }
-+ }
-+ return 0;
-+}
-+
-+static int branch_upstream_matches(const char *full_refname,
-+ const struct upstream_pattern *patterns,
-+ size_t nr_patterns)
++/*
++ * Match the configured upstream of a branch against the registered
++ * --forked patterns. Exact patterns are compared against the full
++ * upstream refname so they are unambiguous; glob patterns are matched
++ * against the abbreviated upstream so that a glob such as origin/...
++ * works as typed.
++ */
++static int filter_forked_match(struct ref_filter *filter, const char *refname)
+{
+ const char *short_name;
+ struct branch *branch;
+ const char *upstream;
++ int i;
+
-+ if (!skip_prefix(full_refname, "refs/heads/", &short_name))
++ if (!skip_prefix(refname, "refs/heads/", &short_name))
+ return 0;
+ branch = branch_get(short_name);
+ if (!branch)
@@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const c
+ upstream = branch_get_upstream(branch, NULL);
+ if (!upstream)
+ return 0;
-+ return upstream_matches(short_upstream_name(upstream),
-+ patterns, nr_patterns);
++
++ for (i = 0; i < filter->forked.nr; i++) {
++ const char *pattern = filter->forked.v[i];
++ if (has_glob_specials(pattern)) {
++ if (!wildmatch(pattern, short_upstream_name(upstream),
++ WM_PATHNAME))
++ return 1;
++ } else if (!strcmp(pattern, upstream)) {
++ return 1;
++ }
++ }
++ return 0;
+}
+
-+static void filter_array_by_forked(struct ref_array *array,
-+ const struct string_list *upstreams)
++int ref_filter_forked_add(struct ref_filter *filter, const char *arg)
+{
-+ struct upstream_pattern *patterns = NULL;
-+ size_t nr_patterns = 0;
-+ int i, kept = 0;
-+
-+ parse_forked_args(upstreams, &patterns, &nr_patterns);
++ struct object_id oid;
++ char *full_ref = NULL;
+
-+ for (i = 0; i < array->nr; i++) {
-+ struct ref_array_item *item = array->items[i];
-+ if (branch_upstream_matches(item->refname,
-+ patterns, nr_patterns))
-+ array->items[kept++] = item;
-+ else
-+ free_ref_array_item(item);
++ if (has_glob_specials(arg)) {
++ strvec_push(&filter->forked, arg);
++ return 0;
+ }
-+ array->nr = kept;
+
-+ upstream_pattern_list_clear(patterns, nr_patterns);
++ if (repo_dwim_ref(the_repository, arg, strlen(arg), &oid,
++ &full_ref, 0) == 1 &&
++ (starts_with(full_ref, "refs/heads/") ||
++ starts_with(full_ref, "refs/remotes/"))) {
++ strvec_push(&filter->forked, full_ref);
++ free(full_ref);
++ return 0;
++ }
++ free(full_ref);
++ return -1;
+}
+
- static GIT_PATH_FUNC(edit_description, "EDIT_DESCRIPTION")
-
- static int edit_branch_description(const char *branch_name)
-@@ 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;
-+ struct string_list forked_upstreams = STRING_LIST_INIT_DUP;
- 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_STRING_LIST(0, "forked", &forked_upstreams, N_("branch"),
-+ N_("list local branches whose upstream matches <branch> (repeatable)")),
- 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,
- list = 1;
-
- if (filter.with_commit || filter.no_commit ||
-- filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
-+ filter.reachable_from || filter.unreachable_from ||
-+ filter.points_at.nr || forked_upstreams.nr)
- list = 1;
-
- noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
-@@ builtin/branch.c: int cmd_branch(int argc,
- ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase);
- ref_sorting_set_sort_flags_all(
- sorting, REF_SORTING_DETACHED_HEAD_FIRST, 1);
-- print_ref_list(&filter, sorting, &format, &output);
-+ print_ref_list(&filter, sorting, &format, &output,
-+ &forked_upstreams);
- print_columns(&output, colopts, NULL);
- string_list_clear(&output, 0);
- ref_sorting_release(sorting);
-@@ builtin/branch.c: int cmd_branch(int argc,
-
- out:
- string_list_clear(&sorting_options, 0);
-+ string_list_clear(&forked_upstreams, 0);
- return ret;
- }
-
- ## ref-filter.c ##
-@@ ref-filter.c: static int filter_one(const struct reference *ref, void *cb_data)
- }
-
- /* Free memory allocated for a ref_array_item */
--static void free_array_item(struct ref_array_item *item)
-+void free_ref_array_item(struct ref_array_item *item)
- {
- free((char *)item->symref);
- if (item->value) {
-@@ ref-filter.c: static int filter_and_format_one(const struct reference *ref, void *cb_data)
-
- strbuf_release(&output);
- strbuf_release(&err);
-- free_array_item(item);
-+ free_ref_array_item(item);
+ /*
+ * We need to seek to the reference right after a given marker but excluding any
+ * matching references. So we seek to the lexicographically next reference.
+@@ ref-filter.c: static struct ref_array_item *apply_ref_filter(const struct reference *ref,
+ if (filter->points_at.nr && !match_points_at(&filter->points_at, ref->oid, ref->name))
+ return NULL;
++ if (filter->forked.nr && !filter_forked_match(filter, ref->name))
++ return NULL;
++
/*
- * Increment the running count of refs that match the filter. If
-@@ ref-filter.c: void ref_array_clear(struct ref_array *array)
- int i;
-
- for (i = 0; i < array->nr; i++)
-- free_array_item(array->items[i]);
-+ free_ref_array_item(array->items[i]);
- FREE_AND_NULL(array->items);
- array->nr = array->alloc = 0;
-
-@@ ref-filter.c: static void reach_filter(struct ref_array *array,
- if (is_merged == include_reached)
- array->items[array->nr++] = array->items[i];
- else
-- free_array_item(item);
-+ free_ref_array_item(item);
- }
-
- clear_commit_marks_many(old_nr, to_clear, ALL_REV_FLAGS);
-@@ ref-filter.c: void pretty_print_ref(const char *name, const struct object_id *oid,
-
- strbuf_release(&err);
- strbuf_release(&output);
-- free_array_item(ref_item);
-+ free_ref_array_item(ref_item);
- }
-
- static int parse_sorting_atom(const char *atom)
+ * A merge filter is applied on refs pointing to commits. Hence
+ * obtain the commit using the 'oid' available and discard all
+@@ ref-filter.c: void ref_filter_init(struct ref_filter *filter)
+ void ref_filter_clear(struct ref_filter *filter)
+ {
+ strvec_clear(&filter->exclude);
++ strvec_clear(&filter->forked);
+ oid_array_clear(&filter->points_at);
+ commit_list_free(filter->with_commit);
+ commit_list_free(filter->no_commit);
## ref-filter.h ##
-@@ ref-filter.h: void filter_and_format_refs(struct ref_filter *filter, unsigned int type,
- struct ref_format *format);
- /* Clear all memory allocated to ref_array */
- void ref_array_clear(struct ref_array *array);
-+/* Free a single item from a ref_array */
-+void free_ref_array_item(struct ref_array_item *item);
- /* Used to verify if the given format is correct and to parse out the used atoms */
- int verify_ref_format(struct ref_format *format);
- /* Sort the given ref_array as per the ref_sorting provided */
+@@ ref-filter.h: struct ref_filter {
+ const char **name_patterns;
+ const char *start_after;
+ struct strvec exclude;
++ struct strvec forked;
+ struct oid_array points_at;
+ struct commit_list *with_commit;
+ struct commit_list *no_commit;
+@@ ref-filter.h: struct ref_format {
+ #define REF_FILTER_INIT { \
+ .points_at = OID_ARRAY_INIT, \
+ .exclude = STRVEC_INIT, \
++ .forked = STRVEC_INIT, \
+ }
+ #define REF_FORMAT_INIT { \
+ .use_color = GIT_COLOR_UNKNOWN, \
+@@ ref-filter.h: void ref_sorting_release(struct ref_sorting *);
+ struct ref_sorting *ref_sorting_options(struct string_list *);
+ /* Function to parse --merged and --no-merged options */
+ int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset);
++/*
++ * Register a --forked <branch> pattern on the filter. The argument is
++ * either a ref, which is resolved to its full refname, or a shell-style
++ * glob. Branches are kept only when their configured upstream matches
++ * one of the registered patterns. Returns -1 if the argument is not a
++ * valid ref or pattern.
++ */
++int ref_filter_forked_add(struct ref_filter *filter, const char *arg);
+ /* Get the current HEAD's description */
+ char *get_head_description(void);
+ /* Set up translated strings in the output. */
## t/t3200-branch.sh ##
@@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
2: 6c95e4e77c ! 2: a7672713f6 branch: let delete_branches warn instead of error on bulk refusal
@@ Metadata
## Commit message ##
branch: let delete_branches warn instead of error on bulk refusal
- Add a warn_only flag to delete_branches() and check_branch_commit()
- so a bulk caller can report not-fully-merged branches as one-line
- warnings and continue, instead of erroring with the four-line "use
- 'git branch -D'" advice that the standalone "git branch -d" path
- emits. Default callers pass 0 and are unaffected.
+ Add a warn-only mode to delete_branches() and check_branch_commit()
+ so a bulk caller can report branches that are not fully merged as a
+ short warning and carry on, rather than erroring with the longer
+ "use 'git branch -D'" advice that the plain "git branch -d" path
+ emits. Existing callers are unaffected.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
## builtin/branch.c ##
@@ builtin/branch.c: static int branch_merged(int kind, const char *name,
+ return merged;
+ }
++enum delete_branch_flags {
++ DELETE_BRANCH_FORCE = (1 << 0),
++ DELETE_BRANCH_QUIET = (1 << 1),
++ DELETE_BRANCH_WARN_ONLY = (1 << 2),
++};
++
static int check_branch_commit(const char *branchname, const char *refname,
const struct object_id *oid, struct commit *head_rev,
- int kinds, int force)
-+ int kinds, int force, int warn_only)
++ int kinds, unsigned int flags)
{
++ int force = flags & DELETE_BRANCH_FORCE;
struct commit *rev = lookup_commit_reference(the_repository, oid);
if (!force && !rev) {
-@@ 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)) {
@@ builtin/branch.c: static int check_branch_commit(const char *branchname, const c
- advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
- _("If you are sure you want to delete it, "
- "run 'git branch -D %s'"), branchname);
-+ if (warn_only) {
++ if (flags & DELETE_BRANCH_WARN_ONLY) {
+ warning(_("the branch '%s' is not fully merged"),
+ branchname);
+ } else {
@@ builtin/branch.c: static int check_branch_commit(const char *branchname, const c
}
return 0;
@@ builtin/branch.c: static void delete_branch_config(const char *branchname)
+ strbuf_release(&buf);
}
- static int delete_branches(int argc, const char **argv, int force, int kinds,
+-static int delete_branches(int argc, const char **argv, int force, int kinds,
- int quiet)
-+ int quiet, int warn_only)
++static int delete_branches(int argc, const char **argv, int kinds,
++ unsigned int flags)
{
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;
++ int force = flags & DELETE_BRANCH_FORCE;
++ int 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,
+
+ for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
+ char *target = NULL;
+- int flags = 0;
++ int ref_flags = 0;
+
+ copy_branchname(&bname, argv[i], allowed_interpret);
+ free(name);
+@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int force, int kinds,
+ RESOLVE_REF_READING
+ | RESOLVE_REF_NO_RECURSE
+ | RESOLVE_REF_ALLOW_BAD_NAME,
+- &oid, &flags);
++ &oid, &ref_flags);
+ if (!target) {
+ if (remote_branch) {
+ error(_("remote-tracking branch '%s' not found"), bname.buf);
+@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int force, int kinds,
+ | RESOLVE_REF_NO_RECURSE
+ | RESOLVE_REF_ALLOW_BAD_NAME,
+ &oid,
+- &flags);
++ &ref_flags);
+ FREE_AND_NULL(virtual_name);
- if (!(flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
+ if (virtual_target)
+@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int force, int kinds,
+ continue;
+ }
+
+- if (!(flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
++ if (!(ref_flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
check_branch_commit(bname.buf, name, &oid, head_rev, kinds,
- force)) {
- ret = 1;
-+ force, warn_only)) {
-+ if (!warn_only)
++ flags)) {
++ if (!(flags & DELETE_BRANCH_WARN_ONLY))
+ ret = 1;
goto next;
}
+ item = string_list_append(&refs_to_delete, name);
+- item->util = xstrdup((flags & REF_ISBROKEN) ? "broken"
+- : (flags & REF_ISSYMREF) ? target
++ item->util = xstrdup((ref_flags & REF_ISBROKEN) ? "broken"
++ : (ref_flags & REF_ISSYMREF) ? target
+ : repo_find_unique_abbrev(the_repository, &oid, DEFAULT_ABBREV));
+
+ next:
@@ builtin/branch.c: int cmd_branch(int argc,
if (delete) {
if (!argc)
die(_("branch name required"));
- ret = delete_branches(argc, argv, delete > 1, filter.kind, quiet);
-+ ret = delete_branches(argc, argv, delete > 1, filter.kind,
-+ quiet, 0);
++ ret = delete_branches(argc, argv, filter.kind,
++ (delete > 1 ? DELETE_BRANCH_FORCE : 0) |
++ (quiet ? DELETE_BRANCH_QUIET : 0));
goto out;
} else if (show_current) {
print_current_branch_name();
3: 004a96f7a4 ! 3: 5ee7643d3a branch: prepare delete_branches for a bulk caller
@@ Metadata
## Commit message ##
branch: prepare delete_branches for a bulk caller
- Add no_head_fallback and dry_run flags to delete_branches() so a
- bulk caller (the upcoming --prune-merged) can ask strictly about
- merged-into-upstream without a silent fallback to HEAD, and
- rehearse deletions with the same "Would delete branch ..." wording
- as the live run. Existing callers pass 0 for both and keep current
- behavior.
-
- When no_head_fallback is set, head_rev stays NULL through to
- branch_merged(), whose "merged to X but not yet merged to HEAD"
- reminder otherwise compares against HEAD. For the bulk caller
- every candidate is known to have an upstream, so HEAD is
- irrelevant. Guard the block on head_rev so the NULL case skips
- it instead of treating "NULL != reference_rev" as "diverges from
- HEAD" and emitting a spurious warning.
+ Teach delete_branches() two new modes for the upcoming
+ --prune-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.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
@@ builtin/branch.c: static int branch_merged(int kind, const char *name,
if (expect < 0)
exit(128);
if (expect == merged)
-@@ builtin/branch.c: static void delete_branch_config(const char *branchname)
- }
+@@ builtin/branch.c: enum delete_branch_flags {
+ DELETE_BRANCH_FORCE = (1 << 0),
+ DELETE_BRANCH_QUIET = (1 << 1),
+ DELETE_BRANCH_WARN_ONLY = (1 << 2),
++ DELETE_BRANCH_NO_HEAD_FALLBACK = (1 << 3),
++ DELETE_BRANCH_DRY_RUN = (1 << 4),
+ };
- static int delete_branches(int argc, const char **argv, int force, int kinds,
-- int quiet, int warn_only)
-+ int quiet, int warn_only, int no_head_fallback,
-+ int dry_run)
- {
- 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,
+ 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,
+ int remote_branch = 0;
+ int force = flags & DELETE_BRANCH_FORCE;
+ int quiet = flags & DELETE_BRANCH_QUIET;
++ int dry_run = flags & DELETE_BRANCH_DRY_RUN;
++ int 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,
}
branch_name_pos = strcspn(fmt, "%");
@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
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 kinds,
free(target);
}
@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
char *refname = name + branch_name_pos;
if (!quiet)
printf(remote_branch
-@@ builtin/branch.c: int cmd_branch(int argc,
- if (!argc)
- die(_("branch name required"));
- ret = delete_branches(argc, argv, delete > 1, filter.kind,
-- quiet, 0);
-+ quiet, 0, 0, 0);
- goto out;
- } else if (show_current) {
- print_current_branch_name();
4: cccfdb831c ! 4: 5f913c445c branch: add --prune-merged <branch>
@@ Commit message
git branch --prune-merged <branch>...
deletes the local branches that "--forked <branch>" would list,
- restricted to those whose tip is reachable from their configured
+ 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.
- Reachability is read from local refs; nothing is fetched. Users
- who want fresh upstream refs run "git fetch" first.
+ Reachability is read from local refs; nothing is fetched. Run
+ "git fetch" first if you want fresh upstream refs.
- Three classes of branches are spared:
+ Three kinds of branches are spared:
* any branch checked out in any worktree;
- * any branch whose upstream no longer resolves locally (its
- disappearance is not, on its own, evidence of integration);
+ * any branch whose upstream no longer resolves locally, since a
+ missing upstream is not by itself a sign of integration;
* any branch whose push destination equals its upstream
- (<branch>@{push} == <branch>@{upstream}). Such a branch
- cannot be distinguished from a freshly pulled trunk that
- just looks "fully merged", e.g. local "main" tracking and
- pushing to "origin/main" right after a pull. Only branches
- that push somewhere other than their upstream (typically
- topics in a fork-based workflow) are treated as candidates.
+ (<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 left
+ alone. Only branches that push somewhere other than their
+ upstream, typically topics in a fork workflow, are candidates.
- Deletion goes through the existing delete_branches() in warn-only
- mode and with the HEAD-fallback disabled: a branch that is not
- yet fully merged to its upstream is reported as a one-line warning
- and skipped, so a single un-mergeable topic does not abort the
- whole sweep. We only act on upstream-merged status.
+ Branches that are not yet merged into their upstream are reported
+ as a short warning and skipped, so one unmerged topic does not
+ abort the whole sweep.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
@@ 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 (--prune-merged <branch>)...
++git branch --prune-merged <branch>...
DESCRIPTION
-----------
@@ Documentation/git-branch.adoc: This option is only applicable in non-verbose mode.
- `master`) or a shell-style glob (e.g. `'origin/*'`). The
- option can be repeated to widen the filter.
+ Print the name of the current branch. In detached `HEAD` state,
+ nothing is printed.
-+`--prune-merged <branch>`::
++`--prune-merged <branch>...`::
+ Delete the local branches that `--forked` would list for the
-+ same _<branch>_, 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. May be given more than once to
-+ union the matches; positional arguments are not accepted.
++ 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
++ --prune-merged origin/main 'feature*'`.
++
+Reachability is checked against whatever the upstream refs say
+locally; nothing is fetched. Run `git fetch` first if you want
@@ 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>] (--prune-merged <branch>)..."),
++ N_("git branch [<options>] --prune-merged <branch>..."),
NULL
};
-@@ builtin/branch.c: static int upstream_matches(const char *short_upstream,
+@@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const char *arg, int unset
return 0;
}
--static int branch_upstream_matches(const char *full_refname,
-+static int branch_upstream_matches(const char *short_branch_name,
- const struct upstream_pattern *patterns,
- size_t nr_patterns)
- {
-- const char *short_name;
-- struct branch *branch;
-+ struct branch *branch = branch_get(short_branch_name);
- const char *upstream;
-
-- if (!skip_prefix(full_refname, "refs/heads/", &short_name))
-- return 0;
-- branch = branch_get(short_name);
- if (!branch)
- return 0;
- upstream = branch_get_upstream(branch, NULL);
-@@ builtin/branch.c: static void filter_array_by_forked(struct ref_array *array,
-
- for (i = 0; i < array->nr; i++) {
- struct ref_array_item *item = array->items[i];
-- if (branch_upstream_matches(item->refname,
-- patterns, nr_patterns))
-+ const char *short_name;
-+ if (skip_prefix(item->refname, "refs/heads/", &short_name) &&
-+ branch_upstream_matches(short_name, patterns, nr_patterns))
- array->items[kept++] = item;
- else
- free_ref_array_item(item);
-@@ builtin/branch.c: static void filter_array_by_forked(struct ref_array *array,
- upstream_pattern_list_clear(patterns, nr_patterns);
- }
-
-+struct forked_cb {
-+ const struct upstream_pattern *patterns;
-+ size_t nr_patterns;
-+ struct string_list *out;
-+};
-+
-+static int collect_forked_branch(const struct reference *ref, void *cb_data)
-+{
-+ struct forked_cb *cb = cb_data;
-+
-+ if (ref->flags & REF_ISSYMREF)
-+ return 0;
-+ if (branch_upstream_matches(ref->name, cb->patterns, cb->nr_patterns))
-+ string_list_append(cb->out, ref->name);
-+ return 0;
-+}
-+
-+static void collect_forked_set(const struct string_list *upstreams,
-+ struct string_list *out)
-+{
-+ struct upstream_pattern *patterns = NULL;
-+ size_t nr_patterns = 0;
-+ struct forked_cb cb;
-+
-+ parse_forked_args(upstreams, &patterns, &nr_patterns);
-+ cb.patterns = patterns;
-+ cb.nr_patterns = nr_patterns;
-+ cb.out = out;
-+
-+ refs_for_each_branch_ref(get_main_ref_store(the_repository),
-+ collect_forked_branch, &cb);
-+
-+ string_list_sort(out);
-+
-+ upstream_pattern_list_clear(patterns, nr_patterns);
-+}
-+
-+static int prune_merged_branches(const struct string_list *upstreams,
++static int prune_merged_branches(int argc, const char **argv,
+ int quiet)
+{
+ struct ref_store *refs = get_main_ref_store(the_repository);
-+ struct string_list candidates = STRING_LIST_INIT_DUP;
++ struct ref_filter filter = REF_FILTER_INIT;
++ struct ref_array candidates;
+ struct strvec deletable = STRVEC_INIT;
-+ struct string_list_item *item;
-+ int ret = 0;
++ int i, ret = 0;
+
-+ if (!upstreams->nr)
++ if (!argc)
+ die(_("--prune-merged requires at least one <branch>"));
+
-+ collect_forked_set(upstreams, &candidates);
++ 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_each_string_list_item(item, &candidates) {
-+ const char *short_name = item->string;
-+ struct branch *branch = branch_get(short_name);
++ filter.kind = FILTER_REFS_BRANCHES;
++ memset(&candidates, 0, sizeof(candidates));
++ filter_refs(&candidates, &filter, filter.kind);
++
++ for (i = 0; i < candidates.nr; i++) {
++ const char *full_name = candidates.items[i]->refname;
++ const char *short_name;
++ struct branch *branch;
+ const char *upstream, *push;
-+ struct strbuf full = STRBUF_INIT;
-+ int skip;
+
-+ strbuf_addf(&full, "refs/heads/%s", short_name);
-+ skip = !!branch_checked_out(full.buf);
-+ strbuf_release(&full);
-+ if (skip)
++ if (!skip_prefix(full_name, "refs/heads/", &short_name))
++ continue;
++ if (branch_checked_out(full_name))
+ continue;
+
++ branch = branch_get(short_name);
+ upstream = branch ? branch_get_upstream(branch, NULL) : NULL;
+ if (!upstream || !refs_ref_exists(refs, upstream))
+ continue;
@@ builtin/branch.c: static void filter_array_by_forked(struct ref_array *array,
+
+ if (deletable.nr)
+ ret = delete_branches(deletable.nr, deletable.v,
-+ 0, /* force */
+ FILTER_REFS_BRANCHES,
-+ quiet,
-+ 1, /* warn_only */
-+ 1, /* no_head_fallback */
-+ 0 /* dry_run */);
++ DELETE_BRANCH_WARN_ONLY |
++ DELETE_BRANCH_NO_HEAD_FALLBACK |
++ (quiet ? DELETE_BRANCH_QUIET : 0));
+
+ strvec_clear(&deletable);
-+ string_list_clear(&candidates, 0);
++ ref_array_clear(&candidates);
++ ref_filter_clear(&filter);
+ return ret;
+}
+
@@ builtin/branch.c: static void filter_array_by_forked(struct ref_array *array,
static int edit_branch_description(const char *branch_name)
@@ 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;
- struct string_list forked_upstreams = STRING_LIST_INIT_DUP;
-+ struct string_list prune_merged_upstreams = STRING_LIST_INIT_DUP;
++ int prune_merged = 0;
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_STRING_LIST(0, "forked", &forked_upstreams, N_("branch"),
- N_("list local branches whose upstream matches <branch> (repeatable)")),
-+ OPT_STRING_LIST(0, "prune-merged", &prune_merged_upstreams, N_("branch"),
-+ N_("delete local branches whose upstream matches <branch> and is merged (repeatable)")),
++ OPT_BOOL(0, "prune-merged", &prune_merged,
++ N_("delete local branches whose upstream matches <branch> and is merged")),
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 && !prune_merged_upstreams.nr &&
++ !show_current && !unset_upstream && !prune_merged &&
+ 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 + !!prune_merged_upstreams.nr;
++ !!unset_upstream + !!prune_merged;
if (noncreate_actions > 1)
usage_with_options(builtin_branch_usage, options);
@@ builtin/branch.c: int cmd_branch(int argc,
- ret = delete_branches(argc, argv, delete > 1, filter.kind,
- quiet, 0, 0, 0);
+ (delete > 1 ? DELETE_BRANCH_FORCE : 0) |
+ (quiet ? DELETE_BRANCH_QUIET : 0));
goto out;
-+ } else if (prune_merged_upstreams.nr) {
-+ if (argc)
-+ die(_("--prune-merged does not take positional arguments; "
-+ "repeat --prune-merged for each <branch>"));
-+ ret = prune_merged_branches(&prune_merged_upstreams, quiet);
++ } else if (prune_merged) {
++ ret = prune_merged_branches(argc, argv, quiet);
+ goto out;
} else if (show_current) {
print_current_branch_name();
ret = 0;
-@@ builtin/branch.c: int cmd_branch(int argc,
- out:
- string_list_clear(&sorting_options, 0);
- string_list_clear(&forked_upstreams, 0);
-+ string_list_clear(&prune_merged_upstreams, 0);
- return ret;
- }
## t/t3200-branch.sh ##
@@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
@@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
+ git -C pm-union branch --set-upstream-to=origin/main two &&
+ git -C pm-union checkout --detach &&
+
-+ git -C pm-union branch --prune-merged origin/next --prune-merged origin/main &&
++ git -C pm-union branch --prune-merged origin/next origin/main &&
+
+ test_must_fail git -C pm-union rev-parse --verify refs/heads/one &&
+ test_must_fail git -C pm-union rev-parse --verify refs/heads/two
@@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
+ test_must_fail git -C pm-push-diff rev-parse --verify refs/heads/topic
+'
+
-+test_expect_success '--prune-merged requires a value' '
++test_expect_success '--prune-merged requires at least one <branch>' '
+ test_must_fail git -C forked branch --prune-merged 2>err &&
-+ test_grep "requires a value" err
++ test_grep "requires at least one <branch>" err
+'
+
-+test_expect_success '--prune-merged rejects positional arguments' '
-+ test_must_fail git -C forked branch --prune-merged origin/one other/foreign 2>err &&
-+ test_grep "does not take positional arguments" err
++test_expect_success '--prune-merged takes positional <branch> arguments' '
++ test_when_finished "rm -rf pm-positional" &&
++ git clone pm-upstream pm-positional &&
++ git -C pm-positional remote add fork ../pm-fork &&
++ test_config -C pm-positional remote.pushDefault fork &&
++ test_config -C pm-positional push.default current &&
++ git -C pm-positional branch one one-commit &&
++ git -C pm-positional branch --set-upstream-to=origin/next one &&
++ git -C pm-positional branch two base &&
++ git -C pm-positional branch --set-upstream-to=origin/main two &&
++ git -C pm-positional checkout --detach &&
++
++ git -C pm-positional branch --prune-merged origin/next origin/main &&
++
++ test_must_fail git -C pm-positional rev-parse --verify refs/heads/one &&
++ test_must_fail git -C pm-positional rev-parse --verify refs/heads/two
+'
+
test_done
5: 5f793f8d0d ! 5: 8e9a735ffe 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
- "git branch --prune-merged". Useful for a topic branch you want
- to develop further after an initial round has been merged
- upstream.
+ "git branch --prune-merged", which is useful for a topic you want
+ to keep developing after an early round of it has been merged
+ upstream. Unless --quiet is given, each skip is reported so the
+ user knows why their topic was kept.
- 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.
+ Explicit deletion with "git branch -d" still uses the normal merge
+ check and ignores this setting.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
@@ Documentation/git-branch.adoc: the upstream refs refreshed.
warnings and skipped; pass them to `git branch -D` explicitly if
## builtin/branch.c ##
-@@ builtin/branch.c: static int prune_merged_branches(const struct string_list *upstreams,
- struct branch *branch = branch_get(short_name);
+@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
+ const char *short_name;
+ struct branch *branch;
const char *upstream, *push;
- struct strbuf full = STRBUF_INIT;
+ struct strbuf key = STRBUF_INIT;
- int skip;
+ int opt_out;
- strbuf_addf(&full, "refs/heads/%s", short_name);
- skip = !!branch_checked_out(full.buf);
-@@ builtin/branch.c: static int prune_merged_branches(const struct string_list *upstreams,
+ if (!skip_prefix(full_name, "refs/heads/", &short_name))
+ continue;
+@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
if (!push || !strcmp(push, upstream))
continue;
@@ builtin/branch.c: static int prune_merged_branches(const struct string_list *ups
## t/t3200-branch.sh ##
-@@ t/t3200-branch.sh: test_expect_success '--prune-merged rejects positional arguments' '
- test_grep "does not take positional arguments" err
+@@ t/t3200-branch.sh: test_expect_success '--prune-merged takes positional <branch> arguments' '
+ test_must_fail git -C pm-positional rev-parse --verify refs/heads/two
'
+test_expect_success '--prune-merged honours branch.<name>.pruneMerged=false' '
6: 1a0d5eab15 ! 6: 511de4788e branch: add --dry-run for --prune-merged
@@ Commit message
branch: add --dry-run for --prune-merged
With --dry-run, --prune-merged prints the local branches it would
- delete, one "Would delete branch <name>" line per candidate, and
- exits without touching any ref.
+ 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.
- The @{push}-vs-@{upstream} and unmerged filtering still applies,
- so the dry-run output is exactly the set that the live run would
- delete.
-
- --dry-run is only meaningful in combination with --prune-merged
- and is rejected otherwise.
+ --dry-run is only meaningful together with --prune-merged and is
+ rejected otherwise.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
@@ 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 (--prune-merged <branch>)...
-+git branch [--dry-run] (--prune-merged <branch>)...
+-git branch --prune-merged <branch>...
++git branch [--dry-run] --prune-merged <branch>...
DESCRIPTION
-----------
@@ Documentation/git-branch.adoc: Branches refused by the "fully merged" safety che
`--verbose`::
## builtin/branch.c ##
-@@ builtin/branch.c: static void collect_forked_set(const struct string_list *upstreams,
+@@ builtin/branch.c: static int parse_opt_forked(const struct option *opt, const char *arg, int unset
}
- static int prune_merged_branches(const struct string_list *upstreams,
+ static int prune_merged_branches(int argc, const char **argv,
- int quiet)
+ int quiet, int dry_run)
{
struct ref_store *refs = get_main_ref_store(the_repository);
- struct string_list candidates = STRING_LIST_INIT_DUP;
-@@ builtin/branch.c: static int prune_merged_branches(const struct string_list *upstreams,
- quiet,
- 1, /* warn_only */
- 1, /* no_head_fallback */
-- 0 /* dry_run */);
-+ dry_run);
+ struct ref_filter filter = REF_FILTER_INIT;
+@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
+ FILTER_REFS_BRANCHES,
+ DELETE_BRANCH_WARN_ONLY |
+ DELETE_BRANCH_NO_HEAD_FALLBACK |
+- (quiet ? DELETE_BRANCH_QUIET : 0));
++ (quiet ? DELETE_BRANCH_QUIET : 0) |
++ (dry_run ? DELETE_BRANCH_DRY_RUN : 0));
strvec_clear(&deletable);
- string_list_clear(&candidates, 0);
+ ref_array_clear(&candidates);
@@ 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;
- struct string_list forked_upstreams = STRING_LIST_INIT_DUP;
- struct string_list prune_merged_upstreams = STRING_LIST_INIT_DUP;
+ int prune_merged = 0;
+ int dry_run = 0;
const char *new_upstream = NULL;
int noncreate_actions = 0;
/* possible options */
@@ builtin/branch.c: int cmd_branch(int argc,
- N_("list local branches whose upstream matches <branch> (repeatable)")),
- OPT_STRING_LIST(0, "prune-merged", &prune_merged_upstreams, N_("branch"),
- N_("delete local branches whose upstream matches <branch> and is merged (repeatable)")),
+ N_("edit the description for the branch")),
+ OPT_BOOL(0, "prune-merged", &prune_merged,
+ N_("delete local branches whose upstream matches <branch> and is merged")),
+ OPT_BOOL(0, "dry-run", &dry_run,
+ N_("with --prune-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 && !prune_merged_upstreams.nr)
++ if (dry_run && !prune_merged)
+ die(_("--dry-run requires --prune-merged"));
+
if (recurse_submodules_explicit) {
if (!submodule_propagate_branches)
die(_("branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled"));
@@ builtin/branch.c: int cmd_branch(int argc,
- if (argc)
- die(_("--prune-merged does not take positional arguments; "
- "repeat --prune-merged for each <branch>"));
-- ret = prune_merged_branches(&prune_merged_upstreams, quiet);
-+ ret = prune_merged_branches(&prune_merged_upstreams, quiet, dry_run);
+ (quiet ? DELETE_BRANCH_QUIET : 0));
+ goto out;
+ } else if (prune_merged) {
+- ret = prune_merged_branches(argc, argv, quiet);
++ ret = prune_merged_branches(argc, argv, quiet, dry_run);
goto out;
} else if (show_current) {
print_current_branch_name();
--
gitgitgadget
next prev parent reply other threads:[~2026-06-05 18:35 UTC|newest]
Thread overview: 124+ 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 ` Harald Nordgren via GitGitGadget [this message]
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-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
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.v13.git.git.1780684553.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