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>

Junio C Hamano committed Jul 29, 2026 at 08:53 UTC 36b42465bff74d8651ed552422e082bc6b048cc6
6 files changed +31 -773
Documentation/config/branch.adoc
-7
@@ -103,10 +103,3 @@ for details).
103 `git branch --edit-description`. Branch description is
104 automatically added to the `format-patch` cover letter or
105 `request-pull` summary.
106 -
107 -`branch.<name>.deleteMerged`::
108 - If set to `false`, branch _<name>_ is exempt from
109 - `git branch --delete-merged`. Useful for a topic branch you
110 - intend to develop further after an initial round has been
111 - merged upstream. Defaults to true. Explicit deletion via
112 - `git branch -d` is unaffected.
Documentation/git-branch.adoc
+1 -48
@@ -13,7 +13,6 @@ git branch [--color[=<when>] | --no-color] [--show-current]
13 [--column[=<options>] | --no-column] [--sort=<key>]
14 [--merged [<commit>]] [--no-merged [<commit>]]
15 [--contains [<commit>]] [--no-contains [<commit>]]
16 - [(--forked <branch>)...]
16 [--points-at <object>] [--format=<format>]
17 [(-r|--remotes) | (-a|--all)]
18 [--list] [<pattern>...]
@@ -25,7 +24,6 @@ git branch (-m|-M) [<old-branch>] <new-branch>
24 git branch (-c|-C) [<old-branch>] <new-branch>
25 git branch (-d|-D) [-r] <branch-name>...
26 git branch --edit-description [<branch-name>]
28 -git branch [--dry-run] (--delete-merged <branch>)... [<pattern>...]
27
28 DESCRIPTION
29 -----------
@@ -53,8 +51,7 @@ merged into the named commit (i.e. the branches whose tip commits are
51 reachable from the named commit) will be listed. With `--no-merged` only
52 branches not merged into the named commit will be listed. If the _<commit>_
53 argument is missing it defaults to `HEAD` (i.e. the tip of the current
56 -branch). With `--forked`, only branches whose configured upstream matches
57 -the given branch or pattern will be listed.
54 +branch).
55
56 The command's second form creates a new branch head named _<branch-name>_
57 which points to the current `HEAD`, or _<start-point>_ if given. As a
@@ -202,42 +199,6 @@ This option is only applicable in non-verbose mode.
199 Print the name of the current branch. In detached `HEAD` state,
200 nothing is printed.
201
205 -`--delete-merged <branch>`::
206 - Delete local branches whose configured upstream matches
207 - _<branch>_, but only when their tip is reachable from that
208 - upstream. In other words, the work on the branch has already
209 - landed on the upstream it tracks, so the local copy is no longer
210 - needed. The option can be repeated to widen the upstream match.
211 - Optional _<pattern>_ arguments limit which local branches are
212 - considered, e.g. `git branch --delete-merged 'origin/*'
213 - 'topic-*'`.
214 -+
215 -A branch is not deleted when:
216 -+
217 ---
218 -* its configured upstream ref no longer exists,
219 -* it is checked out in any worktree,
220 -* pushing it by name to the remote configured by
221 - `branch.<name>.remote` would update its upstream, so it cannot be
222 - distinguished from a branch that just looks "fully merged" right
223 - after a pull, or
224 -* `branch.<name>.deleteMerged` is set to `false`.
225 ---
226 -+
227 -A branch whose work has not yet been merged into its upstream is
228 -silently skipped. Delete it with `git branch -D` if you want to
229 -remove it anyway.
230 -+
231 -A branch that a surviving branch depends on through a chain of local
232 -upstreams is kept, so a branch is never deleted out from under stacked
233 -work.
234 -
235 -`--dry-run`::
236 - With `--delete-merged`, print which branches would be
237 - deleted and exit without touching any ref. Useful for
238 - sanity-checking a wide pattern like `'origin/*'` before
239 - committing to the deletion.
240 -
202 `-v`::
203 `-vv`::
204 `--verbose`::
@@ -350,14 +311,6 @@ superproject's "origin/main", but tracks the submodule's "origin/main".
311 Only list branches whose tips are not reachable from
312 _<commit>_ (`HEAD` if not specified). Implies `--list`.
313
353 -`--forked <branch>`::
354 - Only list branches whose configured upstream matches
355 - _<branch>_. The argument can be a ref (e.g. `origin/main`,
356 - `master`), a remote name like `origin` for the branch its
357 - `origin/HEAD` points at, or a shell-style glob (e.g.
358 - `'origin/*'`). The option can be repeated to widen the
359 - filter. Implies `--list`.
360 -
314 `--points-at <object>`::
315 Only list branches of _<object>_.
316
builtin/branch.c
+30 -245
@@ -21,7 +21,6 @@
21 #include "branch.h"
22 #include "path.h"
23 #include "string-list.h"
24 -#include "strmap.h"
24 #include "column.h"
25 #include "utf8.h"
26 #include "ref-filter.h"
@@ -31,7 +30,7 @@
30 #include "commit-reach.h"
31
32 static const char * const builtin_branch_usage[] = {
34 - N_("git branch [<options>] [-r | -a] [--merged] [--no-merged] [(--forked <branch>)...]"),
33 + N_("git branch [<options>] [-r | -a] [--merged] [--no-merged]"),
34 N_("git branch [<options>] [-f] [--recurse-submodules] <branch-name> [<start-point>]"),
35 N_("git branch [<options>] [-l] [<pattern>...]"),
36 N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
@@ -39,7 +38,6 @@ static const char * const builtin_branch_usage[] = {
38 N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
39 N_("git branch [<options>] [-r | -a] [--points-at]"),
40 N_("git branch [<options>] [-r | -a] [--format]"),
42 - N_("git branch [<options>] (--delete-merged <branch>)... [<pattern>...]"),
41 NULL
42 };
43
@@ -170,13 +168,10 @@ static int branch_merged(int kind, const char *name,
168 * upstream, if any, otherwise with HEAD", we should just
169 * return the result of the repo_in_merge_bases() above without
170 * any of the following code, but during the transition period,
173 - * a gentle reminder is in order. Callers that opt out of the
174 - * HEAD fallback by passing head_rev=NULL are not interested in
175 - * the reminder either: they have already established that the
176 - * branch has an upstream, so HEAD is irrelevant to the decision.
171 + * a gentle reminder is in order.
172 */
178 - if (head_rev && head_rev != reference_rev) {
179 - int expect = repo_in_merge_bases(the_repository, rev, head_rev);
173 + if (head_rev != reference_rev) {
174 + int expect = head_rev ? repo_in_merge_bases(the_repository, rev, head_rev) : 0;
175 if (expect < 0)
176 exit(128);
177 if (expect == merged)
@@ -194,32 +189,20 @@ static int branch_merged(int kind, const char *name,
189 return merged;
190 }
191
197 -enum delete_branch_flags {
198 - DELETE_BRANCH_FORCE = (1 << 0),
199 - DELETE_BRANCH_QUIET = (1 << 1),
200 - DELETE_BRANCH_SKIP_UNMERGED = (1 << 2),
201 - DELETE_BRANCH_NO_HEAD_FALLBACK = (1 << 3),
202 - DELETE_BRANCH_DRY_RUN = (1 << 4),
203 -};
204 -
192 static int check_branch_commit(const char *branchname, const char *refname,
193 const struct object_id *oid, struct commit *head_rev,
207 - int kinds, unsigned int flags)
194 + int kinds, int force)
195 {
196 struct commit *rev = lookup_commit_reference(the_repository, oid);
210 - if (!(flags & DELETE_BRANCH_FORCE) && !rev) {
197 + if (!force && !rev) {
198 error(_("couldn't look up commit object for '%s'"), refname);
199 return -1;
200 }
214 - if (!(flags & DELETE_BRANCH_FORCE) &&
215 - !branch_merged(kinds, branchname, rev, head_rev)) {
216 - if (!(flags & DELETE_BRANCH_SKIP_UNMERGED)) {
217 - error(_("the branch '%s' is not fully merged"),
218 - branchname);
219 - advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
220 - _("If you are sure you want to delete it, "
221 - "run 'git branch -D %s'"), branchname);
222 - }
201 + if (!force && !branch_merged(kinds, branchname, rev, head_rev)) {
202 + error(_("the branch '%s' is not fully merged"), branchname);
203 + advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH,
204 + _("If you are sure you want to delete it, "
205 + "run 'git branch -D %s'"), branchname);
206 return -1;
207 }
208 return 0;
@@ -234,8 +217,8 @@ static void delete_branch_config(const char *branchname)
217 strbuf_release(&buf);
218 }
219
237 -static int delete_branches(int argc, const char **argv, int kinds,
238 - unsigned int flags)
220 +static int delete_branches(int argc, const char **argv, int force, int kinds,
221 + int quiet)
222 {
223 struct commit *head_rev = NULL;
224 struct object_id oid;
@@ -258,7 +241,7 @@ static int delete_branches(int argc, const char **argv, int kinds,
241 remote_branch = 1;
242 allowed_interpret = INTERPRET_BRANCH_REMOTE;
243
261 - flags |= DELETE_BRANCH_FORCE;
244 + force = 1;
245 break;
246 case FILTER_REFS_BRANCHES:
247 fmt = "refs/heads/%s";
@@ -269,13 +252,12 @@ static int delete_branches(int argc, const char **argv, int kinds,
252 }
253 branch_name_pos = strcspn(fmt, "%");
254
272 - if (!(flags & DELETE_BRANCH_FORCE) &&
273 - !(flags & DELETE_BRANCH_NO_HEAD_FALLBACK))
255 + if (!force)
256 head_rev = lookup_commit_reference(the_repository, &head_oid);
257
258 for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
259 char *target = NULL;
278 - int ref_flags = 0;
260 + int flags = 0;
261
262 copy_branchname(the_repository, &bname,
263 argv[i], allowed_interpret);
@@ -305,7 +287,7 @@ static int delete_branches(int argc, const char **argv, int kinds,
287 RESOLVE_REF_READING
288 | RESOLVE_REF_NO_RECURSE
289 | RESOLVE_REF_ALLOW_BAD_NAME,
308 - &oid, &ref_flags);
290 + &oid, &flags);
291 if (!target) {
292 if (remote_branch) {
293 error(_("remote-tracking branch '%s' not found"), bname.buf);
@@ -317,7 +299,7 @@ static int delete_branches(int argc, const char **argv, int kinds,
299 | RESOLVE_REF_NO_RECURSE
300 | RESOLVE_REF_ALLOW_BAD_NAME,
301 &oid,
320 - &ref_flags);
302 + &flags);
303 FREE_AND_NULL(virtual_name);
304
305 if (virtual_target)
@@ -332,39 +314,31 @@ static int delete_branches(int argc, const char **argv, int kinds,
314 continue;
315 }
316
335 - if (!(ref_flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
317 + if (!(flags & (REF_ISSYMREF|REF_ISBROKEN)) &&
318 check_branch_commit(bname.buf, name, &oid, head_rev, kinds,
337 - flags)) {
338 - if (!(flags & DELETE_BRANCH_SKIP_UNMERGED))
339 - ret = 1;
319 + force)) {
320 + ret = 1;
321 goto next;
322 }
323
324 item = string_list_append(&refs_to_delete, name);
344 - item->util = xstrdup((ref_flags & REF_ISBROKEN) ? "broken"
345 - : (ref_flags & REF_ISSYMREF) ? target
325 + item->util = xstrdup((flags & REF_ISBROKEN) ? "broken"
326 + : (flags & REF_ISSYMREF) ? target
327 : repo_find_unique_abbrev(the_repository, &oid, DEFAULT_ABBREV));
328
329 next:
330 free(target);
331 }
332
352 - if (!(flags & DELETE_BRANCH_DRY_RUN) &&
353 - refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
333 + if (refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF))
334 ret = 1;
335
336 for_each_string_list_item(item, &refs_to_delete) {
337 char *describe_ref = item->util;
338 char *name = item->string;
359 - if (flags & DELETE_BRANCH_DRY_RUN) {
360 - if (!(flags & DELETE_BRANCH_QUIET))
361 - printf(remote_branch
362 - ? _("Would delete remote-tracking branch %s (was %s).\n")
363 - : _("Would delete branch %s (was %s).\n"),
364 - name + branch_name_pos, describe_ref);
365 - } else if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
339 + if (!refs_ref_exists(get_main_ref_store(the_repository), name)) {
340 char *refname = name + branch_name_pos;
367 - if (!(flags & DELETE_BRANCH_QUIET))
341 + if (!quiet)
342 printf(remote_branch
343 ? _("Deleted remote-tracking branch %s (was %s).\n")
344 : _("Deleted branch %s (was %s).\n"),
@@ -707,172 +681,6 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int
681 free_worktrees(worktrees);
682 }
683
710 -static int parse_opt_forked(const struct option *opt, const char *arg, int unset)
711 -{
712 - struct ref_filter *filter = opt->value;
713 -
714 - BUG_ON_OPT_NEG(unset);
715 - if (ref_filter_forked_add(filter, arg) < 0)
716 - die(_("'%s' is not a valid branch or pattern"), arg);
717 - return 0;
718 -}
719 -
720 -struct stacked_branch_data {
721 - struct strset *deletable_branch_names;
722 - struct strset *protected_branch_names;
723 - struct strset *visited_branch_names;
724 -};
725 -
726 -static int collect_stacked_branch_bases(const struct reference *ref,
727 - void *cb_data)
728 -{
729 - struct stacked_branch_data *data = cb_data;
730 - const char *branch_name;
731 -
732 - if (!skip_prefix(ref->name, "refs/heads/", &branch_name))
733 - BUG("expected local branch ref, got '%s'", ref->name);
734 - if (strset_contains(data->deletable_branch_names, branch_name))
735 - return 0;
736 -
737 - while (strset_add(data->visited_branch_names, branch_name)) {
738 - struct branch *branch = branch_get(branch_name);
739 - const char *upstream_refname = branch_get_upstream(branch, NULL);
740 - const char *upstream_branch_name;
741 -
742 - if (!upstream_refname ||
743 - !skip_prefix(upstream_refname, "refs/heads/",
744 - &upstream_branch_name) ||
745 - !strset_contains(data->deletable_branch_names,
746 - upstream_branch_name))
747 - break;
748 -
749 - strset_add(data->protected_branch_names, upstream_branch_name);
750 - branch_name = upstream_branch_name;
751 - }
752 -
753 - return 0;
754 -}
755 -
756 -static void protect_stacked_branch_bases(struct ref_store *refs,
757 - struct strset *deletable_branch_names)
758 -{
759 - struct strset protected_branch_names = STRSET_INIT;
760 - struct strset visited_branch_names = STRSET_INIT;
761 - struct stacked_branch_data data = {
762 - .deletable_branch_names = deletable_branch_names,
763 - .protected_branch_names = &protected_branch_names,
764 - .visited_branch_names = &visited_branch_names,
765 - };
766 - struct refs_for_each_ref_options opts = {
767 - .prefix = "refs/heads/",
768 - };
769 - struct hashmap_iter iter;
770 - struct strmap_entry *entry;
771 -
772 - refs_for_each_ref_ext(refs, collect_stacked_branch_bases, &data, &opts);
773 -
774 - strset_for_each_entry(&protected_branch_names, &iter, entry)
775 - strset_remove(deletable_branch_names, entry->key);
776 -
777 - strset_clear(&visited_branch_names);
778 - strset_clear(&protected_branch_names);
779 -}
780 -
781 -static int branch_pushes_to_upstream(struct branch *branch,
782 - const char *upstream)
783 -{
784 - struct remote *remote = remote_get(remote_for_branch(branch, NULL));
785 - char *tracking = NULL;
786 - int ret = 0;
787 -
788 - if (remote)
789 - tracking = apply_refspecs(&remote->fetch, branch->refname);
790 - if (tracking && !strcmp(tracking, upstream))
791 - ret = 1;
792 -
793 - free(tracking);
794 - return ret;
795 -}
796 -
797 -static int delete_merged_branches(const struct strvec *upstreams,
798 - const char **argv, unsigned int flags)
799 -{
800 - struct ref_store *refs = get_main_ref_store(the_repository);
801 - struct ref_filter filter = REF_FILTER_INIT;
802 - struct ref_array candidates = { 0 };
803 - struct strset deletable_branch_names = STRSET_INIT;
804 - struct strvec branches_to_delete = STRVEC_INIT;
805 - struct strbuf key = STRBUF_INIT;
806 - struct hashmap_iter iter;
807 - struct strmap_entry *entry;
808 - int ret = 0;
809 -
810 - for (size_t i = 0; i < upstreams->nr; i++)
811 - if (ref_filter_forked_add(&filter, upstreams->v[i]) < 0)
812 - die(_("'%s' is not a valid branch or pattern"),
813 - upstreams->v[i]);
814 -
815 - filter.kind = FILTER_REFS_BRANCHES;
816 - filter.name_patterns = argv;
817 - filter_refs(&candidates, &filter, filter.kind);
818 -
819 - for (int i = 0; i < candidates.nr; i++) {
820 - const char *branch_refname = candidates.items[i]->refname;
821 - const char *branch_name;
822 - struct branch *branch;
823 - const char *upstream_refname;
824 - int opt_out;
825 -
826 - if (!skip_prefix(branch_refname, "refs/heads/", &branch_name))
827 - BUG("filter returned non-branch ref '%s'", branch_refname);
828 - if (branch_checked_out(branch_refname))
829 - continue;
830 -
831 - branch = branch_get(branch_name);
832 - upstream_refname = branch_get_upstream(branch, NULL);
833 - if (!upstream_refname || !refs_ref_exists(refs, upstream_refname))
834 - continue;
835 - if (branch_pushes_to_upstream(branch, upstream_refname))
836 - continue;
837 - if (check_branch_commit(branch_name, branch_name,
838 - &candidates.items[i]->objectname, NULL,
839 - FILTER_REFS_BRANCHES, DELETE_BRANCH_SKIP_UNMERGED))
840 - continue;
841 -
842 - strbuf_reset(&key);
843 - strbuf_addf(&key, "branch.%s.deletemerged", branch_name);
844 - if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
845 - !opt_out) {
846 - if (!(flags & DELETE_BRANCH_QUIET))
847 - fprintf(stderr,
848 - _("Skipping '%s' (branch.%s.deleteMerged is false)\n"),
849 - branch_name, branch_name);
850 - continue;
851 - }
852 -
853 - strset_add(&deletable_branch_names, branch_name);
854 - }
855 -
856 - protect_stacked_branch_bases(refs, &deletable_branch_names);
857 -
858 - strset_for_each_entry(&deletable_branch_names, &iter, entry)
859 - strvec_push(&branches_to_delete, entry->key);
860 -
861 - if (branches_to_delete.nr)
862 - ret = delete_branches(branches_to_delete.nr, branches_to_delete.v,
863 - FILTER_REFS_BRANCHES,
864 - DELETE_BRANCH_SKIP_UNMERGED |
865 - DELETE_BRANCH_NO_HEAD_FALLBACK |
866 - flags);
867 -
868 - strbuf_release(&key);
869 - strvec_clear(&branches_to_delete);
870 - strset_clear(&deletable_branch_names);
871 - ref_array_clear(&candidates);
872 - ref_filter_clear(&filter);
873 - return ret;
874 -}
875 -
684 static GIT_PATH_FUNC(edit_description, "EDIT_DESCRIPTION")
685
686 static int edit_branch_description(const char *branch_name)
@@ -937,8 +745,6 @@ int cmd_branch(int argc,
745 /* possible actions */
746 int delete = 0, rename = 0, copy = 0, list = 0,
747 unset_upstream = 0, show_current = 0, edit_description = 0;
940 - struct strvec delete_merged = STRVEC_INIT;
941 - int dry_run = 0;
748 const char *new_upstream = NULL;
749 int noncreate_actions = 0;
750 /* possible options */
@@ -992,17 +798,9 @@ int cmd_branch(int argc,
798 OPT_BOOL(0, "create-reflog", &reflog, N_("create the branch's reflog")),
799 OPT_BOOL(0, "edit-description", &edit_description,
800 N_("edit the description for the branch")),
995 - OPT_CALLBACK_F(0, "delete-merged", &delete_merged, N_("branch"),
996 - N_("delete merged branches whose upstream matches <branch> (repeatable)"),
997 - PARSE_OPT_NONEG, parse_opt_strvec),
998 - OPT_BOOL(0, "dry-run", &dry_run,
999 - N_("with --delete-merged, only print which branches would be deleted")),
801 OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE),
802 OPT_MERGED(&filter, N_("print only branches that are merged")),
803 OPT_NO_MERGED(&filter, N_("print only branches that are not merged")),
1003 - OPT_CALLBACK_F(0, "forked", &filter, N_("branch"),
1004 - N_("print only branches whose upstream matches <branch> (repeatable)"),
1005 - PARSE_OPT_NONEG, parse_opt_forked),
804 OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")),
805 OPT_REF_SORT(&sorting_options),
806 OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"),
@@ -1044,24 +842,19 @@ int cmd_branch(int argc,
842 0);
843
844 if (!delete && !rename && !copy && !edit_description && !new_upstream &&
1047 - !show_current && !unset_upstream && !delete_merged.nr &&
1048 - argc == 0)
845 + !show_current && !unset_upstream && argc == 0)
846 list = 1;
847
848 if (filter.with_commit || filter.no_commit ||
1052 - filter.reachable_from || filter.unreachable_from ||
1053 - filter.points_at.nr || filter.forked.nr)
849 + filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
850 list = 1;
851
852 noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
853 !!show_current + !!list + !!edit_description +
1058 - !!unset_upstream + !!delete_merged.nr;
854 + !!unset_upstream;
855 if (noncreate_actions > 1)
856 usage_with_options(builtin_branch_usage, options);
857
1062 - if (dry_run && !delete_merged.nr)
1063 - die(_("--dry-run requires --delete-merged"));
1064 -
858 if (recurse_submodules_explicit) {
859 if (!submodule_propagate_branches)
860 die(_("branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled"));
@@ -1096,14 +889,7 @@ int cmd_branch(int argc,
889 if (delete) {
890 if (!argc)
891 die(_("branch name required"));
1099 - ret = delete_branches(argc, argv, filter.kind,
1100 - (delete > 1 ? DELETE_BRANCH_FORCE : 0) |
1101 - (quiet ? DELETE_BRANCH_QUIET : 0));
1102 - goto out;
1103 - } else if (delete_merged.nr) {
1104 - ret = delete_merged_branches(&delete_merged, argv,
1105 - (quiet ? DELETE_BRANCH_QUIET : 0) |
1106 - (dry_run ? DELETE_BRANCH_DRY_RUN : 0));
892 + ret = delete_branches(argc, argv, delete > 1, filter.kind, quiet);
893 goto out;
894 } else if (show_current) {
895 print_current_branch_name();
@@ -1276,7 +1062,6 @@ int cmd_branch(int argc,
1062 ret = 0;
1063
1064 out:
1279 - strvec_clear(&delete_merged);
1065 string_list_clear(&sorting_options, 0);
1066 return ret;
1067 }
ref-filter.c
-70
@@ -2744,72 +2744,6 @@ static int filter_exclude_match(struct ref_filter *filter, const char *refname)
2744 return match_pattern(filter->exclude.v, refname, filter->ignore_case);
2745 }
2746
2747 -static const char *short_upstream_name(const char *full_ref)
2748 -{
2749 - const char *short_name = full_ref;
2750 -
2751 - if (!skip_prefix(short_name, "refs/heads/", &short_name))
2752 - skip_prefix(short_name, "refs/remotes/", &short_name);
2753 - return short_name;
2754 -}
2755 -
2756 -/*
2757 - * Match the configured upstream of a branch against the registered
2758 - * --forked patterns. Exact patterns are compared against the full
2759 - * upstream refname so they are unambiguous; glob patterns are matched
2760 - * against the abbreviated upstream so that a glob such as origin/...
2761 - * works as typed.
2762 - */
2763 -static int filter_forked_match(struct ref_filter *filter, const char *refname)
2764 -{
2765 - const char *short_name;
2766 - struct branch *branch;
2767 - const char *upstream;
2768 -
2769 - if (!skip_prefix(refname, "refs/heads/", &short_name))
2770 - return 0;
2771 - branch = branch_get(short_name);
2772 - if (!branch)
2773 - return 0;
2774 - upstream = branch_get_upstream(branch, NULL);
2775 - if (!upstream)
2776 - return 0;
2777 -
2778 - for (size_t i = 0; i < filter->forked.nr; i++) {
2779 - const char *pattern = filter->forked.v[i];
2780 - if (has_glob_specials(pattern)) {
2781 - if (!wildmatch(pattern, short_upstream_name(upstream),
2782 - WM_PATHNAME))
2783 - return 1;
2784 - } else if (!strcmp(pattern, upstream)) {
2785 - return 1;
2786 - }
2787 - }
2788 - return 0;
2789 -}
2790 -
2791 -int ref_filter_forked_add(struct ref_filter *filter, const char *arg)
2792 -{
2793 - struct object_id oid;
2794 - char *full_ref = NULL;
2795 -
2796 - if (has_glob_specials(arg)) {
2797 - strvec_push(&filter->forked, arg);
2798 - return 0;
2799 - }
2800 -
2801 - if (repo_dwim_ref(the_repository, arg, strlen(arg), &oid,
2802 - &full_ref, 0) == 1 &&
2803 - (starts_with(full_ref, "refs/heads/") ||
2804 - starts_with(full_ref, "refs/remotes/"))) {
2805 - strvec_push(&filter->forked, full_ref);
2806 - free(full_ref);
2807 - return 0;
2808 - }
2809 - free(full_ref);
2810 - return -1;
2811 -}
2812 -
2747 /*
2748 * We need to seek to the reference right after a given marker but excluding any
2749 * 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,
2979 if (filter->points_at.nr && !match_points_at(&filter->points_at, ref->oid, ref->name))
2980 return NULL;
2981
3048 - if (filter->forked.nr && !filter_forked_match(filter, ref->name))
3049 - return NULL;
3050 -
2982 /*
2983 * A merge filter is applied on refs pointing to commits. Hence
2984 * obtain the commit using the 'oid' available and discard all
@@ -3833,7 +3764,6 @@ void ref_filter_init(struct ref_filter *filter)
3764 void ref_filter_clear(struct ref_filter *filter)
3765 {
3766 strvec_clear(&filter->exclude);
3836 - strvec_clear(&filter->forked);
3767 oid_array_clear(&filter->points_at);
3768 commit_list_free(filter->with_commit);
3769 commit_list_free(filter->no_commit);
ref-filter.h
-10
@@ -67,7 +67,6 @@ struct ref_filter {
67 const char **name_patterns;
68 const char *start_after;
69 struct strvec exclude;
70 - struct strvec forked;
70 struct oid_array points_at;
71 struct commit_list *with_commit;
72 struct commit_list *no_commit;
@@ -111,7 +110,6 @@ struct ref_format {
110 #define REF_FILTER_INIT { \
111 .points_at = OID_ARRAY_INIT, \
112 .exclude = STRVEC_INIT, \
114 - .forked = STRVEC_INIT, \
113 }
114 #define REF_FORMAT_INIT { \
115 .use_color = GIT_COLOR_UNKNOWN, \
@@ -174,14 +172,6 @@ void ref_sorting_release(struct ref_sorting *);
172 struct ref_sorting *ref_sorting_options(struct string_list *);
173 /* Function to parse --merged and --no-merged options */
174 int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset);
177 -/*
178 - * Register a --forked <branch> pattern on the filter. The argument is
179 - * either a ref, which is resolved to its full refname, or a shell-style
180 - * glob. Branches are kept only when their configured upstream matches
181 - * one of the registered patterns. Returns -1 if the argument is not a
182 - * valid ref or pattern.
183 - */
184 -int ref_filter_forked_add(struct ref_filter *filter, const char *arg);
175 /* Get the current HEAD's description */
176 char *get_head_description(void);
177 /* Set up translated strings in the output. */
t/t3200-branch.sh
-393
@@ -1757,397 +1757,4 @@ test_expect_success 'errors if given a bad branch name' '
1757 test_cmp expect actual
1758 '
1759
1760 -test_expect_success '--forked: setup' '
1761 - test_create_repo forked-upstream &&
1762 - (
1763 - cd forked-upstream &&
1764 - test_commit base &&
1765 - git branch one base &&
1766 - git branch two base
1767 - ) &&
1768 -
1769 - test_create_repo forked-other &&
1770 - (
1771 - cd forked-other &&
1772 - test_commit other-base &&
1773 - git branch foreign other-base
1774 - ) &&
1775 -
1776 - git clone forked-upstream forked &&
1777 - (
1778 - cd forked &&
1779 - git remote add -f other ../forked-other &&
1780 - git branch local-base &&
1781 - git branch --track local-one origin/one &&
1782 - git branch --track local-two origin/two &&
1783 - git branch --track local-foreign other/foreign &&
1784 - git branch --track local-onbase local-base &&
1785 -
1786 - git checkout local-one &&
1787 - test_commit --no-tag local-one-work local-one.t &&
1788 - git checkout local-foreign &&
1789 - test_commit --no-tag local-foreign-work local-foreign.t
1790 - )
1791 -'
1792 -
1793 -test_expect_success '--forked <upstream-tracking-branch> filters by upstream' '
1794 - git -C forked branch --forked origin/one --format="%(refname:short)" >actual &&
1795 - echo local-one >expect &&
1796 - test_cmp expect actual
1797 -'
1798 -
1799 -test_expect_success '--forked <glob> filters by wildmatch' '
1800 - git -C forked branch --forked "origin/*" --format="%(refname:short)" >actual &&
1801 - cat >expect <<-\EOF &&
1802 - local-one
1803 - local-two
1804 - main
1805 - EOF
1806 - test_cmp expect actual
1807 -'
1808 -
1809 -test_expect_success '--forked <local-branch> matches branches with local upstream' '
1810 - git -C forked branch --forked local-base --format="%(refname:short)" >actual &&
1811 - echo local-onbase >expect &&
1812 - test_cmp expect actual
1813 -'
1814 -
1815 -test_expect_success '--forked can be repeated to widen the filter' '
1816 - git -C forked branch --forked origin/one --forked other/foreign --format="%(refname:short)" >actual &&
1817 - cat >expect <<-\EOF &&
1818 - local-foreign
1819 - local-one
1820 - EOF
1821 - test_cmp expect actual
1822 -'
1823 -
1824 -test_expect_success '--forked combines literal and glob arguments' '
1825 - git -C forked branch --forked local-base --forked "other/*" --format="%(refname:short)" >actual &&
1826 - cat >expect <<-\EOF &&
1827 - local-foreign
1828 - local-onbase
1829 - EOF
1830 - test_cmp expect actual
1831 -'
1832 -
1833 -test_expect_success '--forked "*/*" covers every remote-tracking upstream' '
1834 - git -C forked branch --forked "*/*" --format="%(refname:short)" >actual &&
1835 - cat >expect <<-\EOF &&
1836 - local-foreign
1837 - local-one
1838 - local-two
1839 - main
1840 - EOF
1841 - test_cmp expect actual
1842 -'
1843 -
1844 -test_expect_success '--forked composes with --no-merged' '
1845 - git -C forked branch --forked "origin/*" --no-merged origin/one \
1846 - --format="%(refname:short)" >actual &&
1847 - echo local-one >expect &&
1848 - test_cmp expect actual
1849 -'
1850 -
1851 -test_expect_success '--forked <remote> uses the branch <remote>/HEAD points at' '
1852 - git -C forked branch --forked origin --format="%(refname:short)" >actual &&
1853 - echo main >expect &&
1854 - test_cmp expect actual
1855 -'
1856 -
1857 -test_expect_success '--forked narrows a <pattern> argument' '
1858 - git -C forked branch --forked "origin/*" "local-*" \
1859 - --format="%(refname:short)" >actual &&
1860 - cat >expect <<-\EOF &&
1861 - local-one
1862 - local-two
1863 - EOF
1864 - test_cmp expect actual
1865 -'
1866 -
1867 -test_expect_success '--forked rejects unknown branch/pattern' '
1868 - test_must_fail git -C forked branch --forked nope 2>err &&
1869 - test_grep "not a valid branch or pattern" err
1870 -'
1871 -
1872 -test_expect_success '--forked requires a value' '
1873 - test_must_fail git -C forked branch --forked 2>err &&
1874 - test_grep "requires a value" err
1875 -'
1876 -
1877 -test_expect_success '--delete-merged: setup' '
1878 - git init -b main upstream &&
1879 - (
1880 - cd upstream &&
1881 - test_commit base &&
1882 - git checkout -b next &&
1883 - test_commit next-work &&
1884 - git checkout main
1885 - ) &&
1886 - git init -b main other &&
1887 - test_commit -C other other-base &&
1888 - git init -b main fork
1889 -'
1890 -
1891 -setup_repo_for_delete_merged () {
1892 - rm -rf repo &&
1893 - git clone upstream repo &&
1894 - (
1895 - cd repo &&
1896 - git remote add fork ../fork &&
1897 - git remote add other ../other &&
1898 - git config push.default current &&
1899 - git fetch other
1900 - )
1901 -}
1902 -
1903 -create_merged_branch () {
1904 - (
1905 - cd repo &&
1906 - git checkout -b "$1" origin/next --track &&
1907 - git commit --allow-empty -m "$1 work" &&
1908 - git push origin "$1:next"
1909 - )
1910 -}
1911 -
1912 -check_branches () {
1913 - git for-each-ref --format="%(refname:short)" refs/heads/ >actual &&
1914 - cat >expect &&
1915 - test_cmp expect actual
1916 -}
1917 -
1918 -test_expect_success '--delete-merged keeps cloned main without a default push remote' '
1919 - setup_repo_for_delete_merged &&
1920 - (
1921 - cd repo &&
1922 - git checkout --detach &&
1923 -
1924 - git branch --delete-merged */* &&
1925 -
1926 - check_branches <<-\EOF
1927 - main
1928 - EOF
1929 - )
1930 -'
1931 -
1932 -test_expect_success '--delete-merged deletes only selected merged branches' '
1933 - setup_repo_for_delete_merged &&
1934 - create_merged_branch also-merged &&
1935 - create_merged_branch merged &&
1936 - (
1937 - cd repo &&
1938 - git checkout -b unmerged origin/next --track &&
1939 - git commit --allow-empty -m "unmerged work" &&
1940 - git checkout -b tracks-other other/main --track &&
1941 - sha=$(git rev-parse --short merged) &&
1942 -
1943 - git branch --dry-run --delete-merged origin/next merged >actual 2>&1 &&
1944 - echo "Would delete branch merged (was $sha)." >expect &&
1945 - test_cmp expect actual &&
1946 - git rev-parse --verify refs/heads/merged &&
1947 -
1948 - check_branches <<-\EOF &&
1949 - also-merged
1950 - main
1951 - merged
1952 - tracks-other
1953 - unmerged
1954 - EOF
1955 -
1956 - git branch --delete-merged origin/next merged >actual 2>&1 &&
1957 - echo "Deleted branch merged (was $sha)." >expect &&
1958 - test_cmp expect actual &&
1959 -
1960 - check_branches <<-\EOF
1961 - also-merged
1962 - main
1963 - tracks-other
1964 - unmerged
1965 - EOF
1966 - )
1967 -'
1968 -
1969 -test_expect_success '--delete-merged keeps main despite a different default push remote' '
1970 - setup_repo_for_delete_merged &&
1971 - create_merged_branch on-next &&
1972 - create_merged_branch checked-out &&
1973 - create_merged_branch upstream-gone &&
1974 - (
1975 - cd repo &&
1976 - git config remote.pushDefault fork &&
1977 - git checkout -b local-to-delete main --track &&
1978 - git update-ref refs/remotes/origin/topic refs/remotes/origin/next &&
1979 - git branch --set-upstream-to=origin/topic upstream-gone &&
1980 - git update-ref -d refs/remotes/origin/topic &&
1981 - git checkout -b tracks-other other/main --track &&
1982 - git checkout checked-out &&
1983 -
1984 - git branch --delete-merged origin/* \
1985 - --delete-merged main &&
1986 -
1987 - check_branches <<-\EOF
1988 - checked-out
1989 - main
1990 - tracks-other
1991 - upstream-gone
1992 - EOF
1993 - )
1994 -'
1995 -
1996 -test_expect_success '--delete-merged keeps the upstream of a surviving branch' '
1997 - setup_repo_for_delete_merged &&
1998 - create_merged_branch feature &&
1999 - (
2000 - cd repo &&
2001 - git checkout -b topic feature --track &&
2002 - git commit --allow-empty -m "topic work" &&
2003 -
2004 - git branch --dry-run --delete-merged origin/next >out &&
2005 - test_grep ! "feature" out &&
2006 -
2007 - git branch --delete-merged origin/next 2>err &&
2008 - test_must_be_empty err &&
2009 -
2010 - check_branches <<-\EOF &&
2011 - feature
2012 - main
2013 - topic
2014 - EOF
2015 -
2016 - git config --local --get-regexp "branch\\.(feature|topic)\\.(merge|remote)" >actual &&
2017 - cat >expect <<-\EOF &&
2018 - branch.feature.remote origin
2019 - branch.feature.merge refs/heads/next
2020 - branch.topic.remote .
2021 - branch.topic.merge refs/heads/feature
2022 - EOF
2023 - test_cmp expect actual
2024 - )
2025 -'
2026 -
2027 -test_expect_success '--delete-merged keeps the upstream chain of a surviving branch' '
2028 - setup_repo_for_delete_merged &&
2029 - (
2030 - cd repo &&
2031 - git config remote.pushDefault fork &&
2032 - git branch lower origin/next --track &&
2033 - git branch mid lower --track &&
2034 - git checkout -b tip mid --track &&
2035 - git commit --allow-empty -m "tip work" &&
2036 -
2037 - git branch --dry-run --delete-merged origin/next \
2038 - --delete-merged lower >actual 2>&1 &&
2039 - test_must_be_empty actual &&
2040 -
2041 - git config --local --get-regexp "branch\\.(lower|mid|tip)\\.(merge|remote)" >actual &&
2042 - cat >expect <<-\EOF &&
2043 - branch.lower.remote origin
2044 - branch.lower.merge refs/heads/next
2045 - branch.mid.remote .
2046 - branch.mid.merge refs/heads/lower
2047 - branch.tip.remote .
2048 - branch.tip.merge refs/heads/mid
2049 - EOF
2050 - test_cmp expect actual &&
2051 -
2052 - git branch --delete-merged origin/next \
2053 - --delete-merged lower >actual 2>&1 &&
2054 - test_must_be_empty actual &&
2055 -
2056 - check_branches <<-\EOF &&
2057 - lower
2058 - main
2059 - mid
2060 - tip
2061 - EOF
2062 -
2063 - git config --local --get-regexp "branch\\.(lower|mid|tip)\\.(merge|remote)" >actual &&
2064 - cat >expect <<-\EOF &&
2065 - branch.lower.remote origin
2066 - branch.lower.merge refs/heads/next
2067 - branch.mid.remote .
2068 - branch.mid.merge refs/heads/lower
2069 - branch.tip.remote .
2070 - branch.tip.merge refs/heads/mid
2071 - EOF
2072 - test_cmp expect actual
2073 - )
2074 -'
2075 -
2076 -test_expect_success '--delete-merged result is independent of stacked branch names' '
2077 - setup_repo_for_delete_merged &&
2078 - (
2079 - cd repo &&
2080 - git branch c-lower origin/next --track &&
2081 - git branch b-mid c-lower --track &&
2082 - git checkout -b a-tip b-mid --track &&
2083 - git commit --allow-empty -m "tip work" &&
2084 -
2085 - git branch --delete-merged origin/next \
2086 - --delete-merged "c-*" &&
2087 -
2088 - check_branches <<-\EOF &&
2089 - a-tip
2090 - b-mid
2091 - c-lower
2092 - main
2093 - EOF
2094 -
2095 - git branch --delete-merged origin/next \
2096 - --delete-merged "c-*" >actual 2>&1 &&
2097 - test_must_be_empty actual &&
2098 -
2099 - check_branches <<-\EOF
2100 - a-tip
2101 - b-mid
2102 - c-lower
2103 - main
2104 - EOF
2105 - )
2106 -'
2107 -
2108 -test_expect_success '--delete-merged requires a value' '
2109 - test_must_fail git -C forked branch --delete-merged 2>err &&
2110 - test_grep "requires a value" err
2111 -'
2112 -
2113 -test_expect_success '--delete-merged honours branch.<name>.deleteMerged=false' '
2114 - setup_repo_for_delete_merged &&
2115 - create_merged_branch deleted &&
2116 - create_merged_branch kept &&
2117 - (
2118 - cd repo &&
2119 - git config branch.kept.deleteMerged false &&
2120 - git checkout --detach &&
2121 -
2122 - git branch --delete-merged origin/next 2>err &&
2123 -
2124 - test_grep "Skipping .kept." err &&
2125 - check_branches <<-\EOF
2126 - kept
2127 - main
2128 - EOF
2129 - )
2130 -'
2131 -
2132 -test_expect_success "branch -d still deletes a deleteMerged=false branch" '
2133 - setup_repo_for_delete_merged &&
2134 - create_merged_branch kept &&
2135 - (
2136 - cd repo &&
2137 - git config branch.kept.deleteMerged false &&
2138 - git checkout --detach &&
2139 -
2140 - git branch -d kept &&
2141 -
2142 - check_branches <<-\EOF
2143 - main
2144 - EOF
2145 - )
2146 -'
2147 -
2148 -test_expect_success '--dry-run without --delete-merged is rejected' '
2149 - test_must_fail git -C forked branch --dry-run 2>err &&
2150 - test_grep "requires --delete-merged" err
2151 -'
2152 -
1760 test_done