Merge branch 'ab/ref-filter-no-contains'

"git tag/branch/for-each-ref" family of commands long allowed to
filter the refs by "--contains X" (show only the refs that are
descendants of X), "--merged X" (show only the refs that are
ancestors of X), "--no-merged X" (show only the refs that are not
ancestors of X).  One curious omission, "--no-contains X" (show
only the refs that are not descendants of X) has been added to
them.

* ab/ref-filter-no-contains:
  tag: add tests for --with and --without
  ref-filter: reflow recently changed branch/tag/for-each-ref docs
  ref-filter: add --no-contains option to tag/branch/for-each-ref
  tag: change --point-at to default to HEAD
  tag: implicitly supply --list given another list-like option
  tag: change misleading --list <pattern> documentation
  parse-options: add OPT_NONEG to the "contains" option
  tag: add more incompatibles mode tests
  for-each-ref: partly change <object> to <commit> in help
  tag tests: fix a typo in a test description
  tag: remove a TODO item from the test suite
  ref-filter: add test for --contains on a non-commit
  ref-filter: make combining --merged & --no-merged an error
  tag doc: reword --[no-]merged to talk about commits, not tips
  tag doc: split up the --[no-]merged documentation
  tag doc: move the description of --[no-]merged earlier
This commit is contained in:
Junio C Hamano 2017-04-11 00:21:50 -07:00
commit d1d3d46146
14 changed files with 438 additions and 70 deletions

View file

@ -10,8 +10,9 @@ SYNOPSIS
[verse] [verse]
'git branch' [--color[=<when>] | --no-color] [-r | -a] 'git branch' [--color[=<when>] | --no-color] [-r | -a]
[--list] [-v [--abbrev=<length> | --no-abbrev]] [--list] [-v [--abbrev=<length> | --no-abbrev]]
[--column[=<options>] | --no-column] [--column[=<options>] | --no-column] [--sort=<key>]
[(--merged | --no-merged | --contains) [<commit>]] [--sort=<key>] [(--merged | --no-merged) [<commit>]]
[--contains [<commit]] [--no-contains [<commit>]]
[--points-at <object>] [--format=<format>] [<pattern>...] [--points-at <object>] [--format=<format>] [<pattern>...]
'git branch' [--set-upstream | --track | --no-track] [-l] [-f] <branchname> [<start-point>] 'git branch' [--set-upstream | --track | --no-track] [-l] [-f] <branchname> [<start-point>]
'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>] 'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
@ -35,11 +36,12 @@ as branch creation.
With `--contains`, shows only the branches that contain the named commit With `--contains`, shows only the branches that contain the named commit
(in other words, the branches whose tip commits are descendants of the (in other words, the branches whose tip commits are descendants of the
named commit). With `--merged`, only branches merged into the named named commit), `--no-contains` inverts it. With `--merged`, only branches
commit (i.e. the branches whose tip commits are reachable from the named merged into the named commit (i.e. the branches whose tip commits are
commit) will be listed. With `--no-merged` only branches not merged into reachable from the named commit) will be listed. With `--no-merged` only
the named commit will be listed. If the <commit> argument is missing it branches not merged into the named commit will be listed. If the <commit>
defaults to `HEAD` (i.e. the tip of the current branch). argument is missing it defaults to `HEAD` (i.e. the tip of the current
branch).
The command's second form creates a new branch head named <branchname> The command's second form creates a new branch head named <branchname>
which points to the current `HEAD`, or <start-point> if given. which points to the current `HEAD`, or <start-point> if given.
@ -218,13 +220,19 @@ start-point is either a local or remote-tracking branch.
Only list branches which contain the specified commit (HEAD Only list branches which contain the specified commit (HEAD
if not specified). Implies `--list`. if not specified). Implies `--list`.
--no-contains [<commit>]::
Only list branches which don't contain the specified commit
(HEAD if not specified). Implies `--list`.
--merged [<commit>]:: --merged [<commit>]::
Only list branches whose tips are reachable from the Only list branches whose tips are reachable from the
specified commit (HEAD if not specified). Implies `--list`. specified commit (HEAD if not specified). Implies `--list`,
incompatible with `--no-merged`.
--no-merged [<commit>]:: --no-merged [<commit>]::
Only list branches whose tips are not reachable from the Only list branches whose tips are not reachable from the
specified commit (HEAD if not specified). Implies `--list`. specified commit (HEAD if not specified). Implies `--list`,
incompatible with `--merged`.
<branchname>:: <branchname>::
The name of the branch to create or delete. The name of the branch to create or delete.
@ -301,13 +309,16 @@ If you are creating a branch that you want to checkout immediately, it is
easier to use the git checkout command with its `-b` option to create easier to use the git checkout command with its `-b` option to create
a branch and check it out with a single command. a branch and check it out with a single command.
The options `--contains`, `--merged` and `--no-merged` serve three related The options `--contains`, `--no-contains`, `--merged` and `--no-merged`
but different purposes: serve four related but different purposes:
- `--contains <commit>` is used to find all branches which will need - `--contains <commit>` is used to find all branches which will need
special attention if <commit> were to be rebased or amended, since those special attention if <commit> were to be rebased or amended, since those
branches contain the specified <commit>. branches contain the specified <commit>.
- `--no-contains <commit>` is the inverse of that, i.e. branches that don't
contain the specified <commit>.
- `--merged` is used to find all branches which can be safely deleted, - `--merged` is used to find all branches which can be safely deleted,
since those branches are fully contained by HEAD. since those branches are fully contained by HEAD.

