All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v2 0/6] fetch: add fetch.pruneBranches config
Date: Mon, 04 May 2026 18:27:24 +0000	[thread overview]
Message-ID: <pull.2285.v2.git.git.1777919250.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2285.git.git.1777671337839.gitgitgadget@gmail.com>

 * 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.<name>.pruneLocalBranches config
   options are gone, replaced by per-branch opt-out via
   branch.<name>.pruneMerged.
 * New git branch --forked <remote> lists local branches whose upstream
   lives on the given remote (read-only building block).
 * New git branch --prune-merged <remote> 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.<name>.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 (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                 | 247 +++++++++++++++++++++++++++++--
 builtin/fetch.c                  |  20 +++
 t/t3200-branch.sh                | 215 +++++++++++++++++++++++++++
 t/t5510-fetch.sh                 |  31 ++++
 7 files changed, 549 insertions(+), 11 deletions(-)


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

Range-diff vs v1:

 -:  ---------- > 1:  e9f8d06a2b branch: add --forked <remote>
 -:  ---------- > 2:  cd4a7e47af branch: let delete_branches warn instead of error on bulk refusal
 -:  ---------- > 3:  c0a5f69eb6 branch: add --prune-merged <remote>
 1:  14e3085ed2 ! 4:  e979fd238b fetch: add fetch.pruneLocalBranches config
     @@ Metadata
      Author: Harald Nordgren <haraldnordgren@gmail.com>
      
       ## Commit message ##
     -    fetch: add fetch.pruneLocalBranches config
     +    fetch: add --prune-merged
      
     -    Introduce a tri-state config option that, when --prune (or
     -    fetch.prune / remote.<name>.prune) removes a remote-tracking
     -    ref, also deletes local branches whose configured upstream is
     -    that ref.
     -
     -    Values:
     -    - false (default): no change in behavior.
     -    - safe: delete only if the local tip is reachable from the
     -      upstream tip, preserving any unpushed work.
     -    - force: delete unconditionally; recoverable only via reflog.
     -
     -    The currently checked-out branch is always preserved.
     +    After a successful fetch from a configured remote, run
     +    'git branch --prune-merged <remote>' to delete local branches
     +    whose push destination ref has just been pruned.
      
          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
      
     - ## Documentation/config/fetch.adoc ##
     -@@
     - 	refs. See also `remote.<name>.pruneTags` and the PRUNING
     - 	section of linkgit:git-fetch[1].
     - 
     -+`fetch.pruneBranches`::
     -+	When set in addition to `fetch.prune` (or `--prune`), also
     -+	delete local branches whose configured upstream
     -+	(`branch.<name>.merge`) is one of the remote-tracking refs
     -+	just removed by pruning. This is useful for cleaning up topic
     -+	branches whose upstream counterpart has been merged and then
     -+	removed. The same effect can be requested per-invocation with
     -+	`--prune-branches[=<mode>]`, or per-remote with
     -+	`remote.<name>.pruneBranches`.
     -++
     -+The currently checked-out branch (in any worktree) is never
     -+deleted. The value is one of:
     -++
     -+--
     -+`false` (the default);;
     -+	Do not delete any local branches. Equivalent to leaving
     -+	the option unset.
     -+`safe`;;
     -+	Delete a local branch only if its tip is an ancestor of
     -+	the upstream remote-tracking ref's last-known position.
     -+	In other words, only delete the branch if it contains no
     -+	commits that the upstream did not also have at the moment
     -+	it was deleted. This catches the common case of a branch
     -+	that was pushed and then squash- or rebase-merged
     -+	upstream (the local branch has no extra commits beyond
     -+	what was pushed), but preserves any branch with unpushed
     -+	local work.
     -+`force`;;
     -+	Delete the local branch unconditionally, even if it
     -+	contains unpushed commits. Use with care: if a remote
     -+	branch is deleted for any reason other than that its
     -+	contents were merged, the corresponding local commits
     -+	will only be retrievable through the reflog.
     -+--
     -++
     -+This option has no effect unless pruning is also enabled, since
     -+local branches are only considered for deletion when their
     -+upstream remote-tracking ref is being pruned in the same fetch.
     -+
     - `fetch.all`::
     - 	If true, fetch will attempt to update all available remotes.
     - 	This behavior can be overridden by passing `--no-all` or by
     -
     - ## Documentation/config/remote.adoc ##
     -@@ Documentation/config/remote.adoc: remote.<name>.pruneTags::
     - See also `remote.<name>.prune` and the PRUNING section of
     - linkgit:git-fetch[1].
     - 
     -+remote.<name>.pruneBranches::
     -+	When pruning is active for this remote and this is set to `safe`
     -+	or `force`, also delete local branches whose upstream
     -+	remote-tracking ref is being pruned. Overrides
     -+	`fetch.pruneBranches` settings, if any. See `fetch.pruneBranches`
     -+	for the meaning of the values.
     -+
     - remote.<name>.promisor::
     - 	When set to true, this remote will be used to fetch promisor
     - 	objects.
     -
       ## Documentation/fetch-options.adoc ##
      @@ Documentation/fetch-options.adoc: See the PRUNING section below for more details.
       +
       See the PRUNING section below for more details.
       
     -+`--prune-branches[=(safe|force)]`::
     -+	When pruning, also delete local branches whose configured
     -+	upstream (`branch.<name>.merge`) is one of the remote-tracking
     -+	refs being pruned. With no value or `safe`, refuse to delete a
     -+	branch with unpushed commits; with `force`, delete it
     -+	regardless. The currently checked-out branch is never
     -+	deleted. See `fetch.pruneBranches` in linkgit:git-config[1] for
     -+	details.
     ++`--prune-merged`::
     ++	After a successful fetch, run `git branch --prune-merged
     ++	<remote>` for the fetched remote, deleting local branches
     ++	that fork from this remote and whose tip is reachable from
     ++	their upstream remote-tracking ref. See linkgit:git-branch[1]
     ++	for the exact selection rules. The currently checked-out
     ++	branch is always preserved.
      +
       endif::git-pull[]
       
       ifndef::git-pull[]
      
     - ## Documentation/git-fetch.adoc ##
     -@@ Documentation/git-fetch.adoc: It's reasonable to e.g. configure `fetch.pruneTags=true` in
     - run, without making every invocation of `git fetch` without `--prune`
     - an error.
     - 
     -+Local branches whose upstream remote-tracking ref is being pruned can
     -+also be deleted automatically with `--prune-branches[=<mode>]` (or its
     -+config equivalents `fetch.pruneBranches` and `remote.<name>.pruneBranches`).
     -+See linkgit:git-config[1] for the data-loss tradeoff between the
     -+`safe` and `force` modes.
     -+
     - Pruning tags with `--prune-tags` also works when fetching a URL
     - instead of a named remote. These will all prune tags not found on
     - origin:
     -
       ## builtin/fetch.c ##
      @@ builtin/fetch.c: static int prune = -1; /* unspecified */
       static int prune_tags = -1; /* unspecified */
       #define PRUNE_TAGS_BY_DEFAULT 0 /* do we prune tags by default? */
       
     -+static int prune_branches = PRUNE_BRANCHES_UNSPECIFIED;
     -+
     -+static int parse_prune_branches_opt(const struct option *opt,
     -+				    const char *arg, int unset)
     -+{
     -+	int *v = opt->value;
     -+	if (unset)
     -+		*v = PRUNE_BRANCHES_OFF;
     -+	else if (arg)
     -+		*v = parse_prune_branches_value(opt->long_name, arg);
     -+	else
     -+		*v = PRUNE_BRANCHES_SAFE;
     -+	return 0;
     -+}
     ++static int prune_merged;
      +
       static int append, dry_run, force, keep, update_head_ok;
       static int write_fetch_head = 1;
       static int verbosity, deepen_relative, set_upstream, refetch;
     -@@ builtin/fetch.c: struct fetch_config {
     - 	int all;
     - 	int prune;
     - 	int prune_tags;
     -+	enum prune_branches_mode prune_branches;
     - 	int show_forced_updates;
     - 	int recurse_submodules;
     - 	int parallel;
     -@@ builtin/fetch.c: static int git_fetch_config(const char *k, const char *v,
     - 		return 0;
     - 	}
     +@@ builtin/fetch.c: static void add_options_to_argv(struct strvec *argv,
     + 		strvec_push(argv, prune ? "--prune" : "--no-prune");
     + 	if (prune_tags != -1)
     + 		strvec_push(argv, prune_tags ? "--prune-tags" : "--no-prune-tags");
     ++	if (prune_merged)
     ++		strvec_push(argv, "--prune-merged");
     + 	if (update_head_ok)
     + 		strvec_push(argv, "--update-head-ok");
     + 	if (force)
     +@@ builtin/fetch.c: static inline void fetch_one_setup_partial(struct remote *remote,
     + 	return;
     + }
       
     -+	if (!strcmp(k, "fetch.prunebranches")) {
     -+		fetch_config->prune_branches = parse_prune_branches_value(k, v);
     -+		return 0;
     -+	}
     -+
     - 	if (!strcmp(k, "fetch.showforcedupdates")) {
     - 		fetch_config->show_forced_updates = git_config_bool(k, v);
     - 		return 0;
     -@@ builtin/fetch.c: out:
     - static int prune_refs(struct display_state *display_state,
     - 		      struct refspec *rs,
     - 		      struct ref_transaction *transaction,
     --		      struct ref *ref_map)
     -+		      struct ref *ref_map,
     -+		      struct ref **stale_refs_out)
     - {
     - 	int result = 0;
     - 	struct ref *ref, *stale_refs = get_stale_heads(rs, ref_map);
     -@@ builtin/fetch.c: static int prune_refs(struct display_state *display_state,
     - cleanup:
     - 	string_list_clear(&refnames, 0);
     - 	strbuf_release(&err);
     --	free_refs(stale_refs);
     -+	if (!result && stale_refs_out)
     -+		*stale_refs_out = stale_refs;
     -+	else
     -+		free_refs(stale_refs);
     -+	return result;
     -+}
     -+
     -+struct prune_branches_cb {
     -+	struct string_list *pruned_refs;
     -+	struct string_list *to_delete;
     -+	struct string_list *skipped_unmerged;
     -+	enum prune_branches_mode mode;
     -+};
     -+
     -+static int collect_branches_to_prune(const struct reference *ref, void *cb_data)
     ++static int prune_merged_for_remote(const struct remote *remote)
      +{
     -+	struct prune_branches_cb *cb = cb_data;
     -+	const char *short_name = ref->name;
     -+	char *full_ref = xstrfmt("refs/heads/%s", short_name);
     -+	const char *upstream;
     -+	struct string_list_item *pruned;
     -+	int result = 0;
     -+
     -+	if (ref->flags & REF_ISSYMREF)
     -+		goto out;
     -+	if (branch_checked_out(full_ref))
     -+		goto out;
     -+
     -+	upstream = branch_get_upstream(branch_get(short_name), NULL);
     -+	if (!upstream)
     -+		goto out;
     ++	struct child_process cmd = CHILD_PROCESS_INIT;
      +
     -+	pruned = string_list_lookup(cb->pruned_refs, upstream);
     -+	if (!pruned)
     -+		goto out;
     -+
     -+	if (cb->mode == PRUNE_BRANCHES_SAFE) {
     -+		struct commit *local = lookup_commit_reference(the_repository,
     -+							       ref->oid);
     -+		struct commit *up = lookup_commit_reference(the_repository,
     -+							    pruned->util);
     -+		int reachable = local && up &&
     -+			repo_in_merge_bases(the_repository, local, up);
     -+
     -+		if (reachable < 0) {
     -+			result = -1;
     -+			goto out;
     -+		}
     -+		if (!reachable) {
     -+			string_list_append(cb->skipped_unmerged, short_name);
     -+			goto out;
     -+		}
     -+	}
     -+
     -+	string_list_append(cb->to_delete, full_ref);
     -+
     -+out:
     -+	free(full_ref);
     -+	return result;
     ++	cmd.git_cmd = 1;
     ++	strvec_pushl(&cmd.args, "branch", "--prune-merged", remote->name, NULL);
     ++	return run_command(&cmd);
      +}
      +
     -+static int do_prune_branches(struct display_state *display_state,
     -+			     struct ref *stale_refs,
     -+			     enum prune_branches_mode mode)
     -+{
     -+	struct string_list pruned_refs = STRING_LIST_INIT_NODUP;
     -+	struct string_list to_delete = STRING_LIST_INIT_DUP;
     -+	struct string_list skipped_unmerged = STRING_LIST_INIT_DUP;
     -+	struct prune_branches_cb cb = {
     -+		.pruned_refs = &pruned_refs,
     -+		.to_delete = &to_delete,
     -+		.skipped_unmerged = &skipped_unmerged,
     -+		.mode = mode,
     -+	};
     -+	struct ref *ref;
     -+	struct string_list_item *item;
     -+	int result = 0;
     -+
     -+	if (!stale_refs)
     -+		return 0;
     -+
     -+	for (ref = stale_refs; ref; ref = ref->next)
     -+		string_list_append(&pruned_refs, ref->name)->util = &ref->new_oid;
     -+	string_list_sort(&pruned_refs);
     -+
     -+	if (refs_for_each_branch_ref(get_main_ref_store(the_repository),
     -+				     collect_branches_to_prune, &cb)) {
     -+		result = -1;
     -+		goto cleanup;
     -+	}
     -+
     -+	if (!dry_run && to_delete.nr)
     -+		result = refs_delete_refs(get_main_ref_store(the_repository),
     -+					  "fetch: prune branches",
     -+					  &to_delete, REF_NO_DEREF);
     + static int fetch_one(struct remote *remote, int argc, const char **argv,
     + 		     int prune_tags_ok, int use_stdin_refspecs,
     + 		     const struct fetch_config *config,
     +@@ builtin/fetch.c: static int fetch_one(struct remote *remote, int argc, const char **argv,
     + 	refspec_clear(&rs);
     + 	transport_disconnect(gtransport);
     + 	gtransport = NULL;
      +
     -+	if (verbosity >= 0) {
     -+		const struct object_id *zero = null_oid(the_repository->hash_algo);
     -+		for_each_string_list_item(item, &to_delete) {
     -+			const char *short_name;
     -+			if (skip_prefix(item->string, "refs/heads/", &short_name))
     -+				display_ref_update(display_state, '-',
     -+						   _("[deleted local]"), NULL,
     -+						   _("(none)"), short_name,
     -+						   zero, zero,
     -+						   transport_summary_width(NULL));
     -+		}
     -+	}
     -+	for_each_string_list_item(item, &skipped_unmerged)
     -+		warning(_("not deleting local branch '%s' that is not "
     -+			  "fully merged into its upstream;\n"
     -+			  "         set fetch.pruneBranches=force to "
     -+			  "delete anyway, or delete manually with "
     -+			  "'git branch -D %s'"),
     -+			item->string, item->string);
     ++	if (!exit_code && prune_merged && remote_via_config &&
     ++	    prune_merged_for_remote(remote))
     ++		exit_code = 1;
      +
     -+cleanup:
     -+	string_list_clear(&pruned_refs, 0);
     -+	string_list_clear(&to_delete, 0);
     -+	string_list_clear(&skipped_unmerged, 0);
     - 	return result;
     + 	return exit_code;
       }
       
     -@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
     - 	if (tags == TAGS_DEFAULT && autotags)
     - 		transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, "1");
     - 	if (prune) {
     -+		struct ref *stale_refs = NULL;
     -+		struct ref **stale_refs_out = prune_branches != PRUNE_BRANCHES_OFF
     -+			? &stale_refs : NULL;
     - 		/*
     - 		 * We only prune based on refspecs specified
     - 		 * explicitly (via command line or configuration); we
     - 		 * don't care whether --tags was specified.
     - 		 */
     - 		if (rs->nr) {
     --			retcode = prune_refs(&display_state, rs, transaction, ref_map);
     -+			retcode = prune_refs(&display_state, rs, transaction,
     -+					     ref_map, stale_refs_out);
     - 		} else {
     - 			retcode = prune_refs(&display_state, &transport->remote->fetch,
     --					     transaction, ref_map);
     -+					     transaction, ref_map, stale_refs_out);
     - 		}
     - 		if (retcode != 0)
     - 			retcode = 1;
     -+		else if (stale_refs &&
     -+			 do_prune_branches(&display_state, stale_refs,
     -+					   prune_branches))
     -+			retcode = 1;
     -+		free_refs(stale_refs);
     - 	}
     - 
     - 	/*
     -@@ builtin/fetch.c: static int fetch_one(struct remote *remote, int argc, const char **argv,
     - 			prune_tags = PRUNE_TAGS_BY_DEFAULT;
     - 	}
     - 
     -+	if (prune_branches == PRUNE_BRANCHES_UNSPECIFIED) {
     -+		/* no command line request */
     -+		if (remote->prune_branches >= 0)
     -+			prune_branches = remote->prune_branches;
     -+		else if (config->prune_branches >= 0)
     -+			prune_branches = config->prune_branches;
     -+		else
     -+			prune_branches = PRUNE_BRANCHES_OFF;
     -+	}
     -+
     - 	maybe_prune_tags = prune_tags_ok && prune_tags;
     - 	if (maybe_prune_tags && remote_via_config)
     - 		refspec_append(&remote->fetch, TAG_REFSPEC);
     -@@ builtin/fetch.c: int cmd_fetch(int argc,
     - 		.display_format = DISPLAY_FORMAT_FULL,
     - 		.prune = -1,
     - 		.prune_tags = -1,
     -+		.prune_branches = PRUNE_BRANCHES_UNSPECIFIED,
     - 		.show_forced_updates = 1,
     - 		.recurse_submodules = RECURSE_SUBMODULES_DEFAULT,
     - 		.parallel = 1,
      @@ builtin/fetch.c: int cmd_fetch(int argc,
       			 N_("prune remote-tracking branches no longer on remote")),
       		OPT_BOOL('P', "prune-tags", &prune_tags,
       			 N_("prune local tags no longer on remote and clobber changed tags")),
     -+		OPT_CALLBACK_F(0, "prune-branches", &prune_branches, N_("mode"),
     -+			       N_("delete local branches whose upstream was pruned ('safe' or 'force')"),
     -+			       PARSE_OPT_OPTARG, parse_prune_branches_opt),
     ++		OPT_BOOL(0, "prune-merged", &prune_merged,
     ++			 N_("after pruning, also delete local branches forked from this remote whose tips are reachable from their upstream")),
       		OPT_CALLBACK_F(0, "recurse-submodules", &recurse_submodules_cli, N_("on-demand"),
       			    N_("control recursive fetching of submodules"),
       			    PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),
      
     - ## remote.c ##
     -@@ remote.c: static struct remote *make_remote(struct remote_state *remote_state,
     - 	CALLOC_ARRAY(ret, 1);
     - 	ret->prune = -1;  /* unspecified */
     - 	ret->prune_tags = -1;  /* unspecified */
     -+	ret->prune_branches = -1;  /* unspecified */
     - 	ret->name = xstrndup(name, len);
     - 	refspec_init_push(&ret->push);
     - 	refspec_init_fetch(&ret->fetch);
     -@@ remote.c: out:
     - }
     - #endif /* WITH_BREAKING_CHANGES */
     - 
     -+int parse_prune_branches_value(const char *k, const char *v)
     -+{
     -+	if (v) {
     -+		if (!strcasecmp(v, "safe"))
     -+			return PRUNE_BRANCHES_SAFE;
     -+		if (!strcasecmp(v, "force"))
     -+			return PRUNE_BRANCHES_FORCE;
     -+	}
     -+	if (git_parse_maybe_bool(v) == 0)
     -+		return PRUNE_BRANCHES_OFF;
     -+	die(_("invalid value for '%s': '%s'"), k, v);
     -+}
     -+
     - static int handle_config(const char *key, const char *value,
     - 			 const struct config_context *ctx, void *cb)
     - {
     -@@ remote.c: static int handle_config(const char *key, const char *value,
     - 		remote->prune = git_config_bool(key, value);
     - 	else if (!strcmp(subkey, "prunetags"))
     - 		remote->prune_tags = git_config_bool(key, value);
     -+	else if (!strcmp(subkey, "prunebranches"))
     -+		remote->prune_branches = parse_prune_branches_value(key, value);
     - 	else if (!strcmp(subkey, "url")) {
     - 		if (!value)
     - 			return config_error_nonbool(key);
     -
     - ## remote.h ##
     -@@ remote.h: enum {
     - #endif /* WITH_BREAKING_CHANGES */
     - };
     - 
     -+enum prune_branches_mode {
     -+	PRUNE_BRANCHES_UNSPECIFIED = -1,
     -+	PRUNE_BRANCHES_OFF = 0,
     -+	PRUNE_BRANCHES_SAFE,
     -+	PRUNE_BRANCHES_FORCE,
     -+};
     -+
     -+int parse_prune_branches_value(const char *k, const char *v);
     -+
     - struct rewrite {
     - 	const char *base;
     - 	size_t baselen;
     -@@ remote.h: struct remote {
     - 	int mirror;
     - 	int prune;
     - 	int prune_tags;
     -+	int prune_branches;
     - 
     - 	/**
     - 	 * The configured helper programs to run on the remote side, for
     -
       ## t/t5510-fetch.sh ##
      @@ t/t5510-fetch.sh: test_expect_success REFFILES 'fetch --prune fails to delete branches' '
       	)
       '
       
     -+test_expect_success 'fetch.pruneBranches: setup parent' '
     -+	git init -b main prune-branches-parent &&
     -+	test_commit -C prune-branches-parent base
     ++test_expect_success 'fetch --prune-merged: setup' '
     ++	git init -b main fetch-pm-parent &&
     ++	test_commit -C fetch-pm-parent base
      +'
      +
     -+test_expect_success 'fetch.pruneBranches=safe deletes merged local branch' '
     -+	git -C prune-branches-parent branch doomed base &&
     -+	git clone prune-branches-parent prune-branches-safe &&
     -+	git -C prune-branches-safe checkout -b doomed --track origin/doomed &&
     -+	git -C prune-branches-safe checkout -b stay &&
     -+	git -C prune-branches-parent branch -D doomed &&
     -+	git -C prune-branches-safe -c fetch.pruneBranches=safe fetch --prune origin &&
     -+	test_must_fail git -C prune-branches-safe rev-parse refs/remotes/origin/doomed &&
     -+	test_must_fail git -C prune-branches-safe rev-parse refs/heads/doomed
     -+'
     ++test_expect_success 'fetch --prune-merged deletes merged local branches' '
     ++	test_when_finished "rm -rf fetch-pm-clone" &&
     ++	git -C fetch-pm-parent branch one base &&
     ++	git clone fetch-pm-parent fetch-pm-clone &&
     ++	git -C fetch-pm-clone branch one --track origin/one &&
     ++	git -C fetch-pm-parent branch -D one &&
      +
     -+test_expect_success 'fetch.pruneBranches=safe keeps unmerged local branch' '
     -+	git -C prune-branches-parent branch doomed base &&
     -+	git clone prune-branches-parent prune-branches-safe-unmerged &&
     -+	git -C prune-branches-safe-unmerged checkout -b doomed --track origin/doomed &&
     -+	test_commit -C prune-branches-safe-unmerged local-only &&
     -+	git -C prune-branches-safe-unmerged checkout -b stay &&
     -+	git -C prune-branches-parent branch -D doomed &&
     -+	git -C prune-branches-safe-unmerged -c fetch.pruneBranches=safe fetch --prune origin 2>err &&
     -+	test_must_fail git -C prune-branches-safe-unmerged rev-parse refs/remotes/origin/doomed &&
     -+	git -C prune-branches-safe-unmerged rev-parse refs/heads/doomed &&
     -+	test_grep "not fully merged" err
     -+'
     -+
     -+test_expect_success 'fetch.pruneBranches=force deletes unmerged local branch' '
     -+	git -C prune-branches-parent branch doomed base &&
     -+	git clone prune-branches-parent prune-branches-force &&
     -+	git -C prune-branches-force checkout -b doomed --track origin/doomed &&
     -+	test_commit -C prune-branches-force local-only-force &&
     -+	git -C prune-branches-force checkout -b stay &&
     -+	git -C prune-branches-parent branch -D doomed &&
     -+	git -C prune-branches-force -c fetch.pruneBranches=force fetch --prune origin &&
     -+	test_must_fail git -C prune-branches-force rev-parse refs/remotes/origin/doomed &&
     -+	test_must_fail git -C prune-branches-force rev-parse refs/heads/doomed
     -+'
     -+
     -+test_expect_success 'fetch.pruneBranches=force never deletes checked-out branch' '
     -+	git -C prune-branches-parent branch doomed base &&
     -+	git clone prune-branches-parent prune-branches-checked-out &&
     -+	git -C prune-branches-checked-out checkout -b doomed --track origin/doomed &&
     -+	git -C prune-branches-parent branch -D doomed &&
     -+	git -C prune-branches-checked-out -c fetch.pruneBranches=force fetch --prune origin &&
     -+	test_must_fail git -C prune-branches-checked-out rev-parse refs/remotes/origin/doomed &&
     -+	git -C prune-branches-checked-out rev-parse refs/heads/doomed
     -+'
     -+
     -+test_expect_success '--prune-branches deletes merged local branch' '
     -+	git -C prune-branches-parent branch doomed base &&
     -+	git clone prune-branches-parent prune-branches-cli &&
     -+	git -C prune-branches-cli checkout -b doomed --track origin/doomed &&
     -+	git -C prune-branches-cli checkout -b stay &&
     -+	git -C prune-branches-parent branch -D doomed &&
     -+	git -C prune-branches-cli fetch --prune --prune-branches origin &&
     -+	test_must_fail git -C prune-branches-cli rev-parse refs/heads/doomed
     -+'
     ++	git -C fetch-pm-clone fetch --prune --prune-merged origin &&
      +
     -+test_expect_success '--no-prune-branches overrides fetch.pruneBranches' '
     -+	git -C prune-branches-parent branch doomed base &&
     -+	git clone prune-branches-parent prune-branches-no-cli &&
     -+	git -C prune-branches-no-cli checkout -b doomed --track origin/doomed &&
     -+	git -C prune-branches-no-cli checkout -b stay &&
     -+	git -C prune-branches-no-cli config fetch.pruneBranches force &&
     -+	git -C prune-branches-parent branch -D doomed &&
     -+	git -C prune-branches-no-cli fetch --prune --no-prune-branches origin &&
     -+	git -C prune-branches-no-cli rev-parse refs/heads/doomed
     ++	test_must_fail git -C fetch-pm-clone rev-parse --verify refs/heads/one
      +'
      +
     -+test_expect_success 'remote.<name>.pruneBranches overrides fetch.pruneBranches' '
     -+	git -C prune-branches-parent branch doomed base &&
     -+	git clone prune-branches-parent prune-branches-per-remote &&
     -+	git -C prune-branches-per-remote checkout -b doomed --track origin/doomed &&
     -+	git -C prune-branches-per-remote checkout -b stay &&
     -+	git -C prune-branches-per-remote config fetch.pruneBranches force &&
     -+	git -C prune-branches-per-remote config remote.origin.pruneBranches false &&
     -+	git -C prune-branches-parent branch -D doomed &&
     -+	git -C prune-branches-per-remote fetch --prune origin &&
     -+	git -C prune-branches-per-remote rev-parse refs/heads/doomed
     ++test_expect_success 'fetch --prune-merged skips unmerged local branches' '
     ++	test_when_finished "rm -rf fetch-pm-unmerged" &&
     ++	git -C fetch-pm-parent branch two base &&
     ++	git clone fetch-pm-parent fetch-pm-unmerged &&
     ++	git -C fetch-pm-unmerged checkout -b two --track origin/two &&
     ++	test_commit -C fetch-pm-unmerged unpushed &&
     ++	git -C fetch-pm-unmerged checkout - &&
     ++	git -C fetch-pm-parent branch -D two &&
     ++
     ++	git -C fetch-pm-unmerged fetch --prune --prune-merged origin 2>err &&
     ++	test_grep "not fully merged" err &&
     ++	git -C fetch-pm-unmerged rev-parse --verify refs/heads/two
      +'
      +
       test_expect_success 'fetch --atomic works with a single branch' '
 -:  ---------- > 5:  0bc5ebbe68 branch: add branch.<name>.pruneMerged opt-out
 -:  ---------- > 6:  66dac97626 branch: add --all-remotes flag

-- 
gitgitgadget

  parent reply	other threads:[~2026-05-04 18:27 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 ` Harald Nordgren via GitGitGadget [this message]
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-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.v2.git.git.1777919250.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=haraldnordgren@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.