tree-walk: support :(attr) matching

This lets us use :(attr) with "git grep <tree-ish>" or "git log". :(attr) requires another round of checking before we can declare that a path is matched. This is done after path matching since we have lots of optimization to take a shortcut when things don't match. Note that if :(attr) is present, we can't return all_entries_interesting / all_entries_not_interesting anymore because we can't be certain about that. Not until match_pathspec_attrs() can tell us "yes all these paths satisfy :(attr)". Second note. Even though we walk a specific tree, we use attributes from _worktree_ (or falling back to the index), not from .gitattributes files on that tree. This by itself is not necessarily wrong, but the user just have to be aware of this. 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 Nov 18, 2018 at 17:48 UTC 5a0b97b34c51eaddf39624857f1ac0e7df4ca2e3
3 files changed +112 -15
Documentation/glossary-content.txt
+2
@@ -404,6 +404,8 @@ these forms:
404 - "`!ATTR`" requires that the attribute `ATTR` be
405 unspecified.
406 +
407 +Note that when matching against a tree object, attributes are still
408 +obtained from working tree, not from the given tree object.
409
410 exclude;;
411 After a path matches any non-exclude pathspec, it will be run
t/t6135-pathspec-with-attrs.sh
+57 -1
@@ -31,7 +31,7 @@ test_expect_success 'setup a tree' '
31 mkdir sub &&
32 while read path
33 do
34 - : >$path &&
34 + echo content >$path &&
35 git add $path || return 1
36 done <expect &&
37 git commit -m "initial commit" &&
@@ -48,6 +48,10 @@ test_expect_success 'pathspec with labels and non existent .gitattributes' '
48 test_must_be_empty actual
49 '
50
51 +test_expect_success 'pathspec with labels and non existent .gitattributes (2)' '
52 + test_must_fail git grep content HEAD -- ":(attr:label)"
53 +'
54 +
55 test_expect_success 'setup .gitattributes' '
56 cat <<-\EOF >.gitattributes &&
57 fileA labelA
@@ -74,6 +78,15 @@ test_expect_success 'check specific set attr' '
78 test_cmp expect actual
79 '
80
81 +test_expect_success 'check specific set attr (2)' '
82 + cat <<-\EOF >expect &&
83 + HEAD:fileSetLabel
84 + HEAD:sub/fileSetLabel
85 + EOF
86 + git grep -l content HEAD ":(attr:label)" >actual &&
87 + test_cmp expect actual
88 +'
89 +
90 test_expect_success 'check specific unset attr' '
91 cat <<-\EOF >expect &&
92 fileUnsetLabel
@@ -83,6 +96,15 @@ test_expect_success 'check specific unset attr' '
96 test_cmp expect actual
97 '
98
99 +test_expect_success 'check specific unset attr (2)' '
100 + cat <<-\EOF >expect &&
101 + HEAD:fileUnsetLabel
102 + HEAD:sub/fileUnsetLabel
103 + EOF
104 + git grep -l content HEAD ":(attr:-label)" >actual &&
105 + test_cmp expect actual
106 +'
107 +
108 test_expect_success 'check specific value attr' '
109 cat <<-\EOF >expect &&
110 fileValue
@@ -94,6 +116,16 @@ test_expect_success 'check specific value attr' '
116 test_must_be_empty actual
117 '
118
119 +test_expect_success 'check specific value attr (2)' '
120 + cat <<-\EOF >expect &&
121 + HEAD:fileValue
122 + HEAD:sub/fileValue
123 + EOF
124 + git grep -l content HEAD ":(attr:label=foo)" >actual &&
125 + test_cmp expect actual &&
126 + test_must_fail git grep -l content HEAD ":(attr:label=bar)"
127 +'
128 +
129 test_expect_success 'check unspecified attr' '
130 cat <<-\EOF >expect &&
131 .gitattributes
@@ -118,6 +150,30 @@ test_expect_success 'check unspecified attr' '
150 test_cmp expect actual
151 '
152
153 +test_expect_success 'check unspecified attr (2)' '
154 + cat <<-\EOF >expect &&
155 + HEAD:.gitattributes
156 + HEAD:fileA
157 + HEAD:fileAB
158 + HEAD:fileAC
159 + HEAD:fileB
160 + HEAD:fileBC
161 + HEAD:fileC
162 + HEAD:fileNoLabel
163 + HEAD:fileWrongLabel
164 + HEAD:sub/fileA
165 + HEAD:sub/fileAB
166 + HEAD:sub/fileAC
167 + HEAD:sub/fileB
168 + HEAD:sub/fileBC
169 + HEAD:sub/fileC
170 + HEAD:sub/fileNoLabel
171 + HEAD:sub/fileWrongLabel
172 + EOF
173 + git grep -l ^ HEAD ":(attr:!label)" >actual &&
174 + test_cmp expect actual
175 +'
176 +
177 test_expect_success 'check multiple unspecified attr' '
178 cat <<-\EOF >expect &&
179 .gitattributes
tree-walk.c
+53 -14
@@ -949,7 +949,8 @@ static enum interesting do_match(struct index_state *istate,
949 PATHSPEC_LITERAL |
950 PATHSPEC_GLOB |
951 PATHSPEC_ICASE |
952 - PATHSPEC_EXCLUDE);
952 + PATHSPEC_EXCLUDE |
953 + PATHSPEC_ATTR);
954
955 if (!ps->nr) {
956 if (!ps->recursive ||
@@ -981,14 +982,20 @@ static enum interesting do_match(struct index_state *istate,
982
983 if (!ps->recursive ||
984 !(ps->magic & PATHSPEC_MAXDEPTH) ||
984 - ps->max_depth == -1)
985 - return all_entries_interesting;
986 -
987 - return within_depth(base_str + matchlen + 1,
988 - baselen - matchlen - 1,
989 - !!S_ISDIR(entry->mode),
990 - ps->max_depth) ?
991 - entry_interesting : entry_not_interesting;
985 + ps->max_depth == -1) {
986 + if (!item->attr_match_nr)
987 + return all_entries_interesting;
988 + else
989 + goto interesting;
990 + }
991 +
992 + if (within_depth(base_str + matchlen + 1,
993 + baselen - matchlen - 1,
994 + !!S_ISDIR(entry->mode),
995 + ps->max_depth))
996 + goto interesting;
997 + else
998 + return entry_not_interesting;
999 }
1000
1001 /* Either there must be no base, or the base must match. */
@@ -996,12 +1003,12 @@ static enum interesting do_match(struct index_state *istate,
1003 if (match_entry(item, entry, pathlen,
1004 match + baselen, matchlen - baselen,
1005 &never_interesting))
999 - return entry_interesting;
1006 + goto interesting;
1007
1008 if (item->nowildcard_len < item->len) {
1009 if (!git_fnmatch(item, match + baselen, entry->path,
1010 item->nowildcard_len - baselen))
1004 - return entry_interesting;
1011 + goto interesting;
1012
1013 /*
1014 * Match all directories. We'll try to
@@ -1022,7 +1029,7 @@ static enum interesting do_match(struct index_state *istate,
1029 !ps_strncmp(item, match + baselen,
1030 entry->path,
1031 item->nowildcard_len - baselen))
1025 - return entry_interesting;
1032 + goto interesting;
1033 }
1034
1035 continue;
@@ -1057,7 +1064,7 @@ match_wildcards:
1064 if (!git_fnmatch(item, match, base->buf + base_offset,
1065 item->nowildcard_len)) {
1066 strbuf_setlen(base, base_offset + baselen);
1060 - return entry_interesting;
1067 + goto interesting;
1068 }
1069
1070 /*
@@ -1071,7 +1078,7 @@ match_wildcards:
1078 !ps_strncmp(item, match, base->buf + base_offset,
1079 item->nowildcard_len)) {
1080 strbuf_setlen(base, base_offset + baselen);
1074 - return entry_interesting;
1081 + goto interesting;
1082 }
1083
1084 strbuf_setlen(base, base_offset + baselen);
@@ -1085,6 +1092,38 @@ match_wildcards:
1092 */
1093 if (ps->recursive && S_ISDIR(entry->mode))
1094 return entry_interesting;
1095 + continue;
1096 +interesting:
1097 + if (item->attr_match_nr) {
1098 + int ret;
1099 +
1100 + /*
1101 + * Must not return all_entries_not_interesting
1102 + * prematurely. We do not know if all entries do not
1103 + * match some attributes with current attr API.
1104 + */
1105 + never_interesting = entry_not_interesting;
1106 +
1107 + /*
1108 + * Consider all directories interesting (because some
1109 + * of those files inside may match some attributes
1110 + * even though the parent dir does not)
1111 + *
1112 + * FIXME: attributes _can_ match directories and we
1113 + * can probably return all_entries_interesting or
1114 + * all_entries_not_interesting here if matched.
1115 + */
1116 + if (S_ISDIR(entry->mode))
1117 + return entry_interesting;
1118 +
1119 + strbuf_add(base, entry->path, pathlen);
1120 + ret = match_pathspec_attrs(istate, base->buf + base_offset,
1121 + base->len - base_offset, item);
1122 + strbuf_setlen(base, base_offset + baselen);
1123 + if (!ret)
1124 + continue;
1125 + }
1126 + return entry_interesting;
1127 }
1128 return never_interesting; /* No matches */
1129 }