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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.