#include "branch.h"
#include "path.h"
#include "string-list.h"
-#include "strmap.h"
#include "column.h"
#include "utf8.h"
#include "ref-filter.h"
#include "commit-reach.h"
static const char * const builtin_branch_usage[] = {
- N_("git branch [<options>] [-r | -a] [--merged] [--no-merged] [(--forked <branch>)...]"),
+ N_("git branch [<options>] [-r | -a] [--merged] [--no-merged]"),
N_("git branch [<options>] [-f] [--recurse-submodules] <branch-name> [<start-point>]"),
N_("git branch [<options>] [-l] [<pattern>...]"),
N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
N_("git branch [<options>] [-r | -a] [--points-at]"),
N_("git branch [<options>] [-r | -a] [--format]"),
- N_("git branch [<options>] (--delete-merged <branch>)... [<pattern>...]"),
NULL
};
* upstream, if any, otherwise with HEAD", we should just
* return the result of the repo_in_merge_bases() above without
* any of the following code, but during the transition period,
- * a gentle reminder is in order. Callers that opt out of the
- * HEAD fallback by passing head_rev=NULL are not interested in
- * the reminder either: they have already established that the
- * branch has an upstream, so HEAD is irrelevant to the decision.
+ * a gentle reminder is in order.
*/
- if (head_rev && head_rev != reference_rev) {
- int expect = repo_in_merge_bases(the_repository, rev, head_rev);
+ if (head_rev != reference_rev) {
+ int expect = head_rev ? repo_in_merge_bases(the_repository, rev, head_rev) : 0;
if (expect < 0)
exit(128);
if (expect == merged)
return merged;
}
-enum delete_branch_flags {
- DELETE_BRANCH_FORCE = (1 << 0),
- DELETE_BRANCH_QUIET = (1 << 1),
- DELETE_BRANCH_SKIP_UNMERGED = (1 << 2),
- DELETE_BRANCH_NO_HEAD_FALLBACK = (1 << 3),
- DELETE_BRANCH_DRY_RUN = (1 << 4),
-};
-
static int check_branch_commit(const char *branchname, const char *refname,
const struct object_id *oid, struct commit *head_rev,
- int kinds, unsigned int flags)
+ int kinds, int force)
{
struct commit *rev = lookup_commit_reference(the_repository, oid);
- if (!(flags & DELETE_BRANCH_FORCE) && !rev) {
+ if (!force && !rev) {
error(_("couldn't look up commit object for '%s'"), refname);
return -1;
}
- if (!(flags & DELETE_BRANCH_FORCE) &&
- !branch_merged(kinds, branchname, rev, head_rev)) {
- if (!(flags & DELETE_BRANCH_SKIP_UNMERGED)) {
- error(_("the branch '%s' is not fully merged"),
- branchname);
- advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
- _("If you are sure you want to delete it, "
- "run 'git branch -D %s'"), branchname);
- }
+ if (!force && !branch_merged(kinds, branchname, rev, head_rev)) {
+ error(_("the branch '%s' is not fully merged"), branchname);
+ advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
+ _("If you are sure you want to delete it, "
+ "run 'git branch -D %s'"), branchname);
return -1;
}
return 0;
strbuf_release(&buf);
}
-static int delete_branches(int argc, const char **argv, int kinds,
- unsigned int flags)
+static int delete_branches(int argc, const char **argv, int force, int kinds,
+ int quiet)
{
struct commit *head_rev = NULL;
struct object_id oid;
remote_branch = 1;
allowed_interpret = INTERPRET_BRANCH_REMOTE;
- flags |= DELETE_BRANCH_FORCE;
+ force = 1;
break;
case FILTER_REFS_BRANCHES:
fmt = "refs/heads/%s";
}
branch_name_pos = strcspn(fmt, "%");
- if (!(flags & DELETE_BRANCH_FORCE) &&
- !(flags & DELETE_BRANCH_NO_HEAD_FALLBACK))
+ if (!force)
head_rev = lookup_commit_reference(the_repository, &head_oid);
for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
char *target = NULL;
- int ref_flags = 0;
+ int flags = 0;
copy_branchname(the_repository, &bname,
argv[i], allowed_interpret);
RESOLVE_REF_READING
| RESOLVE_REF_NO_RECURSE
| RESOLVE_REF_ALLOW_BAD_NAME,
- &oid, &ref_flags);
+ &oid, &flags);
if (!target) {
if (remote_branch) {
error(_("remote-tracking branch '%s' not found"), bname.buf);
| RESOLVE_REF_NO_RECURSE
| RESOLVE_REF_ALLOW_BAD_NAME,
&oid,
- &ref_flags);
+ &flags);
FREE_AND_NULL(virtual_name);
if (virtual_target)
continue;
}
- if (!(ref_flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
+ if (!(flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
check_branch_commit(bname.buf, name, &oid, head_rev, kinds,
- flags)) {
- if (!(flags & DELETE_BRANCH_SKIP_UNMERGED))
- ret = 1;
+ force)) {
+ ret = 1;
goto next;
}
item = string_list_append(&refs_to_delete, name);
- item->util = xstrdup((ref_flags & REF_ISBROKEN) ? "broken"
- : (ref_flags & REF_ISSYMREF) ? target
+ item->util = xstrdup((flags & REF_ISBROKEN) ? "broken"
+ : (flags & REF_ISSYMREF) ? target
: repo_find_unique_abbrev(the_repository, &oid, DEFAULT_ABBREV));
next:
free(target);
}
- if (!(flags & DELETE_BRANCH_DRY_RUN) &&
- refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
+ if (refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
ret = 1;
for_each_string_list_item(item, &refs_to_delete) {
char *describe_ref = item->util;
char *name = item->string;
- if (flags & DELETE_BRANCH_DRY_RUN) {
- if (!(flags & DELETE_BRANCH_QUIET))
- printf(remote_branch
- ? _("Would delete remote-tracking branch %s (was %s).\n")
- : _("Would delete branch %s (was %s).\n"),
- name + branch_name_pos, describe_ref);
- } else if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
+ if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
char *refname = name + branch_name_pos;
- if (!(flags & DELETE_BRANCH_QUIET))
+ if (!quiet)
printf(remote_branch
? _("Deleted remote-tracking branch %s (was %s).\n")
: _("Deleted branch %s (was %s).\n"),
free_worktrees(worktrees);
}
-static int parse_opt_forked(const struct option *opt, const char *arg, int unset)
-{
- struct ref_filter *filter = opt->value;
-
- BUG_ON_OPT_NEG(unset);
- if (ref_filter_forked_add(filter, arg) < 0)
- die(_("'%s' is not a valid branch or pattern"), arg);
- return 0;
-}
-
-struct stacked_branch_data {
- struct strset *deletable_branch_names;
- struct strset *protected_branch_names;
- struct strset *visited_branch_names;
-};
-
-static int collect_stacked_branch_bases(const struct reference *ref,
- void *cb_data)
-{
- struct stacked_branch_data *data = cb_data;
- const char *branch_name;
-
- if (!skip_prefix(ref->name, "refs/heads/", &branch_name))
- BUG("expected local branch ref, got '%s'", ref->name);
- if (strset_contains(data->deletable_branch_names, branch_name))
- return 0;
-
- while (strset_add(data->visited_branch_names, branch_name)) {
- struct branch *branch = branch_get(branch_name);
- const char *upstream_refname = branch_get_upstream(branch, NULL);
- const char *upstream_branch_name;
-
- if (!upstream_refname ||
- !skip_prefix(upstream_refname, "refs/heads/",
- &upstream_branch_name) ||
- !strset_contains(data->deletable_branch_names,
- upstream_branch_name))
- break;
-
- strset_add(data->protected_branch_names, upstream_branch_name);
- branch_name = upstream_branch_name;
- }
-
- return 0;
-}
-
-static void protect_stacked_branch_bases(struct ref_store *refs,
- struct strset *deletable_branch_names)
-{
- struct strset protected_branch_names = STRSET_INIT;
- struct strset visited_branch_names = STRSET_INIT;
- struct stacked_branch_data data = {
- .deletable_branch_names = deletable_branch_names,
- .protected_branch_names = &protected_branch_names,
- .visited_branch_names = &visited_branch_names,
- };
- struct refs_for_each_ref_options opts = {
- .prefix = "refs/heads/",
- };
- struct hashmap_iter iter;
- struct strmap_entry *entry;
-
- refs_for_each_ref_ext(refs, collect_stacked_branch_bases, &data, &opts);
-
- strset_for_each_entry(&protected_branch_names, &iter, entry)
- strset_remove(deletable_branch_names, entry->key);
-
- strset_clear(&visited_branch_names);
- strset_clear(&protected_branch_names);
-}
-
-static int branch_pushes_to_upstream(struct branch *branch,
- const char *upstream)
-{
- struct remote *remote = remote_get(remote_for_branch(branch, NULL));
- char *tracking = NULL;
- int ret = 0;
-
- if (remote)
- tracking = apply_refspecs(&remote->fetch, branch->refname);
- if (tracking && !strcmp(tracking, upstream))
- ret = 1;
-
- free(tracking);
- return ret;
-}
-
-static int delete_merged_branches(const struct strvec *upstreams,
- const char **argv, unsigned int flags)
-{
- struct ref_store *refs = get_main_ref_store(the_repository);
- struct ref_filter filter = REF_FILTER_INIT;
- struct ref_array candidates = { 0 };
- struct strset deletable_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;
-
- for (size_t i = 0; i < upstreams->nr; i++)
- if (ref_filter_forked_add(&filter, upstreams->v[i]) < 0)
- die(_("'%s' is not a valid branch or pattern"),
- upstreams->v[i]);
-
- filter.kind = FILTER_REFS_BRANCHES;
- filter.name_patterns = argv;
- filter_refs(&candidates, &filter, filter.kind);
-
- for (int i = 0; i < candidates.nr; i++) {
- const char *branch_refname = candidates.items[i]->refname;
- 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);
- if (branch_checked_out(branch_refname))
- continue;
-
- branch = branch_get(branch_name);
- upstream_refname = branch_get_upstream(branch, NULL);
- if (!upstream_refname || !refs_ref_exists(refs, upstream_refname))
- continue;
- if (branch_pushes_to_upstream(branch, upstream_refname))
- continue;
- if (check_branch_commit(branch_name, branch_name,
- &candidates.items[i]->objectname, NULL,
- 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);
- }
-
- protect_stacked_branch_bases(refs, &deletable_branch_names);
-
- strset_for_each_entry(&deletable_branch_names, &iter, entry)
- strvec_push(&branches_to_delete, entry->key);
-
- if (branches_to_delete.nr)
- ret = delete_branches(branches_to_delete.nr, branches_to_delete.v,
- FILTER_REFS_BRANCHES,
- DELETE_BRANCH_SKIP_UNMERGED |
- DELETE_BRANCH_NO_HEAD_FALLBACK |
- flags);
-
- strbuf_release(&key);
- strvec_clear(&branches_to_delete);
- strset_clear(&deletable_branch_names);
- ref_array_clear(&candidates);
- ref_filter_clear(&filter);
- return ret;
-}
-
static GIT_PATH_FUNC(edit_description, "EDIT_DESCRIPTION")
static int edit_branch_description(const char *branch_name)
/* possible actions */
int delete = 0, rename = 0, copy = 0, list = 0,
unset_upstream = 0, show_current = 0, edit_description = 0;
- struct strvec delete_merged = STRVEC_INIT;
- int dry_run = 0;
const char *new_upstream = NULL;
int noncreate_actions = 0;
/* possible options */
OPT_BOOL(0, "create-reflog", &reflog, N_("create the branch's reflog")),
OPT_BOOL(0, "edit-description", &edit_description,
N_("edit the description for the branch")),
- OPT_CALLBACK_F(0, "delete-merged", &delete_merged, N_("branch"),
- N_("delete merged branches whose upstream matches <branch> (repeatable)"),
- PARSE_OPT_NONEG, parse_opt_strvec),
- OPT_BOOL(0, "dry-run", &dry_run,
- N_("with --delete-merged, only print which branches would be deleted")),
OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE),
OPT_MERGED(&filter, N_("print only branches that are merged")),
OPT_NO_MERGED(&filter, N_("print only branches that are not merged")),
- OPT_CALLBACK_F(0, "forked", &filter, N_("branch"),
- N_("print only branches whose upstream matches <branch> (repeatable)"),
- PARSE_OPT_NONEG, parse_opt_forked),
OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")),
OPT_REF_SORT(&sorting_options),
OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"),
0);
if (!delete && !rename && !copy && !edit_description && !new_upstream &&
- !show_current && !unset_upstream && !delete_merged.nr &&
- argc == 0)
+ !show_current && !unset_upstream && argc == 0)
list = 1;
if (filter.with_commit || filter.no_commit ||
- filter.reachable_from || filter.unreachable_from ||
- filter.points_at.nr || filter.forked.nr)
+ filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
list = 1;
noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
!!show_current + !!list + !!edit_description +
- !!unset_upstream + !!delete_merged.nr;
+ !!unset_upstream;
if (noncreate_actions > 1)
usage_with_options(builtin_branch_usage, options);
- if (dry_run && !delete_merged.nr)
- die(_("--dry-run requires --delete-merged"));
-
if (recurse_submodules_explicit) {
if (!submodule_propagate_branches)
die(_("branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled"));
if (delete) {
if (!argc)
die(_("branch name required"));
- ret = delete_branches(argc, argv, filter.kind,
- (delete > 1 ? DELETE_BRANCH_FORCE : 0) |
- (quiet ? DELETE_BRANCH_QUIET : 0));
- goto out;
- } else if (delete_merged.nr) {
- ret = delete_merged_branches(&delete_merged, argv,
- (quiet ? DELETE_BRANCH_QUIET : 0) |
- (dry_run ? DELETE_BRANCH_DRY_RUN : 0));
+ ret = delete_branches(argc, argv, delete > 1, filter.kind, quiet);
goto out;
} else if (show_current) {
print_current_branch_name();
ret = 0;
out:
- strvec_clear(&delete_merged);
string_list_clear(&sorting_options, 0);
return ret;
}
test_cmp expect actual
'
-test_expect_success '--forked: setup' '
- test_create_repo forked-upstream &&
- (
- cd forked-upstream &&
- test_commit base &&
- git branch one base &&
- git branch two base
- ) &&
-
- test_create_repo forked-other &&
- (
- cd forked-other &&
- test_commit other-base &&
- git branch foreign other-base
- ) &&
-
- git clone forked-upstream forked &&
- (
- cd forked &&
- git remote add -f other ../forked-other &&
- git branch local-base &&
- git branch --track local-one origin/one &&
- git branch --track local-two origin/two &&
- git branch --track local-foreign other/foreign &&
- git branch --track local-onbase local-base &&
-
- git checkout local-one &&
- test_commit --no-tag local-one-work local-one.t &&
- git checkout local-foreign &&
- test_commit --no-tag local-foreign-work local-foreign.t
- )
-'
-
-test_expect_success '--forked <upstream-tracking-branch> filters by upstream' '
- git -C forked branch --forked origin/one --format="%(refname:short)" >actual &&
- echo local-one >expect &&
- test_cmp expect actual
-'
-
-test_expect_success '--forked <glob> filters by wildmatch' '
- git -C forked branch --forked "origin/*" --format="%(refname:short)" >actual &&
- cat >expect <<-\EOF &&
- local-one
- local-two
- main
- EOF
- test_cmp expect actual
-'
-
-test_expect_success '--forked <local-branch> matches branches with local upstream' '
- git -C forked branch --forked local-base --format="%(refname:short)" >actual &&
- echo local-onbase >expect &&
- test_cmp expect actual
-'
-
-test_expect_success '--forked can be repeated to widen the filter' '
- git -C forked branch --forked origin/one --forked other/foreign --format="%(refname:short)" >actual &&
- cat >expect <<-\EOF &&
- local-foreign
- local-one
- EOF
- test_cmp expect actual
-'
-
-test_expect_success '--forked combines literal and glob arguments' '
- git -C forked branch --forked local-base --forked "other/*" --format="%(refname:short)" >actual &&
- cat >expect <<-\EOF &&
- local-foreign
- local-onbase
- EOF
- test_cmp expect actual
-'
-
-test_expect_success '--forked "*/*" covers every remote-tracking upstream' '
- git -C forked branch --forked "*/*" --format="%(refname:short)" >actual &&
- cat >expect <<-\EOF &&
- local-foreign
- local-one
- local-two
- main
- EOF
- test_cmp expect actual
-'
-
-test_expect_success '--forked composes with --no-merged' '
- git -C forked branch --forked "origin/*" --no-merged origin/one \
- --format="%(refname:short)" >actual &&
- echo local-one >expect &&
- test_cmp expect actual
-'
-
-test_expect_success '--forked <remote> uses the branch <remote>/HEAD points at' '
- git -C forked branch --forked origin --format="%(refname:short)" >actual &&
- echo main >expect &&
- test_cmp expect actual
-'
-
-test_expect_success '--forked narrows a <pattern> argument' '
- git -C forked branch --forked "origin/*" "local-*" \
- --format="%(refname:short)" >actual &&
- cat >expect <<-\EOF &&
- local-one
- local-two
- EOF
- test_cmp expect actual
-'
-
-test_expect_success '--forked rejects unknown branch/pattern' '
- test_must_fail git -C forked branch --forked nope 2>err &&
- test_grep "not a valid branch or pattern" err
-'
-
-test_expect_success '--forked requires a value' '
- test_must_fail git -C forked branch --forked 2>err &&
- test_grep "requires a value" err
-'
-
-test_expect_success '--delete-merged: setup' '
- git init -b main upstream &&
- (
- cd upstream &&
- test_commit base &&
- git checkout -b next &&
- test_commit next-work &&
- git checkout main
- ) &&
- git init -b main other &&
- test_commit -C other other-base &&
- git init -b main fork
-'
-
-setup_repo_for_delete_merged () {
- rm -rf repo &&
- git clone upstream repo &&
- (
- cd repo &&
- git remote add fork ../fork &&
- git remote add other ../other &&
- git config push.default current &&
- git fetch other
- )
-}
-
-create_merged_branch () {
- (
- cd repo &&
- git checkout -b "$1" origin/next --track &&
- git commit --allow-empty -m "$1 work" &&
- git push origin "$1:next"
- )
-}
-
-check_branches () {
- git for-each-ref --format="%(refname:short)" refs/heads/ >actual &&
- cat >expect &&
- test_cmp expect actual
-}
-
-test_expect_success '--delete-merged keeps cloned main without a default push remote' '
- setup_repo_for_delete_merged &&
- (
- cd repo &&
- git checkout --detach &&
-
- git branch --delete-merged */* &&
-
- check_branches <<-\EOF
- main
- EOF
- )
-'
-
-test_expect_success '--delete-merged deletes only selected merged branches' '
- setup_repo_for_delete_merged &&
- create_merged_branch also-merged &&
- create_merged_branch merged &&
- (
- cd repo &&
- git checkout -b unmerged origin/next --track &&
- git commit --allow-empty -m "unmerged work" &&
- git checkout -b tracks-other other/main --track &&
- sha=$(git rev-parse --short merged) &&
-
- git branch --dry-run --delete-merged origin/next merged >actual 2>&1 &&
- echo "Would delete branch merged (was $sha)." >expect &&
- test_cmp expect actual &&
- git rev-parse --verify refs/heads/merged &&
-
- check_branches <<-\EOF &&
- also-merged
- main
- merged
- tracks-other
- unmerged
- EOF
-
- git branch --delete-merged origin/next merged >actual 2>&1 &&
- echo "Deleted branch merged (was $sha)." >expect &&
- test_cmp expect actual &&
-
- check_branches <<-\EOF
- also-merged
- main
- tracks-other
- unmerged
- EOF
- )
-'
-
-test_expect_success '--delete-merged keeps main despite a different default push remote' '
- setup_repo_for_delete_merged &&
- create_merged_branch on-next &&
- create_merged_branch checked-out &&
- create_merged_branch upstream-gone &&
- (
- cd repo &&
- git config remote.pushDefault fork &&
- git checkout -b local-to-delete main --track &&
- git update-ref refs/remotes/origin/topic refs/remotes/origin/next &&
- git branch --set-upstream-to=origin/topic upstream-gone &&
- git update-ref -d refs/remotes/origin/topic &&
- git checkout -b tracks-other other/main --track &&
- git checkout checked-out &&
-
- git branch --delete-merged origin/* \
- --delete-merged main &&
-
- check_branches <<-\EOF
- checked-out
- main
- tracks-other
- upstream-gone
- EOF
- )
-'
-
-test_expect_success '--delete-merged keeps the upstream of a surviving branch' '
- setup_repo_for_delete_merged &&
- create_merged_branch feature &&
- (
- cd repo &&
- git checkout -b topic feature --track &&
- git commit --allow-empty -m "topic work" &&
-
- git branch --dry-run --delete-merged origin/next >out &&
- test_grep ! "feature" out &&
-
- git branch --delete-merged origin/next 2>err &&
- test_must_be_empty err &&
-
- check_branches <<-\EOF &&
- feature
- main
- topic
- EOF
-
- git config --local --get-regexp "branch\\.(feature|topic)\\.(merge|remote)" >actual &&
- cat >expect <<-\EOF &&
- branch.feature.remote origin
- branch.feature.merge refs/heads/next
- branch.topic.remote .
- branch.topic.merge refs/heads/feature
- EOF
- test_cmp expect actual
- )
-'
-
-test_expect_success '--delete-merged keeps the upstream chain of a surviving branch' '
- setup_repo_for_delete_merged &&
- (
- cd repo &&
- git config remote.pushDefault fork &&
- git branch lower origin/next --track &&
- git branch mid lower --track &&
- git checkout -b tip mid --track &&
- git commit --allow-empty -m "tip work" &&
-
- git branch --dry-run --delete-merged origin/next \
- --delete-merged lower >actual 2>&1 &&
- test_must_be_empty actual &&
-
- git config --local --get-regexp "branch\\.(lower|mid|tip)\\.(merge|remote)" >actual &&
- cat >expect <<-\EOF &&
- branch.lower.remote origin
- branch.lower.merge refs/heads/next
- branch.mid.remote .
- branch.mid.merge refs/heads/lower
- branch.tip.remote .
- branch.tip.merge refs/heads/mid
- EOF
- test_cmp expect actual &&
-
- git branch --delete-merged origin/next \
- --delete-merged lower >actual 2>&1 &&
- test_must_be_empty actual &&
-
- check_branches <<-\EOF &&
- lower
- main
- mid
- tip
- EOF
-
- git config --local --get-regexp "branch\\.(lower|mid|tip)\\.(merge|remote)" >actual &&
- cat >expect <<-\EOF &&
- branch.lower.remote origin
- branch.lower.merge refs/heads/next
- branch.mid.remote .
- branch.mid.merge refs/heads/lower
- branch.tip.remote .
- branch.tip.merge refs/heads/mid
- EOF
- test_cmp expect actual
- )
-'
-
-test_expect_success '--delete-merged result is independent of stacked branch names' '
- setup_repo_for_delete_merged &&
- (
- cd repo &&
- git branch c-lower origin/next --track &&
- git branch b-mid c-lower --track &&
- git checkout -b a-tip b-mid --track &&
- git commit --allow-empty -m "tip work" &&
-
- git branch --delete-merged origin/next \
- --delete-merged "c-*" &&
-
- check_branches <<-\EOF &&
- a-tip
- b-mid
- c-lower
- main
- EOF
-
- git branch --delete-merged origin/next \
- --delete-merged "c-*" >actual 2>&1 &&
- test_must_be_empty actual &&
-
- check_branches <<-\EOF
- a-tip
- b-mid
- c-lower
- main
- EOF
- )
-'
-
-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_expect_success '--dry-run without --delete-merged is rejected' '
- test_must_fail git -C forked branch --dry-run 2>err &&
- test_grep "requires --delete-merged" err
-'
-
test_done