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-06-18 19:25:28 +00:00
committed by Junio C Hamano
parent 77fb3683fe
commit da2ec6d312
4 changed files with 51 additions and 2 deletions
+7
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.
+3 -2
View File
@@ -215,10 +215,11 @@ A branch is not deleted when:
+
--
* its upstream remote-tracking branch no longer exists,
* it is checked out in any worktree, or
* it is checked out in any worktree,
* its push destination (`<branch>@{push}`) equals its upstream
(`<branch>@{upstream}`), so it cannot be distinguished from a
branch that just looks "fully merged" right after a pull.
branch that just looks "fully merged" right after a pull, or
* `branch.<name>.deleteMerged` is set to `false`.
--
+
A branch whose work has not yet been merged into its upstream is
+15
View File
@@ -722,6 +722,8 @@ static int delete_merged_branches(int argc, const char **argv,
struct ref_filter filter = REF_FILTER_INIT;
struct ref_array candidates = { 0 };
struct strvec deletable = STRVEC_INIT;
struct strbuf key = STRBUF_INIT;
bool quiet = flags & DELETE_BRANCH_QUIET;
int i, ret = 0;
if (!argc)
@@ -739,6 +741,7 @@ static int delete_merged_branches(int argc, const char **argv,
const char *short_name;
struct branch *branch;
const char *upstream, *push;
int opt_out;
if (!skip_prefix(full_name, "refs/heads/", &short_name))
BUG("filter returned non-branch ref '%s'", full_name);
@@ -753,6 +756,17 @@ static int delete_merged_branches(int argc, const char **argv,
if (!push || !strcmp(push, upstream))
continue;
strbuf_reset(&key);
strbuf_addf(&key, "branch.%s.deletemerged", short_name);
if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
!opt_out) {
if (!quiet)
fprintf(stderr,
_("Skipping '%s' (branch.%s.deleteMerged is false)\n"),
short_name, short_name);
continue;
}
strvec_push(&deletable, short_name);
}
@@ -763,6 +777,7 @@ static int delete_merged_branches(int argc, const char **argv,
DELETE_BRANCH_NO_HEAD_FALLBACK |
flags);
strbuf_release(&key);
strvec_clear(&deletable);
ref_array_clear(&candidates);
ref_filter_clear(&filter);
+26
View File
@@ -1944,4 +1944,30 @@ test_expect_success '--delete-merged requires at least one <branch>' '
test_grep "requires at least one <branch>" err
'
test_expect_success '--delete-merged honours branch.<name>.deleteMerged=false' '
test_when_finished "rm -rf repo" &&
setup_repo_for_delete_merged &&
merged_branch deleted origin/next &&
merged_branch kept origin/next &&
git -C repo config branch.kept.deleteMerged false &&
git -C repo checkout --detach &&
git -C repo branch --delete-merged origin/next 2>err &&
test_grep "Skipping .kept." err &&
test_must_fail git -C repo rev-parse --verify refs/heads/deleted &&
git -C repo rev-parse --verify refs/heads/kept
'
test_expect_success "branch -d still deletes a deleteMerged=false branch" '
test_when_finished "rm -rf repo" &&
setup_repo_for_delete_merged &&
merged_branch kept origin/next &&
git -C repo config branch.kept.deleteMerged false &&
git -C repo checkout --detach &&
git -C repo branch -d kept &&
test_must_fail git -C repo rev-parse --verify refs/heads/kept
'
test_done