ref-filter: add --no-contains option to tag/branch/for-each-ref

Change the tag, branch & for-each-ref commands to have a --no-contains option in addition to their longstanding --contains options. This allows for finding the last-good rollout tag given a known-bad <commit>. Given a hypothetically bad commit cf5c7253e0, the git version to revert to can be found with this hacky two-liner: (git tag -l 'v[0-9]*'; git tag -l --contains cf5c7253e0 'v[0-9]*') | sort | uniq -c | grep -E '^ *1 ' | awk '{print $2}' | tail -n 10 With this new --no-contains option the same can be achieved with: git tag -l --no-contains cf5c7253e0 'v[0-9]*' | sort | tail -n 10 As the filtering machinery is shared between the tag, branch & for-each-ref commands, implement this for those commands too. A practical use for this with "branch" is e.g. finding branches which were branched off between v2.8.0 and v2.10.0: git branch --contains v2.8.0 --no-contains v2.10.0 The "describe" command also has a --contains option, but its semantics are unrelated to what tag/branch/for-each-ref use --contains for. A --no-contains option for "describe" wouldn't make any sense, other than being exactly equivalent to not supplying --contains at all, which would be confusing at best. Add a --without option to "tag" as an alias for --no-contains, for consistency with --with and --contains. The --with option is undocumented, and possibly the only user of it is Junio (<xmqqefy71iej.fsf@gitster.mtv.corp.google.com>). But it's trivial to support, so let's do that. The additions to the the test suite are inverse copies of the corresponding --contains tests. With this change --no-contains for tag, branch & for-each-ref is just as well tested as the existing --contains option. In addition to those tests, add a test for "tag" which asserts that --no-contains won't find tree/blob tags, which is slightly unintuitive, but consistent with how --contains works & is documented. Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Ævar Arnfjörð Bjarmason committed Mar 24, 2017 at 18:40 UTC ac3f5a346860b824e083c5d305757c3260565475
13 files changed +245 -25
Documentation/git-branch.txt
+12 -4
@@ -11,7 +11,8 @@ SYNOPSIS
11 'git branch' [--color[=<when>] | --no-color] [-r | -a]
12 [--list] [-v [--abbrev=<length> | --no-abbrev]]
13 [--column[=<options>] | --no-column]
14 - [(--merged | --no-merged | --contains) [<commit>]] [--sort=<key>]
14 + [(--merged | --no-merged) [<commit>]]
15 + [--contains [<commit]] [--no-contains [<commit>]] [--sort=<key>]
16 [--points-at <object>] [--format=<format>] [<pattern>...]
17 'git branch' [--set-upstream | --track | --no-track] [-l] [-f] <branchname> [<start-point>]
18 'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
@@ -35,7 +36,7 @@ as branch creation.
36
37 With `--contains`, shows only the branches that contain the named commit
38 (in other words, the branches whose tip commits are descendants of the
38 -named commit). With `--merged`, only branches merged into the named
39 +named commit), `--no-contains` inverts it. With `--merged`, only branches merged into the named
40 commit (i.e. the branches whose tip commits are reachable from the named
41 commit) will be listed. With `--no-merged` only branches not merged into
42 the named commit will be listed. If the <commit> argument is missing it
@@ -213,6 +214,10 @@ start-point is either a local or remote-tracking branch.
214 Only list branches which contain the specified commit (HEAD
215 if not specified). Implies `--list`.
216
217 +--no-contains [<commit>]::
218 + Only list branches which don't contain the specified commit
219 + (HEAD if not specified). Implies `--list`.
220 +
221 --merged [<commit>]::
222 Only list branches whose tips are reachable from the
223 specified commit (HEAD if not specified). Implies `--list`,
@@ -298,13 +303,16 @@ If you are creating a branch that you want to checkout immediately, it is
303 easier to use the git checkout command with its `-b` option to create
304 a branch and check it out with a single command.
305
301 -The options `--contains`, `--merged` and `--no-merged` serve three related
302 -but different purposes:
306 +The options `--contains`, `--no-contains`, `--merged` and `--no-merged`
307 +serve four related but different purposes:
308
309 - `--contains <commit>` is used to find all branches which will need
310 special attention if <commit> were to be rebased or amended, since those
311 branches contain the specified <commit>.
312
313 +- `--no-contains <commit>` is the inverse of that, i.e. branches that don't
314 + contain the specified <commit>.
315 +
316 - `--merged` is used to find all branches which can be safely deleted,
317 since those branches are fully contained by HEAD.
318
Documentation/git-for-each-ref.txt
+5 -1
@@ -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>] [(--merged | --no-merged) [<object>]]
14 - [--contains [<object>]]
14 + [--contains [<object>]] [--no-contains [<object>]]
15
16 DESCRIPTION
17 -----------
@@ -81,6 +81,10 @@ OPTIONS
81 Only list refs which contain the specified commit (HEAD if not
82 specified).
83
84 +--no-contains [<object>]::
85 + Only list refs which don't contain the specified commit (HEAD
86 + if not specified).
87 +
88 --ignore-case::
89 Sorting and filtering refs are case insensitive.
90
Documentation/git-tag.txt
+5 -1
@@ -12,7 +12,7 @@ SYNOPSIS
12 'git tag' [-a | -s | -u <keyid>] [-f] [-m <msg> | -F <file>]
13 <tagname> [<commit> | <object>]
14 'git tag' -d <tagname>...
15 -'git tag' [-n[<num>]] -l [--contains <commit>] [--points-at <object>]
15 +'git tag' [-n[<num>]] -l [--contains <commit>] [--contains <commit>] [--points-at <object>]
16 [--column[=<options>] | --no-column] [--create-reflog] [--sort=<key>]
17 [--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]
18 'git tag' -v [--format=<format>] <tagname>...
@@ -130,6 +130,10 @@ This option is only applicable when listing tags without annotation lines.
130 Only list tags which contain the specified commit (HEAD if not
131 specified). Implies `--list`.
132
133 +--no-contains [<commit>]::
134 + Only list tags which don't contain the specified commit (HEAD if
135 + not specified). Implies `--list`.
136 +
137 --merged [<commit>]::
138 Only list tags whose commits are reachable from the specified
139 commit (`HEAD` if not specified), incompatible with `--no-merged`.
builtin/branch.c
+4 -1
@@ -548,7 +548,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
548 OPT_SET_INT('r', "remotes", &filter.kind, N_("act on remote-tracking branches"),
549 FILTER_REFS_REMOTES),
550 OPT_CONTAINS(&filter.with_commit, N_("print only branches that contain the commit")),
551 + OPT_NO_CONTAINS(&filter.no_commit, N_("print only branches that don't contain the commit")),
552 OPT_WITH(&filter.with_commit, N_("print only branches that contain the commit")),
553 + OPT_WITHOUT(&filter.no_commit, N_("print only branches that don't contain the commit")),
554 OPT__ABBREV(&filter.abbrev),
555
556 OPT_GROUP(N_("Specific git-branch actions:")),
@@ -604,7 +606,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
606 if (!delete && !rename && !edit_description && !new_upstream && !unset_upstream && argc == 0)
607 list = 1;
608
607 - if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr)
609 + if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr ||
610 + filter.no_commit)
611 list = 1;
612
613 if (!!delete + !!rename + !!new_upstream +
builtin/for-each-ref.c
+2 -1
@@ -9,7 +9,7 @@ static char const * const for_each_ref_usage[] = {
9 N_("git for-each-ref [<options>] [<pattern>]"),
10 N_("git for-each-ref [--points-at <object>]"),
11 N_("git for-each-ref [(--merged | --no-merged) [<commit>]]"),
12 - N_("git for-each-ref [--contains [<commit>]]"),
12 + N_("git for-each-ref [--contains [<commit>]] [--no-contains [<commit>]]"),
13 NULL
14 };
15
@@ -43,6 +43,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
43 OPT_MERGED(&filter, N_("print only refs that are merged")),
44 OPT_NO_MERGED(&filter, N_("print only refs that are not merged")),
45 OPT_CONTAINS(&filter.with_commit, N_("print only refs which contain the commit")),
46 + OPT_NO_CONTAINS(&filter.no_commit, N_("print only refs which don't contain the commit")),
47 OPT_BOOL(0, "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
48 OPT_END(),
49 };
builtin/tag.c
+6 -2
@@ -22,7 +22,7 @@
22 static const char * const git_tag_usage[] = {
23 N_("git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] <tagname> [<head>]"),
24 N_("git tag -d <tagname>..."),
25 - N_("git tag -l [-n[<num>]] [--contains <commit>] [--points-at <object>]"
25 + N_("git tag -l [-n[<num>]] [--contains <commit>] [--no-contains <commit>] [--points-at <object>]"
26 "\n\t\t[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]"),
27 N_("git tag -v [--format=<format>] <tagname>..."),
28 NULL
@@ -424,7 +424,9 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
424 OPT_GROUP(N_("Tag listing options")),
425 OPT_COLUMN(0, "column", &colopts, N_("show tag list in columns")),
426 OPT_CONTAINS(&filter.with_commit, N_("print only tags that contain the commit")),
427 + OPT_NO_CONTAINS(&filter.no_commit, N_("print only tags that don't contain the commit")),
428 OPT_WITH(&filter.with_commit, N_("print only tags that contain the commit")),
429 + OPT_WITHOUT(&filter.no_commit, N_("print only tags that don't contain the commit")),
430 OPT_MERGED(&filter, N_("print only tags that are merged")),
431 OPT_NO_MERGED(&filter, N_("print only tags that are not merged")),
432 OPT_CALLBACK(0 , "sort", sorting_tail, N_("key"),
@@ -458,7 +460,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
460 if (!cmdmode) {
461 if (argc == 0)
462 cmdmode = 'l';
461 - else if (filter.with_commit ||
463 + else if (filter.with_commit || filter.no_commit ||
464 filter.points_at.nr || filter.merge_commit ||
465 filter.lines != -1)
466 cmdmode = 'l';
@@ -495,6 +497,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
497 die(_("-n option is only allowed in list mode"));
498 if (filter.with_commit)
499 die(_("--contains option is only allowed in list mode"));
500 + if (filter.no_commit)
501 + die(_("--no-contains option is only allowed in list mode"));
502 if (filter.points_at.nr)
503 die(_("--points-at option is only allowed in list mode"));
504 if (filter.merge_commit)
contrib/completion/git-completion.bash
+2 -2
@@ -1093,7 +1093,7 @@ _git_branch ()
1093 --*)
1094 __gitcomp "
1095 --color --no-color --verbose --abbrev= --no-abbrev
1096 - --track --no-track --contains --merged --no-merged
1096 + --track --no-track --contains --no-contains --merged --no-merged
1097 --set-upstream-to= --edit-description --list
1098 --unset-upstream --delete --move --remotes
1099 --column --no-column --sort= --points-at
@@ -2862,7 +2862,7 @@ _git_tag ()
2862 __gitcomp "
2863 --list --delete --verify --annotate --message --file
2864 --sign --cleanup --local-user --force --column --sort=
2865 - --contains --points-at --merged --no-merged --create-reflog
2865 + --contains --no-contains --points-at --merged --no-merged --create-reflog
2866 "
2867 ;;
2868 esac
parse-options.h
+2
@@ -259,6 +259,8 @@ extern int parse_opt_passthru_argv(const struct option *, const char *, int);
259 parse_opt_commits, (intptr_t) "HEAD" \
260 }
261 #define OPT_CONTAINS(v, h) _OPT_CONTAINS_OR_WITH("contains", v, h, PARSE_OPT_NONEG)
262 +#define OPT_NO_CONTAINS(v, h) _OPT_CONTAINS_OR_WITH("no-contains", v, h, PARSE_OPT_NONEG)
263 #define OPT_WITH(v, h) _OPT_CONTAINS_OR_WITH("with", v, h, PARSE_OPT_HIDDEN | PARSE_OPT_NONEG)
264 +#define OPT_WITHOUT(v, h) _OPT_CONTAINS_OR_WITH("without", v, h, PARSE_OPT_HIDDEN | PARSE_OPT_NONEG)
265
266 #endif
ref-filter.c
+13 -6
@@ -1487,6 +1487,7 @@ struct ref_filter_cbdata {
1487 struct ref_array *array;
1488 struct ref_filter *filter;
1489 struct contains_cache contains_cache;
1490 + struct contains_cache no_contains_cache;
1491 };
1492
1493 /*
@@ -1586,11 +1587,11 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
1587 }
1588
1589 static int commit_contains(struct ref_filter *filter, struct commit *commit,
1589 - struct contains_cache *cache)
1590 + struct commit_list *list, struct contains_cache *cache)
1591 {
1592 if (filter->with_commit_tag_algo)
1592 - return contains_tag_algo(commit, filter->with_commit, cache) == CONTAINS_YES;
1593 - return is_descendant_of(commit, filter->with_commit);
1593 + return contains_tag_algo(commit, list, cache) == CONTAINS_YES;
1594 + return is_descendant_of(commit, list);
1595 }
1596
1597 /*
@@ -1780,13 +1781,17 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
1781 * obtain the commit using the 'oid' available and discard all
1782 * non-commits early. The actual filtering is done later.
1783 */
1783 - if (filter->merge_commit || filter->with_commit || filter->verbose) {
1784 + if (filter->merge_commit || filter->with_commit || filter->no_commit || filter->verbose) {
1785 commit = lookup_commit_reference_gently(oid->hash, 1);
1786 if (!commit)
1787 return 0;
1787 - /* We perform the filtering for the '--contains' option */
1788 + /* We perform the filtering for the '--contains' option... */
1789 if (filter->with_commit &&
1789 - !commit_contains(filter, commit, &ref_cbdata->contains_cache))
1790 + !commit_contains(filter, commit, filter->with_commit, &ref_cbdata->contains_cache))
1791 + return 0;
1792 + /* ...or for the `--no-contains' option */
1793 + if (filter->no_commit &&
1794 + commit_contains(filter, commit, filter->no_commit, &ref_cbdata->no_contains_cache))
1795 return 0;
1796 }
1797
@@ -1887,6 +1892,7 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int
1892 filter->kind = type & FILTER_REFS_KIND_MASK;
1893
1894 init_contains_cache(&ref_cbdata.contains_cache);
1895 + init_contains_cache(&ref_cbdata.no_contains_cache);
1896
1897 /* Simple per-ref filtering */
1898 if (!filter->kind)
@@ -1911,6 +1917,7 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int
1917 }
1918
1919 clear_contains_cache(&ref_cbdata.contains_cache);
1920 + clear_contains_cache(&ref_cbdata.no_contains_cache);
1921
1922 /* Filters that need revision walking */
1923 if (filter->merge_commit)
ref-filter.h
+1
@@ -53,6 +53,7 @@ struct ref_filter {
53 const char **name_patterns;
54 struct sha1_array points_at;
55 struct commit_list *with_commit;
56 + struct commit_list *no_commit;
57
58 enum {
59 REF_FILTER_MERGED_NONE = 0,
t/t3201-branch-contains.sh
+51 -3
@@ -1,6 +1,6 @@
1 #!/bin/sh
2
3 -test_description='branch --contains <commit>, --merged, and --no-merged'
3 +test_description='branch --contains <commit>, --no-contains <commit> --merged, and --no-merged'
4
5 . ./test-lib.sh
6
@@ -45,6 +45,22 @@ test_expect_success 'branch --contains master' '
45
46 '
47
48 +test_expect_success 'branch --no-contains=master' '
49 +
50 + git branch --no-contains=master >actual &&
51 + >expect &&
52 + test_cmp expect actual
53 +
54 +'
55 +
56 +test_expect_success 'branch --no-contains master' '
57 +
58 + git branch --no-contains master >actual &&
59 + >expect &&
60 + test_cmp expect actual
61 +
62 +'
63 +
64 test_expect_success 'branch --contains=side' '
65
66 git branch --contains=side >actual &&
@@ -55,6 +71,16 @@ test_expect_success 'branch --contains=side' '
71
72 '
73
74 +test_expect_success 'branch --no-contains=side' '
75 +
76 + git branch --no-contains=side >actual &&
77 + {
78 + echo " master"
79 + } >expect &&
80 + test_cmp expect actual
81 +
82 +'
83 +
84 test_expect_success 'branch --contains with pattern implies --list' '
85
86 git branch --contains=master master >actual &&
@@ -65,6 +91,14 @@ test_expect_success 'branch --contains with pattern implies --list' '
91
92 '
93
94 +test_expect_success 'branch --no-contains with pattern implies --list' '
95 +
96 + git branch --no-contains=master master >actual &&
97 + >expect &&
98 + test_cmp expect actual
99 +
100 +'
101 +
102 test_expect_success 'side: branch --merged' '
103
104 git branch --merged >actual &&
@@ -126,7 +160,9 @@ test_expect_success 'branch --no-merged with pattern implies --list' '
160 test_expect_success 'implicit --list conflicts with modification options' '
161
162 test_must_fail git branch --contains=master -d &&
129 - test_must_fail git branch --contains=master -m foo
163 + test_must_fail git branch --contains=master -m foo &&
164 + test_must_fail git branch --no-contains=master -d &&
165 + test_must_fail git branch --no-contains=master -m foo
166
167 '
168
@@ -136,7 +172,8 @@ test_expect_success 'Assert that --contains only works on commits, not trees & b
172 Some blob
173 EOF
174 ) &&
139 - test_must_fail git branch --contains $blob
175 + test_must_fail git branch --contains $blob &&
176 + test_must_fail git branch --no-contains $blob
177 '
178
179 # We want to set up a case where the walk for the tracking info
@@ -168,4 +205,15 @@ test_expect_success 'branch --merged with --verbose' '
205 test_i18ncmp expect actual
206 '
207
208 +test_expect_success 'branch --contains combined with --no-contains' '
209 + git branch --contains zzz --no-contains topic >actual &&
210 + cat >expect <<-\EOF &&
211 + master
212 + side
213 + zzz
214 + EOF
215 + test_cmp expect actual
216 +
217 +'
218 +
219 test_done
t/t6302-for-each-ref-filter.sh
+16
@@ -93,6 +93,22 @@ test_expect_success 'filtering with --contains' '
93 test_cmp expect actual
94 '
95
96 +test_expect_success 'filtering with --no-contains' '
97 + cat >expect <<-\EOF &&
98 + refs/tags/one
99 + EOF
100 + git for-each-ref --format="%(refname)" --no-contains=two >actual &&
101 + test_cmp expect actual
102 +'
103 +
104 +test_expect_success 'filtering with --contains and --no-contains' '
105 + cat >expect <<-\EOF &&
106 + refs/tags/two
107 + EOF
108 + git for-each-ref --format="%(refname)" --contains=two --no-contains=three >actual &&
109 + test_cmp expect actual
110 +'
111 +
112 test_expect_success '%(color) must fail' '
113 test_must_fail git for-each-ref --format="%(color)%(refname)"
114 '
t/t7004-tag.sh
+126 -4
@@ -1424,6 +1424,23 @@ test_expect_success 'checking that first commit is in all tags (relative)' "
1424 test_cmp expected actual
1425 "
1426
1427 +# All the --contains tests above, but with --no-contains
1428 +test_expect_success 'checking that first commit is not listed in any tag with --no-contains (hash)' "
1429 + >expected &&
1430 + git tag -l --no-contains $hash1 v* >actual &&
1431 + test_cmp expected actual
1432 +"
1433 +
1434 +test_expect_success 'checking that first commit is in all tags (tag)' "
1435 + git tag -l --no-contains v1.0 v* >actual &&
1436 + test_cmp expected actual
1437 +"
1438 +
1439 +test_expect_success 'checking that first commit is in all tags (relative)' "
1440 + git tag -l --no-contains HEAD~2 v* >actual &&
1441 + test_cmp expected actual
1442 +"
1443 +
1444 cat > expected <<EOF
1445 v2.0
1446 EOF
@@ -1433,6 +1450,17 @@ test_expect_success 'checking that second commit only has one tag' "
1450 test_cmp expected actual
1451 "
1452
1453 +cat > expected <<EOF
1454 +v0.2.1
1455 +v1.0
1456 +v1.0.1
1457 +v1.1.3
1458 +EOF
1459 +
1460 +test_expect_success 'inverse of the last test, with --no-contains' "
1461 + git tag -l --no-contains $hash2 v* >actual &&
1462 + test_cmp expected actual
1463 +"
1464
1465 cat > expected <<EOF
1466 EOF
@@ -1442,6 +1470,19 @@ test_expect_success 'checking that third commit has no tags' "
1470 test_cmp expected actual
1471 "
1472
1473 +cat > expected <<EOF
1474 +v0.2.1
1475 +v1.0
1476 +v1.0.1
1477 +v1.1.3
1478 +v2.0
1479 +EOF
1480 +
1481 +test_expect_success 'conversely --no-contains on the third commit lists all tags' "
1482 + git tag -l --no-contains $hash3 v* >actual &&
1483 + test_cmp expected actual
1484 +"
1485 +
1486 # how about a simple merge?
1487
1488 test_expect_success 'creating simple branch' '
@@ -1463,6 +1504,19 @@ test_expect_success 'checking that branch head only has one tag' "
1504 test_cmp expected actual
1505 "
1506
1507 +cat > expected <<EOF
1508 +v0.2.1
1509 +v1.0
1510 +v1.0.1
1511 +v1.1.3
1512 +v2.0
1513 +EOF
1514 +
1515 +test_expect_success 'checking that branch head with --no-contains lists all but one tag' "
1516 + git tag -l --no-contains $hash4 v* >actual &&
1517 + test_cmp expected actual
1518 +"
1519 +
1520 test_expect_success 'merging original branch into this branch' '
1521 git merge --strategy=ours master &&
1522 git tag v4.0
@@ -1477,6 +1531,20 @@ test_expect_success 'checking that original branch head has one tag now' "
1531 test_cmp expected actual
1532 "
1533
1534 +cat > expected <<EOF
1535 +v0.2.1
1536 +v1.0
1537 +v1.0.1
1538 +v1.1.3
1539 +v2.0
1540 +v3.0
1541 +EOF
1542 +
1543 +test_expect_success 'checking that original branch head with --no-contains lists all but one tag now' "
1544 + git tag -l --no-contains $hash3 v* >actual &&
1545 + test_cmp expected actual
1546 +"
1547 +
1548 cat > expected <<EOF
1549 v0.2.1
1550 v1.0
@@ -1497,6 +1565,12 @@ test_expect_success 'checking that --contains can be used in non-list mode' '
1565 test_cmp expected actual
1566 '
1567
1568 +test_expect_success 'checking that initial commit is in all tags with --no-contains' "
1569 + >expected &&
1570 + git tag -l --no-contains $hash1 v* >actual &&
1571 + test_cmp expected actual
1572 +"
1573 +
1574 # mixing modes and options:
1575
1576 test_expect_success 'mixing incompatibles modes and options is forbidden' '
@@ -1522,10 +1596,13 @@ test_expect_success 'mixing incompatibles modes and options is forbidden' '
1596 test_must_fail git tag -l -F some file &&
1597 test_must_fail git tag -v -s &&
1598 test_must_fail git tag --contains tag-tree &&
1525 - test_must_fail git tag --contains tag-blob
1599 + test_must_fail git tag --contains tag-blob &&
1600 + test_must_fail git tag --no-contains tag-tree &&
1601 + test_must_fail git tag --no-contains tag-blob &&
1602 + test_must_fail git tag --contains --no-contains
1603 '
1604
1528 -for option in --contains --merged --no-merged --points-at
1605 +for option in --contains --no-contains --merged --no-merged --points-at
1606 do
1607 test_expect_success "mixing incompatible modes with $option is forbidden" "
1608 test_must_fail git tag -d $option HEAD &&
@@ -1792,7 +1869,7 @@ run_with_limited_stack () {
1869 test_lazy_prereq ULIMIT_STACK_SIZE 'run_with_limited_stack true'
1870
1871 # we require ulimit, this excludes Windows
1795 -test_expect_success ULIMIT_STACK_SIZE '--contains works in a deep repo' '
1872 +test_expect_success ULIMIT_STACK_SIZE '--contains and --no-contains work in a deep repo' '
1873 >expect &&
1874 i=1 &&
1875 while test $i -lt 8000
@@ -1808,7 +1885,9 @@ EOF"
1885 git checkout master &&
1886 git tag far-far-away HEAD^ &&
1887 run_with_limited_stack git tag --contains HEAD >actual &&
1811 - test_cmp expect actual
1888 + test_cmp expect actual &&
1889 + run_with_limited_stack git tag --no-contains HEAD >actual &&
1890 + test_line_count ">" 10 actual
1891 '
1892
1893 test_expect_success '--format should list tags as per format given' '
@@ -1870,4 +1949,47 @@ test_expect_success 'ambiguous branch/tags not marked' '
1949 test_cmp expect actual
1950 '
1951
1952 +test_expect_success '--contains combined with --no-contains' '
1953 + (
1954 + git init no-contains &&
1955 + cd no-contains &&
1956 + test_commit v0.1 &&
1957 + test_commit v0.2 &&
1958 + test_commit v0.3 &&
1959 + test_commit v0.4 &&
1960 + test_commit v0.5 &&
1961 + cat >expected <<-\EOF &&
1962 + v0.2
1963 + v0.3
1964 + v0.4
1965 + EOF
1966 + git tag --contains v0.2 --no-contains v0.5 >actual &&
1967 + test_cmp expected actual
1968 + )
1969 +'
1970 +
1971 +# As the docs say, list tags which contain a specified *commit*. We
1972 +# don't recurse down to tags for trees or blobs pointed to by *those*
1973 +# commits.
1974 +test_expect_success 'Does --[no-]contains stop at commits? Yes!' '
1975 + cd no-contains &&
1976 + blob=$(git rev-parse v0.3:v0.3.t) &&
1977 + tree=$(git rev-parse v0.3^{tree}) &&
1978 + git tag tag-blob $blob &&
1979 + git tag tag-tree $tree &&
1980 + git tag --contains v0.3 >actual &&
1981 + cat >expected <<-\EOF &&
1982 + v0.3
1983 + v0.4
1984 + v0.5
1985 + EOF
1986 + test_cmp expected actual &&
1987 + git tag --no-contains v0.3 >actual &&
1988 + cat >expected <<-\EOF &&
1989 + v0.1
1990 + v0.2
1991 + EOF
1992 + test_cmp expected actual
1993 +'
1994 +
1995 test_done