View file

@ -11,7 +11,7 @@ SYNOPSIS
'git for-each-ref' [--count=<count>] [--shell|--perl|--python|--tcl] 'git for-each-ref' [--count=<count>] [--shell|--perl|--python|--tcl]
[(--sort=<key>)...] [--format=<format>] [<pattern>...] [(--sort=<key>)...] [--format=<format>] [<pattern>...]
[--points-at <object>] [(--merged | --no-merged) [<object>]] [--points-at <object>] [(--merged | --no-merged) [<object>]]
[--contains [<object>]] [--contains [<object>]] [--no-contains [<object>]]
DESCRIPTION DESCRIPTION
----------- -----------
@ -69,16 +69,22 @@ OPTIONS
--merged [<object>]:: --merged [<object>]::
Only list refs whose tips are reachable from the Only list refs whose tips are reachable from the
specified commit (HEAD if not specified). specified commit (HEAD if not specified),
incompatible with `--no-merged`.
--no-merged [<object>]:: --no-merged [<object>]::
Only list refs whose tips are not reachable from the Only list refs whose tips are not reachable from the
specified commit (HEAD if not specified). specified commit (HEAD if not specified),
incompatible with `--merged`.
--contains [<object>]:: --contains [<object>]::
Only list refs which contain the specified commit (HEAD if not Only list refs which contain the specified commit (HEAD if not
specified). specified).
--no-contains [<object>]::
Only list refs which don't contain the specified commit (HEAD
if not specified).
--ignore-case:: --ignore-case::
Sorting and filtering refs are case insensitive. Sorting and filtering refs are case insensitive.

View file

@ -12,9 +12,10 @@ SYNOPSIS
'git tag' [-a | -s | -u <keyid>] [-f] [-m <msg> | -F <file>] 'git tag' [-a | -s | -u <keyid>] [-f] [-m <msg> | -F <file>]
<tagname> [<commit> | <object>] <tagname> [<commit> | <object>]
'git tag' -d <tagname>... 'git tag' -d <tagname>...
'git tag' [-n[<num>]] -l [--contains <commit>] [--points-at <object>] 'git tag' [-n[<num>]] -l [--contains <commit>] [--contains <commit>]
[--column[=<options>] | --no-column] [--create-reflog] [--sort=<key>] [--points-at <object>] [--column[=<options>] | --no-column]
[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...] [--create-reflog] [--sort=<key>] [--format=<format>]
[--[no-]merged [<commit>]] [<pattern>...]
'git tag' -v [--format=<format>] <tagname>... 'git tag' -v [--format=<format>] <tagname>...
DESCRIPTION DESCRIPTION
@ -82,18 +83,24 @@ OPTIONS
-n<num>:: -n<num>::
<num> specifies how many lines from the annotation, if any, <num> specifies how many lines from the annotation, if any,
are printed when using -l. are printed when using -l. Implies `--list`.
The default is not to print any annotation lines. +
If no number is given to `-n`, only the first line is printed. The default is not to print any annotation lines.
If the tag is not annotated, the commit message is displayed instead. If no number is given to `-n`, only the first line is printed.
If the tag is not annotated, the commit message is displayed instead.
-l <pattern>:: -l::
--list <pattern>:: --list::
List tags with names that match the given pattern (or all if no List tags. With optional `<pattern>...`, e.g. `git tag --list
pattern is given). Running "git tag" without arguments also 'v-*'`, list only the tags that match the pattern(s).
lists all tags. The pattern is a shell wildcard (i.e., matched +
using fnmatch(3)). Multiple patterns may be given; if any of Running "git tag" without arguments also lists all tags. The pattern
them matches, the tag is shown. is a shell wildcard (i.e., matched using fnmatch(3)). Multiple
patterns may be given; if any of them matches, the tag is shown.
+
This option is implicitly supplied if any other list-like option such
as `--contains` is provided. See the documentation for each of those
options for details.
--sort=<key>:: --sort=<key>::
Sort based on the key given. Prefix `-` to sort in Sort based on the key given. Prefix `-` to sort in
@ -122,10 +129,23 @@ This option is only applicable when listing tags without annotation lines.
--contains [<commit>]:: --contains [<commit>]::
Only list tags which contain the specified commit (HEAD if not Only list tags which contain the specified commit (HEAD if not
specified). specified). Implies `--list`.
--no-contains [<commit>]::
Only list tags which don't contain the specified commit (HEAD if
not specified). Implies `--list`.
--merged [<commit>]::
Only list tags whose commits are reachable from the specified
commit (`HEAD` if not specified), incompatible with `--no-merged`.
--no-merged [<commit>]::
Only list tags whose commits are not reachable from the specified
commit (`HEAD` if not specified), incompatible with `--merged`.
--points-at <object>:: --points-at <object>::
Only list tags of the given object. Only list tags of the given object (HEAD if not
specified). Implies `--list`.
-m <msg>:: -m <msg>::
--message=<msg>:: --message=<msg>::
@ -173,11 +193,6 @@ This option is only applicable when listing tags without annotation lines.
that of linkgit:git-for-each-ref[1]. When unspecified, that of linkgit:git-for-each-ref[1]. When unspecified,
defaults to `%(refname:strip=2)`. defaults to `%(refname:strip=2)`.
--[no-]merged [<commit>]::
Only list tags whose tips are reachable, or not reachable
if `--no-merged` is used, from the specified commit (`HEAD`
if not specified).
CONFIGURATION CONFIGURATION
------------- -------------
By default, 'git tag' in sign-with-default mode (-s) will use your By default, 'git tag' in sign-with-default mode (-s) will use your

