tag: support --sort=<spec>

--sort=version:refname (or --sort=v:refname for short) sorts tags as if they are versions. --sort=-refname reverses the order (with or without ":version"). versioncmp() is copied from string/strverscmp.c in glibc commit ee9247c38a8def24a59eb5cfb7196a98bef8cfdc, reformatted to Git coding style. The implementation is under LGPL-2.1 and according to [1] I can relicense it to GPLv2. [1] http://www.gnu.org/licenses/gpl-faq.html#AllCompatibility Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Nguyễn Thái Ngọc Duy committed Feb 27, 2014 at 19:56 UTC 9ef176b55c373ba087b2d84e77b5713578891927
6 files changed +211 -5
Documentation/git-tag.txt
+6
@@ -95,6 +95,12 @@ OPTIONS
95 using fnmatch(3)). Multiple patterns may be given; if any of
96 them matches, the tag is shown.
97
98 +--sort=<type>::
99 + Sort in a specific order. Supported type is "refname"
100 + (lexicographic order), "version:refname" or "v:refname" (tag
101 + names are treated as versions). Prepend "-" to reverse sort
102 + order.
103 +
104 --column[=<options>]::
105 --no-column::
106 Display tag listing in columns. See configuration variable
Makefile
+1
@@ -884,6 +884,7 @@ LIB_OBJS += userdiff.o
884 LIB_OBJS += utf8.o
885 LIB_OBJS += varint.o
886 LIB_OBJS += version.o
887 +LIB_OBJS += versioncmp.o
888 LIB_OBJS += walker.o
889 LIB_OBJS += wildmatch.o
890 LIB_OBJS += wrapper.o
builtin/tag.c
+66 -5
@@ -27,9 +27,16 @@ static const char * const git_tag_usage[] = {
27 NULL
28 };
29
30 +#define STRCMP_SORT 0 /* must be zero */
31 +#define VERCMP_SORT 1
32 +#define SORT_MASK 0x7fff
33 +#define REVERSE_SORT 0x8000
34 +
35 struct tag_filter {
36 const char **patterns;
37 int lines;
38 + int sort;
39 + struct string_list tags;
40 struct commit_list *with_commit;
41 };
42
@@ -166,7 +173,10 @@ static int show_reference(const char *refname, const unsigned char *sha1,
173 return 0;
174
175 if (!filter->lines) {
169 - printf("%s\n", refname);
176 + if (filter->sort)
177 + string_list_append(&filter->tags, refname);
178 + else
179 + printf("%s\n", refname);
180 return 0;
181 }
182 printf("%-15s ", refname);
@@ -177,17 +187,39 @@ static int show_reference(const char *refname, const unsigned char *sha1,
187 return 0;
188 }
189
190 +static int sort_by_version(const void *a_, const void *b_)
191 +{
192 + const struct string_list_item *a = a_;
193 + const struct string_list_item *b = b_;
194 + return versioncmp(a->string, b->string);
195 +}
196 +
197 static int list_tags(const char **patterns, int lines,
181 - struct commit_list *with_commit)
198 + struct commit_list *with_commit, int sort)
199 {
200 struct tag_filter filter;
201
202 filter.patterns = patterns;
203 filter.lines = lines;
204 + filter.sort = sort;
205 filter.with_commit = with_commit;
206 + memset(&filter.tags, 0, sizeof(filter.tags));
207 + filter.tags.strdup_strings = 1;
208
209 for_each_tag_ref(show_reference, (void *) &filter);
190 -
210 + if (sort) {
211 + int i;
212 + if ((sort & SORT_MASK) == VERCMP_SORT)
213 + qsort(filter.tags.items, filter.tags.nr,
214 + sizeof(struct string_list_item), sort_by_version);
215 + if (sort & REVERSE_SORT)
216 + for (i = filter.tags.nr - 1; i >= 0; i--)
217 + printf("%s\n", filter.tags.items[i].string);
218 + else
219 + for (i = 0; i < filter.tags.nr; i++)
220 + printf("%s\n", filter.tags.items[i].string);
221 + string_list_clear(&filter.tags, 0);
222 + }
223 return 0;
224 }
225
@@ -427,6 +459,29 @@ static int parse_opt_points_at(const struct option *opt __attribute__((unused)),
459 return 0;
460 }
461
462 +static int parse_opt_sort(const struct option *opt, const char *arg, int unset)
463 +{
464 + int *sort = opt->value;
465 + int flags = 0;
466 +
467 + if (*arg == '-') {
468 + flags |= REVERSE_SORT;
469 + arg++;
470 + }
471 + if (starts_with(arg, "version:")) {
472 + *sort = VERCMP_SORT;
473 + arg += 8;
474 + } else if (starts_with(arg, "v:")) {
475 + *sort = VERCMP_SORT;
476 + arg += 2;
477 + } else
478 + *sort = STRCMP_SORT;
479 + if (strcmp(arg, "refname"))
480 + die(_("unsupported sort specification %s"), arg);
481 + *sort |= flags;
482 + return 0;
483 +}
484 +
485 int cmd_tag(int argc, const char **argv, const char *prefix)
486 {
487 struct strbuf buf = STRBUF_INIT;
@@ -437,7 +492,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
492 struct create_tag_options opt;
493 char *cleanup_arg = NULL;
494 int annotate = 0, force = 0, lines = -1;
440 - int cmdmode = 0;
495 + int cmdmode = 0, sort = 0;
496 const char *msgfile = NULL, *keyid = NULL;
497 struct msg_arg msg = { 0, STRBUF_INIT };
498 struct commit_list *with_commit = NULL;
@@ -462,6 +517,10 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
517 N_("use another key to sign the tag")),
518 OPT__FORCE(&force, N_("replace the tag if exists")),
519 OPT_COLUMN(0, "column", &colopts, N_("show tag list in columns")),
520 + {
521 + OPTION_CALLBACK, 0, "sort", &sort, N_("type"), N_("sort tags"),
522 + PARSE_OPT_NONEG, parse_opt_sort
523 + },
524
525 OPT_GROUP(N_("Tag listing options")),
526 {
@@ -509,7 +568,9 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
568 copts.padding = 2;
569 run_column_filter(colopts, &copts);
570 }
512 - ret = list_tags(argv, lines == -1 ? 0 : lines, with_commit);
571 + if (lines != -1 && sort)
572 + die(_("--sort and -n are incompatible"));
573 + ret = list_tags(argv, lines == -1 ? 0 : lines, with_commit, sort);
574 if (column_active(colopts))
575 stop_column_filter();
576 return ret;
cache.h
+2
@@ -1367,4 +1367,6 @@ int stat_validity_check(struct stat_validity *sv, const char *path);
1367 */
1368 void stat_validity_update(struct stat_validity *sv, int fd);
1369
1370 +int versioncmp(const char *s1, const char *s2);
1371 +
1372 #endif /* CACHE_H */
t/t7004-tag.sh
+43
@@ -1380,4 +1380,47 @@ test_expect_success 'multiple --points-at are OR-ed together' '
1380 test_cmp expect actual
1381 '
1382
1383 +test_expect_success 'lexical sort' '
1384 + git tag foo1.3 &&
1385 + git tag foo1.6 &&
1386 + git tag foo1.10 &&
1387 + git tag -l --sort=refname "foo*" >actual &&
1388 + cat >expect <<EOF &&
1389 +foo1.10
1390 +foo1.3
1391 +foo1.6
1392 +EOF
1393 + test_cmp expect actual
1394 +'
1395 +
1396 +test_expect_success 'version sort' '
1397 + git tag -l --sort=version:refname "foo*" >actual &&
1398 + cat >expect <<EOF &&
1399 +foo1.3
1400 +foo1.6
1401 +foo1.10
1402 +EOF
1403 + test_cmp expect actual
1404 +'
1405 +
1406 +test_expect_success 'reverse version sort' '
1407 + git tag -l --sort=-version:refname "foo*" >actual &&
1408 + cat >expect <<EOF &&
1409 +foo1.10
1410 +foo1.6
1411 +foo1.3
1412 +EOF
1413 + test_cmp expect actual
1414 +'
1415 +
1416 +test_expect_success 'reverse lexical sort' '
1417 + git tag -l --sort=-refname "foo*" >actual &&
1418 + cat >expect <<EOF &&
1419 +foo1.6
1420 +foo1.3
1421 +foo1.10
1422 +EOF
1423 + test_cmp expect actual
1424 +'
1425 +
1426 test_done
versioncmp.c new
+93
@@ -0,0 +1,93 @@
1 +#include "cache.h"
2 +
3 +/*
4 + * versioncmp(): copied from string/strverscmp.c in glibc commit
5 + * ee9247c38a8def24a59eb5cfb7196a98bef8cfdc, reformatted to Git coding
6 + * style. The implementation is under LGPL-2.1 and Git relicenses it
7 + * to GPLv2.
8 + */
9 +
10 +/*
11 + * states: S_N: normal, S_I: comparing integral part, S_F: comparing
12 + * fractionnal parts, S_Z: idem but with leading Zeroes only
13 + */
14 +#define S_N 0x0
15 +#define S_I 0x3
16 +#define S_F 0x6
17 +#define S_Z 0x9
18 +
19 +/* result_type: CMP: return diff; LEN: compare using len_diff/diff */
20 +#define CMP 2
21 +#define LEN 3
22 +
23 +
24 +/*
25 + * Compare S1 and S2 as strings holding indices/version numbers,
26 + * returning less than, equal to or greater than zero if S1 is less
27 + * than, equal to or greater than S2 (for more info, see the texinfo
28 + * doc).
29 + */
30 +
31 +int versioncmp(const char *s1, const char *s2)
32 +{
33 + const unsigned char *p1 = (const unsigned char *) s1;
34 + const unsigned char *p2 = (const unsigned char *) s2;
35 + unsigned char c1, c2;
36 + int state, diff;
37 +
38 + /*
39 + * Symbol(s) 0 [1-9] others
40 + * Transition (10) 0 (01) d (00) x
41 + */
42 + static const uint8_t next_state[] = {
43 + /* state x d 0 */
44 + /* S_N */ S_N, S_I, S_Z,
45 + /* S_I */ S_N, S_I, S_I,
46 + /* S_F */ S_N, S_F, S_F,
47 + /* S_Z */ S_N, S_F, S_Z
48 + };
49 +
50 + static const int8_t result_type[] = {
51 + /* state x/x x/d x/0 d/x d/d d/0 0/x 0/d 0/0 */
52 +
53 + /* S_N */ CMP, CMP, CMP, CMP, LEN, CMP, CMP, CMP, CMP,
54 + /* S_I */ CMP, -1, -1, +1, LEN, LEN, +1, LEN, LEN,
55 + /* S_F */ CMP, CMP, CMP, CMP, CMP, CMP, CMP, CMP, CMP,
56 + /* S_Z */ CMP, +1, +1, -1, CMP, CMP, -1, CMP, CMP
57 + };
58 +
59 + if (p1 == p2)
60 + return 0;
61 +
62 + c1 = *p1++;
63 + c2 = *p2++;
64 + /* Hint: '0' is a digit too. */
65 + state = S_N + ((c1 == '0') + (isdigit (c1) != 0));
66 +
67 + while ((diff = c1 - c2) == 0) {
68 + if (c1 == '\0')
69 + return diff;
70 +
71 + state = next_state[state];
72 + c1 = *p1++;
73 + c2 = *p2++;
74 + state += (c1 == '0') + (isdigit (c1) != 0);
75 + }
76 +
77 + state = result_type[state * 3 + (((c2 == '0') + (isdigit (c2) != 0)))];
78 +
79 + switch (state) {
80 + case CMP:
81 + return diff;
82 +
83 + case LEN:
84 + while (isdigit (*p1++))
85 + if (!isdigit (*p2++))
86 + return 1;
87 +
88 + return isdigit (*p2) ? -1 : diff;
89 +
90 + default:
91 + return state;
92 + }
93 +}