All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>,
	Johannes Sixt <j6t@kdbg.org>,
	Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v4 0/6] fetch: add fetch.pruneBranches config
Date: Tue, 05 May 2026 19:23:52 +0000	[thread overview]
Message-ID: <pull.2285.v4.git.git.1778009038.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2285.v3.git.git.1777965747.gitgitgadget@gmail.com>

I ran this on one of my repos and wiped out the local master branch! That
was a bad surprise, so I made an update now to treat the default branch in a
special way (never prune it).

 * 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.

Harald Nordgren (6):
  branch: add --forked <remote>
  branch: let delete_branches warn instead of error on bulk refusal
  branch: add --prune-merged <remote>
  fetch: add --prune-merged
  branch: add branch.<name>.pruneMerged opt-out
  branch: add --all-remotes flag

 Documentation/config/branch.adoc |   7 +
 Documentation/fetch-options.adoc |   8 +
 Documentation/git-branch.adoc    |  32 ++++
 builtin/branch.c                 | 289 +++++++++++++++++++++++++++++--
 builtin/fetch.c                  |  20 +++
 t/t3200-branch.sh                | 247 ++++++++++++++++++++++++++
 t/t5510-fetch.sh                 |  31 ++++
 7 files changed, 623 insertions(+), 11 deletions(-)


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

