Merge branch 'st/verify-tag'
authorJunio C Hamano <gitster@pobox.com>
Tue, 31 Jan 2017 21:14:58 +0000 (13:14 -0800)
committerJunio C Hamano <gitster@pobox.com>
Tue, 31 Jan 2017 21:14:58 +0000 (13:14 -0800)
"git tag" and "git verify-tag" learned to put GPG verification
status in their "--format=<placeholders>" output format.

* st/verify-tag:
t/t7004-tag: Add --format specifier tests
t/t7030-verify-tag: Add --format specifier tests
builtin/tag: add --format argument for tag -v
builtin/verify-tag: add --format to verify-tag
ref-filter: add function to print single ref_array_item
gpg-interface, tag: add GPG_VERIFY_OMIT_STATUS flag

1  2 
Documentation/git-tag.txt
builtin/tag.c
ref-filter.c
ref-filter.h
t/t7004-tag.sh
index 5055a9682393409c1ed07e1e417015f46e46a5bc,3bb5e3c23f29bec970368133ae7bb7cd1f1f4881..8e70c5b6a4117034eb55969bd0a8d0702ae75d71
@@@ -15,7 -15,7 +15,7 @@@ SYNOPSI
  'git tag' [-n[<num>]] -l [--contains <commit>] [--points-at <object>]
        [--column[=<options>] | --no-column] [--create-reflog] [--sort=<key>]
        [--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]
- 'git tag' -v <tagname>...
+ 'git tag' -v [--format=<format>] <tagname>...
  
  DESCRIPTION
  -----------
@@@ -101,17 -101,13 +101,17 @@@ OPTION
        multiple times, in which case the last key becomes the primary
        key. Also supports "version:refname" or "v:refname" (tag
        names are treated as versions). The "version:refname" sort
 -      order can also be affected by the
 -      "versionsort.prereleaseSuffix" configuration variable.
 +      order can also be affected by the "versionsort.suffix"
 +      configuration variable.
        The keys supported are the same as those in `git for-each-ref`.
        Sort order defaults to the value configured for the `tag.sort`
        variable if it exists, or lexicographic order otherwise. See
        linkgit:git-config[1].
  
 +-i::
 +--ignore-case::
 +      Sorting and filtering tags are case insensitive.
 +
  --column[=<options>]::
  --no-column::
        Display tag listing in columns. See configuration variable
@@@ -257,8 -253,9 +257,8 @@@ On Automatic followin
  ~~~~~~~~~~~~~~~~~~~~~~
  
  If you are following somebody else's tree, you are most likely
 -using remote-tracking branches (`refs/heads/origin` in traditional
 -layout, or `refs/remotes/origin/master` in the separate-remote
 -layout).  You usually want the tags from the other end.
 +using remote-tracking branches (eg. `refs/remotes/origin/master`).
 +You usually want the tags from the other end.
  
  On the other hand, if you are fetching because you would want a
  one-shot merge from somebody else, you typically do not want to
diff --combined builtin/tag.c
index 73df728114e81ac87dd1b84604947d739d948fb6,fbb85ba3dc0dd2886b4239ab40215ac27b3ba1e9..e40c4a96763a371b104a5bf47e6d839ed93e8ef5
@@@ -24,7 -24,7 +24,7 @@@ static const char * const git_tag_usage
        N_("git tag -d <tagname>..."),
        N_("git tag -l [-n[<num>]] [--contains <commit>] [--points-at <object>]"
                "\n\t\t[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]"),
-       N_("git tag -v <tagname>..."),
+       N_("git tag -v [--format=<format>] <tagname>..."),
        NULL
  };
  
@@@ -66,9 -66,10 +66,10 @@@ static int list_tags(struct ref_filter 
  }
  
  typedef int (*each_tag_name_fn)(const char *name, const char *ref,
-                               const unsigned char *sha1);
+                               const unsigned char *sha1, const void *cb_data);
  
- static int for_each_tag_name(const char **argv, each_tag_name_fn fn)
+ static int for_each_tag_name(const char **argv, each_tag_name_fn fn,
+                            const void *cb_data)
  {
        const char **p;
        char ref[PATH_MAX];
                        had_error = 1;
                        continue;
                }
-               if (fn(*p, ref, sha1))
+               if (fn(*p, ref, sha1, cb_data))
                        had_error = 1;
        }
        return had_error;
  }
  
  static int delete_tag(const char *name, const char *ref,
-                               const unsigned char *sha1)
+                     const unsigned char *sha1, const void *cb_data)
  {
        if (delete_ref(ref, sha1, 0))
                return 1;
  }
  
  static int verify_tag(const char *name, const char *ref,
-                               const unsigned char *sha1)
+                     const unsigned char *sha1, const void *cb_data)
  {
-       return gpg_verify_tag(sha1, name, GPG_VERIFY_VERBOSE);
+       int flags;
+       const char *fmt_pretty = cb_data;
+       flags = GPG_VERIFY_VERBOSE;
+       if (fmt_pretty)
+               flags = GPG_VERIFY_OMIT_STATUS;
+       if (gpg_verify_tag(sha1, name, flags))
+               return -1;
+       if (fmt_pretty)
+               pretty_print_ref(name, sha1, fmt_pretty);
+       return 0;
  }
  
  static int do_sign(struct strbuf *buffer)
@@@ -335,7 -349,6 +349,7 @@@ int cmd_tag(int argc, const char **argv
        struct ref_filter filter;
        static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
        const char *format = NULL;
 +      int icase = 0;
        struct option options[] = {
                OPT_CMDMODE('l', "list", &cmdmode, N_("list tag names"), 'l'),
                { OPTION_INTEGER, 'n', NULL, &filter.lines, N_("n"),
                        N_("print only tags of the object"), 0, parse_opt_object_name
                },
                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_END()
        };
  
        }
        if (!sorting)
                sorting = ref_default_sorting();
 +      sorting->ignore_case = icase;
 +      filter.ignore_case = icase;
        if (cmdmode == 'l') {
                int ret;
                if (column_active(colopts)) {
        if (filter.merge_commit)
                die(_("--merged and --no-merged option are only allowed with -l"));
        if (cmdmode == 'd')
-               return for_each_tag_name(argv, delete_tag);
-       if (cmdmode == 'v')
-               return for_each_tag_name(argv, verify_tag);
+               return for_each_tag_name(argv, delete_tag, NULL);
+       if (cmdmode == 'v') {
+               if (format)
+                       verify_ref_format(format);
+               return for_each_tag_name(argv, verify_tag, format);
+       }
  
        if (msg.given || msgfile) {
                if (msg.given && msgfile)
diff --combined ref-filter.c
index 1a978405e6b97d1f57d75b867ddeae30f091ef19,77d4e533429e09701d53ba15d9e5f537cddc5608..5f4b08792bd82d5a70097af18d9c7869c32bceab
@@@ -13,7 -13,6 +13,7 @@@
  #include "utf8.h"
  #include "git-compat-util.h"
  #include "version.h"
 +#include "trailer.h"
  
  typedef enum { FIELD_STR, FIELD_ULONG, FIELD_TIME } cmp_type;
  
@@@ -41,7 -40,7 +41,7 @@@ static struct used_atom 
                enum { RR_NORMAL, RR_SHORTEN, RR_TRACK, RR_TRACKSHORT }
                        remote_ref;
                struct {
 -                      enum { C_BARE, C_BODY, C_BODY_DEP, C_LINES, C_SIG, C_SUB } option;
 +                      enum { C_BARE, C_BODY, C_BODY_DEP, C_LINES, C_SIG, C_SUB, C_TRAILERS } option;
                        unsigned int nlines;
                } contents;
                enum { O_FULL, O_SHORT } objectname;
@@@ -86,13 -85,6 +86,13 @@@ static void subject_atom_parser(struct 
        atom->u.contents.option = C_SUB;
  }
  
 +static void trailers_atom_parser(struct used_atom *atom, const char *arg)
 +{
 +      if (arg)
 +              die(_("%%(trailers) does not take arguments"));
 +      atom->u.contents.option = C_TRAILERS;
 +}
 +
  static void contents_atom_parser(struct used_atom *atom, const char *arg)
  {
        if (!arg)
                atom->u.contents.option = C_SIG;
        else if (!strcmp(arg, "subject"))
                atom->u.contents.option = C_SUB;
 +      else if (!strcmp(arg, "trailers"))
 +              atom->u.contents.option = C_TRAILERS;
        else if (skip_prefix(arg, "lines=", &arg)) {
                atom->u.contents.option = C_LINES;
                if (strtoul_ui(arg, 10, &atom->u.contents.nlines))
@@@ -204,7 -194,6 +204,7 @@@ static struct 
        { "creatordate", FIELD_TIME },
        { "subject", FIELD_STR, subject_atom_parser },
        { "body", FIELD_STR, body_atom_parser },
 +      { "trailers", FIELD_STR, trailers_atom_parser },
        { "contents", FIELD_STR, contents_atom_parser },
        { "upstream", FIELD_STR, remote_ref_atom_parser },
        { "push", FIELD_STR, remote_ref_atom_parser },
@@@ -246,7 -235,7 +246,7 @@@ int parse_ref_filter_atom(const char *a
  {
        const char *sp;
        const char *arg;
 -      int i, at;
 +      int i, at, atom_len;
  
        sp = atom;
        if (*sp == '*' && sp < ep)
                        return i;
        }
  
 +      /*
 +       * If the atom name has a colon, strip it and everything after
 +       * it off - it specifies the format for this entry, and
 +       * shouldn't be used for checking against the valid_atom
 +       * table.
 +       */
 +      arg = memchr(sp, ':', ep - sp);
 +      atom_len = (arg ? arg : ep) - sp;
 +
        /* Is the atom a valid one? */
        for (i = 0; i < ARRAY_SIZE(valid_atom); i++) {
                int len = strlen(valid_atom[i].name);
 -
 -              /*
 -               * If the atom name has a colon, strip it and everything after
 -               * it off - it specifies the format for this entry, and
 -               * shouldn't be used for checking against the valid_atom
 -               * table.
 -               */
 -              arg = memchr(sp, ':', ep - sp);
 -              if (len == (arg ? arg : ep) - sp &&
 -                  !memcmp(valid_atom[i].name, sp, len))
 +              if (len == atom_len && !memcmp(valid_atom[i].name, sp, len))
                        break;
        }
  
@@@ -796,7 -785,6 +796,7 @@@ static void grab_sub_body_contents(stru
                        name++;
                if (strcmp(name, "subject") &&
                    strcmp(name, "body") &&
 +                  strcmp(name, "trailers") &&
                    !starts_with(name, "contents"))
                        continue;
                if (!subpos)
                        /*  Size is the length of the message after removing the signature */
                        append_lines(&s, subpos, contents_end - subpos, atom->u.contents.nlines);
                        v->s = strbuf_detach(&s, NULL);
 +              } else if (atom->u.contents.option == C_TRAILERS) {
 +                      struct trailer_info info;
 +
 +                      /* Search for trailer info */
 +                      trailer_info_get(&info, subpos);
 +                      v->s = xmemdupz(info.trailer_start,
 +                                      info.trailer_end - info.trailer_start);
 +                      trailer_info_release(&info);
                } else if (atom->u.contents.option == C_BARE)
                        v->s = xstrdup(subpos);
        }
@@@ -1037,7 -1017,7 +1037,7 @@@ static void populate_value(struct ref_a
  
                        head = resolve_ref_unsafe("HEAD", RESOLVE_REF_READING,
                                                  sha1, NULL);
 -                      if (!strcmp(ref->refname, head))
 +                      if (head && !strcmp(ref->refname, head))
                                v->s = "*";
                        else
                                v->s = " ";
@@@ -1251,14 -1231,8 +1251,14 @@@ static int commit_contains(struct ref_f
   * matches a pattern "refs/heads/mas") or a wildcard (e.g. the same ref
   * matches "refs/heads/mas*", too).
   */
 -static int match_pattern(const char **patterns, const char *refname)
 +static int match_pattern(const struct ref_filter *filter, const char *refname)
  {
 +      const char **patterns = filter->name_patterns;
 +      unsigned flags = 0;
 +
 +      if (filter->ignore_case)
 +              flags |= WM_CASEFOLD;
 +
        /*
         * When no '--format' option is given we need to skip the prefix
         * for matching refs of tags and branches.
               skip_prefix(refname, "refs/", &refname));
  
        for (; *patterns; patterns++) {
 -              if (!wildmatch(*patterns, refname, 0, NULL))
 +              if (!wildmatch(*patterns, refname, flags, NULL))
                        return 1;
        }
        return 0;
   * matches a pattern "refs/heads/" but not "refs/heads/m") or a
   * wildcard (e.g. the same ref matches "refs/heads/m*", too).
   */
 -static int match_name_as_path(const char **pattern, const char *refname)
 +static int match_name_as_path(const struct ref_filter *filter, const char *refname)
  {
 +      const char **pattern = filter->name_patterns;
        int namelen = strlen(refname);
 +      unsigned flags = WM_PATHNAME;
 +
 +      if (filter->ignore_case)
 +              flags |= WM_CASEFOLD;
 +
        for (; *pattern; pattern++) {
                const char *p = *pattern;
                int plen = strlen(p);
@@@ -1312,8 -1280,8 +1312,8 @@@ static int filter_pattern_match(struct 
        if (!*filter->name_patterns)
                return 1; /* No pattern always matches */
        if (filter->match_as_path)
 -              return match_name_as_path(filter->name_patterns, refname);
 -      return match_pattern(filter->name_patterns, refname);
 +              return match_name_as_path(filter, refname);
 +      return match_pattern(filter, refname);
  }
  
  /*
@@@ -1361,7 -1329,7 +1361,7 @@@ static struct ref_array_item *new_ref_a
        return ref;
  }
  
- static int filter_ref_kind(struct ref_filter *filter, const char *refname)
+ static int ref_kind_from_refname(const char *refname)
  {
        unsigned int i;
  
                { "refs/tags/", FILTER_REFS_TAGS}
        };
  
-       if (filter->kind == FILTER_REFS_BRANCHES ||
-           filter->kind == FILTER_REFS_REMOTES ||
-           filter->kind == FILTER_REFS_TAGS)
-               return filter->kind;
-       else if (!strcmp(refname, "HEAD"))
+       if (!strcmp(refname, "HEAD"))
                return FILTER_REFS_DETACHED_HEAD;
  
        for (i = 0; i < ARRAY_SIZE(ref_kind); i++) {
        return FILTER_REFS_OTHERS;
  }
  
+ static int filter_ref_kind(struct ref_filter *filter, const char *refname)
+ {
+       if (filter->kind == FILTER_REFS_BRANCHES ||
+           filter->kind == FILTER_REFS_REMOTES ||
+           filter->kind == FILTER_REFS_TAGS)
+               return filter->kind;
+       return ref_kind_from_refname(refname);
+ }
  /*
   * A call-back given to for_each_ref().  Filter refs and keep them for
   * later object processing.
@@@ -1568,20 -1541,18 +1573,20 @@@ static int cmp_ref_sorting(struct ref_s
        struct atom_value *va, *vb;
        int cmp;
        cmp_type cmp_type = used_atom[s->atom].type;
 +      int (*cmp_fn)(const char *, const char *);
  
        get_ref_atom_value(a, s->atom, &va);
        get_ref_atom_value(b, s->atom, &vb);
 +      cmp_fn = s->ignore_case ? strcasecmp : strcmp;
        if (s->version)
                cmp = versioncmp(va->s, vb->s);
        else if (cmp_type == FIELD_STR)
 -              cmp = strcmp(va->s, vb->s);
 +              cmp = cmp_fn(va->s, vb->s);
        else {
                if (va->ul < vb->ul)
                        cmp = -1;
                else if (va->ul == vb->ul)
 -                      cmp = strcmp(a->refname, b->refname);
 +                      cmp = cmp_fn(a->refname, b->refname);
                else
                        cmp = 1;
        }
@@@ -1607,7 -1578,7 +1612,7 @@@ static int compare_refs(const void *a_
  void ref_array_sort(struct ref_sorting *sorting, struct ref_array *array)
  {
        ref_sorting = sorting;
 -      qsort(array->items, array->nr, sizeof(struct ref_array_item *), compare_refs);
 +      QSORT(array->items, array->nr, compare_refs);
  }
  
  static void append_literal(const char *cp, const char *ep, struct ref_formatting_state *state)
@@@ -1671,6 -1642,16 +1676,16 @@@ void show_ref_array_item(struct ref_arr
        putchar('\n');
  }
  
+ void pretty_print_ref(const char *name, const unsigned char *sha1,
+               const char *format)
+ {
+       struct ref_array_item *ref_item;
+       ref_item = new_ref_array_item(name, sha1, 0);
+       ref_item->kind = ref_kind_from_refname(name);
+       show_ref_array_item(ref_item, format, 0);
+       free_array_item(ref_item);
+ }
  /*  If no sorting option is given, use refname to sort as default */
  struct ref_sorting *ref_default_sorting(void)
  {
diff --combined ref-filter.h
index fc55fa3574620bfdc76b2997afc56909528e2275,3cf3d3f590484e246c0b0bd52a44f72071ad377b..7b05592baf00661a08f973d0a218f7e0afd99453
@@@ -29,7 -29,6 +29,7 @@@ struct ref_sorting 
        struct ref_sorting *next;
        int atom; /* index into used_atom array (internal) */
        unsigned reverse : 1,
 +              ignore_case : 1,
                version : 1;
  };
  
@@@ -63,7 -62,6 +63,7 @@@ struct ref_filter 
  
        unsigned int with_commit_tag_algo : 1,
                match_as_path : 1,
 +              ignore_case : 1,
                detached : 1;
        unsigned int kind,
                lines;
@@@ -109,4 -107,11 +109,11 @@@ struct ref_sorting *ref_default_sorting
  /*  Function to parse --merged and --no-merged options */
  int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset);
  
+ /*
+  * Print a single ref, outside of any ref-filter. Note that the
+  * name must be a fully qualified refname.
+  */
+ void pretty_print_ref(const char *name, const unsigned char *sha1,
+               const char *format);
  #endif /*  REF_FILTER_H  */
diff --combined t/t7004-tag.sh
index 1cfa8a21d23173e51e95906c00342fa13b85d955,b53a2e5e41f9394c83556260d17083e00e612d2e..b676b90c7dfd8bf743e788f07aedc2a4a8053540
@@@ -27,30 -27,6 +27,30 @@@ test_expect_success 'listing all tags i
        test $(git tag | wc -l) -eq 0
  '
  
 +test_expect_success 'sort tags, ignore case' '
 +      (
 +              git init sort &&
 +              cd sort &&
 +              test_commit initial &&
 +              git tag tag-one &&
 +              git tag TAG-two &&
 +              git tag -l >actual &&
 +              cat >expected <<-\EOF &&
 +              TAG-two
 +              initial
 +              tag-one
 +              EOF
 +              test_cmp expected actual &&
 +              git tag -l -i >actual &&
 +              cat >expected <<-\EOF &&
 +              initial
 +              tag-one
 +              TAG-two
 +              EOF
 +              test_cmp expected actual
 +      )
 +'
 +
  test_expect_success 'looking for a tag in an empty tree should fail' \
        '! (tag_exists mytag)'
  
@@@ -105,9 -81,6 +105,9 @@@ test_expect_success 'listing all tags i
  test_expect_success 'listing a tag using a matching pattern should succeed' \
        'git tag -l mytag'
  
 +test_expect_success 'listing a tag with --ignore-case' \
 +      'test $(git tag -l --ignore-case MYTAG) = mytag'
 +
  test_expect_success \
        'listing a tag using a matching pattern should output that tag' \
        'test $(git tag -l mytag) = mytag'
@@@ -149,11 -122,11 +149,11 @@@ test_expect_success '--force can creat
        tag_exists mytag'
  
  test_expect_success '--force is moot with a non-existing tag name' '
 +      test_when_finished git tag -d newtag forcetag &&
        git tag newtag >expect &&
        git tag --force forcetag >actual &&
        test_cmp expect actual
  '
 -git tag -d newtag forcetag
  
  # deleting tags:
  
@@@ -324,9 -297,11 +324,9 @@@ EO
  '
  
  test_expect_success 'listing tags in column with column.*' '
 -      git config column.tag row &&
 -      git config column.ui dense &&
 +      test_config column.tag row &&
 +      test_config column.ui dense &&
        COLUMNS=40 git tag -l >actual &&
 -      git config --unset column.ui &&
 -      git config --unset column.tag &&
        cat >expected <<\EOF &&
  a1      aa1   cba     t210    t211
  v0.2.1  v1.0  v1.0.1  v1.1.3
@@@ -339,8 -314,9 +339,8 @@@ test_expect_success 'listing tag with -
  '
  
  test_expect_success 'listing tags -n in column with column.ui ignored' '
 -      git config column.ui "row dense" &&
 +      test_config column.ui "row dense" &&
        COLUMNS=40 git tag -l -n >actual &&
 -      git config --unset column.ui &&
        cat >expected <<\EOF &&
  a1              Foo
  aa1             Foo
@@@ -871,6 -847,22 +871,22 @@@ test_expect_success GPG 'verifying a fo
        test_must_fail git tag -v forged-tag
  '
  
+ test_expect_success 'verifying a proper tag with --format pass and format accordingly' '
+       cat >expect <<-\EOF
+       tagname : signed-tag
+       EOF &&
+       git tag -v --format="tagname : %(tag)" "signed-tag" >actual &&
+       test_cmp expect actual
+ '
+ test_expect_success 'verifying a forged tag with --format fail and format accordingly' '
+       cat >expect <<-\EOF
+       tagname : forged-tag
+       EOF &&
+       test_must_fail git tag -v --format="tagname : %(tag)" "forged-tag" >actual &&
+       test_cmp expect actual
+ '
  # blank and empty messages for signed tags:
  
  get_tag_header empty-signed-tag $commit commit $time >expect
@@@ -1224,10 -1216,11 +1240,10 @@@ test_expect_success GPG,RFC1991 
  '
  
  # try to sign with bad user.signingkey
 -git config user.signingkey BobTheMouse
  test_expect_success GPG \
        'git tag -s fails if gpg is misconfigured (bad key)' \
 -      'test_must_fail git tag -s -m tail tag-gpg-failure'
 -git config --unset user.signingkey
 +      'test_config user.signingkey BobTheMouse &&
 +      test_must_fail git tag -s -m tail tag-gpg-failure'
  
  # try to produce invalid signature
  test_expect_success GPG \
@@@ -1507,7 -1500,7 +1523,7 @@@ test_expect_success 'reverse lexical so
  '
  
  test_expect_success 'configured lexical sort' '
 -      git config tag.sort "v:refname" &&
 +      test_config tag.sort "v:refname" &&
        git tag -l "foo*" >actual &&
        cat >expect <<-\EOF &&
        foo1.3
  '
  
  test_expect_success 'option override configured sort' '
 +      test_config tag.sort "v:refname" &&
        git tag -l --sort=-refname "foo*" >actual &&
        cat >expect <<-\EOF &&
        foo1.6
@@@ -1533,12 -1525,13 +1549,12 @@@ test_expect_success 'invalid sort param
  '
  
  test_expect_success 'invalid sort parameter in configuratoin' '
 -      git config tag.sort "v:notvalid" &&
 +      test_config tag.sort "v:notvalid" &&
        test_must_fail git tag -l "foo*"
  '
  
  test_expect_success 'version sort with prerelease reordering' '
 -      git config --unset tag.sort &&
 -      git config versionsort.prereleaseSuffix -rc &&
 +      test_config versionsort.prereleaseSuffix -rc &&
        git tag foo1.6-rc1 &&
        git tag foo1.6-rc2 &&
        git tag -l --sort=version:refname "foo*" >actual &&
  '
  
  test_expect_success 'reverse version sort with prerelease reordering' '
 +      test_config versionsort.prereleaseSuffix -rc &&
        git tag -l --sort=-version:refname "foo*" >actual &&
        cat >expect <<-\EOF &&
        foo1.10
        test_cmp expect actual
  '
  
 +test_expect_success 'version sort with prerelease reordering and common leading character' '
 +      test_config versionsort.prereleaseSuffix -before &&
 +      git tag foo1.7-before1 &&
 +      git tag foo1.7 &&
 +      git tag foo1.7-after1 &&
 +      git tag -l --sort=version:refname "foo1.7*" >actual &&
 +      cat >expect <<-\EOF &&
 +      foo1.7-before1
 +      foo1.7
 +      foo1.7-after1
 +      EOF
 +      test_cmp expect actual
 +'
 +
 +test_expect_success 'version sort with prerelease reordering, multiple suffixes and common leading character' '
 +      test_config versionsort.prereleaseSuffix -before &&
 +      git config --add versionsort.prereleaseSuffix -after &&
 +      git tag -l --sort=version:refname "foo1.7*" >actual &&
 +      cat >expect <<-\EOF &&
 +      foo1.7-before1
 +      foo1.7-after1
 +      foo1.7
 +      EOF
 +      test_cmp expect actual
 +'
 +
 +test_expect_success 'version sort with prerelease reordering, multiple suffixes match the same tag' '
 +      test_config versionsort.prereleaseSuffix -bar &&
 +      git config --add versionsort.prereleaseSuffix -foo-baz &&
 +      git config --add versionsort.prereleaseSuffix -foo-bar &&
 +      git tag foo1.8-foo-bar &&
 +      git tag foo1.8-foo-baz &&
 +      git tag foo1.8 &&
 +      git tag -l --sort=version:refname "foo1.8*" >actual &&
 +      cat >expect <<-\EOF &&
 +      foo1.8-foo-baz
 +      foo1.8-foo-bar
 +      foo1.8
 +      EOF
 +      test_cmp expect actual
 +'
 +
 +test_expect_success 'version sort with prerelease reordering, multiple suffixes match starting at the same position' '
 +      test_config versionsort.prereleaseSuffix -pre &&
 +      git config --add versionsort.prereleaseSuffix -prerelease &&
 +      git tag foo1.9-pre1 &&
 +      git tag foo1.9-pre2 &&
 +      git tag foo1.9-prerelease1 &&
 +      git tag -l --sort=version:refname "foo1.9*" >actual &&
 +      cat >expect <<-\EOF &&
 +      foo1.9-pre1
 +      foo1.9-pre2
 +      foo1.9-prerelease1
 +      EOF
 +      test_cmp expect actual
 +'
 +
 +test_expect_success 'version sort with general suffix reordering' '
 +      test_config versionsort.suffix -alpha &&
 +      git config --add versionsort.suffix -beta &&
 +      git config --add versionsort.suffix ""  &&
 +      git config --add versionsort.suffix -gamma &&
 +      git config --add versionsort.suffix -delta &&
 +      git tag foo1.10-alpha &&
 +      git tag foo1.10-beta &&
 +      git tag foo1.10-gamma &&
 +      git tag foo1.10-delta &&
 +      git tag foo1.10-unlisted-suffix &&
 +      git tag -l --sort=version:refname "foo1.10*" >actual &&
 +      cat >expect <<-\EOF &&
 +      foo1.10-alpha
 +      foo1.10-beta
 +      foo1.10
 +      foo1.10-unlisted-suffix
 +      foo1.10-gamma
 +      foo1.10-delta
 +      EOF
 +      test_cmp expect actual
 +'
 +
 +test_expect_success 'versionsort.suffix overrides versionsort.prereleaseSuffix' '
 +      test_config versionsort.suffix -before &&
 +      test_config versionsort.prereleaseSuffix -after &&
 +      git tag -l --sort=version:refname "foo1.7*" >actual &&
 +      cat >expect <<-\EOF &&
 +      foo1.7-before1
 +      foo1.7
 +      foo1.7-after1
 +      EOF
 +      test_cmp expect actual
 +'
 +
 +test_expect_success 'version sort with very long prerelease suffix' '
 +      test_config versionsort.prereleaseSuffix -very-looooooooooooooooooooooooong-prerelease-suffix &&
 +      git tag -l --sort=version:refname
 +'
 +
  run_with_limited_stack () {
        (ulimit -s 128 && "$@")
  }
  
  test_expect_success '--format should list tags as per format given' '
        cat >expect <<-\EOF &&
 -      refname : refs/tags/foo1.10
 -      refname : refs/tags/foo1.3
 -      refname : refs/tags/foo1.6
 -      refname : refs/tags/foo1.6-rc1
 -      refname : refs/tags/foo1.6-rc2
 +      refname : refs/tags/v1.0
 +      refname : refs/tags/v1.0.1
 +      refname : refs/tags/v1.1.3
        EOF
 -      git tag -l --format="refname : %(refname)" "foo*" >actual &&
 +      git tag -l --format="refname : %(refname)" "v1*" >actual &&
        test_cmp expect actual
  '