diff --git a/Documentation/git-for-each-ref.txt b/Documentation/git-for-each-ref.txt index 7f8d9a5b5f..e49d5782fc 100644 --- a/Documentation/git-for-each-ref.txt +++ b/Documentation/git-for-each-ref.txt @@ -10,6 +10,8 @@ SYNOPSIS [verse] 'git for-each-ref' [--count=] [--shell|--perl|--python|--tcl] [(--sort=)...] [--format=] [...] + [--points-at ] [(--merged | --no-merged) []] + [--contains []] DESCRIPTION ----------- @@ -62,6 +64,20 @@ OPTIONS the specified host language. This is meant to produce a scriptlet that can directly be `eval`ed. +--points-at :: + Only list refs which points at the given object. + +--merged []:: + Only list refs whose tips are reachable from the + specified commit (HEAD if not specified). + +--no-merged []:: + Only list refs whose tips are not reachable from the + specified commit (HEAD if not specified). + +--contains []:: + Only list tags which contain the specified commit (HEAD if not + specified). FIELD NAMES ----------- diff --git a/builtin/branch.c b/builtin/branch.c index 58aa84f1e8..4fc8beb23c 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -636,6 +636,10 @@ static int print_ref_list(int kinds, int detached, int verbose, int abbrev, stru cb.pattern = pattern; cb.ret = 0; for_each_rawref(append_ref, &cb); + /* + * The following implementation is currently duplicated in ref-filter. It + * will eventually be removed when we port branch.c to use ref-filter APIs. + */ if (merge_filter != NO_FILTER) { struct commit *filter; filter = lookup_commit_reference_gently(merge_filter_ref, 0); @@ -746,6 +750,10 @@ static void rename_branch(const char *oldname, const char *newname, int force) strbuf_release(&newsection); } +/* + * This function is duplicated in ref-filter. It will eventually be removed + * when we port branch.c to use ref-filter APIs. + */ static int opt_parse_merge_filter(const struct option *opt, const char *arg, int unset) { merge_filter = ((opt->long_name[0] == 'n') @@ -821,18 +829,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix) OPT__COLOR(&branch_use_color, N_("use colored output")), OPT_SET_INT('r', "remotes", &kinds, N_("act on remote-tracking branches"), REF_REMOTE_BRANCH), - { - OPTION_CALLBACK, 0, "contains", &with_commit, N_("commit"), - N_("print only branches that contain the commit"), - PARSE_OPT_LASTARG_DEFAULT, - parse_opt_with_commit, (intptr_t)"HEAD", - }, - { - OPTION_CALLBACK, 0, "with", &with_commit, N_("commit"), - N_("print only branches that contain the commit"), - PARSE_OPT_HIDDEN | PARSE_OPT_LASTARG_DEFAULT, - parse_opt_with_commit, (intptr_t) "HEAD", - }, + OPT_CONTAINS(&with_commit, N_("print only branches that contain the commit")), + OPT_WITH(&with_commit, N_("print only branches that contain the commit")), OPT__ABBREV(&abbrev), OPT_GROUP(N_("Specific git-branch actions:")), diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c index 7919206187..40f343b6a3 100644 --- a/builtin/for-each-ref.c +++ b/builtin/for-each-ref.c @@ -7,6 +7,9 @@ static char const * const for_each_ref_usage[] = { N_("git for-each-ref [] []"), + N_("git for-each-ref [--points-at ]"), + N_("git for-each-ref [(--merged | --no-merged) []]"), + N_("git for-each-ref [--contains []]"), NULL }; @@ -34,9 +37,18 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix) OPT_STRING( 0 , "format", &format, N_("format"), N_("format to use for the output")), OPT_CALLBACK(0 , "sort", sorting_tail, N_("key"), N_("field name to sort on"), &parse_opt_ref_sorting), + OPT_CALLBACK(0, "points-at", &filter.points_at, + N_("object"), N_("print only refs which points at the given object"), + parse_opt_object_name), + OPT_MERGED(&filter, N_("print only refs that are 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_END(), }; + memset(&array, 0, sizeof(array)); + memset(&filter, 0, sizeof(filter)); + parse_options(argc, argv, prefix, opts, for_each_ref_usage, 0); if (maxcount < 0) { error("invalid --count argument: `%d'", maxcount); @@ -55,8 +67,6 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix) /* for warn_ambiguous_refs */ git_config(git_default_config, NULL); - memset(&array, 0, sizeof(array)); - memset(&filter, 0, sizeof(filter)); filter.name_patterns = argv; filter_refs(&array, &filter, FILTER_REFS_ALL | FILTER_REFS_INCLUDE_BROKEN); ref_array_sort(sorting, &array); diff --git a/builtin/tag.c b/builtin/tag.c index cccca99104..471d6b1ab8 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -56,6 +56,10 @@ static int match_pattern(const char **patterns, const char *ref) return 0; } +/* + * This is currently duplicated in ref-filter.c, and will eventually be + * removed as we port tag.c to use the ref-filter APIs. + */ static const unsigned char *match_points_at(const char *refname, const unsigned char *sha1) { @@ -82,6 +86,11 @@ static int in_commit_list(const struct commit_list *want, struct commit *c) return 0; } +/* + * The entire code segment for supporting the --contains option has been + * copied over to ref-filter.{c,h}. This will be deleted evetually when + * we port tag.c to use ref-filter APIs. + */ enum contains_result { CONTAINS_UNKNOWN = -1, CONTAINS_NO = 0, @@ -546,23 +555,6 @@ static int strbuf_check_tag_ref(struct strbuf *sb, const char *name) return check_refname_format(sb->buf, 0); } -static int parse_opt_points_at(const struct option *opt __attribute__((unused)), - const char *arg, int unset) -{ - unsigned char sha1[20]; - - if (unset) { - sha1_array_clear(&points_at); - return 0; - } - if (!arg) - return error(_("switch 'points-at' requires an object")); - if (get_sha1(arg, sha1)) - return error(_("malformed object name '%s'"), arg); - sha1_array_append(&points_at, sha1); - return 0; -} - static int parse_opt_sort(const struct option *opt, const char *arg, int unset) { int *sort = opt->value; @@ -610,25 +602,15 @@ int cmd_tag(int argc, const char **argv, const char *prefix) OPT_GROUP(N_("Tag listing options")), OPT_COLUMN(0, "column", &colopts, N_("show tag list in columns")), + OPT_CONTAINS(&with_commit, N_("print only tags that contain the commit")), + OPT_WITH(&with_commit, N_("print only tags that contain the commit")), { OPTION_CALLBACK, 0, "sort", &tag_sort, N_("type"), N_("sort tags"), PARSE_OPT_NONEG, parse_opt_sort }, { - OPTION_CALLBACK, 0, "contains", &with_commit, N_("commit"), - N_("print only tags that contain the commit"), - PARSE_OPT_LASTARG_DEFAULT, - parse_opt_with_commit, (intptr_t)"HEAD", - }, - { - OPTION_CALLBACK, 0, "with", &with_commit, N_("commit"), - N_("print only tags that contain the commit"), - PARSE_OPT_HIDDEN | PARSE_OPT_LASTARG_DEFAULT, - parse_opt_with_commit, (intptr_t)"HEAD", - }, - { - OPTION_CALLBACK, 0, "points-at", NULL, N_("object"), - N_("print only tags of the object"), 0, parse_opt_points_at + OPTION_CALLBACK, 0, "points-at", &points_at, N_("object"), + N_("print only tags of the object"), 0, parse_opt_object_name }, OPT_END() }; diff --git a/parse-options-cb.c b/parse-options-cb.c index 5ab6ed6b08..239898d946 100644 --- a/parse-options-cb.c +++ b/parse-options-cb.c @@ -5,6 +5,7 @@ #include "color.h" #include "string-list.h" #include "argv-array.h" +#include "sha1-array.h" /*----- some often used options -----*/ @@ -77,7 +78,7 @@ int parse_opt_verbosity_cb(const struct option *opt, const char *arg, return 0; } -int parse_opt_with_commit(const struct option *opt, const char *arg, int unset) +int parse_opt_commits(const struct option *opt, const char *arg, int unset) { unsigned char sha1[20]; struct commit *commit; @@ -93,6 +94,22 @@ int parse_opt_with_commit(const struct option *opt, const char *arg, int unset) return 0; } +int parse_opt_object_name(const struct option *opt, const char *arg, int unset) +{ + unsigned char sha1[20]; + + if (unset) { + sha1_array_clear(opt->value); + return 0; + } + if (!arg) + return -1; + if (get_sha1(arg, sha1)) + return error(_("malformed object name '%s'"), arg); + sha1_array_append(opt->value, sha1); + return 0; +} + int parse_opt_tertiary(const struct option *opt, const char *arg, int unset) { int *target = opt->value; diff --git a/parse-options.h b/parse-options.h index 6ca8388a53..a2bc34e9d7 100644 --- a/parse-options.h +++ b/parse-options.h @@ -223,7 +223,8 @@ extern int parse_opt_approxidate_cb(const struct option *, const char *, int); extern int parse_opt_expiry_date_cb(const struct option *, const char *, int); extern int parse_opt_color_flag_cb(const struct option *, const char *, int); extern int parse_opt_verbosity_cb(const struct option *, const char *, int); -extern int parse_opt_with_commit(const struct option *, const char *, int); +extern int parse_opt_object_name(const struct option *, const char *, int); +extern int parse_opt_commits(const struct option *, const char *, int); extern int parse_opt_tertiary(const struct option *, const char *, int); extern int parse_opt_string_list(const struct option *, const char *, int); extern int parse_opt_noop_cb(const struct option *, const char *, int); @@ -251,5 +252,12 @@ extern int parse_opt_passthru_argv(const struct option *, const char *, int); { OPTION_CALLBACK, (s), (l), (v), (a), (h), (f), parse_opt_passthru } #define OPT_PASSTHRU_ARGV(s, l, v, a, h, f) \ { OPTION_CALLBACK, (s), (l), (v), (a), (h), (f), parse_opt_passthru_argv } +#define _OPT_CONTAINS_OR_WITH(name, variable, help, flag) \ + { OPTION_CALLBACK, 0, name, (variable), N_("commit"), (help), \ + PARSE_OPT_LASTARG_DEFAULT | flag, \ + parse_opt_commits, (intptr_t) "HEAD" \ + } +#define OPT_CONTAINS(v, h) _OPT_CONTAINS_OR_WITH("contains", v, h, 0) +#define OPT_WITH(v, h) _OPT_CONTAINS_OR_WITH("with", v, h, PARSE_OPT_HIDDEN) #endif diff --git a/ref-filter.c b/ref-filter.c index f38dee4f60..46963a5a42 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -9,6 +9,7 @@ #include "tag.h" #include "quote.h" #include "ref-filter.h" +#include "revision.h" typedef enum { FIELD_STR, FIELD_ULONG, FIELD_TIME } cmp_type; @@ -817,6 +818,114 @@ static void get_ref_atom_value(struct ref_array_item *ref, int atom, struct atom *v = &ref->value[atom]; } +enum contains_result { + CONTAINS_UNKNOWN = -1, + CONTAINS_NO = 0, + CONTAINS_YES = 1 +}; + +/* + * Mimicking the real stack, this stack lives on the heap, avoiding stack + * overflows. + * + * At each recursion step, the stack items points to the commits whose + * ancestors are to be inspected. + */ +struct contains_stack { + int nr, alloc; + struct contains_stack_entry { + struct commit *commit; + struct commit_list *parents; + } *contains_stack; +}; + +static int in_commit_list(const struct commit_list *want, struct commit *c) +{ + for (; want; want = want->next) + if (!hashcmp(want->item->object.sha1, c->object.sha1)) + return 1; + return 0; +} + +/* + * Test whether the candidate or one of its parents is contained in the list. + * Do not recurse to find out, though, but return -1 if inconclusive. + */ +static enum contains_result contains_test(struct commit *candidate, + const struct commit_list *want) +{ + /* was it previously marked as containing a want commit? */ + if (candidate->object.flags & TMP_MARK) + return 1; + /* or marked as not possibly containing a want commit? */ + if (candidate->object.flags & UNINTERESTING) + return 0; + /* or are we it? */ + if (in_commit_list(want, candidate)) { + candidate->object.flags |= TMP_MARK; + return 1; + } + + if (parse_commit(candidate) < 0) + return 0; + + return -1; +} + +static void push_to_contains_stack(struct commit *candidate, struct contains_stack *contains_stack) +{ + ALLOC_GROW(contains_stack->contains_stack, contains_stack->nr + 1, contains_stack->alloc); + contains_stack->contains_stack[contains_stack->nr].commit = candidate; + contains_stack->contains_stack[contains_stack->nr++].parents = candidate->parents; +} + +static enum contains_result contains_tag_algo(struct commit *candidate, + const struct commit_list *want) +{ + struct contains_stack contains_stack = { 0, 0, NULL }; + int result = contains_test(candidate, want); + + if (result != CONTAINS_UNKNOWN) + return result; + + push_to_contains_stack(candidate, &contains_stack); + while (contains_stack.nr) { + struct contains_stack_entry *entry = &contains_stack.contains_stack[contains_stack.nr - 1]; + struct commit *commit = entry->commit; + struct commit_list *parents = entry->parents; + + if (!parents) { + commit->object.flags |= UNINTERESTING; + contains_stack.nr--; + } + /* + * If we just popped the stack, parents->item has been marked, + * therefore contains_test will return a meaningful 0 or 1. + */ + else switch (contains_test(parents->item, want)) { + case CONTAINS_YES: + commit->object.flags |= TMP_MARK; + contains_stack.nr--; + break; + case CONTAINS_NO: + entry->parents = parents->next; + break; + case CONTAINS_UNKNOWN: + push_to_contains_stack(parents->item, &contains_stack); + break; + } + } + free(contains_stack.contains_stack); + return contains_test(candidate, want); +} + +static int commit_contains(struct ref_filter *filter, struct commit *commit) +{ + if (filter->with_commit_tag_algo) + return contains_tag_algo(commit, filter->with_commit); + return is_descendant_of(commit, filter->with_commit); +} + /* * Return 1 if the refname matches one of the patterns, otherwise 0. * A pattern can be path prefix (e.g. a refname "refs/heads/master" @@ -842,6 +951,38 @@ static int match_name_as_path(const char **pattern, const char *refname) return 0; } +/* + * Given a ref (sha1, refname), check if the ref belongs to the array + * of sha1s. If the given ref is a tag, check if the given tag points + * at one of the sha1s in the given sha1 array. + * the given sha1_array. + * NEEDSWORK: + * 1. Only a single level of inderection is obtained, we might want to + * change this to account for multiple levels (e.g. annotated tags + * pointing to annotated tags pointing to a commit.) + * 2. As the refs are cached we might know what refname peels to without + * the need to parse the object via parse_object(). peel_ref() might be a + * more efficient alternative to obtain the pointee. + */ +static const unsigned char *match_points_at(struct sha1_array *points_at, + const unsigned char *sha1, + const char *refname) +{ + const unsigned char *tagged_sha1 = NULL; + struct object *obj; + + if (sha1_array_lookup(points_at, sha1) >= 0) + return sha1; + obj = parse_object(sha1); + if (!obj) + die(_("malformed object at '%s'"), refname); + if (obj->type == OBJ_TAG) + tagged_sha1 = ((struct tag *)obj)->tagged->sha1; + if (tagged_sha1 && sha1_array_lookup(points_at, tagged_sha1) >= 0) + return tagged_sha1; + return NULL; +} + /* Allocate space for a new ref_array_item and copy the objectname and flag to it */ static struct ref_array_item *new_ref_array_item(const char *refname, const unsigned char *objectname, @@ -866,6 +1007,7 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid, struct ref_filter_cbdata *ref_cbdata = cb_data; struct ref_filter *filter = ref_cbdata->filter; struct ref_array_item *ref; + struct commit *commit = NULL; if (flag & REF_BAD_NAME) { warning("ignoring ref with broken name %s", refname); @@ -880,12 +1022,31 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid, if (*filter->name_patterns && !match_name_as_path(filter->name_patterns, refname)) return 0; + if (filter->points_at.nr && !match_points_at(&filter->points_at, oid->hash, refname)) + return 0; + + /* + * A merge filter is applied on refs pointing to commits. Hence + * obtain the commit using the 'oid' available and discard all + * non-commits early. The actual filtering is done later. + */ + if (filter->merge_commit || filter->with_commit) { + commit = lookup_commit_reference_gently(oid->hash, 1); + if (!commit) + return 0; + /* We perform the filtering for the '--contains' option */ + if (filter->with_commit && + !commit_contains(filter, commit)) + return 0; + } + /* * We do not open the object yet; sort may only need refname * to do its job and the resulting list may yet to be pruned * by maxcount logic. */ ref = new_ref_array_item(refname, oid->hash, flag); + ref->commit = commit; REALLOC_ARRAY(ref_cbdata->array->items, ref_cbdata->array->nr + 1); ref_cbdata->array->items[ref_cbdata->array->nr++] = ref; @@ -911,6 +1072,50 @@ void ref_array_clear(struct ref_array *array) array->nr = array->alloc = 0; } +static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata) +{ + struct rev_info revs; + int i, old_nr; + struct ref_filter *filter = ref_cbdata->filter; + struct ref_array *array = ref_cbdata->array; + struct commit **to_clear = xcalloc(sizeof(struct commit *), array->nr); + + init_revisions(&revs, NULL); + + for (i = 0; i < array->nr; i++) { + struct ref_array_item *item = array->items[i]; + add_pending_object(&revs, &item->commit->object, item->refname); + to_clear[i] = item->commit; + } + + filter->merge_commit->object.flags |= UNINTERESTING; + add_pending_object(&revs, &filter->merge_commit->object, ""); + + revs.limited = 1; + if (prepare_revision_walk(&revs)) + die(_("revision walk setup failed")); + + old_nr = array->nr; + array->nr = 0; + + for (i = 0; i < old_nr; i++) { + struct ref_array_item *item = array->items[i]; + struct commit *commit = item->commit; + + int is_merged = !!(commit->object.flags & UNINTERESTING); + + if (is_merged == (filter->merge == REF_FILTER_MERGED_INCLUDE)) + array->items[array->nr++] = array->items[i]; + else + free_array_item(item); + } + + for (i = 0; i < old_nr; i++) + clear_commit_marks(to_clear[i], ALL_REV_FLAGS); + clear_commit_marks(filter->merge_commit, ALL_REV_FLAGS); + free(to_clear); +} + /* * API for filtering a set of refs. Based on the type of refs the user * has requested, we iterate through those refs and apply filters @@ -920,17 +1125,24 @@ void ref_array_clear(struct ref_array *array) int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int type) { struct ref_filter_cbdata ref_cbdata; + int ret = 0; ref_cbdata.array = array; ref_cbdata.filter = filter; + /* Simple per-ref filtering */ if (type & (FILTER_REFS_ALL | FILTER_REFS_INCLUDE_BROKEN)) - return for_each_rawref(ref_filter_handler, &ref_cbdata); + ret = for_each_rawref(ref_filter_handler, &ref_cbdata); else if (type & FILTER_REFS_ALL) - return for_each_ref(ref_filter_handler, &ref_cbdata); - else + ret = for_each_ref(ref_filter_handler, &ref_cbdata); + else if (type) die("filter_refs: invalid type"); - return 0; + + /* Filters that need revision walking */ + if (filter->merge_commit) + do_merge_filter(&ref_cbdata); + + return ret; } static int cmp_ref_sorting(struct ref_sorting *s, struct ref_array_item *a, struct ref_array_item *b) @@ -1104,3 +1316,22 @@ int parse_opt_ref_sorting(const struct option *opt, const char *arg, int unset) s->atom = parse_ref_filter_atom(arg, arg+len); return 0; } + +int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset) +{ + struct ref_filter *rf = opt->value; + unsigned char sha1[20]; + + rf->merge = starts_with(opt->long_name, "no") + ? REF_FILTER_MERGED_OMIT + : REF_FILTER_MERGED_INCLUDE; + + if (get_sha1(arg, sha1)) + die(_("malformed object name %s"), arg); + + rf->merge_commit = lookup_commit_reference_gently(sha1, 0); + if (!rf->merge_commit) + return opterror(opt, "must point to a commit", 0); + + return 0; +} diff --git a/ref-filter.h b/ref-filter.h index 699798400b..6bf27d8bc4 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -31,6 +31,7 @@ struct ref_array_item { unsigned char objectname[20]; int flag; const char *symref; + struct commit *commit; struct atom_value *value; char refname[FLEX_ARRAY]; }; @@ -42,6 +43,17 @@ struct ref_array { struct ref_filter { const char **name_patterns; + struct sha1_array points_at; + struct commit_list *with_commit; + + enum { + REF_FILTER_MERGED_NONE = 0, + REF_FILTER_MERGED_INCLUDE, + REF_FILTER_MERGED_OMIT + } merge; + struct commit *merge_commit; + + unsigned int with_commit_tag_algo : 1; }; struct ref_filter_cbdata { @@ -49,6 +61,15 @@ struct ref_filter_cbdata { struct ref_filter *filter; }; +/* Macros for checking --merged and --no-merged options */ +#define _OPT_MERGED_NO_MERGED(option, filter, h) \ + { OPTION_CALLBACK, 0, option, (filter), N_("commit"), (h), \ + PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \ + parse_opt_merge_filter, (intptr_t) "HEAD" \ + } +#define OPT_MERGED(f, h) _OPT_MERGED_NO_MERGED("merged", f, h) +#define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED("no-merged", f, h) + /* * API for filtering a set of refs. Based on the type of refs the user * has requested, we iterate through those refs and apply filters @@ -70,5 +91,7 @@ void show_ref_array_item(struct ref_array_item *info, const char *format, int qu int parse_opt_ref_sorting(const struct option *opt, const char *arg, int unset); /* Default sort option based on refname */ struct ref_sorting *ref_default_sorting(void); +/* Function to parse --merged and --no-merged options */ +int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset); #endif /* REF_FILTER_H */ diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh new file mode 100755 index 0000000000..505a360161 --- /dev/null +++ b/t/t6302-for-each-ref-filter.sh @@ -0,0 +1,84 @@ +#!/bin/sh + +test_description='test for-each-refs usage of ref-filter APIs' + +. ./test-lib.sh +. "$TEST_DIRECTORY"/lib-gpg.sh + +if ! test_have_prereq GPG +then + skip_all="skipping for-each-ref tests, GPG not available" + test_done +fi + +test_expect_success 'setup some history and refs' ' + test_commit one && + test_commit two && + test_commit three && + git checkout -b side && + test_commit four && + git tag -s -m "A signed tag message" signed-tag && + git tag -s -m "Annonated doubly" double-tag signed-tag && + git checkout master && + git update-ref refs/odd/spot master +' + +test_expect_success 'filtering with --points-at' ' + cat >expect <<-\EOF && + refs/heads/master + refs/odd/spot + refs/tags/three + EOF + git for-each-ref --format="%(refname)" --points-at=master >actual && + test_cmp expect actual +' + +test_expect_success 'check signed tags with --points-at' ' + sed -e "s/Z$//" >expect <<-\EOF && + refs/heads/side Z + refs/tags/four Z + refs/tags/signed-tag four + EOF + git for-each-ref --format="%(refname) %(*subject)" --points-at=side >actual && + test_cmp expect actual +' + +test_expect_success 'filtering with --merged' ' + cat >expect <<-\EOF && + refs/heads/master + refs/odd/spot + refs/tags/one + refs/tags/three + refs/tags/two + EOF + git for-each-ref --format="%(refname)" --merged=master >actual && + test_cmp expect actual +' + +test_expect_success 'filtering with --no-merged' ' + cat >expect <<-\EOF && + refs/heads/side + refs/tags/double-tag + refs/tags/four + refs/tags/signed-tag + EOF + git for-each-ref --format="%(refname)" --no-merged=master >actual && + test_cmp expect actual +' + +test_expect_success 'filtering with --contains' ' + cat >expect <<-\EOF && + refs/heads/master + refs/heads/side + refs/odd/spot + refs/tags/double-tag + refs/tags/four + refs/tags/signed-tag + refs/tags/three + refs/tags/two + EOF + git for-each-ref --format="%(refname)" --contains=two >actual && + test_cmp expect actual +' + +test_done