Skip to contentSkip to conversation

fetch: add fetch.pruneLocalBranches config

PATCH Superseded Harald Nordgren via GitGitGadget 6 participants 292 messages May 1 – Aug 7 Documentation Page 4 of 4

lore
Share & tools
lore
#282 lore raw

v25 cover branch: delete-merged

In reply to Harald Nordgren via GitGitGadget

Delete branches that have already been merged on upstream.

Changes in v25:

* Wrap long lines in tests.

Changes in v24:

* Map branches through the configured push and fetch refspecs before deciding whether pushing would update their upstream.
* Protect only the direct upstream of surviving work, delete deeper merged bases and clear stale upstream configuration.
* Clarify that --delete-merged takes upstream patterns in docs.

Changes in v23:

* Use a loop-local size_t iterator for --forked patterns.
* Simplify upstream-prefix stripping into an explicit fallback.

Changes in v22:

* Loop variable int when appropriate to avoid typecast.

Changes in v21:

* Change loop variables to prevent CI timeout.

Changes in v20:

* Protect branches transitively required by a surviving local upstream stack. Traverse upstream chains once and defer delete-set mutation until traversal completes.
* Make stacked-branch handling independent of ref iteration order and update the documentation accordingly.
* Clarify variable names with regards to branch names (short) to reduce confusion.

Changes in v19:

* Fix bug where dry-run would still remove config, added test coverage.
* Redesigned --delete-merged as a repeatable upstream selector with optional positional patterns limiting deletion scope.
* Protect same-name upstream branches independently of push-default configuration.
* Simplified flags handling where local caching became complicated when mutating values.
* Clarified assertions in tests.

Changes in v18:

* Instead of keeping the whole chain of upstream branches, keep only the ones an unmerged branch still needs. When a kept (merged) branch in turn tracks a branch that is being deleted, clear its now-stale upstream config.
* Rework spare_stacked_bases() to record the kept bases and, in a second pass, clear the upstream of any whose own base is going away. Build the to-delete list with strset_for_each_entry() instead of re-walking the candidate array.

Changes in v17:

* Keep a merged branch when another surviving branch still tracks it as its upstream, so --delete-merged no longer deletes a branch out from under one stacked on top of it.
* Move the --dry-run and branch.<name>.deleteMerged opt-out fully into their own commits.

Changes in v16:

* Convert delete_merged_branches() to take an unsigned int flags argument instead of separate quiet/dry_run booleans, matching delete_branches()
* Reuse the strbuf across the skip-config loop (strbuf_reset per iteration, single strbuf_release after) instead of allocating and freeing it each time
* Rewrite the --delete-merged tests as integration tests: branches that land commits upstream, with deletion and the checked-out, upstream-gone, and push-equals-upstream safety cases exercised together in one run and output asserted via test_cmp
* Collapse the many per-aspect test repos into a single reused repo set up by a setup_repo_for_delete_merged helper, and rename helpers off the old pm_/prune naming
* Nest single-repo setup sequences in ( cd ... ) subshells instead of prefixing every command with -C

Changes in v15:

* Renamed --prune-merged to --delete-merged throughout. Not necessarily final, but something to advance the discussion.
* --delete-merged now silently skips not-yet-merged branches instead of warning.
* Initialized the delete_branches() flag locals where declared. Only force stays deferred.
* delete_branches()/check_branch_commit() doc and code cleanups: redundant branch NULL checks dropped, ref_array candidates = { 0 }, a BUG() for the unreachable non-branch ref, and reworked --delete-merged doc wording.
* Broadened the --forked tests (local commits for realism, remote add -f, --forked coverage), renamed the misleading trunk fixture, and replaced the misnamed detached branch with git checkout --detach.

Changes in v14:

