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
next prev 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox