Merge branch 'jk/describe-omit-some-refs'

"git describe" and "git name-rev" have been taught to take more
than one refname patterns to restrict the set of refs to base their
naming output on, and also learned to take negative patterns to
name refs not to be used for naming via their "--exclude" option.

* jk/describe-omit-some-refs:
  describe: teach describe negative pattern matches
  describe: teach --match to accept multiple patterns
  name-rev: add support to exclude refs by pattern match
  name-rev: extend --refs to accept multiple patterns
  doc: add documentation for OPT_STRING_LIST
This commit is contained in:
Junio C Hamano 2017-02-27 13:57:11 -08:00
commit 1b324988ac
8 changed files with 187 additions and 21 deletions

View File

@ -83,7 +83,20 @@ OPTIONS
--match <pattern>::
Only consider tags matching the given `glob(7)` pattern,
excluding the "refs/tags/" prefix. This can be used to avoid
leaking private tags from the repository.
leaking private tags from the repository. If given multiple times, a
list of patterns will be accumulated, and tags matching any of the
patterns will be considered. Use `--no-match` to clear and reset the
list of patterns.
--exclude <pattern>::
Do not consider tags matching the given `glob(7)` pattern, excluding
the "refs/tags/" prefix. This can be used to narrow the tag space and
find only tags matching some meaningful criteria. If given multiple
times, a list of patterns will be accumulated and tags matching any
of the patterns will be excluded. When combined with --match a tag will
be considered when it matches at least one --match pattern and does not
match any of the --exclude patterns. Use `--no-exclude` to clear and
reset the list of patterns.
--always::
Show uniquely abbreviated commit object as fallback.

View File

@ -26,7 +26,18 @@ OPTIONS
--refs=<pattern>::
Only use refs whose names match a given shell pattern. The pattern
can be one of branch name, tag name or fully qualified ref name.
can be one of branch name, tag name or fully qualified ref name. If
given multiple times, use refs whose names match any of the given shell
patterns. Use `--no-refs` to clear any previous ref patterns given.
--exclude=<pattern>::
Do not use any ref whose name matches a given shell pattern. The
pattern can be one of branch name, tag name or fully qualified ref
name. If given multiple times, a ref will be excluded when it matches
any of the given patterns. When used together with --refs, a ref will
be used as a match only when it matches at least one --refs pattern and
does not match any --exclude patterns. Use `--no-exclude` to clear the
list of exclude patterns.
--all::
List all commits reachable from all refs

View File

@ -168,6 +168,11 @@ There are some macros to easily define options:
Introduce an option with string argument.
The string argument is put into `str_var`.
`OPT_STRING_LIST(short, long, &struct string_list, arg_str, description)`::
Introduce an option with string argument.
The string argument is stored as an element in `string_list`.
Use of `--no-option` will clear the list of preceding values.
`OPT_INTEGER(short, long, &int_var, description)`::
Introduce an option with integer argument.
The integer is put into `int_var`.

View File

