Subject: [PATCH v12 3/6] branch: prepare delete_branches for a bulk caller

From: "Harald Nordgren via GitGitGadget" <[email-masked]>
Date: Wed, 03 Jun 2026 09:04:36 +0000

From: Harald Nordgren <[email-masked]>

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.

Signed-off-by: Harald Nordgren <[email-masked]>
---
 builtin/branch.c | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index 93d8eae891..09afdd9257 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -169,10 +169,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)
@@ -225,7 +228,8 @@ static void delete_branch_config(const char *branchname)
 }
 
 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;
@@ -259,7 +263,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
 	}
 	branch_name_pos = strcspn(fmt, "%");
 
-	if (!force)
+	if (!force && !no_head_fallback)
 		head_rev = lookup_commit_reference(the_repository, &head_oid);
 
 	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
@@ -330,13 +334,20 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
 		free(target);
 	}
 
-	if (refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
+	if (!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 (dry_run) {
+			if (!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 (!quiet)
 				printf(remote_branch
@@ -1003,7 +1014,7 @@ 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();
-- 
gitgitgadget