Skip to content

Commit ac3f5a3

Browse files
avargitster
authored andcommitted
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 cf5c725, the git version to revert to can be found with this hacky two-liner: (git tag -l 'v[0-9]*'; git tag -l --contains cf5c725 '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 cf5c725 '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 (<[email protected]>). 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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 1e0c3b6 commit ac3f5a3

File tree

13 files changed

+245
-25
lines changed

13 files changed

+245
-25
lines changed

Documentation/git-branch.txt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ SYNOPSIS
1111
'git branch' [--color[=<when>] | --no-color] [-r | -a]
1212
[--list] [-v [--abbrev=<length> | --no-abbrev]]
1313
[--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>]
1516
[--points-at <object>] [--format=<format>] [<pattern>...]
1617
'git branch' [--set-upstream | --track | --no-track] [-l] [-f] <branchname> [<start-point>]
1718
'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
@@ -35,7 +36,7 @@ as branch creation.
3536

3637
With `--contains`, shows only the branches that contain the named commit
3738
(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
3940
commit (i.e. the branches whose tip commits are reachable from the named
4041
commit) will be listed. With `--no-merged` only branches not merged into
4142
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.
213214
Only list branches which contain the specified commit (HEAD
214215
if not specified). Implies `--list`.
215216

217+
--no-contains [<commit>]::
218+
Only list branches which don't contain the specified commit
219+
(HEAD if not specified). Implies `--list`.
220+
216221
--merged [<commit>]::
217222
Only list branches whose tips are reachable from the
218223
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
298303
easier to use the git checkout command with its `-b` option to create
299304
a branch and check it out with a single command.
300305

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:
303308

304309
- `--contains <commit>` is used to find all branches which will need
305310
special attention if <commit> were to be rebased or amended, since those
306311
branches contain the specified <commit>.
307312

313+
- `--no-contains <commit>` is the inverse of that, i.e. branches that don't
314+
contain the specified <commit>.
315+
308316
- `--merged` is used to find all branches which can be safely deleted,
309317
since those branches are fully contained by HEAD.
310318

Documentation/git-for-each-ref.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ SYNOPSIS
1111
'git for-each-ref' [--count=<count>] [--shell|--perl|--python|--tcl]
1212
[(--sort=<key>)...] [--format=<format>] [<pattern>...]
1313
[--points-at <object>] [(--merged | --no-merged) [<object>]]
14-
[--contains [<object>]]
14+
[--contains [<object>]] [--no-contains [<object>]]
1515

1616
DESCRIPTION
1717
-----------
@@ -81,6 +81,10 @@ OPTIONS
8181
Only list refs which contain the specified commit (HEAD if not
8282
specified).
8383

84+
--no-contains [<object>]::
85+
Only list refs which don't contain the specified commit (HEAD
86+
if not specified).
87+
8488
--ignore-case::
8589
Sorting and filtering refs are case insensitive.
8690

Documentation/git-tag.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ SYNOPSIS
1212
'git tag' [-a | -s | -u <keyid>] [-f] [-m <msg> | -F <file>]
1313
<tagname> [<commit> | <object>]
1414
'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>]
1616
[--column[=<options>] | --no-column] [--create-reflog] [--sort=<key>]
1717
[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]
1818
'git tag' -v [--format=<format>] <tagname>...
@@ -130,6 +130,10 @@ This option is only applicable when listing tags without annotation lines.
130130
Only list tags which contain the specified commit (HEAD if not
131131
specified). Implies `--list`.
132132

133+
--no-contains [<commit>]::
134+
Only list tags which don't contain the specified commit (HEAD if
135+
not specified). Implies `--list`.
136+
133137
--merged [<commit>]::
134138
Only list tags whose commits are reachable from the specified
135139
commit (`HEAD` if not specified), incompatible with `--no-merged`.

builtin/branch.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
548548
OPT_SET_INT('r', "remotes", &filter.kind, N_("act on remote-tracking branches"),
549549
FILTER_REFS_REMOTES),
550550
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")),
551552
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")),
552554
OPT__ABBREV(&filter.abbrev),
553555

554556
OPT_GROUP(N_("Specific git-branch actions:")),
@@ -604,7 +606,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
604606
if (!delete && !rename && !edit_description && !new_upstream && !unset_upstream && argc == 0)
605607
list = 1;
606608

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)
608611
list = 1;
609612

610613
if (!!delete + !!rename + !!new_upstream +

builtin/for-each-ref.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ static char const * const for_each_ref_usage[] = {
99
N_("git for-each-ref [<options>] [<pattern>]"),
1010
N_("git for-each-ref [--points-at <object>]"),
1111
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>]]"),
1313
NULL
1414
};
1515

@@ -43,6 +43,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
4343
OPT_MERGED(&filter, N_("print only refs that are merged")),
4444
OPT_NO_MERGED(&filter, N_("print only refs that are not merged")),
4545
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")),
4647
OPT_BOOL(0, "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
4748
OPT_END(),
4849
};

