branch: add branch.<name>.deleteMerged opt-out

Setting branch.<name>.deleteMerged=false exempts that branch from
"git branch --delete-merged", which is useful for a topic you want
to keep developing after an early round of it has been merged
upstream. Unless --quiet is given, each skip is reported so the
user knows why their topic was kept.

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

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Harald Nordgren
2026-08-05 14:24:36 +00:00
committed by Junio C Hamano
parent 15bcdf43bb
commit 3d1f0df6e4
4 changed files with 59 additions and 1 deletions

View File

@@ -102,3 +102,10 @@ for details).
`git branch --edit-description`. Branch description is
automatically added to the `format-patch` cover letter or
`request-pull` summary.
`branch.<name>.deleteMerged`::
If set to `false`, branch _<name>_ is exempt from
`git branch --delete-merged`. Useful for a topic branch you
intend to develop further after an initial round has been
merged upstream. Defaults to true. Explicit deletion via
`git branch -d` is unaffected.

View File

@@ -224,7 +224,8 @@ A branch is not deleted when:
distinguished from a branch that just looks fully merged right
after a pull; this is determined by the remote's configured push and
fetch refspecs,
* it is the local upstream of a branch that is not being deleted.
* it is the local upstream of a branch that is not being deleted, or
* `branch.<name>.deleteMerged` is set to `false`.
--
+
When such a local upstream branch has its own upstream deleted by the

View File

@@ -818,6 +818,7 @@ static int delete_merged_branches(const struct strvec *upstreams,
struct strset deletable_branch_names = STRSET_INIT;
struct strset protected_branch_names = STRSET_INIT;
struct strvec branches_to_delete = STRVEC_INIT;
struct strbuf key = STRBUF_INIT;
struct hashmap_iter iter;
struct strmap_entry *entry;
int ret = 0;
@@ -836,6 +837,7 @@ static int delete_merged_branches(const struct strvec *upstreams,
const char *branch_name;
struct branch *branch;
const char *upstream_refname;
int opt_out;
if (!skip_prefix(branch_refname, "refs/heads/", &branch_name))
BUG("filter returned non-branch ref '%s'", branch_refname);
@@ -853,6 +855,17 @@ static int delete_merged_branches(const struct strvec *upstreams,
FILTER_REFS_BRANCHES, DELETE_BRANCH_SKIP_UNMERGED))
continue;
strbuf_reset(&key);
strbuf_addf(&key, "branch.%s.deletemerged", branch_name);
if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
!opt_out) {
if (!(flags & DELETE_BRANCH_QUIET))
fprintf(stderr,
_("Skipping '%s' (branch.%s.deleteMerged is false)\n"),
branch_name, branch_name);
continue;
}
strset_add(&deletable_branch_names, branch_name);
}
@@ -873,6 +886,7 @@ static int delete_merged_branches(const struct strvec *upstreams,
clear_deleted_upstreams(&protected_branch_names,
&deletable_branch_names);
strbuf_release(&key);
strvec_clear(&branches_to_delete);
strset_clear(&protected_branch_names);
strset_clear(&deletable_branch_names);

View File

@@ -2101,4 +2101,40 @@ test_expect_success '--delete-merged requires a value' '
test_must_fail git -C forked branch --delete-merged 2>err &&
test_grep "requires a value" err
'
test_expect_success '--delete-merged honours branch.<name>.deleteMerged=false' '
setup_repo_for_delete_merged &&
create_merged_branch deleted &&
create_merged_branch kept &&
(
cd repo &&
git config branch.kept.deleteMerged false &&
git checkout --detach &&
git branch --delete-merged origin/next 2>err &&
test_grep "Skipping .kept." err &&
check_branches <<-\EOF
kept
main
EOF
)
'
test_expect_success "branch -d still deletes a deleteMerged=false branch" '
setup_repo_for_delete_merged &&
create_merged_branch kept &&
(
cd repo &&
git config branch.kept.deleteMerged false &&
git checkout --detach &&
git branch -d kept &&
check_branches <<-\EOF
main
EOF
)
'
test_done