* Fixed a git branch -d -r regression (broke t5404/t5505/t5514): the remotes path set a local force but not the DELETE_BRANCH_FORCE bit that check_branch_commit() reads, so it wrongly ran the merge check.
* Made flags the single source of truth in delete_branches() so the bit and the derived locals can't disagree.
* Works locally, but GitHub CI has problems that are there for other branches too, hopefully not related
(https://github.com/git/git/pull/2285).

Changes in v13:

* 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 patterns as positional arguments (e.g. git branch --prune-merged origin/main 'feature*') instead of repeating the option.

Changes in v12:

* Reworked --forked from a standalone action into a --list-mode filter.
* Switched --forked and --prune-merged to repeatable OPT_STRING_LIST options.
* Dropped the bare-remote-name resolution for --forked, the argument is now a ref or a glob.

Changes in v11:

* The flags now take a branch, not a remote. --forked and --prune-merged accept a literal upstream short name like origin/main or a wildmatch pattern like origin/. The old --all-remotes flag is gone, since origin/ covers that case.
* The prune guard now compares @{push} against @{upstream}. A branch is spared when these are equal. That is the trunk like case, such as local main tracking and pushing to origin/main, where "fully merged to upstream" cannot be told apart from "just pulled". Only branches that push somewhere other than their upstream, typically fork based topics, are candidates. The earlier /HEAD by name guard that the reviewer rejected is gone.
* New --dry-run for --prune-merged.

Changes in v10:

* --forked / --prune-merged now take a branch glob instead of a remote name — origin, origin/*, origin/release-- all work. This replaces the remote-only form and subsumes the old --all-remotes flag, which has been dropped.
* New --dry-run for --prune-merged.

Changes in v9:

* --force no longer has special meaning with --prune-merged; reachability is always enforced. Use git branch -D to delete an unmerged branch. Matches how git branch's other read/safe actions treat --force.
* Synopsis drops [-f]; "not fully merged" hint points at git branch -D.
* Dropped the --prune-merged --force tests.

Changes in v8:

* Delete only when the branch's work is actually reachable from its upstream
* Skip branches whose upstream is gone (even with --force)
* Simplified the internal safety flag to live in one place

Changes in v7:

* --prune-merged now checks if a branch is merged into its own upstream first. If the upstream is gone, it checks against the remote's default branch instead. If neither exists, the branch is refused (use --force to delete anyway).

Changes in v6:

* --prune-merged now measures merged-ness against the remote's default branch instead of the candidate's upstream — so the decision no longer depends on which branch happens to be checked out locally.
* delete_branches() / check_branch_commit() gained a per-candidate override that lets a caller substitute a different "what counts as merged" reference (or skip the check). branch -d callers pass NULL and keep their existing semantics.
* prune_merged_branches() resolves each candidate's push-remote HEAD and threads it through, so --prune-merged --all-remotes measures each candidate against its own remote rather than a single global reference.

Changes in v5:

* Drop commit 'fetch: add --prune-merged'

Changes in v4:

* Resolve each remote's HEAD and collect the targets into a protected_default_refs set in collect_forked_set.
* In prune_merged_branches, skip a candidate when its upstream is a protected default ref and the local branch name matches the default branch's leaf name (so a local main tracking origin/main is spared, but a renamed trunk tracking origin/main is not).
* Also skip when the candidate's push ref points at a protected default ref, so a topic branch configured to push to origin/main is never pruned.
* Tests: spare the local default branch; only protect by matching leaf name (not by upstream alone); spare a branch whose push ref is the remote default.

Changes in v3:

* s/remote-tracking refs/remote-tracking branches/g

Changes in v2:

* The whole feature moved out of git fetch and into git branch. git fetch --prune-merged now just calls git branch --prune-merged after fetching.
* The fetch.pruneLocalBranches and remote..pruneLocalBranches config options are gone, replaced by per-branch opt-out via branch..pruneMerged.
* New git branch --forked lists local branches whose upstream lives on the given remote (read-only building block).
* New git branch --prune-merged deletes those branches, but only if their tip is reachable from the upstream tracking ref; --force skips that safety check.
* New git branch --all-remotes lets --forked/--prune-merged operate across every configured remote at once.
* The currently checked-out branch in any worktree is always preserved.
* branch..pruneMerged=false lets you exempt a branch (e.g. a long-running topic branch) even with --force; doesn't affect explicit git branch -d.
* delete_branches() got a warn_only mode so bulk deletion prints a one-line warning per skipped branch instead of the noisy four-line hint that git branch -d shows.
* New section in git-branch docs; git-fetch docs trimmed to just mention --prune-merged.
* New tests in t3200-branch.sh for the new branch flags; t5510-fetch.sh shrunk since most logic moved.

Harald Nordgren (7):
branch: add --forked filter for --list mode
branch: convert delete_branches() to a flags argument
branch: let delete_branches skip unmerged branches on bulk refusal
branch: prepare delete_branches for a bulk caller
branch: add --delete-merged <pattern>
branch: add branch.<name>.deleteMerged opt-out
branch: add --dry-run for --delete-merged

 Documentation/config/branch.adoc |   7 +
 Documentation/git-branch.adoc    |  52 +++-
 builtin/branch.c                 | 312 ++++++++++++++++++++---
 ref-filter.c                     |  70 +++++
 ref-filter.h                     |  10 +
 t/t3200-branch.sh                | 421 +++++++++++++++++++++++++++++++
 6 files changed, 841 insertions(+), 31 deletions(-)

base-commit: 5b2471720c93ee30e5764a19f3d3b3ae9ec9712a
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2285%2FHaraldNordgren%2Ffetch-prune-local-branches-v25
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2285/HaraldNordgren/fetch-prune-local-branches-v25
Pull-Request: https://github.com/git/git/pull/2285

Range-diff vs v24:

1: 59157142b3 ! 1: 2f8953b339 branch: add --forked filter for --list mode

     @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
      +'
      +
      +test_expect_success '--forked <upstream-tracking-branch> filters by upstream' '
     -+	git -C forked branch --forked origin/one --format="%(refname:short)" >actual &&
     ++	git -C forked branch --forked origin/one \
     ++		--format="%(refname:short)" >actual &&
      +	echo local-one >expect &&
      +	test_cmp expect actual
      +'
      +
      +test_expect_success '--forked <glob> filters by wildmatch' '
     -+	git -C forked branch --forked "origin/*" --format="%(refname:short)" >actual &&
     ++	git -C forked branch --forked "origin/*" \
     ++		--format="%(refname:short)" >actual &&
      +	cat >expect <<-\EOF &&
      +	local-one
      +	local-two
     @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
      +'
      +
      +test_expect_success '--forked <local-branch> matches branches with local upstream' '
     -+	git -C forked branch --forked local-base --format="%(refname:short)" >actual &&
     ++	git -C forked branch --forked local-base \
     ++		--format="%(refname:short)" >actual &&
      +	echo local-onbase >expect &&
      +	test_cmp expect actual
      +'
      +
      +test_expect_success '--forked can be repeated to widen the filter' '
     -+	git -C forked branch --forked origin/one --forked other/foreign --format="%(refname:short)" >actual &&
     ++	git -C forked branch --forked origin/one \
     ++		--forked other/foreign \
     ++		--format="%(refname:short)" >actual &&
      +	cat >expect <<-\EOF &&
      +	local-foreign
      +	local-one
     @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
      +'
      +
      +test_expect_success '--forked combines literal and glob arguments' '
     -+	git -C forked branch --forked local-base --forked "other/*" --format="%(refname:short)" >actual &&
     ++	git -C forked branch --forked local-base \
     ++		--forked "other/*" \
     ++		--format="%(refname:short)" >actual &&
      +	cat >expect <<-\EOF &&
      +	local-foreign
      +	local-onbase
     @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
      +'
      +
      +test_expect_success '--forked "*/*" covers every remote-tracking upstream' '
     -+	git -C forked branch --forked "*/*" --format="%(refname:short)" >actual &&
     ++	git -C forked branch --forked "*/*" \
     ++		--format="%(refname:short)" >actual &&
      +	cat >expect <<-\EOF &&
      +	local-foreign
      +	local-one
     @@ t/t3200-branch.sh: test_expect_success 'errors if given a bad branch name' '
      +'
      +
      +test_expect_success '--forked composes with --no-merged' '
     -+	git -C forked branch --forked "origin/*" --no-merged origin/one --format="%(refname:short)" >actual &&
     ++	git -C forked branch --forked "origin/*" \
     ++		--no-merged origin/one \
     ++		--format="%(refname:short)" >actual &&
      +	echo local-one >expect &&
      +	test_cmp expect actual
      +'
      +
      +test_expect_success '--forked <remote> uses the branch <remote>/HEAD points at' '
     -+	git -C forked branch --forked origin --format="%(refname:short)" >actual &&
     ++	git -C forked branch --forked origin \
     ++		--format="%(refname:short)" >actual &&
      +	echo main >expect &&
      +	test_cmp expect actual
      +'
      +
      +test_expect_success '--forked narrows a <pattern> argument' '
     -+	git -C forked branch --forked "origin/*" "local-*" --format="%(refname:short)" >actual &&
     ++	git -C forked branch --forked "origin/*" "local-*" \
     ++		--format="%(refname:short)" >actual &&
      +	cat >expect <<-\EOF &&
      +	local-one
      +	local-two
 2:  5d310da9aa = 2:  2e80fc3791 branch: convert delete_branches() to a flags argument
 3:  263e90f6ed = 3:  4ab81f0b0a branch: let delete_branches skip unmerged branches on bulk refusal
 4:  22e31fd35c = 4:  83cc3fd6a5 branch: prepare delete_branches for a bulk caller
 5:  d9f93e66c9 ! 5:  a5ef60a394 branch: add --delete-merged <pattern>
     @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
      +	(
      +		cd repo &&
      +		git checkout -b topic &&
     -+		git config remote.origin.push "refs/heads/topic:refs/heads/published" &&
     ++		git config remote.origin.push \
     ++			"refs/heads/topic:refs/heads/published" &&
      +		git push origin &&
      +		git branch --set-upstream-to=origin/published topic &&
      +		git checkout -b other-topic --track origin/published &&
     @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
      +		topic
      +		EOF
      +
     -+		git config --local --get-regexp "branch\\.(feature|topic)\\.(merge|remote)" >actual &&
     ++		pattern="branch\\.(feature|topic)\\.(merge|remote)" &&
     ++		git config --local --get-regexp "$pattern" >actual &&
      +		cat >expect <<-\EOF &&
      +		branch.feature.remote origin
      +		branch.feature.merge refs/heads/next
     @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
      +		git commit --allow-empty -m "tip work" &&
      +		sha=$(git rev-parse --short lower) &&
      +
     -+		git branch --delete-merged origin/next --delete-merged lower >actual 2>&1 &&
     ++		git branch --delete-merged origin/next \
     ++			--delete-merged lower >actual 2>&1 &&
      +		echo "Deleted branch lower (was $sha)." >expect &&
      +		test_cmp expect actual &&
      +
     @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
      +		tip
      +		EOF
      +
     -+		git config --local --get-regexp "branch\\.(mid|tip)\\.(merge|remote)" >actual &&
     ++		pattern="branch\\.(mid|tip)\\.(merge|remote)" &&
     ++		git config --local --get-regexp "$pattern" >actual &&
      +		cat >expect <<-\EOF &&
      +		branch.tip.remote .
      +		branch.tip.merge refs/heads/mid
     @@ t/t3200-branch.sh: test_expect_success '--forked requires a value' '
      +		main
      +		EOF
      +
     -+		git branch --delete-merged origin/next --delete-merged "c-*" >actual 2>&1 &&
     ++		git branch --delete-merged origin/next \
     ++			--delete-merged "c-*" >actual 2>&1 &&
      +		test_must_be_empty actual &&
      +
      +		check_branches <<-\EOF
 6:  aa845096d8 = 6:  c2d1fdebe8 branch: add branch.<name>.deleteMerged opt-out
 7:  96ebfd6ec6 ! 7:  5a936bcb93 branch: add --dry-run for --delete-merged
     @@ t/t3200-branch.sh: test_expect_success '--delete-merged deletes only selected me
       		git checkout -b tracks-other --track other/main &&
       		sha=$(git rev-parse --short merged) &&
       
     -+		git branch --dry-run --delete-merged origin/next merged >actual 2>&1 &&
     ++		git branch --dry-run --delete-merged origin/next merged \
     ++			>actual 2>&1 &&
      +		echo "Would delete branch merged (was $sha)." >expect &&
      +		test_cmp expect actual &&
      +		git rev-parse --verify refs/heads/merged &&
     @@ t/t3200-branch.sh: test_expect_success '--delete-merged clears the deleted upstr
       		git commit --allow-empty -m "tip work" &&
       		sha=$(git rev-parse --short lower) &&
       
     -+		git branch --dry-run --delete-merged origin/next --delete-merged lower >actual 2>&1 &&
     ++		git branch --dry-run --delete-merged origin/next \
     ++			--delete-merged lower >actual 2>&1 &&
      +		echo "Would delete branch lower (was $sha)." >expect &&
      +		test_cmp expect actual &&
      +
     -+		git config --local --get-regexp "branch\\.(lower|mid|tip)\\.(merge|remote)" >actual &&
     ++		pattern="branch\\.(lower|mid|tip)\\.(merge|remote)" &&
     ++		git config --local --get-regexp "$pattern" >actual &&
      +		cat >expect <<-\EOF &&
      +		branch.lower.remote origin
      +		branch.lower.merge refs/heads/next
     @@ t/t3200-branch.sh: test_expect_success '--delete-merged clears the deleted upstr
      +		EOF
      +		test_cmp expect actual &&
      +
     - 		git branch --delete-merged origin/next --delete-merged lower >actual 2>&1 &&
     + 		git branch --delete-merged origin/next \
     + 			--delete-merged lower >actual 2>&1 &&
       		echo "Deleted branch lower (was $sha)." >expect &&
     - 		test_cmp expect actual &&
      @@ t/t3200-branch.sh: test_expect_success "branch -d still deletes a deleteMerged=false branch" '
       	)
       '
Signature · 1 line
gitgitgadget
#283 lore raw

PATCH v25 1/7 branch: add --forked filter for --list mode

From: Harald Nordgren <[email protected]>

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"), a remote name like "origin" for the branch its origin/HEAD points at, or a shell glob (e.g. "origin/*"), and may be repeated to widen the filter.

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 are already merged into origin/main, and --no-merged inverts the question.

This is the building block for --delete-merged, which deletes the listed branches once they have landed on their upstream.

---
 Documentation/git-branch.adoc |  12 +++-
 builtin/branch.c              |  18 ++++-
 ref-filter.c                  |  70 +++++++++++++++++++
 ref-filter.h                  |  10 +++
 t/t3200-branch.sh             | 127 ++++++++++++++++++++++++++++++++++
 5 files changed, 234 insertions(+), 3 deletions(-)
Documentation/git-branch.adoc +11 -1
index c0afddc424..b0d66a6deb 100644
@@ -13,6 +13,7 @@ git branch [--color[=<when>] | --no-color] [--show-current]
 	   [--column[=<options>] | --no-column] [--sort=<key>]
 	   [--merged [<commit>]] [--no-merged [<commit>]]
 	   [--contains [<commit>]] [--no-contains [<commit>]]
+	   [(--forked <branch>)...]
 	   [--points-at <object>] [--format=<format>]
 	   [(-r|--remotes) | (-a|--all)]
 	   [--list] [<pattern>...]
@@ -51,7 +52,8 @@ 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
@@ -311,6 +313,14 @@ 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>`::
+	Only list branches whose configured upstream matches
+	_<branch>_. The argument can be a ref (e.g. `origin/main`,
+	`master`), a remote name like `origin` for the branch its
+	`origin/HEAD` points at, or a shell-style glob (e.g.
+	`'origin/*'`). The option can be repeated to widen the
+	filter. Implies `--list`.
+
 `--points-at <object>`::
 	Only list branches of _<object>_.
 
builtin/branch.c +16 -2
index 031a4a9d05..1ab4356188 100644
@@ -30,7 +30,7 @@
 #include "commit-reach.h"
 
 static const char * const builtin_branch_usage[] = {
-	N_("git branch [<options>] [-r | -a] [--merged] [--no-merged]"),
+	N_("git branch [<options>] [-r | -a] [--merged] [--no-merged] [(--forked <branch>)...]"),
 	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>..."),
@@ -674,6 +674,16 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int
 	free_worktrees(worktrees);
 }
 
+static int parse_opt_forked(const struct option *opt, const char *arg, int unset)
+{
+	struct ref_filter *filter = opt->value;
+
+	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")
 
 static int edit_branch_description(const char *branch_name)
@@ -794,6 +804,9 @@ 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"),
@@ -839,7 +852,8 @@ 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 || filter.forked.nr)
 		list = 1;
 
 	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
ref-filter.c +70 -0

88 lines not on this page · raw message

ref-filter.h +10 -0
index 120221b47f..9361296e2a 100644
@@ -67,6 +67,7 @@ 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;
@@ -110,6 +111,7 @@ 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, \
@@ -172,6 +174,14 @@ 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 +127 -0

131 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#284 lore raw

PATCH v25 2/7 branch: convert delete_branches() to a flags argument

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

delete_branches() takes separate force and quiet parameters, while check_branch_commit() takes force. The next commits would grow this collection further. Replace them with a single unsigned flags argument and an enum.

Test the FORCE and QUIET bits directly from flags at each use site so that mutating or forwarding flags cannot leave cached values stale.

No change in behavior.

---
 builtin/branch.c | 40 ++++++++++++++++++++++++----------------
 1 file changed, 24 insertions(+), 16 deletions(-)
builtin/branch.c +24 -16
index 1ab4356188..db7cb01190 100644
@@ -189,16 +189,22 @@ 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),
+};
+
 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, unsigned int flags)
 {
 	struct commit *rev = lookup_commit_reference(the_repository, oid);
-	if (!force && !rev) {
+	if (!(flags & DELETE_BRANCH_FORCE) && !rev) {
 		error(_("couldn't look up commit object for '%s'"), refname);
 		return -1;
 	}
-	if (!force && !branch_merged(kinds, branchname, rev, head_rev)) {
+	if (!(flags & DELETE_BRANCH_FORCE) &&
+	    !branch_merged(kinds, branchname, rev, head_rev)) {
 		error(_("the branch '%s' is not fully merged"), branchname);
 		advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
 				  _("If you are sure you want to delete it, "
@@ -217,8 +223,8 @@ static void delete_branch_config(const char *branchname)
 	strbuf_release(&buf);
 }
 
-static int delete_branches(int argc, const char **argv, int force, int kinds,
-			   int quiet)
+static int delete_branches(int argc, const char **argv, int kinds,
+			   unsigned int flags)
 {
 	struct commit *head_rev = NULL;
 	struct object_id oid;
@@ -241,7 +247,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
 		remote_branch = 1;
 		allowed_interpret = INTERPRET_BRANCH_REMOTE;
 
-		force = 1;
+		flags |= DELETE_BRANCH_FORCE;
 		break;
 	case FILTER_REFS_BRANCHES:
 		fmt = "refs/heads/%s";
@@ -252,12 +258,12 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
 	}
 	branch_name_pos = strcspn(fmt, "%");
 
-	if (!force)
+	if (!(flags & DELETE_BRANCH_FORCE))
 		head_rev = lookup_commit_reference(the_repository, &head_oid);
 
 	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
 		char *target = NULL;
-		int flags = 0;
+		int ref_flags = 0;
 
 		copy_branchname(the_repository, &bname,
 				argv[i], allowed_interpret);
@@ -280,7 +286,7 @@ 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);
@@ -292,7 +298,7 @@ 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 (virtual_target)
@@ -307,16 +313,16 @@ 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)) {
+					flags)) {
 			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:
@@ -331,7 +337,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
 		char *name = item->string;
 		if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
 			char *refname = name + branch_name_pos;
-			if (!quiet)
+			if (!(flags & DELETE_BRANCH_QUIET))
 				printf(remote_branch
 					? _("Deleted remote-tracking branch %s (was %s).\n")
 					: _("Deleted branch %s (was %s).\n"),
@@ -896,7 +902,9 @@ 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, filter.kind,
+				      (delete > 1 ? DELETE_BRANCH_FORCE : 0) |
+				      (quiet ? DELETE_BRANCH_QUIET : 0));
 		goto out;
 	} else if (show_current) {
 		print_current_branch_name();
Signature · 1 line
gitgitgadget
#285 lore raw

PATCH v25 3/7 branch: let delete_branches skip unmerged branches on bulk refusal

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Add a skip-unmerged mode to delete_branches() and check_branch_commit() so a bulk caller can silently skip branches that are not fully merged and carry on, rather than erroring with the "use 'git branch -D'" advice that the plain "git branch -d" path emits. Existing callers are unaffected.

---
 builtin/branch.c | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)
builtin/branch.c +10 -5
index db7cb01190..c44f710a48 100644
@@ -192,6 +192,7 @@ static int branch_merged(int kind, const char *name,
 enum delete_branch_flags {
 	DELETE_BRANCH_FORCE = (1 << 0),
 	DELETE_BRANCH_QUIET = (1 << 1),
+	DELETE_BRANCH_SKIP_UNMERGED = (1 << 2),
 };
 
 static int check_branch_commit(const char *branchname, const char *refname,
@@ -205,10 +206,13 @@ static int check_branch_commit(const char *branchname, const char *refname,
 	}
 	if (!(flags & DELETE_BRANCH_FORCE) &&
 	    !branch_merged(kinds, branchname, rev, head_rev)) {
-		error(_("the branch '%s' is not fully merged"), branchname);
-		advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
-				  _("If you are sure you want to delete it, "
-				  "run 'git branch -D %s'"), branchname);
+		if (!(flags & DELETE_BRANCH_SKIP_UNMERGED)) {
+			error(_("the branch '%s' is not fully merged"),
+			      branchname);
+			advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
+					  _("If you are sure you want to delete it, "
+					  "run 'git branch -D %s'"), branchname);
+		}
 		return -1;
 	}
 	return 0;
@@ -316,7 +320,8 @@ static int delete_branches(int argc, const char **argv, int kinds,
 		if (!(ref_flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
 		    check_branch_commit(bname.buf, name, &oid, head_rev, kinds,
 					flags)) {
-			ret = 1;
+			if (!(flags & DELETE_BRANCH_SKIP_UNMERGED))
+				ret = 1;
 			goto next;
 		}
 
Signature · 1 line
gitgitgadget
#286 lore raw

PATCH v25 4/7 branch: prepare delete_branches for a bulk caller

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Teach delete_branches() a new mode for the upcoming --delete-merged caller that checks whether a branch is merged into its upstream without falling back to HEAD when there is no upstream. Existing callers keep their current behavior.

---
 builtin/branch.c | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)
builtin/branch.c +9 -4
index c44f710a48..7b0aa68572 100644
@@ -168,10 +168,13 @@ static int branch_merged(int kind, const char *name,
 	 * upstream, if any, otherwise with HEAD", we should just
 	 * return the result of the repo_in_merge_bases() above without
 	 * any of the following code, but during the transition period,
-	 * a gentle reminder is in order.
+	 * a gentle reminder is in order.  Callers that opt out of the
+	 * HEAD fallback by passing head_rev=NULL are not interested in
+	 * the reminder either: they have already established that the
+	 * branch has an upstream, so HEAD is irrelevant to the decision.
 	 */
-	if (head_rev != reference_rev) {
-		int expect = head_rev ? repo_in_merge_bases(the_repository, rev, head_rev) : 0;
+	if (head_rev && head_rev != reference_rev) {
+		int expect = repo_in_merge_bases(the_repository, rev, head_rev);
 		if (expect < 0)
 			exit(128);
 		if (expect == merged)
@@ -193,6 +196,7 @@ enum delete_branch_flags {
 	DELETE_BRANCH_FORCE = (1 << 0),
 	DELETE_BRANCH_QUIET = (1 << 1),
 	DELETE_BRANCH_SKIP_UNMERGED = (1 << 2),
+	DELETE_BRANCH_NO_HEAD_FALLBACK = (1 << 3),
 };
 
 static int check_branch_commit(const char *branchname, const char *refname,
@@ -262,7 +266,8 @@ static int delete_branches(int argc, const char **argv, int kinds,
 	}
 	branch_name_pos = strcspn(fmt, "%");
 
-	if (!(flags & DELETE_BRANCH_FORCE))
+	if (!(flags & DELETE_BRANCH_FORCE) &&
+	    !(flags & DELETE_BRANCH_NO_HEAD_FALLBACK))
 		head_rev = lookup_commit_reference(the_repository, &head_oid);
 
 	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
Signature · 1 line
gitgitgadget
#287 lore raw

PATCH v25 5/7 branch: add --delete-merged <pattern>

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

    git branch (--delete-merged <pattern>)... [<branch-pattern>...]

deletes local branches matching the optional branch patterns when their configured upstream matches one of the --delete-merged arguments and their tip is reachable from that upstream. The work has already landed on the upstream they track, so the local copy is no longer needed.

Each <pattern> may name a ref, a remote, or a shell glob. The option can be repeated to widen the upstream match. Keeping the candidate patterns as positional arguments lets users bound the set of local branches that may be deleted independently of the upstream selection.

A branch is not deleted when:

* it is checked out in any worktree
* its configured upstream ref no longer exists, since a missing upstream is not by itself a sign of integration
* pushing it to the remote configured by branch.<name>.remote would update its upstream, as determined by that remote's configured push and fetch refspecs. For example, a local "main" that tracks "origin/main" is kept even when remote.pushDefault names a fork. Right after a pull it merely looks fully merged.
* it is the local upstream of a branch that is not being deleted, so no branch is deleted out from under stacked work.

A branch whose work is not yet merged into its upstream is silently skipped, so one unmerged topic does not abort the whole sweep.

Collect protected local upstreams without changing the candidate set during ref iteration, then remove them after iteration. This makes the result independent of ref iteration order. If a protected branch's own upstream is deleted by the same sweep, clear its upstream configuration.

---
 Documentation/git-branch.adoc |  33 +++++
 builtin/branch.c              | 195 +++++++++++++++++++++++++++++-
 t/t3200-branch.sh             | 219 ++++++++++++++++++++++++++++++++++
 3 files changed, 445 insertions(+), 2 deletions(-)
Documentation/git-branch.adoc +33 -0
index b0d66a6deb..4766178204 100644
@@ -25,6 +25,7 @@ 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 (--delete-merged <pattern>)... [<branch-pattern>...]
 
 DESCRIPTION
 -----------
@@ -201,6 +202,38 @@ This option is only applicable in non-verbose mode.
 	Print the name of the current branch. In detached `HEAD` state,
 	nothing is printed.
 
+`--delete-merged <pattern>`::
+	Delete local branches whose configured upstream matches
+	_<pattern>_, but only when their tip is reachable from that
+	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. _<pattern>_ may name a ref, a remote (using the branch its
+	`HEAD` points at), or a shell-style glob. The option can be
+	repeated to widen the upstream match.
+	Optional _<branch-pattern>_ arguments limit which local branches
+	are considered, e.g. `git branch --delete-merged 'origin/*'
+	'topic-*'`.
++
+A branch is not deleted when:
++
+--
+* its configured upstream ref no longer exists,
+* it is checked out in any worktree,
+* pushing it to the remote configured by
+  `branch.<name>.remote` would update its upstream, so it cannot be
+  distinguished from a branch that just looks fully merged right
+  after a pull; this is determined by the remote's configured push and
+  fetch refspecs,
+* it is the local upstream of a branch that is not being deleted.
+--
++
+When such a local upstream branch has its own upstream deleted by the
+same operation, its upstream configuration is cleared.
++
+A branch whose work has not yet been merged into its upstream is
+silently skipped. Delete it with `git branch -D` if you want to
+remove it anyway.
+
 `-v`::
 `-vv`::
 `--verbose`::
builtin/branch.c +193 -2

249 lines not on this page · raw message

t/t3200-branch.sh +219 -0

223 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#288 lore raw

PATCH v25 6/7 branch: add branch.<name>.deleteMerged opt-out

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Setting branch.<name>.deleteMerged=false exempts that branch from "git branch --delete-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.

Explicit deletion with "git branch -d" still uses the normal merge check and ignores this setting.

---
 Documentation/config/branch.adoc |  7 +++++++
 Documentation/git-branch.adoc    |  3 ++-
 builtin/branch.c                 | 14 +++++++++++++
 t/t3200-branch.sh                | 36 ++++++++++++++++++++++++++++++++
 4 files changed, 59 insertions(+), 1 deletion(-)
Documentation/config/branch.adoc +7 -0
index 5a85fde8de..c2acd89c99 100644
@@ -103,3 +103,10 @@ for details).
 	`git branch --edit-description`. Branch description is
 	automatically added to the `format-patch` cover letter or
 	`request-pull` summary.
+
+`branch.<name>.deleteMerged`::
+	If set to `false`, branch _<name>_ is exempt from
+	`git branch --delete-merged`.  Useful for a topic branch you
+	intend to develop further after an initial round has been
+	merged upstream.  Defaults to true.  Explicit deletion via
+	`git branch -d` is unaffected.
Documentation/git-branch.adoc +2 -1
index 4766178204..cfaac4b90f 100644
@@ -224,7 +224,8 @@ A branch is not deleted when:
   distinguished from a branch that just looks fully merged right
   after a pull; this is determined by the remote's configured push and
   fetch refspecs,
-* it is the local upstream of a branch that is not being deleted.
+* it is the local upstream of a branch that is not being deleted, or
+* `branch.<name>.deleteMerged` is set to `false`.
 --
 +
 When such a local upstream branch has its own upstream deleted by the
builtin/branch.c +14 -0
index f1a73bcea1..2d0c4f51ea 100644
@@ -818,6 +818,7 @@ static int delete_merged_branches(const struct strvec *upstreams,
 	struct strset deletable_branch_names = STRSET_INIT;
 	struct strset protected_branch_names = STRSET_INIT;
 	struct strvec branches_to_delete = STRVEC_INIT;
+	struct strbuf key = STRBUF_INIT;
 	struct hashmap_iter iter;
 	struct strmap_entry *entry;
 	int ret = 0;
@@ -836,6 +837,7 @@ static int delete_merged_branches(const struct strvec *upstreams,
 		const char *branch_name;
 		struct branch *branch;
 		const char *upstream_refname;
+		int opt_out;
 
 		if (!skip_prefix(branch_refname, "refs/heads/", &branch_name))
 			BUG("filter returned non-branch ref '%s'", branch_refname);
@@ -853,6 +855,17 @@ static int delete_merged_branches(const struct strvec *upstreams,
 					FILTER_REFS_BRANCHES, DELETE_BRANCH_SKIP_UNMERGED))
 			continue;
 
+		strbuf_reset(&key);
+		strbuf_addf(&key, "branch.%s.deletemerged", branch_name);
+		if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
+		    !opt_out) {
+			if (!(flags & DELETE_BRANCH_QUIET))
+				fprintf(stderr,
+					_("Skipping '%s' (branch.%s.deleteMerged is false)\n"),
+					branch_name, branch_name);
+			continue;
+		}
+
 		strset_add(&deletable_branch_names, branch_name);
 	}
 
@@ -873,6 +886,7 @@ static int delete_merged_branches(const struct strvec *upstreams,
 		clear_deleted_upstreams(&protected_branch_names,
 					&deletable_branch_names);
 
+	strbuf_release(&key);
 	strvec_clear(&branches_to_delete);
 	strset_clear(&protected_branch_names);
 	strset_clear(&deletable_branch_names);
t/t3200-branch.sh +36 -0
index 496072a3fe..31ca1719d9 100755
@@ -2103,4 +2103,40 @@ test_expect_success '--delete-merged requires a value' '
 	test_must_fail git -C forked branch --delete-merged 2>err &&
 	test_grep "requires a value" err
 '
+
+test_expect_success '--delete-merged honours branch.<name>.deleteMerged=false' '
+	setup_repo_for_delete_merged &&
+	create_merged_branch deleted &&
+	create_merged_branch kept &&
+	(
+		cd repo &&
+		git config branch.kept.deleteMerged false &&
+		git checkout --detach &&
+
+		git branch --delete-merged origin/next 2>err &&
+
+		test_grep "Skipping .kept." err &&
+		check_branches <<-\EOF
+		kept
+		main
+		EOF
+	)
+'
+
+test_expect_success "branch -d still deletes a deleteMerged=false branch" '
+	setup_repo_for_delete_merged &&
+	create_merged_branch kept &&
+	(
+		cd repo &&
+		git config branch.kept.deleteMerged false &&
+		git checkout --detach &&
+
+		git branch -d kept &&
+
+		check_branches <<-\EOF
+		main
+		EOF
+	)
+'
+
 test_done
Signature · 1 line
gitgitgadget
#289 lore raw

PATCH v25 7/7 branch: add --dry-run for --delete-merged

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

"git branch --dry-run --delete-merged ..." prints one line per ref that would be deleted without modifying refs or branch configuration.

--dry-run is only meaningful together with --delete-merged and is rejected otherwise.

---
 Documentation/git-branch.adoc |  8 ++++++-
 builtin/branch.c              | 23 ++++++++++++++++----
 t/t3200-branch.sh             | 41 ++++++++++++++++++++++++++++++++++-
 3 files changed, 66 insertions(+), 6 deletions(-)
Documentation/git-branch.adoc +7 -1
index cfaac4b90f..bfdf459329 100644
@@ -25,7 +25,7 @@ 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 (--delete-merged <pattern>)... [<branch-pattern>...]
+git branch [--dry-run] (--delete-merged <pattern>)... [<branch-pattern>...]
 
 DESCRIPTION
 -----------
@@ -235,6 +235,12 @@ A branch whose work has not yet been merged into its upstream is
 silently skipped. Delete it with `git branch -D` if you want to
 remove it anyway.
 
+`--dry-run`::
+	With `--delete-merged`, print which branches would be
+	deleted and exit without touching any ref.  Useful for
+	sanity-checking a wide pattern like `'origin/*'` before
+	committing to the deletion.
+
 `-v`::
 `-vv`::
 `--verbose`::
builtin/branch.c +19 -4
index 2d0c4f51ea..57ee384d23 100644
@@ -200,6 +200,7 @@ enum delete_branch_flags {
 	DELETE_BRANCH_QUIET = (1 << 1),
 	DELETE_BRANCH_SKIP_UNMERGED = (1 << 2),
 	DELETE_BRANCH_NO_HEAD_FALLBACK = (1 << 3),
+	DELETE_BRANCH_DRY_RUN = (1 << 4),
 };
 
 static int check_branch_commit(const char *branchname, const char *refname,
@@ -342,13 +343,20 @@ static int delete_branches(int argc, const char **argv, int kinds,
 		free(target);
 	}
 
-	if (refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
+	if (!(flags & DELETE_BRANCH_DRY_RUN) &&
+	    refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
 		ret = 1;
 
 	for_each_string_list_item(item, &refs_to_delete) {
 		char *describe_ref = item->util;
 		char *name = item->string;
-		if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
+		if (flags & DELETE_BRANCH_DRY_RUN) {
+			if (!(flags & DELETE_BRANCH_QUIET))
+				printf(remote_branch
+					? _("Would delete remote-tracking branch %s (was %s).\n")
+					: _("Would delete branch %s (was %s).\n"),
+					name + branch_name_pos, describe_ref);
+		} else if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
 			char *refname = name + branch_name_pos;
 			if (!(flags & DELETE_BRANCH_QUIET))
 				printf(remote_branch
@@ -882,7 +890,7 @@ static int delete_merged_branches(const struct strvec *upstreams,
 				      DELETE_BRANCH_NO_HEAD_FALLBACK |
 				      flags);
 
-	if (!ret)
+	if (!ret && !(flags & DELETE_BRANCH_DRY_RUN))
 		clear_deleted_upstreams(&protected_branch_names,
 					&deletable_branch_names);
 
@@ -960,6 +968,7 @@ int cmd_branch(int argc,
 	int delete = 0, rename = 0, copy = 0, list = 0,
 	    unset_upstream = 0, show_current = 0, edit_description = 0;
 	struct strvec delete_merged = STRVEC_INIT;
+	int dry_run = 0;
 	const char *new_upstream = NULL;
 	int noncreate_actions = 0;
 	/* possible options */
@@ -1016,6 +1025,8 @@ int cmd_branch(int argc,
 		OPT_CALLBACK_F(0, "delete-merged", &delete_merged, N_("pattern"),
 			N_("delete merged branches whose upstream matches <pattern> (repeatable)"),
 			PARSE_OPT_NONEG, parse_opt_strvec),
+		OPT_BOOL(0, "dry-run", &dry_run,
+			N_("with --delete-merged, only print which branches would be deleted")),
 		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")),
@@ -1078,6 +1089,9 @@ int cmd_branch(int argc,
 	if (noncreate_actions > 1)
 		usage_with_options(builtin_branch_usage, options);
 
+	if (dry_run && !delete_merged.nr)
+		die(_("--dry-run requires --delete-merged"));
+
 	if (recurse_submodules_explicit) {
 		if (!submodule_propagate_branches)
 			die(_("branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled"));
@@ -1118,7 +1132,8 @@ int cmd_branch(int argc,
 		goto out;
 	} else if (delete_merged.nr) {
 		ret = delete_merged_branches(&delete_merged, argv,
-					     quiet ? DELETE_BRANCH_QUIET : 0);
+					     (quiet ? DELETE_BRANCH_QUIET : 0) |
+					     (dry_run ? DELETE_BRANCH_DRY_RUN : 0));
 		goto out;
 	} else if (show_current) {
 		print_current_branch_name();
t/t3200-branch.sh +40 -1

65 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#290 lore raw

Re: [PATCH v25 0/7] branch: delete-merged

In reply to Harald Nordgren via GitGitGadget
Quoted 5 lines · Harald Nordgren via GitGitGadget

Delete branches that have already been merged on upstream.

Changes in v25:

* Wrap long lines in tests.

The interdiff looks just as expected.

Shall we mark the topic for 'next' now?

Thanks.

#291 lore raw

Re: [PATCH v25 0/7] branch: delete-merged

Quoted 11 lines · Junio C Hamano
Quoted 5 lines · Harald Nordgren via GitGitGadget

Delete branches that have already been merged on upstream.

Changes in v25:

* Wrap long lines in tests.

The interdiff looks just as expected.

Shall we mark the topic for 'next' now?

It looks ready to me

Thanks

Phillip