ref-filter: allow merged and no-merged filters
Enable ref-filter to process multiple merged and no-merged filters, and extend functionality to git branch, git tag and git for-each-ref. This provides an easy way to check for branches that are "graduation candidates:" $ git branch --no-merged master --merged next If passed more than one merged (or more than one no-merged) filter, refs must be reachable from any one of the merged commits, and reachable from none of the no-merged commits. Signed-off-by: Aaron Lipman <alipman88@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Aaron Lipman committed
Sep 15, 2020 at 22:08 UTC
21bf933928c02372633b88aa6c4d9d71271d42b3
13 files changed
+92
-65
Documentation/filters.txt
+4
@@ -1,3 +1,7 @@
1
When combining multiple `--contains` and `--no-contains` filters, only
2
references that contain at least one of the `--contains` commits and
3
contain none of the `--no-contains` commits are shown.
4
+
5
+When combining multiple `--merged` and `--no-merged` filters, only
6
+references that are reachable from at least one of the `--merged`
7
+commits and from none of the `--no-merged` commits are shown.
Documentation/git-branch.txt
+3
-5
@@ -11,7 +11,7 @@ SYNOPSIS
11
'git branch' [--color[=<when>] | --no-color] [--show-current]
12
[-v [--abbrev=<length> | --no-abbrev]]
13
[--column[=<options>] | --no-column] [--sort=<key>]
14
- [(--merged | --no-merged) [<commit>]]
14
+ [--merged [<commit>]] [--no-merged [<commit>]]
15
[--contains [<commit>]] [--no-contains [<commit>]]
16
[--points-at <object>] [--format=<format>]
17
[(-r | --remotes) | (-a | --all)]
@@ -252,13 +252,11 @@ start-point is either a local or remote-tracking branch.
252
253
--merged [<commit>]::
254
Only list branches whose tips are reachable from the
255
- specified commit (HEAD if not specified). Implies `--list`,
256
- incompatible with `--no-merged`.
255
+ specified commit (HEAD if not specified). Implies `--list`.
256
257
--no-merged [<commit>]::
258
Only list branches whose tips are not reachable from the
260
- specified commit (HEAD if not specified). Implies `--list`,
261
- incompatible with `--merged`.
259
+ specified commit (HEAD if not specified). Implies `--list`.
260
261
<branchname>::
262
The name of the branch to create or delete.
Documentation/git-for-each-ref.txt
+3
-5
@@ -11,7 +11,7 @@ SYNOPSIS
11
'git for-each-ref' [--count=<count>] [--shell|--perl|--python|--tcl]
12
[(--sort=<key>)...] [--format=<format>] [<pattern>...]
13
[--points-at=<object>]
14
- (--merged[=<object>] | --no-merged[=<object>])
14
+ [--merged[=<object>]] [--no-merged[=<object>]]
15
[--contains[=<object>]] [--no-contains[=<object>]]
16
17
DESCRIPTION
@@ -76,13 +76,11 @@ OPTIONS
76
77
--merged[=<object>]::
78
Only list refs whose tips are reachable from the
79
- specified commit (HEAD if not specified),
80
- incompatible with `--no-merged`.
79
+ specified commit (HEAD if not specified).
80
81
--no-merged[=<object>]::
82
Only list refs whose tips are not reachable from the
84
- specified commit (HEAD if not specified),
85
- incompatible with `--merged`.
83
+ specified commit (HEAD if not specified).
84
85
--contains[=<object>]::
86
Only list refs which contain the specified commit (HEAD if not
Documentation/git-tag.txt
+3
-3
@@ -15,7 +15,7 @@ SYNOPSIS
15
'git tag' [-n[<num>]] -l [--contains <commit>] [--no-contains <commit>]
16
[--points-at <object>] [--column[=<options>] | --no-column]
17
[--create-reflog] [--sort=<key>] [--format=<format>]
18
- [--[no-]merged [<commit>]] [<pattern>...]
18
+ [--merged <commit>] [--no-merged <commit>] [<pattern>...]
19
'git tag' -v [--format=<format>] <tagname>...
20
21
DESCRIPTION
@@ -149,11 +149,11 @@ This option is only applicable when listing tags without annotation lines.
149
150
--merged [<commit>]::
151
Only list tags whose commits are reachable from the specified
152
- commit (`HEAD` if not specified), incompatible with `--no-merged`.
152
+ commit (`HEAD` if not specified).
153
154
--no-merged [<commit>]::
155
Only list tags whose commits are not reachable from the specified
156
- commit (`HEAD` if not specified), incompatible with `--merged`.
156
+ commit (`HEAD` if not specified).
157
158
--points-at <object>::
159
Only list tags of the given object (HEAD if not
builtin/branch.c
+3
-3
@@ -26,7 +26,7 @@
26
#include "commit-reach.h"
27
28
static const char * const builtin_branch_usage[] = {
29
- N_("git branch [<options>] [-r | -a] [--merged | --no-merged]"),
29
+ N_("git branch [<options>] [-r | -a] [--merged] [--no-merged]"),
30
N_("git branch [<options>] [-l] [-f] <branch-name> [<start-point>]"),
31
N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
32
N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"),
@@ -688,8 +688,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
688
!show_current && !unset_upstream && argc == 0)
689
list = 1;
690
691
- if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr ||
692
- filter.no_commit)
691
+ if (filter.with_commit || filter.no_commit ||
692
+ filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
693
list = 1;
694
695
if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
builtin/for-each-ref.c
+1
-1
@@ -9,7 +9,7 @@
9
static char const * const for_each_ref_usage[] = {
10
N_("git for-each-ref [<options>] [<pattern>]"),
11
N_("git for-each-ref [--points-at <object>]"),
12
- N_("git for-each-ref [(--merged | --no-merged) [<commit>]]"),
12
+ N_("git for-each-ref [--merged [<commit>]] [--no-merged [<commit>]]"),
13
N_("git for-each-ref [--contains [<commit>]] [--no-contains [<commit>]]"),
14
NULL
15
};
builtin/tag.c
+4
-4
@@ -26,7 +26,7 @@ static const char * const git_tag_usage[] = {
26
"\t\t<tagname> [<head>]"),
27
N_("git tag -d <tagname>..."),
28
N_("git tag -l [-n[<num>]] [--contains <commit>] [--no-contains <commit>] [--points-at <object>]\n"
29
- "\t\t[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]"),
29
+ "\t\t[--format=<format>] [--merged <commit>] [--no-merged <commit>] [<pattern>...]"),
30
N_("git tag -v [--format=<format>] <tagname>..."),
31
NULL
32
};
@@ -457,8 +457,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
457
if (argc == 0)
458
cmdmode = 'l';
459
else if (filter.with_commit || filter.no_commit ||
460
- filter.points_at.nr || filter.merge_commit ||
461
- filter.lines != -1)
460
+ filter.reachable_from || filter.unreachable_from ||
461
+ filter.points_at.nr || filter.lines != -1)
462
cmdmode = 'l';
463
}
464
@@ -509,7 +509,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
509
die(_("--no-contains option is only allowed in list mode"));
510
if (filter.points_at.nr)
511
die(_("--points-at option is only allowed in list mode"));
512
- if (filter.merge_commit)
512
+ if (filter.reachable_from || filter.unreachable_from)
513
die(_("--merged and --no-merged options are only allowed in list mode"));
514
if (cmdmode == 'd')
515
return for_each_tag_name(argv, delete_tag, NULL);
ref-filter.c
+35
-29
@@ -2167,9 +2167,9 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
2167
* obtain the commit using the 'oid' available and discard all
2168
* non-commits early. The actual filtering is done later.
2169
*/
2170
- if (filter->merge_commit || filter->with_commit || filter->no_commit || filter->verbose) {
2171
- commit = lookup_commit_reference_gently(the_repository, oid,
2172
- 1);
2170
+ if (filter->reachable_from || filter->unreachable_from ||
2171
+ filter->with_commit || filter->no_commit || filter->verbose) {
2172
+ commit = lookup_commit_reference_gently(the_repository, oid, 1);
2173
if (!commit)
2174
return 0;
2175
/* We perform the filtering for the '--contains' option... */
@@ -2231,13 +2231,20 @@ void ref_array_clear(struct ref_array *array)
2231
}
2232
}
2233
2234
-static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata)
2234
+static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata, int reachable)
2235
{
2236
struct rev_info revs;
2237
int i, old_nr;
2238
- struct ref_filter *filter = ref_cbdata->filter;
2238
struct ref_array *array = ref_cbdata->array;
2239
struct commit **to_clear = xcalloc(sizeof(struct commit *), array->nr);
2240
+ struct commit_list *rl;
2241
+
2242
+ struct commit_list *check_reachable_list = reachable ?
2243
+ ref_cbdata->filter->reachable_from :
2244
+ ref_cbdata->filter->unreachable_from;
2245
+
2246
+ if (!check_reachable_list)
2247
+ return;
2248
2249
repo_init_revisions(the_repository, &revs, NULL);
2250
@@ -2247,8 +2254,11 @@ static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata)
2254
to_clear[i] = item->commit;
2255
}
2256
2250
- filter->merge_commit->object.flags |= UNINTERESTING;
2251
- add_pending_object(&revs, &filter->merge_commit->object, "");
2257
+ for (rl = check_reachable_list; rl; rl = rl->next) {
2258
+ struct commit *merge_commit = rl->item;
2259
+ merge_commit->object.flags |= UNINTERESTING;
2260
+ add_pending_object(&revs, &merge_commit->object, "");
2261
+ }
2262
2263
revs.limited = 1;
2264
if (prepare_revision_walk(&revs))
@@ -2263,14 +2273,19 @@ static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata)
2273
2274
int is_merged = !!(commit->object.flags & UNINTERESTING);
2275
2266
- if (is_merged == (filter->merge == REF_FILTER_MERGED_INCLUDE))
2276
+ if (is_merged == reachable)
2277
array->items[array->nr++] = array->items[i];
2278
else
2279
free_array_item(item);
2280
}
2281
2282
clear_commit_marks_many(old_nr, to_clear, ALL_REV_FLAGS);
2273
- clear_commit_marks(filter->merge_commit, ALL_REV_FLAGS);
2283
+
2284
+ while (check_reachable_list) {
2285
+ struct commit *merge_commit = pop_commit(&check_reachable_list);
2286
+ clear_commit_marks(merge_commit, ALL_REV_FLAGS);
2287
+ }
2288
+
2289
free(to_clear);
2290
}
2291
@@ -2322,8 +2337,8 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int
2337
clear_contains_cache(&ref_cbdata.no_contains_cache);
2338
2339
/* Filters that need revision walking */
2325
- if (filter->merge_commit)
2326
- do_merge_filter(&ref_cbdata);
2340
+ do_merge_filter(&ref_cbdata, DO_MERGE_FILTER_REACHABLE);
2341
+ do_merge_filter(&ref_cbdata, DO_MERGE_FILTER_UNREACHABLE);
2342
2343
return ret;
2344
}
@@ -2541,31 +2556,22 @@ int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset)
2556
{
2557
struct ref_filter *rf = opt->value;
2558
struct object_id oid;
2544
- int no_merged = starts_with(opt->long_name, "no");
2559
+ struct commit *merge_commit;
2560
2561
BUG_ON_OPT_NEG(unset);
2562
2548
- if (rf->merge) {
2549
- if (no_merged) {
2550
- return error(_("option `%s' is incompatible with --merged"),
2551
- opt->long_name);
2552
- } else {
2553
- return error(_("option `%s' is incompatible with --no-merged"),
2554
- opt->long_name);
2555
- }
2556
- }
2557
-
2558
- rf->merge = no_merged
2559
- ? REF_FILTER_MERGED_OMIT
2560
- : REF_FILTER_MERGED_INCLUDE;
2561
-
2563
if (get_oid(arg, &oid))
2564
die(_("malformed object name %s"), arg);
2565
2565
- rf->merge_commit = lookup_commit_reference_gently(the_repository,
2566
- &oid, 0);
2567
- if (!rf->merge_commit)
2566
+ merge_commit = lookup_commit_reference_gently(the_repository, &oid, 0);
2567
+
2568
+ if (!merge_commit)
2569
return error(_("option `%s' must point to a commit"), opt->long_name);
2570
2571
+ if (starts_with(opt->long_name, "no"))
2572
+ commit_list_insert(merge_commit, &rf->unreachable_from);
2573
+ else
2574
+ commit_list_insert(merge_commit, &rf->reachable_from);
2575
+
2576
return 0;
2577
}
ref-filter.h
+5
-7
@@ -23,6 +23,9 @@
23
#define FILTER_REFS_DETACHED_HEAD 0x0020
24
#define FILTER_REFS_KIND_MASK (FILTER_REFS_ALL | FILTER_REFS_DETACHED_HEAD)
25
26
+#define DO_MERGE_FILTER_UNREACHABLE 0
27
+#define DO_MERGE_FILTER_REACHABLE 1
28
+
29
struct atom_value;
30
31
struct ref_sorting {
@@ -54,13 +57,8 @@ struct ref_filter {
57
struct oid_array points_at;
58
struct commit_list *with_commit;
59
struct commit_list *no_commit;
57
-
58
- enum {
59
- REF_FILTER_MERGED_NONE = 0,
60
- REF_FILTER_MERGED_INCLUDE,
61
- REF_FILTER_MERGED_OMIT
62
- } merge;
63
- struct commit *merge_commit;
60
+ struct commit_list *reachable_from;
61
+ struct commit_list *unreachable_from;
62
63
unsigned int with_commit_tag_algo : 1,
64
match_as_path : 1,
t/t3200-branch.sh
-4
@@ -1298,10 +1298,6 @@ test_expect_success '--merged catches invalid object names' '
1298
test_must_fail git branch --merged 0000000000000000000000000000000000000000
1299
'
1300
1301
-test_expect_success '--merged is incompatible with --no-merged' '
1302
- test_must_fail git branch --merged HEAD --no-merged HEAD
1303
-'
1304
-
1301
test_expect_success '--list during rebase' '
1302
test_when_finished "reset_rebase" &&
1303
git checkout master &&
t/t3201-branch-contains.sh
+27
@@ -187,6 +187,16 @@ test_expect_success 'multiple branch --contains' '
187
test_cmp expect actual
188
'
189
190
+test_expect_success 'multiple branch --merged' '
191
+ git branch --merged next --merged master >actual &&
192
+ cat >expect <<-\EOF &&
193
+ master
194
+ * next
195
+ side
196
+ EOF
197
+ test_cmp expect actual
198
+'
199
+
200
test_expect_success 'multiple branch --no-contains' '
201
git branch --no-contains side --no-contains side2 >actual &&
202
cat >expect <<-\EOF &&
@@ -195,6 +205,14 @@ test_expect_success 'multiple branch --no-contains' '
205
test_cmp expect actual
206
'
207
208
+test_expect_success 'multiple branch --no-merged' '
209
+ git branch --no-merged next --no-merged master >actual &&
210
+ cat >expect <<-\EOF &&
211
+ side2
212
+ EOF
213
+ test_cmp expect actual
214
+'
215
+
216
test_expect_success 'branch --contains combined with --no-contains' '
217
git checkout -b seen master &&
218
git merge side &&
@@ -207,6 +225,15 @@ test_expect_success 'branch --contains combined with --no-contains' '
225
test_cmp expect actual
226
'
227
228
+test_expect_success 'branch --merged combined with --no-merged' '
229
+ git branch --merged seen --no-merged next >actual &&
230
+ cat >expect <<-\EOF &&
231
+ * seen
232
+ side2
233
+ EOF
234
+ test_cmp expect actual
235
+'
236
+
237
# We want to set up a case where the walk for the tracking info
238
# of one branch crosses the tip of another branch (and make sure
239
# that the latter walk does not mess up our flag to see if it was
t/t6302-for-each-ref-filter.sh
+2
-2
@@ -437,8 +437,8 @@ test_expect_success 'check %(if:notequals=<string>)' '
437
test_cmp expect actual
438
'
439
440
-test_expect_success '--merged is incompatible with --no-merged' '
441
- test_must_fail git for-each-ref --merged HEAD --no-merged HEAD
440
+test_expect_success '--merged is compatible with --no-merged' '
441
+ git for-each-ref --merged HEAD --no-merged HEAD
442
'
443
444
test_expect_success 'validate worktree atom' '
t/t7004-tag.sh
+2
-2
@@ -2015,8 +2015,8 @@ test_expect_success '--merged can be used in non-list mode' '
2015
test_cmp expect actual
2016
'
2017
2018
-test_expect_success '--merged is incompatible with --no-merged' '
2019
- test_must_fail git tag --merged HEAD --no-merged HEAD
2018
+test_expect_success '--merged is compatible with --no-merged' '
2019
+ git tag --merged HEAD --no-merged HEAD
2020
'
2021
2022
test_expect_success '--merged shows merged tags' '