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