View file

@ -562,7 +562,9 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
OPT_SET_INT('r', "remotes", &filter.kind, N_("act on remote-tracking branches"), OPT_SET_INT('r', "remotes", &filter.kind, N_("act on remote-tracking branches"),
FILTER_REFS_REMOTES), FILTER_REFS_REMOTES),
OPT_CONTAINS(&filter.with_commit, N_("print only branches that contain the commit")), OPT_CONTAINS(&filter.with_commit, N_("print only branches that contain the commit")),
OPT_NO_CONTAINS(&filter.no_commit, N_("print only branches that don't contain the commit")),
OPT_WITH(&filter.with_commit, N_("print only branches that contain the commit")), OPT_WITH(&filter.with_commit, N_("print only branches that contain the commit")),
OPT_WITHOUT(&filter.no_commit, N_("print only branches that don't contain the commit")),
OPT__ABBREV(&filter.abbrev), OPT__ABBREV(&filter.abbrev),
OPT_GROUP(N_("Specific git-branch actions:")), OPT_GROUP(N_("Specific git-branch actions:")),
@ -618,7 +620,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
if (!delete && !rename && !edit_description && !new_upstream && !unset_upstream && argc == 0) if (!delete && !rename && !edit_description && !new_upstream && !unset_upstream && argc == 0)
list = 1; list = 1;
if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr) if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr ||
filter.no_commit)
list = 1; list = 1;
if (!!delete + !!rename + !!new_upstream + if (!!delete + !!rename + !!new_upstream +

View file

@ -8,8 +8,8 @@
static char const * const for_each_ref_usage[] = { static char const * const for_each_ref_usage[] = {
N_("git for-each-ref [<options>] [<pattern>]"), N_("git for-each-ref [<options>] [<pattern>]"),
N_("git for-each-ref [--points-at <object>]"), N_("git for-each-ref [--points-at <object>]"),
N_("git for-each-ref [(--merged | --no-merged) [<object>]]"), N_("git for-each-ref [(--merged | --no-merged) [<commit>]]"),
N_("git for-each-ref [--contains [<object>]]"), N_("git for-each-ref [--contains [<commit>]] [--no-contains [<commit>]]"),
NULL NULL
}; };
@ -43,6 +43,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
OPT_MERGED(&filter, N_("print only refs that are merged")), OPT_MERGED(&filter, N_("print only refs that are merged")),
OPT_NO_MERGED(&filter, N_("print only refs that are not merged")), OPT_NO_MERGED(&filter, N_("print only refs that are not merged")),
OPT_CONTAINS(&filter.with_commit, N_("print only refs which contain the commit")), OPT_CONTAINS(&filter.with_commit, N_("print only refs which contain the commit")),
OPT_NO_CONTAINS(&filter.no_commit, N_("print only refs which don't contain the commit")),
OPT_BOOL(0, "ignore-case", &icase, N_("sorting and filtering are case insensitive")), OPT_BOOL(0, "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
OPT_END(), OPT_END(),
}; };

View file

@ -22,7 +22,7 @@
static const char * const git_tag_usage[] = { static const char * const git_tag_usage[] = {
N_("git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] <tagname> [<head>]"), N_("git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] <tagname> [<head>]"),
N_("git tag -d <tagname>..."), N_("git tag -d <tagname>..."),
N_("git tag -l [-n[<num>]] [--contains <commit>] [--points-at <object>]" N_("git tag -l [-n[<num>]] [--contains <commit>] [--no-contains <commit>] [--points-at <object>]"
"\n\t\t[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]"), "\n\t\t[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]"),
N_("git tag -v [--format=<format>] <tagname>..."), N_("git tag -v [--format=<format>] <tagname>..."),
NULL NULL
@ -424,14 +424,17 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
OPT_GROUP(N_("Tag listing options")), OPT_GROUP(N_("Tag listing options")),
OPT_COLUMN(0, "column", &colopts, N_("show tag list in columns")), OPT_COLUMN(0, "column", &colopts, N_("show tag list in columns")),
OPT_CONTAINS(&filter.with_commit, N_("print only tags that contain the commit")), OPT_CONTAINS(&filter.with_commit, N_("print only tags that contain the commit")),
OPT_NO_CONTAINS(&filter.no_commit, N_("print only tags that don't contain the commit")),
OPT_WITH(&filter.with_commit, N_("print only tags that contain the commit")), OPT_WITH(&filter.with_commit, N_("print only tags that contain the commit")),
OPT_WITHOUT(&filter.no_commit, N_("print only tags that don't contain the commit")),
OPT_MERGED(&filter, N_("print only tags that are merged")), OPT_MERGED(&filter, N_("print only tags that are merged")),
OPT_NO_MERGED(&filter, N_("print only tags that are not merged")), OPT_NO_MERGED(&filter, N_("print only tags that are not merged")),
OPT_CALLBACK(0 , "sort", sorting_tail, N_("key"), OPT_CALLBACK(0 , "sort", sorting_tail, N_("key"),
N_("field name to sort on"), &parse_opt_ref_sorting), N_("field name to sort on"), &parse_opt_ref_sorting),
{ {
OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"), OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"),
N_("print only tags of the object"), 0, parse_opt_object_name N_("print only tags of the object"), PARSE_OPT_LASTARG_DEFAULT,
parse_opt_object_name, (intptr_t) "HEAD"
}, },
OPT_STRING( 0 , "format", &format, N_("format"), N_("format to use for the output")), OPT_STRING( 0 , "format", &format, N_("format"), N_("format to use for the output")),
OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")), OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
@ -454,8 +457,14 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
} }
create_tag_object = (opt.sign || annotate || msg.given || msgfile); create_tag_object = (opt.sign || annotate || msg.given || msgfile);
if (argc == 0 && !cmdmode) if (!cmdmode) {
cmdmode = 'l'; if (argc == 0)
cmdmode = 'l';
else if (filter.with_commit || filter.no_commit ||
filter.points_at.nr || filter.merge_commit ||
filter.lines != -1)
cmdmode = 'l';
}
if ((create_tag_object || force) && (cmdmode != 0)) if ((create_tag_object || force) && (cmdmode != 0))
usage_with_options(git_tag_usage, options); usage_with_options(git_tag_usage, options);
@ -485,13 +494,15 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
return ret; return ret;
} }
if (filter.lines != -1) if (filter.lines != -1)
die(_("-n option is only allowed with -l.")); die(_("-n option is only allowed in list mode"));
if (filter.with_commit) if (filter.with_commit)
die(_("--contains option is only allowed with -l.")); die(_("--contains option is only allowed in list mode"));
if (filter.no_commit)
die(_("--no-contains option is only allowed in list mode"));
if (filter.points_at.nr) if (filter.points_at.nr)
die(_("--points-at option is only allowed with -l.")); die(_("--points-at option is only allowed in list mode"));
if (filter.merge_commit) if (filter.merge_commit)
die(_("--merged and --no-merged option are only allowed with -l")); die(_("--merged and --no-merged options are only allowed in list mode"));
if (cmdmode == 'd') if (cmdmode == 'd')
return for_each_tag_name(argv, delete_tag, NULL); return for_each_tag_name(argv, delete_tag, NULL);
if (cmdmode == 'v') { if (cmdmode == 'v') {

View file

@ -1194,7 +1194,7 @@ _git_branch ()
--*) --*)
__gitcomp " __gitcomp "
--color --no-color --verbose --abbrev= --no-abbrev --color --no-color --verbose --abbrev= --no-abbrev
--track --no-track --contains --merged --no-merged --track --no-track --contains --no-contains --merged --no-merged
--set-upstream-to= --edit-description --list --set-upstream-to= --edit-description --list
--unset-upstream --delete --move --remotes --unset-upstream --delete --move --remotes
--column --no-column --sort= --points-at --column --no-column --sort= --points-at
@ -3023,7 +3023,7 @@ _git_tag ()
__gitcomp " __gitcomp "
--list --delete --verify --annotate --message --file --list --delete --verify --annotate --message --file
--sign --cleanup --local-user --force --column --sort= --sign --cleanup --local-user --force --column --sort=
--contains --points-at --merged --no-merged --create-reflog --contains --no-contains --points-at --merged --no-merged --create-reflog
" "
;; ;;
esac esac

