From: Junio C Hamano Date: Wed, 29 Jul 2026 15:53:32 +0000 (-0700) Subject: Revert "Merge branch 'hn/branch-delete-merged' into next" X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=36b42465bff74d8651ed552422e082bc6b048cc6;p=thirdparty%2Fgit.git Revert "Merge branch 'hn/branch-delete-merged' into next" This reverts commit c61a87bdf5fd46f4921228da399264eced14f380, reversing changes made to 57c0e98bfb6395faec6e8ec9b4b6374364f8898b. cf. <80bd230e-7b8c-41d3-af1c-fa84b0c7b1c4@gmail.com> --- diff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc index c2acd89c99..5a85fde8de 100644 --- a/Documentation/config/branch.adoc +++ b/Documentation/config/branch.adoc @@ -103,10 +103,3 @@ for details). `git branch --edit-description`. Branch description is automatically added to the `format-patch` cover letter or `request-pull` summary. - -`branch..deleteMerged`:: - If set to `false`, branch __ 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. diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc index 51dda15114..c0afddc424 100644 --- a/Documentation/git-branch.adoc +++ b/Documentation/git-branch.adoc @@ -13,7 +13,6 @@ git branch [--color[=] | --no-color] [--show-current] [--column[=] | --no-column] [--sort=] [--merged []] [--no-merged []] [--contains []] [--no-contains []] - [(--forked )...] [--points-at ] [--format=] [(-r|--remotes) | (-a|--all)] [--list] [...] @@ -25,7 +24,6 @@ git branch (-m|-M) [] git branch (-c|-C) [] git branch (-d|-D) [-r] ... git branch --edit-description [] -git branch [--dry-run] (--delete-merged )... [...] DESCRIPTION ----------- @@ -53,8 +51,7 @@ merged into the named commit (i.e. the branches whose tip commits are reachable from the named commit) will be listed. With `--no-merged` only branches not merged into the named commit will be listed. If the __ argument is missing it defaults to `HEAD` (i.e. the tip of the current -branch). With `--forked`, only branches whose configured upstream matches -the given branch or pattern will be listed. +branch). The command's second form creates a new branch head named __ which points to the current `HEAD`, or __ if given. As a @@ -202,42 +199,6 @@ This option is only applicable in non-verbose mode. Print the name of the current branch. In detached `HEAD` state, nothing is printed. -`--delete-merged `:: - Delete local branches whose configured upstream matches - __, but only when their tip is reachable from that - upstream. In other words, the work on the branch has already - landed on the upstream it tracks, so the local copy is no longer - needed. The option can be repeated to widen the upstream match. - Optional __ arguments limit which local branches are - considered, e.g. `git branch --delete-merged 'origin/*' - 'topic-*'`. -+ -A branch is not deleted when: -+ --- -* its configured upstream ref no longer exists, -* it is checked out in any worktree, -* pushing it by name to the remote configured by - `branch..remote` would update its upstream, so it cannot be - distinguished from a branch that just looks "fully merged" right - after a pull, or -* `branch..deleteMerged` is set to `false`. --- -+ -A branch whose work has not yet been merged into its upstream is -silently skipped. Delete it with `git branch -D` if you want to -remove it anyway. -+ -A branch that a surviving branch depends on through a chain of local -upstreams is kept, so a branch is never deleted out from under stacked -work. - -`--dry-run`:: - With `--delete-merged`, print which branches would be - deleted and exit without touching any ref. Useful for - sanity-checking a wide pattern like `'origin/*'` before - committing to the deletion. - `-v`:: `-vv`:: `--verbose`:: @@ -350,14 +311,6 @@ superproject's "origin/main", but tracks the submodule's "origin/main". Only list branches whose tips are not reachable from __ (`HEAD` if not specified). Implies `--list`. -`--forked `:: - Only list branches whose configured upstream matches - __. The argument can be a ref (e.g. `origin/main`, - `master`), a remote name like `origin` for the branch its - `origin/HEAD` points at, or a shell-style glob (e.g. - `'origin/*'`). The option can be repeated to widen the - filter. Implies `--list`. - `--points-at `:: Only list branches of __. diff --git a/builtin/branch.c b/builtin/branch.c index 985e36d68d..357209748d 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -21,7 +21,6 @@ #include "branch.h" #include "path.h" #include "string-list.h" -#include "strmap.h" #include "column.h" #include "utf8.h" #include "ref-filter.h" @@ -31,7 +30,7 @@ #include "commit-reach.h" static const char * const builtin_branch_usage[] = { - N_("git branch [] [-r | -a] [--merged] [--no-merged] [(--forked )...]"), + N_("git branch [] [-r | -a] [--merged] [--no-merged]"), N_("git branch [] [-f] [--recurse-submodules] []"), N_("git branch [] [-l] [...]"), N_("git branch [] [-r] (-d | -D) ..."), @@ -39,7 +38,6 @@ static const char * const builtin_branch_usage[] = { N_("git branch [] (-c | -C) [] "), N_("git branch [] [-r | -a] [--points-at]"), N_("git branch [] [-r | -a] [--format]"), - N_("git branch [] (--delete-merged )... [...]"), NULL }; @@ -170,13 +168,10 @@ static int branch_merged(int kind, const char *name, * 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) @@ -194,32 +189,20 @@ static int branch_merged(int kind, const char *name, 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; @@ -234,8 +217,8 @@ static void delete_branch_config(const char *branchname) 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; @@ -258,7 +241,7 @@ static int delete_branches(int argc, const char **argv, int kinds, remote_branch = 1; allowed_interpret = INTERPRET_BRANCH_REMOTE; - flags |= DELETE_BRANCH_FORCE; + force = 1; break; case FILTER_REFS_BRANCHES: fmt = "refs/heads/%s"; @@ -269,13 +252,12 @@ static int delete_branches(int argc, const char **argv, int kinds, } 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); @@ -305,7 +287,7 @@ static int delete_branches(int argc, const char **argv, int kinds, 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); @@ -317,7 +299,7 @@ static int delete_branches(int argc, const char **argv, int kinds, | RESOLVE_REF_NO_RECURSE | RESOLVE_REF_ALLOW_BAD_NAME, &oid, - &ref_flags); + &flags); FREE_AND_NULL(virtual_name); if (virtual_target) @@ -332,39 +314,31 @@ static int delete_branches(int argc, const char **argv, int kinds, 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"), @@ -707,172 +681,6 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int 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) @@ -937,8 +745,6 @@ int cmd_branch(int argc, /* 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 */ @@ -992,17 +798,9 @@ int cmd_branch(int argc, 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 (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 (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"), @@ -1044,24 +842,19 @@ int cmd_branch(int argc, 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")); @@ -1096,14 +889,7 @@ int cmd_branch(int argc, 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(); @@ -1276,7 +1062,6 @@ int cmd_branch(int argc, ret = 0; out: - strvec_clear(&delete_merged); string_list_clear(&sorting_options, 0); return ret; } diff --git a/ref-filter.c b/ref-filter.c index bdf54f6f59..29aca08ce7 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -2744,72 +2744,6 @@ static int filter_exclude_match(struct ref_filter *filter, const char *refname) return match_pattern(filter->exclude.v, refname, filter->ignore_case); } -static const char *short_upstream_name(const char *full_ref) -{ - const char *short_name = full_ref; - - if (!skip_prefix(short_name, "refs/heads/", &short_name)) - skip_prefix(short_name, "refs/remotes/", &short_name); - return short_name; -} - -/* - * Match the configured upstream of a branch against the registered - * --forked patterns. Exact patterns are compared against the full - * upstream refname so they are unambiguous; glob patterns are matched - * against the abbreviated upstream so that a glob such as origin/... - * works as typed. - */ -static int filter_forked_match(struct ref_filter *filter, const char *refname) -{ - const char *short_name; - struct branch *branch; - const char *upstream; - - if (!skip_prefix(refname, "refs/heads/", &short_name)) - return 0; - branch = branch_get(short_name); - if (!branch) - return 0; - upstream = branch_get_upstream(branch, NULL); - if (!upstream) - return 0; - - for (size_t i = 0; i < filter->forked.nr; i++) { - const char *pattern = filter->forked.v[i]; - if (has_glob_specials(pattern)) { - if (!wildmatch(pattern, short_upstream_name(upstream), - WM_PATHNAME)) - return 1; - } else if (!strcmp(pattern, upstream)) { - return 1; - } - } - return 0; -} - -int ref_filter_forked_add(struct ref_filter *filter, const char *arg) -{ - struct object_id oid; - char *full_ref = NULL; - - if (has_glob_specials(arg)) { - strvec_push(&filter->forked, arg); - return 0; - } - - if (repo_dwim_ref(the_repository, arg, strlen(arg), &oid, - &full_ref, 0) == 1 && - (starts_with(full_ref, "refs/heads/") || - starts_with(full_ref, "refs/remotes/"))) { - strvec_push(&filter->forked, full_ref); - free(full_ref); - return 0; - } - free(full_ref); - return -1; -} - /* * We need to seek to the reference right after a given marker but excluding any * matching references. So we seek to the lexicographically next reference. @@ -3045,9 +2979,6 @@ static struct ref_array_item *apply_ref_filter(const struct reference *ref, if (filter->points_at.nr && !match_points_at(&filter->points_at, ref->oid, ref->name)) return NULL; - if (filter->forked.nr && !filter_forked_match(filter, ref->name)) - return NULL; - /* * A merge filter is applied on refs pointing to commits. Hence * obtain the commit using the 'oid' available and discard all @@ -3833,7 +3764,6 @@ void ref_filter_init(struct ref_filter *filter) void ref_filter_clear(struct ref_filter *filter) { strvec_clear(&filter->exclude); - strvec_clear(&filter->forked); oid_array_clear(&filter->points_at); commit_list_free(filter->with_commit); commit_list_free(filter->no_commit); diff --git a/ref-filter.h b/ref-filter.h index 9361296e2a..120221b47f 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -67,7 +67,6 @@ struct ref_filter { const char **name_patterns; const char *start_after; struct strvec exclude; - struct strvec forked; struct oid_array points_at; struct commit_list *with_commit; struct commit_list *no_commit; @@ -111,7 +110,6 @@ struct ref_format { #define REF_FILTER_INIT { \ .points_at = OID_ARRAY_INIT, \ .exclude = STRVEC_INIT, \ - .forked = STRVEC_INIT, \ } #define REF_FORMAT_INIT { \ .use_color = GIT_COLOR_UNKNOWN, \ @@ -174,14 +172,6 @@ void ref_sorting_release(struct ref_sorting *); struct ref_sorting *ref_sorting_options(struct string_list *); /* Function to parse --merged and --no-merged options */ int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset); -/* - * Register a --forked pattern on the filter. The argument is - * either a ref, which is resolved to its full refname, or a shell-style - * glob. Branches are kept only when their configured upstream matches - * one of the registered patterns. Returns -1 if the argument is not a - * valid ref or pattern. - */ -int ref_filter_forked_add(struct ref_filter *filter, const char *arg); /* Get the current HEAD's description */ char *get_head_description(void); /* Set up translated strings in the output. */ diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index 443512677a..b5a56ff25d 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -1757,397 +1757,4 @@ test_expect_success 'errors if given a bad branch name' ' 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 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 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 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 uses the branch /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 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..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