builtin/tag.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
static const char * const git_tag_usage[] = {
2323
N_("git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] <tagname> [<head>]"),
2424
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>]"
2626
"\n\t\t[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]"),
2727
N_("git tag -v [--format=<format>] <tagname>..."),
2828
NULL
@@ -424,7 +424,9 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
424424
OPT_GROUP(N_("Tag listing options")),
425425
OPT_COLUMN(0, "column", &colopts, N_("show tag list in columns")),
426426
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")),
427428
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")),
428430
OPT_MERGED(&filter, N_("print only tags that are merged")),
429431
OPT_NO_MERGED(&filter, N_("print only tags that are not merged")),
430432
OPT_CALLBACK(0 , "sort", sorting_tail, N_("key"),
@@ -458,7 +460,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
458460
if (!cmdmode) {
459461
if (argc == 0)
460462
cmdmode = 'l';
461-
else if (filter.with_commit ||
463+
else if (filter.with_commit || filter.no_commit ||
462464
filter.points_at.nr || filter.merge_commit ||
463465
filter.lines != -1)
464466
cmdmode = 'l';
@@ -495,6 +497,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
495497
die(_("-n option is only allowed in list mode"));
496498
if (filter.with_commit)
497499
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"));
498502
if (filter.points_at.nr)
499503
die(_("--points-at option is only allowed in list mode"));
500504
if (filter.merge_commit)

contrib/completion/git-completion.bash

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,7 +1093,7 @@ _git_branch ()
10931093
--*)
10941094
__gitcomp "
10951095
--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
10971097
--set-upstream-to= --edit-description --list
10981098
--unset-upstream --delete --move --remotes
10991099
--column --no-column --sort= --points-at
@@ -2862,7 +2862,7 @@ _git_tag ()
28622862
__gitcomp "
28632863
--list --delete --verify --annotate --message --file
28642864
--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
28662866
"
28672867
;;
28682868
esac

parse-options.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ extern int parse_opt_passthru_argv(const struct option *, const char *, int);
259259
parse_opt_commits, (intptr_t) "HEAD" \
260260
}
261261
#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)
262263
#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)
263265

264266
#endif

ref-filter.c

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,7 @@ struct ref_filter_cbdata {
14871487
struct ref_array *array;
14881488
struct ref_filter *filter;
14891489
struct contains_cache contains_cache;
1490+
struct contains_cache no_contains_cache;
14901491
};
14911492

14921493
/*
@@ -1586,11 +1587,11 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
15861587
}
15871588

15881589
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)
15901591
{
15911592
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);
15941595
}
15951596

15961597
/*
@@ -1780,13 +1781,17 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
17801781
* obtain the commit using the 'oid' available and discard all
17811782
* non-commits early. The actual filtering is done later.
17821783
*/
1783-
if (filter->merge_commit || filter->with_commit || filter->verbose) {
1784+
if (filter->merge_commit || filter->with_commit || filter->no_commit || filter->verbose) {
17841785
commit = lookup_commit_reference_gently(oid->hash, 1);
17851786
if (!commit)
17861787
return 0;
1787-
/* We perform the filtering for the '--contains' option */
1788+
/* We perform the filtering for the '--contains' option... */
17881789
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))
17901795
return 0;
17911796
}
17921797

@@ -1887,6 +1892,7 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int
18871892
filter->kind = type & FILTER_REFS_KIND_MASK;
18881893

18891894
init_contains_cache(&ref_cbdata.contains_cache);
1895+
init_contains_cache(&ref_cbdata.no_contains_cache);
18901896

18911897
/* Simple per-ref filtering */
18921898
if (!filter->kind)
@@ -1911,6 +1917,7 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int
19111917
}
19121918

19131919
clear_contains_cache(&ref_cbdata.contains_cache);
1920+
clear_contains_cache(&ref_cbdata.no_contains_cache);
19141921

19151922
/* Filters that need revision walking */
19161923
if (filter->merge_commit)

ref-filter.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ struct ref_filter {
5353
const char **name_patterns;
5454
struct sha1_array points_at;
5555
struct commit_list *with_commit;
56+
struct commit_list *no_commit;
5657

5758
enum {
5859
REF_FILTER_MERGED_NONE = 0,

t/t3201-branch-contains.sh

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/sh
22

3-
test_description='branch --contains <commit>, --merged, and --no-merged'
3+
test_description='branch --contains <commit>, --no-contains <commit> --merged, and --no-merged'
44

55
. ./test-lib.sh
66

@@ -45,6 +45,22 @@ test_expect_success 'branch --contains master' '
4545
4646
'
4747

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+
4864
test_expect_success 'branch --contains=side' '
4965
5066
git branch --contains=side >actual &&
@@ -55,6 +71,16 @@ test_expect_success 'branch --contains=side' '
5571
5672
'
5773

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+
5884
test_expect_success 'branch --contains with pattern implies --list' '
5985
6086
git branch --contains=master master >actual &&
@@ -65,6 +91,14 @@ test_expect_success 'branch --contains with pattern implies --list' '
6591
6692
'
6793

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+
68102
test_expect_success 'side: branch --merged' '
69103
70104
git branch --merged >actual &&
@@ -126,7 +160,9 @@ test_expect_success 'branch --no-merged with pattern implies --list' '
126160
test_expect_success 'implicit --list conflicts with modification options' '
127161
128162
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
130166
131167
'
132168

@@ -136,7 +172,8 @@ test_expect_success 'Assert that --contains only works on commits, not trees & b
136172
Some blob
137173
EOF
138174
) &&
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
140177
'
141178

142179
# 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' '
168205
test_i18ncmp expect actual
169206
'
170207

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+
171219
test_done

t/t6302-for-each-ref-filter.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ test_expect_success 'filtering with --contains' '
9393
test_cmp expect actual
9494
'
9595

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+
96112
test_expect_success '%(color) must fail' '
97113
test_must_fail git for-each-ref --format="%(color)%(refname)"
98114
'

0 commit comments

Comments
 (0)