Range-diff vs v3:

 1:  77e67d4b8b = 1:  77e67d4b8b branch: add --forked <remote>
 2:  807c9f981f = 2:  807c9f981f branch: let delete_branches warn instead of error on bulk refusal
 3:  49dc853403 ! 3:  77beb620d7 branch: add --prune-merged <remote>
     @@ builtin/branch.c: static int collect_forked_branch(const struct reference *ref,
       }
       
      -static int list_forked_branches(int argc, const char **argv)
     ++static void collect_default_branch_refs(const struct string_list *remote_names,
     ++					struct string_list *out)
     ++{
     ++	struct ref_store *refs = get_main_ref_store(the_repository);
     ++	struct string_list_item *item;
     ++
     ++	for_each_string_list_item(item, remote_names) {
     ++		struct strbuf head = STRBUF_INIT;
     ++		const char *target;
     ++
     ++		strbuf_addf(&head, "refs/remotes/%s/HEAD", item->string);
     ++		target = refs_resolve_ref_unsafe(refs, head.buf,
     ++						 RESOLVE_REF_NO_RECURSE,
     ++						 NULL, NULL);
     ++		if (target && starts_with(target, "refs/remotes/"))
     ++			string_list_insert(out, target);
     ++		strbuf_release(&head);
     ++	}
     ++}
     ++
      +static void collect_forked_set(int argc, const char **argv,
     ++			       struct string_list *protected_default_refs,
      +			       struct string_list *out)
       {
       	struct string_list remote_names = STRING_LIST_INIT_NODUP;
     @@ builtin/branch.c: static int collect_forked_branch(const struct reference *ref,
      -	for_each_string_list_item(item, &out)
      -		puts(item->string);
      +	string_list_sort(out);
     ++
     ++	if (protected_default_refs)
     ++		collect_default_branch_refs(&remote_names, protected_default_refs);
       
       	string_list_clear(&remote_names, 0);
       	string_list_clear(&tracking_refs, 0);
     @@ builtin/branch.c: static int collect_forked_branch(const struct reference *ref,
      +	if (!argc)
      +		die(_("--forked requires at least one <remote>"));
      +
     -+	collect_forked_set(argc, argv, &out);
     ++	collect_forked_set(argc, argv, NULL, &out);
      +	for_each_string_list_item(item, &out)
      +		puts(item->string);
      +
     @@ builtin/branch.c: static int collect_forked_branch(const struct reference *ref,
      +				 int quiet)
      +{
      +	struct string_list candidates = STRING_LIST_INIT_DUP;
     ++	struct string_list protected_default_refs = STRING_LIST_INIT_DUP;
      +	struct strvec deletable = STRVEC_INIT;
      +	struct string_list_item *item;
      +	int n_not_merged = 0;
     @@ builtin/branch.c: static int collect_forked_branch(const struct reference *ref,
      +	if (!argc)
      +		die(_("--prune-merged requires at least one <remote>"));
      +
     -+	collect_forked_set(argc, argv, &candidates);
     ++	collect_forked_set(argc, argv, &protected_default_refs, &candidates);
      +
      +	for_each_string_list_item(item, &candidates) {
      +		const char *short_name = item->string;
      +		struct strbuf full = STRBUF_INIT;
      +		struct branch *branch;
      +		const char *push_ref;
     ++		const char *upstream;
      +
      +		strbuf_addf(&full, "refs/heads/%s", short_name);
      +		if (branch_checked_out(full.buf)) {
     @@ builtin/branch.c: static int collect_forked_branch(const struct reference *ref,
      +		strbuf_release(&full);
      +
      +		branch = branch_get(short_name);
     ++		upstream = branch ? branch_get_upstream(branch, NULL) : NULL;
     ++		if (upstream &&
     ++		    string_list_has_string(&protected_default_refs, upstream)) {
     ++			const char *leaf = strrchr(upstream, '/');
     ++			if (leaf && !strcmp(leaf + 1, short_name))
     ++				continue;
     ++		}
     ++
      +		push_ref = branch ? branch_get_push(branch, NULL) : NULL;
      +		if (!push_ref)
      +			continue;
      +		if (refs_ref_exists(get_main_ref_store(the_repository),
      +				    push_ref))
      +			continue;
     ++		if (string_list_has_string(&protected_default_refs, push_ref))
     ++			continue;
      +
      +		strvec_push(&deletable, short_name);
      +	}
     @@ builtin/branch.c: static int collect_forked_branch(const struct reference *ref,
      +
      +	strvec_clear(&deletable);
      +	string_list_clear(&candidates, 0);
     ++	string_list_clear(&protected_default_refs, 0);
      +	return ret;
      +}
      +
     @@ t/t3200-branch.sh: test_expect_success '--forked requires at least one <remote>'
      +	test_when_finished "rm -rf pm-pushdiff" &&
      +	git clone pm-upstream pm-pushdiff &&
      +	git -C pm-pushdiff config push.default current &&
     -+	git -C pm-pushdiff branch --track topic-a origin/main &&
     ++	git -C pm-pushdiff branch --track topic-a origin/one &&
      +
      +	git -C pm-pushdiff branch --force --prune-merged origin &&
      +
      +	test_must_fail git -C pm-pushdiff rev-parse --verify refs/heads/topic-a
      +'
     ++
     ++test_expect_success '--prune-merged spares the local default branch' '
     ++	test_when_finished "rm -rf pm-default" &&
     ++	git clone pm-upstream pm-default &&
     ++	git -C pm-default config push.default current &&
     ++	git -C pm-default checkout --detach &&
     ++	git -C pm-default branch --prune-merged origin &&
     ++	git -C pm-default rev-parse --verify refs/heads/main
     ++'
     ++
     ++test_expect_success '--prune-merged protects only the default branch by name, not by upstream' '
     ++	test_when_finished "rm -rf pm-default-alias" &&
     ++	git clone pm-upstream pm-default-alias &&
     ++	git -C pm-default-alias config push.default current &&
     ++	git -C pm-default-alias branch --track trunk origin/main &&
     ++	git -C pm-default-alias checkout --detach &&
     ++	git -C pm-default-alias branch --force --prune-merged origin &&
     ++	git -C pm-default-alias rev-parse --verify refs/heads/main &&
     ++	test_must_fail git -C pm-default-alias rev-parse --verify refs/heads/trunk
     ++'
     ++
     ++test_expect_success '--prune-merged spares branches whose push ref is the default branch' '
     ++	test_when_finished "rm -rf pm-pushdefault" &&
     ++	git clone pm-upstream pm-pushdefault &&
     ++	git -C pm-pushdefault branch --track topic origin/one &&
     ++	git -C pm-pushdefault config --add remote.origin.push refs/heads/topic:refs/heads/main &&
     ++	git -C pm-pushdefault update-ref -d refs/remotes/origin/one &&
     ++	git -C pm-pushdefault update-ref -d refs/remotes/origin/main &&
     ++	git -C pm-pushdefault checkout --detach &&
     ++	git -C pm-pushdefault branch --prune-merged origin &&
     ++	git -C pm-pushdefault rev-parse --verify refs/heads/topic
     ++'
      +
       test_done
 4:  938bf7c794 = 4:  98cfdb87d2 fetch: add --prune-merged
 5:  b2e7c97298 ! 5:  c645526bb5 branch: add branch.<name>.pruneMerged opt-out
     @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
      +		struct strbuf key = STRBUF_INIT;
       		struct branch *branch;
       		const char *push_ref;
     + 		const char *upstream;
      +		int opt_out = 0;
       
       		strbuf_addf(&full, "refs/heads/%s", short_name);
     @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
       			continue;
       		}
       		strbuf_release(&full);
     +@@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv, int force,
     + 		if (upstream &&
     + 		    string_list_has_string(&protected_default_refs, upstream)) {
     + 			const char *leaf = strrchr(upstream, '/');
     +-			if (leaf && !strcmp(leaf + 1, short_name))
     ++			if (leaf && !strcmp(leaf + 1, short_name)) {
     ++				strbuf_release(&key);
     + 				continue;
     ++			}
     + 		}
       
     - 		branch = branch_get(short_name);
       		push_ref = branch ? branch_get_push(branch, NULL) : NULL;
      -		if (!push_ref)
      +		if (!push_ref) {
     @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
      +			strbuf_release(&key);
      +			continue;
      +		}
     ++		if (string_list_has_string(&protected_default_refs, push_ref)) {
     ++			strbuf_release(&key);
     + 			continue;
     +-		if (string_list_has_string(&protected_default_refs, push_ref))
     ++		}
      +
      +		strbuf_addf(&key, "branch.%s.prunemerged", short_name);
      +		if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
     @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
       	}
      
       ## t/t3200-branch.sh ##
     -@@ t/t3200-branch.sh: test_expect_success '--prune-merged deletes when push ref differs from upstream'
     - 	test_must_fail git -C pm-pushdiff rev-parse --verify refs/heads/topic-a
     +@@ t/t3200-branch.sh: test_expect_success '--prune-merged spares branches whose push ref is the defaul
     + 	git -C pm-pushdefault rev-parse --verify refs/heads/topic
       '
       
      +test_expect_success '--prune-merged honours branch.<name>.pruneMerged=false' '
 6:  6462642cd0 ! 6:  690242d89b branch: add --all-remotes flag
     @@ builtin/branch.c: static void copy_or_rename_branch(const char *oldname, const c
       static void parse_forked_args(int argc, const char **argv,
       			      struct string_list *remote_names,
       			      struct string_list *tracking_refs)
     -@@ builtin/branch.c: static int collect_forked_branch(const struct reference *ref, void *cb_data)
     - 	return 0;
     +@@ builtin/branch.c: static void collect_default_branch_refs(const struct string_list *remote_names,
     + 	}
       }
       
      -static void collect_forked_set(int argc, const char **argv,
      +static void collect_forked_set(int argc, const char **argv, int all_remotes,
     + 			       struct string_list *protected_default_refs,
       			       struct string_list *out)
       {
     - 	struct string_list remote_names = STRING_LIST_INIT_NODUP;
      @@ builtin/branch.c: static void collect_forked_set(int argc, const char **argv,
       	};
       
     @@ builtin/branch.c: static void collect_forked_set(int argc, const char **argv,
      +	if (!argc && !all_remotes)
      +		die(_("--forked requires at least one <remote> or --all-remotes"));
       
     --	collect_forked_set(argc, argv, &out);
     -+	collect_forked_set(argc, argv, all_remotes, &out);
     +-	collect_forked_set(argc, argv, NULL, &out);
     ++	collect_forked_set(argc, argv, all_remotes, NULL, &out);
       	for_each_string_list_item(item, &out)
       		puts(item->string);
       
     @@ builtin/branch.c: static int list_forked_branches(int argc, const char **argv)
      +				 int all_remotes, int force, int quiet)
       {
       	struct string_list candidates = STRING_LIST_INIT_DUP;
     - 	struct strvec deletable = STRVEC_INIT;
     + 	struct string_list protected_default_refs = STRING_LIST_INIT_DUP;
      @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv, int force,
       	int n_not_merged = 0;
       	int ret = 0;
     @@ builtin/branch.c: static int prune_merged_branches(int argc, const char **argv,
      +	if (!argc && !all_remotes)
      +		die(_("--prune-merged requires at least one <remote> or --all-remotes"));
       
     --	collect_forked_set(argc, argv, &candidates);
     -+	collect_forked_set(argc, argv, all_remotes, &candidates);
     +-	collect_forked_set(argc, argv, &protected_default_refs, &candidates);
     ++	collect_forked_set(argc, argv, all_remotes, &protected_default_refs,
     ++			   &candidates);
       
       	for_each_string_list_item(item, &candidates) {
       		const char *short_name = item->string;

-- 
gitgitgadget

  parent reply	other threads:[~2026-05-05 19:24 UTC|newest]

Thread overview: 70+ 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     ` Harald Nordgren via GitGitGadget [this message]
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-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

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.v4.git.git.1778009038.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=haraldnordgren@gmail.com \
    --cc=j6t@kdbg.org \
    --cc=kristofferhaugsbakk@fastmail.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.