tag, branch, for-each-ref: add --ignore-case for sorting and filtering

This options makes sorting ignore case, which is great when you have branches named bug-12-do-something, Bug-12-do-some-more and BUG-12-do-what and want to group them together. Sorting externally may not be an option because we lose coloring and column layout from git-branch and git-tag. The same could be said for filtering, but it's probably less important because you can always go with the ugly pattern [bB][uU][gG]-* if you're desperate. You can't have case-sensitive filtering and case-insensitive sorting (or the other way around) with this though. For branch and tag, that should be no problem. for-each-ref, as a plumbing, might want finer control. But we can always add --{filter,sort}-ignore-case when there is a need for it. 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 Dec 4, 2016 at 09:52 UTC 3bb16a8bf2ec02c4cc633c3efd4c012e55ee0c2d
10 files changed +112 -17
Documentation/git-branch.txt
+4
@@ -118,6 +118,10 @@ OPTIONS
118 default to color output.
119 Same as `--color=never`.
120
121 +-i::
122 +--ignore-case::
123 + Sorting and filtering branches are case insensitive.
124 +
125 --column[=<options>]::
126 --no-column::
127 Display branch listing in columns. See configuration variable
Documentation/git-for-each-ref.txt
+3
@@ -79,6 +79,9 @@ OPTIONS
79 Only list refs which contain the specified commit (HEAD if not
80 specified).
81
82 +--ignore-case::
83 + Sorting and filtering refs are case insensitive.
84 +
85 FIELD NAMES
86 -----------
87
Documentation/git-tag.txt
+4
@@ -108,6 +108,10 @@ OPTIONS
108 variable if it exists, or lexicographic order otherwise. See
109 linkgit:git-config[1].
110
111 +-i::
112 +--ignore-case::
113 + Sorting and filtering tags are case insensitive.
114 +
115 --column[=<options>]::
116 --no-column::
117 Display tag listing in columns. See configuration variable
builtin/branch.c
+14 -9
@@ -512,15 +512,6 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
512 if (filter->verbose)
513 maxwidth = calc_maxwidth(&array, strlen(remote_prefix));
514
515 - /*
516 - * If no sorting parameter is given then we default to sorting
517 - * by 'refname'. This would give us an alphabetically sorted
518 - * array with the 'HEAD' ref at the beginning followed by
519 - * local branches 'refs/heads/...' and finally remote-tacking
520 - * branches 'refs/remotes/...'.
521 - */
522 - if (!sorting)
523 - sorting = ref_default_sorting();
515 ref_array_sort(sorting, &array);
516
517 for (i = 0; i < array.nr; i++)
@@ -645,6 +636,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
636 const char *new_upstream = NULL;
637 enum branch_track track;
638 struct ref_filter filter;
639 + int icase = 0;
640 static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
641
642 struct option options[] = {
@@ -686,6 +678,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
678 OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"),
679 N_("print only branches of the object"), 0, parse_opt_object_name
680 },
681 + OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
682 OPT_END(),
683 };
684
@@ -723,6 +716,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
716
717 if (filter.abbrev == -1)
718 filter.abbrev = DEFAULT_ABBREV;
719 + filter.ignore_case = icase;
720 +
721 finalize_colopts(&colopts, -1);
722 if (filter.verbose) {
723 if (explicitly_enable_column(colopts))
@@ -744,6 +739,16 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
739 if ((filter.kind & FILTER_REFS_BRANCHES) && filter.detached)
740 filter.kind |= FILTER_REFS_DETACHED_HEAD;
741 filter.name_patterns = argv;
742 + /*
743 + * If no sorting parameter is given then we default to sorting
744 + * by 'refname'. This would give us an alphabetically sorted
745 + * array with the 'HEAD' ref at the beginning followed by
746 + * local branches 'refs/heads/...' and finally remote-tacking
747 + * branches 'refs/remotes/...'.
748 + */
749 + if (!sorting)
750 + sorting = ref_default_sorting();
751 + sorting->ignore_case = icase;
752 print_ref_list(&filter, sorting);
753 print_columns(&output, colopts, NULL);
754 string_list_clear(&output, 0);
builtin/for-each-ref.c
+4 -1
@@ -18,7 +18,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
18 int i;
19 const char *format = "%(objectname) %(objecttype)\t%(refname)";
20 struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
21 - int maxcount = 0, quote_style = 0;
21 + int maxcount = 0, quote_style = 0, icase = 0;
22 struct ref_array array;
23 struct ref_filter filter;
24
@@ -43,6 +43,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
43 OPT_MERGED(&filter, N_("print only refs that are merged")),
44 OPT_NO_MERGED(&filter, N_("print only refs that are not merged")),
45 OPT_CONTAINS(&filter.with_commit, N_("print only refs which contain the commit")),
46 + OPT_BOOL(0, "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
47 OPT_END(),
48 };
49
@@ -63,6 +64,8 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
64
65 if (!sorting)
66 sorting = ref_default_sorting();
67 + sorting->ignore_case = icase;
68 + filter.ignore_case = icase;
69
70 /* for warn_ambiguous_refs */
71 git_config(git_default_config, NULL);
builtin/tag.c
+4
@@ -335,6 +335,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
335 struct ref_filter filter;
336 static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
337 const char *format = NULL;
338 + int icase = 0;
339 struct option options[] = {
340 OPT_CMDMODE('l', "list", &cmdmode, N_("list tag names"), 'l'),
341 { OPTION_INTEGER, 'n', NULL, &filter.lines, N_("n"),
@@ -370,6 +371,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
371 N_("print only tags of the object"), 0, parse_opt_object_name
372 },
373 OPT_STRING( 0 , "format", &format, N_("format"), N_("format to use for the output")),
374 + OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
375 OPT_END()
376 };
377
@@ -401,6 +403,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
403 }
404 if (!sorting)
405 sorting = ref_default_sorting();
406 + sorting->ignore_case = icase;
407 + filter.ignore_case = icase;
408 if (cmdmode == 'l') {
409 int ret;
410 if (column_active(colopts)) {
ref-filter.c
+21 -7
@@ -1231,8 +1231,14 @@ static int commit_contains(struct ref_filter *filter, struct commit *commit)
1231 * matches a pattern "refs/heads/mas") or a wildcard (e.g. the same ref
1232 * matches "refs/heads/mas*", too).
1233 */
1234 -static int match_pattern(const char **patterns, const char *refname)
1234 +static int match_pattern(const struct ref_filter *filter, const char *refname)
1235 {
1236 + const char **patterns = filter->name_patterns;
1237 + unsigned flags = 0;
1238 +
1239 + if (filter->ignore_case)
1240 + flags |= WM_CASEFOLD;
1241 +
1242 /*
1243 * When no '--format' option is given we need to skip the prefix
1244 * for matching refs of tags and branches.
@@ -1243,7 +1249,7 @@ static int match_pattern(const char **patterns, const char *refname)
1249 skip_prefix(refname, "refs/", &refname));
1250
1251 for (; *patterns; patterns++) {
1246 - if (!wildmatch(*patterns, refname, 0, NULL))
1252 + if (!wildmatch(*patterns, refname, flags, NULL))
1253 return 1;
1254 }
1255 return 0;
@@ -1255,9 +1261,15 @@ static int match_pattern(const char **patterns, const char *refname)
1261 * matches a pattern "refs/heads/" but not "refs/heads/m") or a
1262 * wildcard (e.g. the same ref matches "refs/heads/m*", too).
1263 */
1258 -static int match_name_as_path(const char **pattern, const char *refname)
1264 +static int match_name_as_path(const struct ref_filter *filter, const char *refname)
1265 {
1266 + const char **pattern = filter->name_patterns;
1267 int namelen = strlen(refname);
1268 + unsigned flags = WM_PATHNAME;
1269 +
1270 + if (filter->ignore_case)
1271 + flags |= WM_CASEFOLD;
1272 +
1273 for (; *pattern; pattern++) {
1274 const char *p = *pattern;
1275 int plen = strlen(p);
@@ -1280,8 +1292,8 @@ static int filter_pattern_match(struct ref_filter *filter, const char *refname)
1292 if (!*filter->name_patterns)
1293 return 1; /* No pattern always matches */
1294 if (filter->match_as_path)
1283 - return match_name_as_path(filter->name_patterns, refname);
1284 - return match_pattern(filter->name_patterns, refname);
1295 + return match_name_as_path(filter, refname);
1296 + return match_pattern(filter, refname);
1297 }
1298
1299 /*
@@ -1536,18 +1548,20 @@ static int cmp_ref_sorting(struct ref_sorting *s, struct ref_array_item *a, stru
1548 struct atom_value *va, *vb;
1549 int cmp;
1550 cmp_type cmp_type = used_atom[s->atom].type;
1551 + int (*cmp_fn)(const char *, const char *);
1552
1553 get_ref_atom_value(a, s->atom, &va);
1554 get_ref_atom_value(b, s->atom, &vb);
1555 + cmp_fn = s->ignore_case ? strcasecmp : strcmp;
1556 if (s->version)
1557 cmp = versioncmp(va->s, vb->s);
1558 else if (cmp_type == FIELD_STR)
1545 - cmp = strcmp(va->s, vb->s);
1559 + cmp = cmp_fn(va->s, vb->s);
1560 else {
1561 if (va->ul < vb->ul)
1562 cmp = -1;
1563 else if (va->ul == vb->ul)
1550 - cmp = strcmp(a->refname, b->refname);
1564 + cmp = cmp_fn(a->refname, b->refname);
1565 else
1566 cmp = 1;
1567 }
ref-filter.h
+2
@@ -29,6 +29,7 @@ struct ref_sorting {
29 struct ref_sorting *next;
30 int atom; /* index into used_atom array (internal) */
31 unsigned reverse : 1,
32 + ignore_case : 1,
33 version : 1;
34 };
35
@@ -62,6 +63,7 @@ struct ref_filter {
63
64 unsigned int with_commit_tag_algo : 1,
65 match_as_path : 1,
66 + ignore_case : 1,
67 detached : 1;
68 unsigned int kind,
69 lines;
t/t3203-branch-output.sh
+29
@@ -89,6 +89,11 @@ test_expect_success 'git branch --list -v pattern shows branch summaries' '
89 awk "{print \$NF}" <tmp >actual &&
90 test_cmp expect actual
91 '
92 +test_expect_success 'git branch --ignore-case --list -v pattern shows branch summaries' '
93 + git branch --list --ignore-case -v BRANCH* >tmp &&
94 + awk "{print \$NF}" <tmp >actual &&
95 + test_cmp expect actual
96 +'
97
98 test_expect_success 'git branch -v pattern does not show branch summaries' '
99 test_must_fail git branch -v branch*
@@ -196,4 +201,28 @@ test_expect_success 'local-branch symrefs shortened properly' '
201 test_cmp expect actual
202 '
203
204 +test_expect_success 'sort branches, ignore case' '
205 + (
206 + git init sort-icase &&
207 + cd sort-icase &&
208 + test_commit initial &&
209 + git branch branch-one &&
210 + git branch BRANCH-two &&
211 + git branch --list | awk "{print \$NF}" >actual &&
212 + cat >expected <<-\EOF &&
213 + BRANCH-two
214 + branch-one
215 + master
216 + EOF
217 + test_cmp expected actual &&
218 + git branch --list -i | awk "{print \$NF}" >actual &&
219 + cat >expected <<-\EOF &&
220 + branch-one
221 + BRANCH-two
222 + master
223 + EOF
224 + test_cmp expected actual
225 + )
226 +'
227 +
228 test_done
t/t7004-tag.sh
+27
@@ -27,6 +27,30 @@ test_expect_success 'listing all tags in an empty tree should output nothing' '
27 test $(git tag | wc -l) -eq 0
28 '
29
30 +test_expect_success 'sort tags, ignore case' '
31 + (
32 + git init sort &&
33 + cd sort &&
34 + test_commit initial &&
35 + git tag tag-one &&
36 + git tag TAG-two &&
37 + git tag -l >actual &&
38 + cat >expected <<-\EOF &&
39 + TAG-two
40 + initial
41 + tag-one
42 + EOF
43 + test_cmp expected actual &&
44 + git tag -l -i >actual &&
45 + cat >expected <<-\EOF &&
46 + initial
47 + tag-one
48 + TAG-two
49 + EOF
50 + test_cmp expected actual
51 + )
52 +'
53 +
54 test_expect_success 'looking for a tag in an empty tree should fail' \
55 '! (tag_exists mytag)'
56
@@ -81,6 +105,9 @@ test_expect_success 'listing all tags if one exists should output that tag' '
105 test_expect_success 'listing a tag using a matching pattern should succeed' \
106 'git tag -l mytag'
107
108 +test_expect_success 'listing a tag with --ignore-case' \
109 + 'test $(git tag -l --ignore-case MYTAG) = mytag'
110 +
111 test_expect_success \
112 'listing a tag using a matching pattern should output that tag' \
113 'test $(git tag -l mytag) = mytag'