@ -28,7 +28,8 @@ static int abbrev = -1; /* unspecified */
static int max_candidates = 10;
static struct hashmap names;
static int have_util;
static const char *pattern;
static struct string_list patterns = STRING_LIST_INIT_NODUP;
static struct string_list exclude_patterns = STRING_LIST_INIT_NODUP;
static int always;
static const char *dirty;
@ -129,9 +130,40 @@ static int get_name(const char *path, const struct object_id *oid, int flag, voi
if (!all && !is_tag)
return 0;
/* Accept only tags that match the pattern, if given */
if (pattern && (!is_tag || wildmatch(pattern, path + 10, 0, NULL)))
return 0;
/*
* If we're given exclude patterns, first exclude any tag which match
* any of the exclude pattern.
*/
if (exclude_patterns.nr) {
struct string_list_item *item;
if (!is_tag)
return 0;
for_each_string_list_item(item, &exclude_patterns) {
if (!wildmatch(item->string, path + 10, 0, NULL))
return 0;
}
}
/*
* If we're given patterns, accept only tags which match at least one
* pattern.
*/
if (patterns.nr) {
struct string_list_item *item;
if (!is_tag)
return 0;
for_each_string_list_item(item, &patterns) {
if (!wildmatch(item->string, path + 10, 0, NULL))
break;
/* If we get here, no pattern matched. */
return 0;
}
}
/* Is it annotated? */
if (!peel_ref(path, peeled.hash)) {
@ -404,8 +436,10 @@ int cmd_describe(int argc, const char **argv, const char *prefix)
N_("only output exact matches"), 0),
OPT_INTEGER(0, "candidates", &max_candidates,
N_("consider <n> most recent tags (default: 10)")),
OPT_STRING(0, "match", &pattern, N_("pattern"),
OPT_STRING_LIST(0, "match", &patterns, N_("pattern"),
N_("only consider tags matching <pattern>")),
OPT_STRING_LIST(0, "exclude", &exclude_patterns, N_("pattern"),
N_("do not consider tags matching <pattern>")),
OPT_BOOL(0, "always", &always,
N_("show abbreviated commit object as fallback")),
{OPTION_STRING, 0, "dirty", &dirty, N_("mark"),
@ -430,6 +464,7 @@ int cmd_describe(int argc, const char **argv, const char *prefix)
die(_("--long is incompatible with --abbrev=0"));
if (contains) {
struct string_list_item *item;
struct argv_array args;
argv_array_init(&args);
@ -440,8 +475,10 @@ int cmd_describe(int argc, const char **argv, const char *prefix)
argv_array_push(&args, "--always");
if (!all) {
argv_array_push(&args, "--tags");
if (pattern)
argv_array_pushf(&args, "--refs=refs/tags/%s", pattern);
for_each_string_list_item(item, &patterns)
argv_array_pushf(&args, "--refs=refs/tags/%s", item->string);
for_each_string_list_item(item, &exclude_patterns)
argv_array_pushf(&args, "--exclude=refs/tags/%s", item->string);
}
if (argc)
argv_array_pushv(&args, argv);

View File

@ -108,7 +108,8 @@ static const char *name_ref_abbrev(const char *refname, int shorten_unambiguous)
struct name_ref_data {
int tags_only;
int name_only;
const char *ref_filter;
struct string_list ref_filters;
struct string_list exclude_filters;
};
static struct tip_table {
@ -150,18 +151,49 @@ static int name_ref(const char *path, const struct object_id *oid, int flags, vo
if (data->tags_only && !starts_with(path, "refs/tags/"))
return 0;
if (data->ref_filter) {
switch (subpath_matches(path, data->ref_filter)) {
case -1: /* did not match */
return 0;
case 0: /* matched fully */
break;
default: /* matched subpath */
can_abbreviate_output = 1;
break;
if (data->exclude_filters.nr) {
struct string_list_item *item;
for_each_string_list_item(item, &data->exclude_filters) {
if (subpath_matches(path, item->string) >= 0)
return 0;
}
}
if (data->ref_filters.nr) {
struct string_list_item *item;
int matched = 0;
/* See if any of the patterns match. */
for_each_string_list_item(item, &data->ref_filters) {
/*
* Check all patterns even after finding a match, so
* that we can see if a match with a subpath exists.
* When a user asked for 'refs/tags/v*' and 'v1.*',
* both of which match, the user is showing her
* willingness to accept a shortened output by having
* the 'v1.*' in the acceptable refnames, so we
* shouldn't stop when seeing 'refs/tags/v1.4' matches
* 'refs/tags/v*'. We should show it as 'v1.4'.
*/
switch (subpath_matches(path, item->string)) {
case -1: /* did not match */
break;
case 0: /* matched fully */
matched = 1;
break;
default: /* matched subpath */
matched = 1;
can_abbreviate_output = 1;
break;
}
}
/* If none of the patterns matched, stop now */
if (!matched)
return 0;
}
add_to_tip_table(oid->hash, path, can_abbreviate_output);
while (o && o->type == OBJ_TAG) {
@ -306,12 +338,14 @@ int cmd_name_rev(int argc, const char **argv, const char *prefix)
{
struct object_array revs = OBJECT_ARRAY_INIT;
int all = 0, transform_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
struct name_ref_data data = { 0, 0, NULL };
struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
struct option opts[] = {
OPT_BOOL(0, "name-only", &data.name_only, N_("print only names (no SHA-1)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
OPT_STRING(0, "refs", &data.ref_filter, N_("pattern"),
OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
N_("only use refs matching <pattern>")),
OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
N_("ignore refs matching <pattern>")),
OPT_GROUP(""),
OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")),
OPT_BOOL(0, "stdin", &transform_stdin, N_("read from stdin")),

View File

@ -1207,6 +1207,7 @@ _git_describe ()
__gitcomp "
--all --tags --contains --abbrev= --candidates=
--exact-match --debug --long --match --always --first-parent
--exclude
"
return
esac

View File

@ -99,6 +99,44 @@ test_expect_success '--cherry-pick bar does not come up empty (II)' '
test_cmp actual.named expect
'
test_expect_success 'name-rev multiple --refs combine inclusive' '
git rev-list --left-right --cherry-pick F...E -- bar >actual &&
git name-rev --stdin --name-only --refs="*tags/F" --refs="*tags/E" \
<actual >actual.named &&
test_cmp actual.named expect
'
cat >expect <<EOF
<tags/F
EOF
test_expect_success 'name-rev --refs excludes non-matched patterns' '
git rev-list --left-right --right-only --cherry-pick F...E -- bar >>expect &&
git rev-list --left-right --cherry-pick F...E -- bar >actual &&
git name-rev --stdin --name-only --refs="*tags/F" \
<actual >actual.named &&
test_cmp actual.named expect
'
cat >expect <<EOF
<tags/F
EOF
test_expect_success 'name-rev --exclude excludes matched patterns' '
git rev-list --left-right --right-only --cherry-pick F...E -- bar >>expect &&
git rev-list --left-right --cherry-pick F...E -- bar >actual &&
git name-rev --stdin --name-only --refs="*tags/*" --exclude="*E" \
<actual >actual.named &&
test_cmp actual.named expect
'
test_expect_success 'name-rev --no-refs clears the refs list' '
git rev-list --left-right --cherry-pick F...E -- bar >expect &&
git name-rev --stdin --name-only --refs="*tags/F" --refs="*tags/E" --no-refs --refs="*tags/G" \
<expect >actual &&
test_cmp actual expect
'
cat >expect <<EOF
+tags/F
=tags/D

View File

@ -182,6 +182,10 @@ check_describe "test2-lightweight-*" --tags --match="test2-*"
check_describe "test2-lightweight-*" --long --tags --match="test2-*" HEAD^
check_describe "test1-lightweight-*" --long --tags --match="test1-*" --match="test2-*" HEAD^
check_describe "test2-lightweight-*" --long --tags --match="test1-*" --no-match --match="test2-*" HEAD^
test_expect_success 'name-rev with exact tags' '
echo A >expect &&
tag_object=$(git rev-parse refs/tags/A) &&
@ -206,4 +210,27 @@ test_expect_success 'describe --contains with the exact tags' '
test_cmp expect actual
'
test_expect_success 'describe --contains and --match' '
echo "A^0" >expect &&
tagged_commit=$(git rev-parse "refs/tags/A^0") &&
test_must_fail git describe --contains --match="B" $tagged_commit &&
git describe --contains --match="B" --match="A" $tagged_commit >actual &&
test_cmp expect actual
'
test_expect_success 'describe --exclude' '
echo "c~1" >expect &&
tagged_commit=$(git rev-parse "refs/tags/A^0") &&
test_must_fail git describe --contains --match="B" $tagged_commit &&
git describe --contains --match="?" --exclude="A" $tagged_commit >actual &&
test_cmp expect actual
'
test_expect_success 'describe --contains and --no-match' '
echo "A^0" >expect &&
tagged_commit=$(git rev-parse "refs/tags/A^0") &&
git describe --contains --match="B" --no-match $tagged_commit >actual &&
test_cmp expect actual
'
test_done