View file

@ -258,7 +258,9 @@ extern int parse_opt_passthru_argv(const struct option *, const char *, int);
PARSE_OPT_LASTARG_DEFAULT | flag, \ PARSE_OPT_LASTARG_DEFAULT | flag, \
parse_opt_commits, (intptr_t) "HEAD" \ parse_opt_commits, (intptr_t) "HEAD" \
} }
#define OPT_CONTAINS(v, h) _OPT_CONTAINS_OR_WITH("contains", v, h, 0) #define OPT_CONTAINS(v, h) _OPT_CONTAINS_OR_WITH("contains", v, h, PARSE_OPT_NONEG)
#define OPT_WITH(v, h) _OPT_CONTAINS_OR_WITH("with", v, h, PARSE_OPT_HIDDEN) #define OPT_NO_CONTAINS(v, h) _OPT_CONTAINS_OR_WITH("no-contains", v, h, PARSE_OPT_NONEG)
#define OPT_WITH(v, h) _OPT_CONTAINS_OR_WITH("with", v, h, PARSE_OPT_HIDDEN | PARSE_OPT_NONEG)
#define OPT_WITHOUT(v, h) _OPT_CONTAINS_OR_WITH("without", v, h, PARSE_OPT_HIDDEN | PARSE_OPT_NONEG)
#endif #endif

View file

@ -1487,6 +1487,7 @@ struct ref_filter_cbdata {
struct ref_array *array; struct ref_array *array;
struct ref_filter *filter; struct ref_filter *filter;
struct contains_cache contains_cache; struct contains_cache contains_cache;
struct contains_cache no_contains_cache;
}; };
/* /*
@ -1586,11 +1587,11 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
} }
static int commit_contains(struct ref_filter *filter, struct commit *commit, static int commit_contains(struct ref_filter *filter, struct commit *commit,
struct contains_cache *cache) struct commit_list *list, struct contains_cache *cache)
{ {
if (filter->with_commit_tag_algo) if (filter->with_commit_tag_algo)
return contains_tag_algo(commit, filter->with_commit, cache) == CONTAINS_YES; return contains_tag_algo(commit, list, cache) == CONTAINS_YES;
return is_descendant_of(commit, filter->with_commit); return is_descendant_of(commit, list);
} }
/* /*
@ -1780,13 +1781,17 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
* obtain the commit using the 'oid' available and discard all * obtain the commit using the 'oid' available and discard all
* non-commits early. The actual filtering is done later. * non-commits early. The actual filtering is done later.
*/ */
if (filter->merge_commit || filter->with_commit || filter->verbose) { if (filter->merge_commit || filter->with_commit || filter->no_commit || filter->verbose) {
commit = lookup_commit_reference_gently(oid->hash, 1); commit = lookup_commit_reference_gently(oid->hash, 1);
if (!commit) if (!commit)
return 0; return 0;
/* We perform the filtering for the '--contains' option */ /* We perform the filtering for the '--contains' option... */
if (filter->with_commit && if (filter->with_commit &&
!commit_contains(filter, commit, &ref_cbdata->contains_cache)) !commit_contains(filter, commit, filter->with_commit, &ref_cbdata->contains_cache))
return 0;
/* ...or for the `--no-contains' option */
if (filter->no_commit &&
commit_contains(filter, commit, filter->no_commit, &ref_cbdata->no_contains_cache))
return 0; return 0;
} }
@ -1887,6 +1892,7 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int
filter->kind = type & FILTER_REFS_KIND_MASK; filter->kind = type & FILTER_REFS_KIND_MASK;
init_contains_cache(&ref_cbdata.contains_cache); init_contains_cache(&ref_cbdata.contains_cache);
init_contains_cache(&ref_cbdata.no_contains_cache);
/* Simple per-ref filtering */ /* Simple per-ref filtering */
if (!filter->kind) if (!filter->kind)
@ -1911,6 +1917,7 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int
} }
clear_contains_cache(&ref_cbdata.contains_cache); clear_contains_cache(&ref_cbdata.contains_cache);
clear_contains_cache(&ref_cbdata.no_contains_cache);
/* Filters that need revision walking */ /* Filters that need revision walking */
if (filter->merge_commit) if (filter->merge_commit)
@ -2084,8 +2091,17 @@ int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset)
{ {
struct ref_filter *rf = opt->value; struct ref_filter *rf = opt->value;
unsigned char sha1[20]; unsigned char sha1[20];
int no_merged = starts_with(opt->long_name, "no");
rf->merge = starts_with(opt->long_name, "no") if (rf->merge) {
if (no_merged) {
return opterror(opt, "is incompatible with --merged", 0);
} else {
return opterror(opt, "is incompatible with --no-merged", 0);
}
}
rf->merge = no_merged
? REF_FILTER_MERGED_OMIT ? REF_FILTER_MERGED_OMIT
: REF_FILTER_MERGED_INCLUDE; : REF_FILTER_MERGED_INCLUDE;

View file

@ -53,6 +53,7 @@ struct ref_filter {
const char **name_patterns; const char **name_patterns;
struct sha1_array points_at; struct sha1_array points_at;
struct commit_list *with_commit; struct commit_list *with_commit;
struct commit_list *no_commit;
enum { enum {
REF_FILTER_MERGED_NONE = 0, REF_FILTER_MERGED_NONE = 0,

View file

@ -978,6 +978,10 @@ test_expect_success '--merged catches invalid object names' '
test_must_fail git branch --merged 0000000000000000000000000000000000000000 test_must_fail git branch --merged 0000000000000000000000000000000000000000
' '
test_expect_success '--merged is incompatible with --no-merged' '
test_must_fail git branch --merged HEAD --no-merged HEAD
'
test_expect_success 'tracking with unexpected .fetch refspec' ' test_expect_success 'tracking with unexpected .fetch refspec' '
rm -rf a b c d && rm -rf a b c d &&
git init a && git init a &&

View file

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
test_description='branch --contains <commit>, --merged, and --no-merged' test_description='branch --contains <commit>, --no-contains <commit> --merged, and --no-merged'
. ./test-lib.sh . ./test-lib.sh
@ -45,6 +45,22 @@ test_expect_success 'branch --contains master' '
' '
test_expect_success 'branch --no-contains=master' '
git branch --no-contains=master >actual &&
>expect &&
test_cmp expect actual
'
test_expect_success 'branch --no-contains master' '
git branch --no-contains master >actual &&
>expect &&
test_cmp expect actual
'
test_expect_success 'branch --contains=side' ' test_expect_success 'branch --contains=side' '
git branch --contains=side >actual && git branch --contains=side >actual &&
@ -55,6 +71,16 @@ test_expect_success 'branch --contains=side' '
' '
test_expect_success 'branch --no-contains=side' '
git branch --no-contains=side >actual &&
{
echo " master"
} >expect &&
test_cmp expect actual
'
test_expect_success 'branch --contains with pattern implies --list' ' test_expect_success 'branch --contains with pattern implies --list' '
git branch --contains=master master >actual && git branch --contains=master master >actual &&
@ -65,6 +91,14 @@ test_expect_success 'branch --contains with pattern implies --list' '
' '
test_expect_success 'branch --no-contains with pattern implies --list' '
git branch --no-contains=master master >actual &&
>expect &&
test_cmp expect actual
'
test_expect_success 'side: branch --merged' ' test_expect_success 'side: branch --merged' '
git branch --merged >actual && git branch --merged >actual &&
@ -126,10 +160,22 @@ test_expect_success 'branch --no-merged with pattern implies --list' '
test_expect_success 'implicit --list conflicts with modification options' ' test_expect_success 'implicit --list conflicts with modification options' '
test_must_fail git branch --contains=master -d && test_must_fail git branch --contains=master -d &&
test_must_fail git branch --contains=master -m foo test_must_fail git branch --contains=master -m foo &&
test_must_fail git branch --no-contains=master -d &&
test_must_fail git branch --no-contains=master -m foo
' '
test_expect_success 'Assert that --contains only works on commits, not trees & blobs' '
test_must_fail git branch --contains master^{tree} &&
blob=$(git hash-object -w --stdin <<-\EOF
Some blob
EOF
) &&
test_must_fail git branch --contains $blob &&
test_must_fail git branch --no-contains $blob
'
# We want to set up a case where the walk for the tracking info # We want to set up a case where the walk for the tracking info
# of one branch crosses the tip of another branch (and make sure # of one branch crosses the tip of another branch (and make sure
# that the latter walk does not mess up our flag to see if it was # that the latter walk does not mess up our flag to see if it was
@ -159,4 +205,15 @@ test_expect_success 'branch --merged with --verbose' '
test_i18ncmp expect actual test_i18ncmp expect actual
' '
test_expect_success 'branch --contains combined with --no-contains' '
git branch --contains zzz --no-contains topic >actual &&
cat >expect <<-\EOF &&
master
side
zzz
EOF
test_cmp expect actual
'
test_done test_done

View file

@ -93,6 +93,22 @@ test_expect_success 'filtering with --contains' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success 'filtering with --no-contains' '
cat >expect <<-\EOF &&
refs/tags/one
EOF
git for-each-ref --format="%(refname)" --no-contains=two >actual &&
test_cmp expect actual
'
test_expect_success 'filtering with --contains and --no-contains' '
cat >expect <<-\EOF &&
refs/tags/two
EOF
git for-each-ref --format="%(refname)" --contains=two --no-contains=three >actual &&
test_cmp expect actual
'
test_expect_success '%(color) must fail' ' test_expect_success '%(color) must fail' '
test_must_fail git for-each-ref --format="%(color)%(refname)" test_must_fail git for-each-ref --format="%(color)%(refname)"
' '
@ -421,4 +437,8 @@ test_expect_success 'check %(if:notequals=<string>)' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success '--merged is incompatible with --no-merged' '
test_must_fail git for-each-ref --merged HEAD --no-merged HEAD
'
test_done test_done

View file

@ -16,7 +16,6 @@ tag_exists () {
git show-ref --quiet --verify refs/tags/"$1" git show-ref --quiet --verify refs/tags/"$1"
} }
# todo: git tag -l now returns always zero, when fixed, change this test
test_expect_success 'listing all tags in an empty tree should succeed' ' test_expect_success 'listing all tags in an empty tree should succeed' '
git tag -l && git tag -l &&
git tag git tag
@ -119,6 +118,18 @@ test_expect_success 'listing all tags if one exists should succeed' '
git tag git tag
' '
cat >expect <<EOF
mytag
EOF
test_expect_success 'Multiple -l or --list options are equivalent to one -l option' '
git tag -l -l >actual &&
test_cmp expect actual &&
git tag --list --list >actual &&
test_cmp expect actual &&
git tag --list -l --list >actual &&
test_cmp expect actual
'
test_expect_success 'listing all tags if one exists should output that tag' ' test_expect_success 'listing all tags if one exists should output that tag' '
test $(git tag -l) = mytag && test $(git tag -l) = mytag &&
test $(git tag) = mytag test $(git tag) = mytag
@ -136,9 +147,8 @@ test_expect_success \
'listing a tag using a matching pattern should output that tag' \ 'listing a tag using a matching pattern should output that tag' \
'test $(git tag -l mytag) = mytag' 'test $(git tag -l mytag) = mytag'
# todo: git tag -l now returns always zero, when fixed, change this test
test_expect_success \ test_expect_success \
'listing tags using a non-matching pattern should suceed' \ 'listing tags using a non-matching pattern should succeed' \
'git tag -l xxx' 'git tag -l xxx'
test_expect_success \ test_expect_success \
@ -338,6 +348,19 @@ test_expect_success 'tag -l can accept multiple patterns' '
test_cmp expect actual test_cmp expect actual
' '
# Between v1.7.7 & v2.13.0 a fair reading of the git-tag documentation
# could leave you with the impression that "-l <pattern> -l <pattern>"
# was how we wanted to accept multiple patterns.
#
# This test should not imply that this is a sane thing to support. but
# since the documentation was worded like it was let's at least find
# out if we're going to break this long-documented form of taking
# multiple patterns.
test_expect_success 'tag -l <pattern> -l <pattern> works, as our buggy documentation previously suggested' '
git tag -l "v1*" -l "v0*" >actual &&
test_cmp expect actual
'
test_expect_success 'listing tags in column' ' test_expect_success 'listing tags in column' '
COLUMNS=40 git tag -l --column=row >actual && COLUMNS=40 git tag -l --column=row >actual &&
cat >expected <<\EOF && cat >expected <<\EOF &&
@ -620,6 +643,11 @@ test_expect_success \
git tag -n0 -l tag-one-line >actual && git tag -n0 -l tag-one-line >actual &&
test_cmp expect actual && test_cmp expect actual &&
git tag -n0 | grep "^tag-one-line" >actual &&
test_cmp expect actual &&
git tag -n0 tag-one-line >actual &&
test_cmp expect actual &&
echo "tag-one-line A msg" >expect && echo "tag-one-line A msg" >expect &&
git tag -n1 -l | grep "^tag-one-line" >actual && git tag -n1 -l | grep "^tag-one-line" >actual &&
test_cmp expect actual && test_cmp expect actual &&
@ -633,6 +661,17 @@ test_expect_success \
test_cmp expect actual test_cmp expect actual
' '
test_expect_success 'The -n 100 invocation means -n --list 100, not -n100' '
>expect &&
git tag -n 100 >actual &&
test_cmp expect actual &&
git tag -m "A msg" 100 &&
echo "100 A msg" >expect &&
git tag -n 100 >actual &&
test_cmp expect actual
'
test_expect_success \ test_expect_success \
'listing the zero-lines message of a non-signed tag should succeed' ' 'listing the zero-lines message of a non-signed tag should succeed' '
git tag -m "" tag-zero-lines && git tag -m "" tag-zero-lines &&
@ -1383,6 +1422,23 @@ test_expect_success 'checking that first commit is in all tags (relative)' "
test_cmp expected actual test_cmp expected actual
" "
# All the --contains tests above, but with --no-contains
test_expect_success 'checking that first commit is not listed in any tag with --no-contains (hash)' "
>expected &&
git tag -l --no-contains $hash1 v* >actual &&
test_cmp expected actual
"
test_expect_success 'checking that first commit is in all tags (tag)' "
git tag -l --no-contains v1.0 v* >actual &&
test_cmp expected actual
"
test_expect_success 'checking that first commit is in all tags (relative)' "
git tag -l --no-contains HEAD~2 v* >actual &&
test_cmp expected actual
"
cat > expected <<EOF cat > expected <<EOF
v2.0 v2.0
EOF EOF
@ -1392,6 +1448,17 @@ test_expect_success 'checking that second commit only has one tag' "
test_cmp expected actual test_cmp expected actual
" "
cat > expected <<EOF
v0.2.1
v1.0
v1.0.1
v1.1.3
EOF
test_expect_success 'inverse of the last test, with --no-contains' "
git tag -l --no-contains $hash2 v* >actual &&
test_cmp expected actual
"
cat > expected <<EOF cat > expected <<EOF
EOF EOF
@ -1401,6 +1468,19 @@ test_expect_success 'checking that third commit has no tags' "
test_cmp expected actual test_cmp expected actual
" "
cat > expected <<EOF
v0.2.1
v1.0
v1.0.1
v1.1.3
v2.0
EOF
test_expect_success 'conversely --no-contains on the third commit lists all tags' "
git tag -l --no-contains $hash3 v* >actual &&
test_cmp expected actual
"
# how about a simple merge? # how about a simple merge?
test_expect_success 'creating simple branch' ' test_expect_success 'creating simple branch' '
@ -1422,6 +1502,19 @@ test_expect_success 'checking that branch head only has one tag' "
test_cmp expected actual test_cmp expected actual
" "
cat > expected <<EOF
v0.2.1
v1.0
v1.0.1
v1.1.3
v2.0
EOF
test_expect_success 'checking that branch head with --no-contains lists all but one tag' "
git tag -l --no-contains $hash4 v* >actual &&
test_cmp expected actual
"
test_expect_success 'merging original branch into this branch' ' test_expect_success 'merging original branch into this branch' '
git merge --strategy=ours master && git merge --strategy=ours master &&
git tag v4.0 git tag v4.0
@ -1436,6 +1529,20 @@ test_expect_success 'checking that original branch head has one tag now' "
test_cmp expected actual test_cmp expected actual
" "
cat > expected <<EOF
v0.2.1
v1.0
v1.0.1
v1.1.3
v2.0
v3.0
EOF
test_expect_success 'checking that original branch head with --no-contains lists all but one tag now' "
git tag -l --no-contains $hash3 v* >actual &&
test_cmp expected actual
"
cat > expected <<EOF cat > expected <<EOF
v0.2.1 v0.2.1
v1.0 v1.0
@ -1451,21 +1558,76 @@ test_expect_success 'checking that initial commit is in all tags' "
test_cmp expected actual test_cmp expected actual
" "
test_expect_success 'checking that --contains can be used in non-list mode' '
git tag --contains $hash1 v* >actual &&
test_cmp expected actual
'
test_expect_success 'checking that initial commit is in all tags with --no-contains' "
>expected &&
git tag -l --no-contains $hash1 v* >actual &&
test_cmp expected actual
"
# mixing modes and options: # mixing modes and options:
test_expect_success 'mixing incompatibles modes and options is forbidden' ' test_expect_success 'mixing incompatibles modes and options is forbidden' '
test_must_fail git tag -a && test_must_fail git tag -a &&
test_must_fail git tag -a -l &&
test_must_fail git tag -s &&
test_must_fail git tag -s -l &&
test_must_fail git tag -m &&
test_must_fail git tag -m -l &&
test_must_fail git tag -m "hlagh" &&
test_must_fail git tag -m "hlagh" -l &&
test_must_fail git tag -F &&
test_must_fail git tag -F -l &&
test_must_fail git tag -f &&
test_must_fail git tag -f -l &&
test_must_fail git tag -a -s -m -F &&
test_must_fail git tag -a -s -m -F -l &&
test_must_fail git tag -l -v && test_must_fail git tag -l -v &&
test_must_fail git tag -n 100 && test_must_fail git tag -l -d &&
test_must_fail git tag -l -v -d &&
test_must_fail git tag -n 100 -v &&
test_must_fail git tag -l -m msg && test_must_fail git tag -l -m msg &&
test_must_fail git tag -l -F some file && test_must_fail git tag -l -F some file &&
test_must_fail git tag -v -s test_must_fail git tag -v -s &&
test_must_fail git tag --contains tag-tree &&
test_must_fail git tag --contains tag-blob &&
test_must_fail git tag --no-contains tag-tree &&
test_must_fail git tag --no-contains tag-blob &&
test_must_fail git tag --contains --no-contains &&
test_must_fail git tag --no-with HEAD &&
test_must_fail git tag --no-without HEAD
' '
for option in --contains --with --no-contains --without --merged --no-merged --points-at
do
test_expect_success "mixing incompatible modes with $option is forbidden" "
test_must_fail git tag -d $option HEAD &&
test_must_fail git tag -d $option HEAD some-tag &&
test_must_fail git tag -v $option HEAD
"
test_expect_success "Doing 'git tag --list-like $option <commit> <pattern> is permitted" "
git tag -n $option HEAD HEAD &&
git tag $option HEAD HEAD &&
git tag $option
"
done
# check points-at # check points-at
test_expect_success '--points-at cannot be used in non-list mode' ' test_expect_success '--points-at can be used in non-list mode' '
test_must_fail git tag --points-at=v4.0 foo echo v4.0 >expect &&
git tag --points-at=v4.0 "v*" >actual &&
test_cmp expect actual
'
test_expect_success '--points-at is a synonym for --points-at HEAD' '
echo v4.0 >expect &&
git tag --points-at >actual &&
test_cmp expect actual
' '
test_expect_success '--points-at finds lightweight tags' ' test_expect_success '--points-at finds lightweight tags' '
@ -1707,7 +1869,7 @@ run_with_limited_stack () {
test_lazy_prereq ULIMIT_STACK_SIZE 'run_with_limited_stack true' test_lazy_prereq ULIMIT_STACK_SIZE 'run_with_limited_stack true'
# we require ulimit, this excludes Windows # we require ulimit, this excludes Windows
test_expect_success ULIMIT_STACK_SIZE '--contains works in a deep repo' ' test_expect_success ULIMIT_STACK_SIZE '--contains and --no-contains work in a deep repo' '
>expect && >expect &&
i=1 && i=1 &&
while test $i -lt 8000 while test $i -lt 8000
@ -1723,7 +1885,9 @@ EOF"
git checkout master && git checkout master &&
git tag far-far-away HEAD^ && git tag far-far-away HEAD^ &&
run_with_limited_stack git tag --contains HEAD >actual && run_with_limited_stack git tag --contains HEAD >actual &&
test_cmp expect actual test_cmp expect actual &&
run_with_limited_stack git tag --no-contains HEAD >actual &&
test_line_count ">" 10 actual
' '
test_expect_success '--format should list tags as per format given' ' test_expect_success '--format should list tags as per format given' '
@ -1742,8 +1906,17 @@ test_expect_success 'setup --merged test tags' '
git tag mergetest-3 HEAD git tag mergetest-3 HEAD
' '
test_expect_success '--merged cannot be used in non-list mode' ' test_expect_success '--merged can be used in non-list mode' '
test_must_fail git tag --merged=mergetest-2 foo cat >expect <<-\EOF &&
mergetest-1
mergetest-2
EOF
git tag --merged=mergetest-2 "mergetest*" >actual &&
test_cmp expect actual
'
test_expect_success '--merged is incompatible with --no-merged' '
test_must_fail git tag --merged HEAD --no-merged HEAD
' '
test_expect_success '--merged shows merged tags' ' test_expect_success '--merged shows merged tags' '
@ -1763,6 +1936,11 @@ test_expect_success '--no-merged show unmerged tags' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success '--no-merged can be used in non-list mode' '
git tag --no-merged=mergetest-2 mergetest-* >actual &&
test_cmp expect actual
'
test_expect_success 'ambiguous branch/tags not marked' ' test_expect_success 'ambiguous branch/tags not marked' '
git tag ambiguous && git tag ambiguous &&
git branch ambiguous && git branch ambiguous &&
@ -1771,4 +1949,47 @@ test_expect_success 'ambiguous branch/tags not marked' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success '--contains combined with --no-contains' '
(
git init no-contains &&
cd no-contains &&
test_commit v0.1 &&
test_commit v0.2 &&
test_commit v0.3 &&
test_commit v0.4 &&
test_commit v0.5 &&
cat >expected <<-\EOF &&
v0.2
v0.3
v0.4
EOF
git tag --contains v0.2 --no-contains v0.5 >actual &&
test_cmp expected actual
)
'
# As the docs say, list tags which contain a specified *commit*. We
# don't recurse down to tags for trees or blobs pointed to by *those*
# commits.
test_expect_success 'Does --[no-]contains stop at commits? Yes!' '
cd no-contains &&
blob=$(git rev-parse v0.3:v0.3.t) &&
tree=$(git rev-parse v0.3^{tree}) &&
git tag tag-blob $blob &&
git tag tag-tree $tree &&
git tag --contains v0.3 >actual &&
cat >expected <<-\EOF &&
v0.3
v0.4
v0.5
EOF
test_cmp expected actual &&
git tag --no-contains v0.3 >actual &&
cat >expected <<-\EOF &&
v0.1
v0.2
EOF
test_cmp expected actual
'
test_done test_done