]>
Commit | Line | Data |
---|---|---|
ee1f0b1a | 1 | #include "common.h" |
73b51450 | 2 | #include "repository.h" |
ee1f0b1a | 3 | #include "filebuf.h" |
7d490872 | 4 | #include "attr_file.h" |
823c0e9c | 5 | #include "attrcache.h" |
f917481e RB |
6 | #include "git2/blob.h" |
7 | #include "git2/tree.h" | |
7d490872 | 8 | #include "index.h" |
ee1f0b1a RB |
9 | #include <ctype.h> |
10 | ||
7d490872 RB |
11 | static void attr_file_free(git_attr_file *file) |
12 | { | |
e6e8530a RB |
13 | bool unlock = !git_mutex_lock(&file->lock); |
14 | git_attr_file__clear_rules(file, false); | |
7d490872 | 15 | git_pool_clear(&file->pool); |
e6e8530a RB |
16 | if (unlock) |
17 | git_mutex_unlock(&file->lock); | |
18 | git_mutex_free(&file->lock); | |
19 | ||
7d490872 RB |
20 | git__memzero(file, sizeof(*file)); |
21 | git__free(file); | |
22 | } | |
ee1f0b1a | 23 | |
f917481e | 24 | int git_attr_file__new( |
7d490872 | 25 | git_attr_file **out, |
823c0e9c RB |
26 | git_attr_file_entry *entry, |
27 | git_attr_file_source source) | |
73b51450 | 28 | { |
7d490872 | 29 | git_attr_file *attrs = git__calloc(1, sizeof(git_attr_file)); |
ae9e29fd | 30 | GITERR_CHECK_ALLOC(attrs); |
df743c7d | 31 | |
e6e8530a RB |
32 | if (git_mutex_init(&attrs->lock) < 0) { |
33 | giterr_set(GITERR_OS, "Failed to initialize lock"); | |
34 | git__free(attrs); | |
35 | return -1; | |
36 | } | |
37 | ||
1e5e02b4 | 38 | git_pool_init(&attrs->pool, 1); |
7d490872 | 39 | GIT_REFCOUNT_INC(attrs); |
823c0e9c | 40 | attrs->entry = entry; |
7d490872 RB |
41 | attrs->source = source; |
42 | *out = attrs; | |
43 | return 0; | |
44 | } | |
f917481e | 45 | |
e6e8530a | 46 | int git_attr_file__clear_rules(git_attr_file *file, bool need_lock) |
7d490872 RB |
47 | { |
48 | unsigned int i; | |
49 | git_attr_rule *rule; | |
f917481e | 50 | |
e6e8530a RB |
51 | if (need_lock && git_mutex_lock(&file->lock) < 0) { |
52 | giterr_set(GITERR_OS, "Failed to lock attribute file"); | |
53 | return -1; | |
54 | } | |
55 | ||
7d490872 RB |
56 | git_vector_foreach(&file->rules, i, rule) |
57 | git_attr_rule__free(rule); | |
58 | git_vector_free(&file->rules); | |
e6e8530a RB |
59 | |
60 | if (need_lock) | |
61 | git_mutex_unlock(&file->lock); | |
62 | ||
63 | return 0; | |
7d490872 | 64 | } |
f917481e | 65 | |
7d490872 RB |
66 | void git_attr_file__free(git_attr_file *file) |
67 | { | |
68 | if (!file) | |
69 | return; | |
70 | GIT_REFCOUNT_DEC(file, attr_file_free); | |
71 | } | |
19fa2bc1 | 72 | |
7d490872 RB |
73 | static int attr_file_oid_from_index( |
74 | git_oid *oid, git_repository *repo, const char *path) | |
75 | { | |
76 | int error; | |
77 | git_index *idx; | |
78 | size_t pos; | |
79 | const git_index_entry *entry; | |
80 | ||
81 | if ((error = git_repository_index__weakptr(&idx, repo)) < 0 || | |
82 | (error = git_index__find_pos(&pos, idx, path, 0, 0)) < 0) | |
83 | return error; | |
84 | ||
85 | if (!(entry = git_index_get_byindex(idx, pos))) | |
86 | return GIT_ENOTFOUND; | |
87 | ||
88 | *oid = entry->id; | |
19fa2bc1 | 89 | return 0; |
7d490872 RB |
90 | } |
91 | ||
92 | int git_attr_file__load( | |
93 | git_attr_file **out, | |
94 | git_repository *repo, | |
9f779aac | 95 | git_attr_session *attr_session, |
823c0e9c RB |
96 | git_attr_file_entry *entry, |
97 | git_attr_file_source source, | |
98 | git_attr_file_parser parser) | |
7d490872 RB |
99 | { |
100 | int error = 0; | |
101 | git_blob *blob = NULL; | |
102 | git_buf content = GIT_BUF_INIT; | |
7d490872 | 103 | git_attr_file *file; |
823c0e9c | 104 | struct stat st; |
9f779aac | 105 | bool nonexistent = false; |
7d490872 RB |
106 | |
107 | *out = NULL; | |
108 | ||
823c0e9c RB |
109 | switch (source) { |
110 | case GIT_ATTR_FILE__IN_MEMORY: | |
111 | /* in-memory attribute file doesn't need data */ | |
112 | break; | |
113 | case GIT_ATTR_FILE__FROM_INDEX: { | |
7d490872 | 114 | git_oid id; |
73b51450 | 115 | |
823c0e9c | 116 | if ((error = attr_file_oid_from_index(&id, repo, entry->path)) < 0 || |
7d490872 RB |
117 | (error = git_blob_lookup(&blob, repo, &id)) < 0) |
118 | return error; | |
119 | ||
72d00241 VM |
120 | /* Do not assume that data straight from the ODB is NULL-terminated; |
121 | * copy the contents of a file to a buffer to work on */ | |
122 | git_buf_put(&content, git_blob_rawcontent(blob), git_blob_rawsize(blob)); | |
823c0e9c RB |
123 | break; |
124 | } | |
125 | case GIT_ATTR_FILE__FROM_FILE: { | |
24b8ed2b | 126 | int fd = -1; |
823c0e9c | 127 | |
9f779aac | 128 | /* For open or read errors, pretend that we got ENOTFOUND. */ |
823c0e9c RB |
129 | /* TODO: issue warning when warning API is available */ |
130 | ||
9f779aac ET |
131 | if (p_stat(entry->fullpath, &st) < 0 || |
132 | S_ISDIR(st.st_mode) || | |
133 | (fd = git_futils_open_ro(entry->fullpath)) < 0 || | |
134 | (error = git_futils_readbuffer_fd(&content, fd, (size_t)st.st_size)) < 0) | |
135 | nonexistent = true; | |
24b8ed2b PS |
136 | |
137 | if (fd >= 0) | |
9f779aac | 138 | p_close(fd); |
823c0e9c | 139 | |
823c0e9c RB |
140 | break; |
141 | } | |
142 | default: | |
143 | giterr_set(GITERR_INVALID, "Unknown file source %d", source); | |
144 | return -1; | |
7d490872 RB |
145 | } |
146 | ||
823c0e9c | 147 | if ((error = git_attr_file__new(&file, entry, source)) < 0) |
7d490872 RB |
148 | goto cleanup; |
149 | ||
9f779aac ET |
150 | /* store the key of the attr_reader; don't bother with cache |
151 | * invalidation during the same attr reader session. | |
152 | */ | |
153 | if (attr_session) | |
154 | file->session_key = attr_session->key; | |
155 | ||
72d00241 | 156 | if (parser && (error = parser(repo, file, git_buf_cstr(&content))) < 0) { |
7d490872 | 157 | git_attr_file__free(file); |
823c0e9c RB |
158 | goto cleanup; |
159 | } | |
160 | ||
9f779aac ET |
161 | /* write cache breakers */ |
162 | if (nonexistent) | |
163 | file->nonexistent = 1; | |
164 | else if (source == GIT_ATTR_FILE__FROM_INDEX) | |
823c0e9c RB |
165 | git_oid_cpy(&file->cache_data.oid, git_blob_id(blob)); |
166 | else if (source == GIT_ATTR_FILE__FROM_FILE) | |
167 | git_futils_filestamp_set_from_stat(&file->cache_data.stamp, &st); | |
168 | /* else always cacheable */ | |
169 | ||
170 | *out = file; | |
7d490872 RB |
171 | |
172 | cleanup: | |
173 | git_blob_free(blob); | |
174 | git_buf_free(&content); | |
175 | ||
176 | return error; | |
73b51450 RB |
177 | } |
178 | ||
9f779aac ET |
179 | int git_attr_file__out_of_date( |
180 | git_repository *repo, | |
181 | git_attr_session *attr_session, | |
182 | git_attr_file *file) | |
7d490872 RB |
183 | { |
184 | if (!file) | |
185 | return 1; | |
186 | ||
9f779aac ET |
187 | /* we are never out of date if we just created this data in the same |
188 | * attr_session; otherwise, nonexistent files must be invalidated | |
189 | */ | |
190 | if (attr_session && attr_session->key == file->session_key) | |
191 | return 0; | |
192 | else if (file->nonexistent) | |
193 | return 1; | |
194 | ||
823c0e9c RB |
195 | switch (file->source) { |
196 | case GIT_ATTR_FILE__IN_MEMORY: | |
197 | return 0; | |
198 | ||
199 | case GIT_ATTR_FILE__FROM_FILE: | |
200 | return git_futils_filestamp_check( | |
201 | &file->cache_data.stamp, file->entry->fullpath); | |
202 | ||
203 | case GIT_ATTR_FILE__FROM_INDEX: { | |
7d490872 RB |
204 | int error; |
205 | git_oid id; | |
206 | ||
823c0e9c RB |
207 | if ((error = attr_file_oid_from_index( |
208 | &id, repo, file->entry->path)) < 0) | |
7d490872 RB |
209 | return error; |
210 | ||
211 | return (git_oid__cmp(&file->cache_data.oid, &id) != 0); | |
212 | } | |
213 | ||
823c0e9c RB |
214 | default: |
215 | giterr_set(GITERR_INVALID, "Invalid file type %d", file->source); | |
216 | return -1; | |
217 | } | |
7d490872 RB |
218 | } |
219 | ||
220 | static int sort_by_hash_and_name(const void *a_raw, const void *b_raw); | |
221 | static void git_attr_rule__clear(git_attr_rule *rule); | |
222 | static bool parse_optimized_patterns( | |
223 | git_attr_fnmatch *spec, | |
224 | git_pool *pool, | |
225 | const char *pattern); | |
226 | ||
f917481e | 227 | int git_attr_file__parse_buffer( |
823c0e9c | 228 | git_repository *repo, git_attr_file *attrs, const char *data) |
ee1f0b1a | 229 | { |
ae9e29fd | 230 | int error = 0; |
7d490872 | 231 | const char *scan = data, *context = NULL; |
ee1f0b1a RB |
232 | git_attr_rule *rule = NULL; |
233 | ||
f917481e | 234 | /* if subdir file path, convert context for file paths */ |
823c0e9c RB |
235 | if (attrs->entry && |
236 | git_path_root(attrs->entry->path) < 0 && | |
237 | !git__suffixcmp(attrs->entry->path, "/" GIT_ATTR_FILE)) | |
238 | context = attrs->entry->path; | |
a51cd8e6 | 239 | |
e6e8530a RB |
240 | if (git_mutex_lock(&attrs->lock) < 0) { |
241 | giterr_set(GITERR_OS, "Failed to lock attribute file"); | |
242 | return -1; | |
243 | } | |
244 | ||
ae9e29fd | 245 | while (!error && *scan) { |
ee1f0b1a | 246 | /* allocate rule if needed */ |
17ef678c RB |
247 | if (!rule && !(rule = git__calloc(1, sizeof(*rule)))) { |
248 | error = -1; | |
249 | break; | |
ee1f0b1a RB |
250 | } |
251 | ||
17ef678c RB |
252 | rule->match.flags = |
253 | GIT_ATTR_FNMATCH_ALLOWNEG | GIT_ATTR_FNMATCH_ALLOWMACRO; | |
254 | ||
ee1f0b1a | 255 | /* parse the next "pattern attr attr attr" line */ |
4ba64794 | 256 | if (!(error = git_attr_fnmatch__parse( |
7d490872 | 257 | &rule->match, &attrs->pool, context, &scan)) && |
19fa2bc1 | 258 | !(error = git_attr_assignment__parse( |
7d490872 | 259 | repo, &attrs->pool, &rule->assigns, &scan))) |
73b51450 RB |
260 | { |
261 | if (rule->match.flags & GIT_ATTR_FNMATCH_MACRO) | |
e3a2a04c | 262 | /* TODO: warning if macro found in file below repo root */ |
73b51450 RB |
263 | error = git_attr_cache__insert_macro(repo, rule); |
264 | else | |
265 | error = git_vector_insert(&attrs->rules, rule); | |
266 | } | |
ee1f0b1a RB |
267 | |
268 | /* if the rule wasn't a pattern, on to the next */ | |
ae9e29fd | 269 | if (error < 0) { |
c6d2a2c0 | 270 | git_attr_rule__clear(rule); /* reset rule contents */ |
ee1f0b1a | 271 | if (error == GIT_ENOTFOUND) |
ae9e29fd | 272 | error = 0; |
ee1f0b1a RB |
273 | } else { |
274 | rule = NULL; /* vector now "owns" the rule */ | |
275 | } | |
276 | } | |
277 | ||
e6e8530a | 278 | git_mutex_unlock(&attrs->lock); |
a51cd8e6 | 279 | git_attr_rule__free(rule); |
f917481e | 280 | |
ee1f0b1a RB |
281 | return error; |
282 | } | |
283 | ||
19fa2bc1 | 284 | uint32_t git_attr_file__name_hash(const char *name) |
ee1f0b1a | 285 | { |
19fa2bc1 | 286 | uint32_t h = 5381; |
ee1f0b1a RB |
287 | int c; |
288 | assert(name); | |
289 | while ((c = (int)*name++) != 0) | |
290 | h = ((h << 5) + h) + c; | |
291 | return h; | |
292 | } | |
293 | ||
ee1f0b1a RB |
294 | int git_attr_file__lookup_one( |
295 | git_attr_file *file, | |
f554611a | 296 | git_attr_path *path, |
ee1f0b1a RB |
297 | const char *attr, |
298 | const char **value) | |
299 | { | |
b8457baa | 300 | size_t i; |
ee1f0b1a RB |
301 | git_attr_name name; |
302 | git_attr_rule *rule; | |
303 | ||
304 | *value = NULL; | |
305 | ||
306 | name.name = attr; | |
307 | name.name_hash = git_attr_file__name_hash(attr); | |
308 | ||
309 | git_attr_file__foreach_matching_rule(file, path, i, rule) { | |
11d9f6b3 | 310 | size_t pos; |
ee1f0b1a | 311 | |
11d9f6b3 | 312 | if (!git_vector_bsearch(&pos, &rule->assigns, &name)) { |
ee1f0b1a RB |
313 | *value = ((git_attr_assignment *) |
314 | git_vector_get(&rule->assigns, pos))->value; | |
315 | break; | |
316 | } | |
317 | } | |
318 | ||
ab43ad2f | 319 | return 0; |
ee1f0b1a RB |
320 | } |
321 | ||
823c0e9c | 322 | int git_attr_file__load_standalone(git_attr_file **out, const char *path) |
7d490872 RB |
323 | { |
324 | int error; | |
325 | git_attr_file *file; | |
326 | git_buf content = GIT_BUF_INIT; | |
327 | ||
823c0e9c | 328 | error = git_attr_file__new(&file, NULL, GIT_ATTR_FILE__FROM_FILE); |
7d490872 RB |
329 | if (error < 0) |
330 | return error; | |
331 | ||
823c0e9c RB |
332 | error = git_attr_cache__alloc_file_entry( |
333 | &file->entry, NULL, path, &file->pool); | |
7d490872 RB |
334 | if (error < 0) { |
335 | git_attr_file__free(file); | |
336 | return error; | |
337 | } | |
338 | /* because the cache entry is allocated from the file's own pool, we | |
339 | * don't have to free it - freeing file+pool will free cache entry, too. | |
340 | */ | |
341 | ||
342 | if (!(error = git_futils_readbuffer(&content, path))) { | |
823c0e9c | 343 | error = git_attr_file__parse_buffer(NULL, file, content.ptr); |
7d490872 RB |
344 | git_buf_free(&content); |
345 | } | |
346 | ||
347 | if (error < 0) | |
348 | git_attr_file__free(file); | |
349 | else | |
350 | *out = file; | |
351 | ||
352 | return error; | |
353 | } | |
ee1f0b1a | 354 | |
ab43ad2f | 355 | bool git_attr_fnmatch__match( |
df743c7d | 356 | git_attr_fnmatch *match, |
f554611a | 357 | git_attr_path *path) |
ee1f0b1a | 358 | { |
90997e40 | 359 | const char *relpath = path->path; |
7d490872 RB |
360 | const char *filename; |
361 | int flags = 0; | |
ee1f0b1a | 362 | |
6069042f CMN |
363 | /* |
364 | * If the rule was generated in a subdirectory, we must only | |
365 | * use it for paths inside that directory. We can thus return | |
366 | * a non-match if the prefixes don't match. | |
367 | */ | |
368 | if (match->containing_dir) { | |
369 | if (match->flags & GIT_ATTR_FNMATCH_ICASE) { | |
370 | if (git__strncasecmp(path->path, match->containing_dir, match->containing_dir_length)) | |
371 | return 0; | |
372 | } else { | |
373 | if (git__prefixcmp(path->path, match->containing_dir)) | |
374 | return 0; | |
375 | } | |
90997e40 ET |
376 | |
377 | relpath += match->containing_dir_length; | |
6069042f CMN |
378 | } |
379 | ||
7d490872 RB |
380 | if (match->flags & GIT_ATTR_FNMATCH_ICASE) |
381 | flags |= FNM_CASEFOLD; | |
916fcbd6 RB |
382 | if (match->flags & GIT_ATTR_FNMATCH_LEADINGDIR) |
383 | flags |= FNM_LEADING_DIR; | |
7d490872 RB |
384 | |
385 | if (match->flags & GIT_ATTR_FNMATCH_FULLPATH) { | |
90997e40 | 386 | filename = relpath; |
7d490872 RB |
387 | flags |= FNM_PATHNAME; |
388 | } else { | |
389 | filename = path->basename; | |
390 | ||
391 | if (path->is_dir) | |
392 | flags |= FNM_LEADING_DIR; | |
393 | } | |
df743c7d | 394 | |
f554611a | 395 | if ((match->flags & GIT_ATTR_FNMATCH_DIRECTORY) && !path->is_dir) { |
34593aae ET |
396 | bool samename; |
397 | ||
f554611a RB |
398 | /* for attribute checks or root ignore checks, fail match */ |
399 | if (!(match->flags & GIT_ATTR_FNMATCH_IGNORE) || | |
400 | path->basename == path->path) | |
401 | return false; | |
402 | ||
f554611a | 403 | flags |= FNM_LEADING_DIR; |
c02a0e46 | 404 | |
ef6d0722 | 405 | /* fail match if this is a file with same name as ignored folder */ |
34593aae | 406 | samename = (match->flags & GIT_ATTR_FNMATCH_ICASE) ? |
90997e40 ET |
407 | !strcasecmp(match->pattern, relpath) : |
408 | !strcmp(match->pattern, relpath); | |
ef6d0722 ET |
409 | |
410 | if (samename) | |
411 | return false; | |
412 | ||
90997e40 | 413 | return (p_fnmatch(match->pattern, relpath, flags) != FNM_NOMATCH); |
f554611a RB |
414 | } |
415 | ||
f25bc0b2 RB |
416 | /* if path is a directory prefix of a negated pattern, then match */ |
417 | if ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) && path->is_dir) { | |
90997e40 | 418 | size_t pathlen = strlen(relpath); |
f25bc0b2 RB |
419 | bool prefixed = (pathlen <= match->length) && |
420 | ((match->flags & GIT_ATTR_FNMATCH_ICASE) ? | |
90997e40 ET |
421 | !strncasecmp(match->pattern, relpath, pathlen) : |
422 | !strncmp(match->pattern, relpath, pathlen)); | |
f25bc0b2 RB |
423 | |
424 | if (prefixed && git_path_at_end_of_segment(&match->pattern[pathlen])) | |
425 | return true; | |
426 | } | |
427 | ||
7d490872 | 428 | return (p_fnmatch(match->pattern, filename, flags) != FNM_NOMATCH); |
df743c7d RB |
429 | } |
430 | ||
ab43ad2f | 431 | bool git_attr_rule__match( |
df743c7d | 432 | git_attr_rule *rule, |
f554611a | 433 | git_attr_path *path) |
df743c7d | 434 | { |
ab43ad2f | 435 | bool matched = git_attr_fnmatch__match(&rule->match, path); |
ee1f0b1a | 436 | |
73b51450 | 437 | if (rule->match.flags & GIT_ATTR_FNMATCH_NEGATIVE) |
ab43ad2f | 438 | matched = !matched; |
ee1f0b1a RB |
439 | |
440 | return matched; | |
441 | } | |
442 | ||
443 | git_attr_assignment *git_attr_rule__lookup_assignment( | |
444 | git_attr_rule *rule, const char *name) | |
445 | { | |
11d9f6b3 | 446 | size_t pos; |
ee1f0b1a RB |
447 | git_attr_name key; |
448 | key.name = name; | |
449 | key.name_hash = git_attr_file__name_hash(name); | |
450 | ||
11d9f6b3 PK |
451 | if (git_vector_bsearch(&pos, &rule->assigns, &key)) |
452 | return NULL; | |
ee1f0b1a | 453 | |
11d9f6b3 | 454 | return git_vector_get(&rule->assigns, pos); |
ee1f0b1a RB |
455 | } |
456 | ||
457 | int git_attr_path__init( | |
4c09e19a | 458 | git_attr_path *info, const char *path, const char *base, git_dir_flag dir_flag) |
ee1f0b1a | 459 | { |
ca1b6e54 RB |
460 | ssize_t root; |
461 | ||
d58336dd RB |
462 | /* build full path as best we can */ |
463 | git_buf_init(&info->full, 0); | |
adc9bdb3 | 464 | |
ca1b6e54 RB |
465 | if (git_path_join_unrooted(&info->full, path, base, &root) < 0) |
466 | return -1; | |
467 | ||
468 | info->path = info->full.ptr + root; | |
d58336dd RB |
469 | |
470 | /* remove trailing slashes */ | |
471 | while (info->full.size > 0) { | |
472 | if (info->full.ptr[info->full.size - 1] != '/') | |
473 | break; | |
474 | info->full.size--; | |
475 | } | |
476 | info->full.ptr[info->full.size] = '\0'; | |
477 | ||
478 | /* skip leading slashes in path */ | |
479 | while (*info->path == '/') | |
480 | info->path++; | |
481 | ||
482 | /* find trailing basename component */ | |
483 | info->basename = strrchr(info->path, '/'); | |
484 | if (info->basename) | |
485 | info->basename++; | |
486 | if (!info->basename || !*info->basename) | |
487 | info->basename = info->path; | |
488 | ||
4c09e19a W |
489 | switch (dir_flag) |
490 | { | |
491 | case GIT_DIR_FLAG_FALSE: | |
492 | info->is_dir = 0; | |
493 | break; | |
494 | ||
495 | case GIT_DIR_FLAG_TRUE: | |
496 | info->is_dir = 1; | |
497 | break; | |
498 | ||
499 | case GIT_DIR_FLAG_UNKNOWN: | |
500 | default: | |
501 | info->is_dir = (int)git_path_isdir(info->full.ptr); | |
502 | break; | |
503 | } | |
adc9bdb3 | 504 | |
ae9e29fd | 505 | return 0; |
ee1f0b1a RB |
506 | } |
507 | ||
d58336dd RB |
508 | void git_attr_path__free(git_attr_path *info) |
509 | { | |
510 | git_buf_free(&info->full); | |
511 | info->path = NULL; | |
512 | info->basename = NULL; | |
513 | } | |
514 | ||
ee1f0b1a RB |
515 | /* |
516 | * From gitattributes(5): | |
517 | * | |
518 | * Patterns have the following format: | |
519 | * | |
520 | * - A blank line matches no files, so it can serve as a separator for | |
521 | * readability. | |
522 | * | |
523 | * - A line starting with # serves as a comment. | |
524 | * | |
525 | * - An optional prefix ! which negates the pattern; any matching file | |
526 | * excluded by a previous pattern will become included again. If a negated | |
527 | * pattern matches, this will override lower precedence patterns sources. | |
528 | * | |
529 | * - If the pattern ends with a slash, it is removed for the purpose of the | |
530 | * following description, but it would only find a match with a directory. In | |
531 | * other words, foo/ will match a directory foo and paths underneath it, but | |
532 | * will not match a regular file or a symbolic link foo (this is consistent | |
533 | * with the way how pathspec works in general in git). | |
534 | * | |
535 | * - If the pattern does not contain a slash /, git treats it as a shell glob | |
536 | * pattern and checks for a match against the pathname without leading | |
537 | * directories. | |
538 | * | |
539 | * - Otherwise, git treats the pattern as a shell glob suitable for consumption | |
540 | * by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will | |
541 | * not match a / in the pathname. For example, "Documentation/\*.html" matches | |
542 | * "Documentation/git.html" but not "Documentation/ppc/ppc.html". A leading | |
543 | * slash matches the beginning of the pathname; for example, "/\*.c" matches | |
544 | * "cat-file.c" but not "mozilla-sha1/sha1.c". | |
545 | */ | |
546 | ||
547 | /* | |
0d0fa7c3 | 548 | * This will return 0 if the spec was filled out, |
ee1f0b1a RB |
549 | * GIT_ENOTFOUND if the fnmatch does not require matching, or |
550 | * another error code there was an actual problem. | |
551 | */ | |
4ba64794 | 552 | int git_attr_fnmatch__parse( |
ee1f0b1a | 553 | git_attr_fnmatch *spec, |
19fa2bc1 | 554 | git_pool *pool, |
7d490872 | 555 | const char *context, |
ee1f0b1a RB |
556 | const char **base) |
557 | { | |
4ba64794 RB |
558 | const char *pattern, *scan; |
559 | int slash_count, allow_space; | |
ee1f0b1a | 560 | |
df743c7d | 561 | assert(spec && base && *base); |
ee1f0b1a | 562 | |
4ba64794 RB |
563 | if (parse_optimized_patterns(spec, pool, *base)) |
564 | return 0; | |
565 | ||
566 | spec->flags = (spec->flags & GIT_ATTR_FNMATCH__INCOMING); | |
567 | allow_space = ((spec->flags & GIT_ATTR_FNMATCH_ALLOWSPACE) != 0); | |
568 | ||
ee1f0b1a RB |
569 | pattern = *base; |
570 | ||
0f49200c | 571 | while (git__isspace(*pattern)) pattern++; |
ee1f0b1a | 572 | if (!*pattern || *pattern == '#') { |
df743c7d RB |
573 | *base = git__next_line(pattern); |
574 | return GIT_ENOTFOUND; | |
ee1f0b1a RB |
575 | } |
576 | ||
4ba64794 | 577 | if (*pattern == '[' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWMACRO) != 0) { |
73b51450 RB |
578 | if (strncmp(pattern, "[attr]", 6) == 0) { |
579 | spec->flags = spec->flags | GIT_ATTR_FNMATCH_MACRO; | |
580 | pattern += 6; | |
73b51450 | 581 | } |
df743c7d | 582 | /* else a character range like [a-e]* which is accepted */ |
73b51450 RB |
583 | } |
584 | ||
4ba64794 | 585 | if (*pattern == '!' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWNEG) != 0) { |
a0cacc82 RB |
586 | spec->flags = spec->flags | |
587 | GIT_ATTR_FNMATCH_NEGATIVE | GIT_ATTR_FNMATCH_LEADINGDIR; | |
ee1f0b1a | 588 | pattern++; |
ee1f0b1a RB |
589 | } |
590 | ||
ee1f0b1a RB |
591 | slash_count = 0; |
592 | for (scan = pattern; *scan != '\0'; ++scan) { | |
df743c7d | 593 | /* scan until (non-escaped) white space */ |
2a99df69 | 594 | if (git__isspace(*scan) && *(scan - 1) != '\\') { |
5c54e216 | 595 | if (!allow_space || (*scan != ' ' && *scan != '\t' && *scan != '\r')) |
2a99df69 RB |
596 | break; |
597 | } | |
ee1f0b1a RB |
598 | |
599 | if (*scan == '/') { | |
73b51450 | 600 | spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH; |
ee1f0b1a | 601 | slash_count++; |
d58336dd RB |
602 | if (pattern == scan) |
603 | pattern++; | |
ee1f0b1a | 604 | } |
14a513e0 | 605 | /* remember if we see an unescaped wildcard in pattern */ |
41a82592 | 606 | else if (git__iswildcard(*scan) && |
14a513e0 RB |
607 | (scan == pattern || (*(scan - 1) != '\\'))) |
608 | spec->flags = spec->flags | GIT_ATTR_FNMATCH_HASWILD; | |
ee1f0b1a RB |
609 | } |
610 | ||
611 | *base = scan; | |
df743c7d | 612 | |
2d160ef7 ET |
613 | if ((spec->length = scan - pattern) == 0) |
614 | return GIT_ENOTFOUND; | |
ee1f0b1a | 615 | |
5c54e216 CMN |
616 | /* |
617 | * Remove one trailing \r in case this is a CRLF delimited | |
618 | * file, in the case of Icon\r\r\n, we still leave the first | |
619 | * \r there to match against. | |
620 | */ | |
621 | if (pattern[spec->length - 1] == '\r') | |
622 | if (--spec->length == 0) | |
623 | return GIT_ENOTFOUND; | |
624 | ||
ee1f0b1a RB |
625 | if (pattern[spec->length - 1] == '/') { |
626 | spec->length--; | |
73b51450 | 627 | spec->flags = spec->flags | GIT_ATTR_FNMATCH_DIRECTORY; |
ee1f0b1a | 628 | if (--slash_count <= 0) |
73b51450 | 629 | spec->flags = spec->flags & ~GIT_ATTR_FNMATCH_FULLPATH; |
ee1f0b1a | 630 | } |
ac16bd0a RB |
631 | if ((spec->flags & GIT_ATTR_FNMATCH_NOLEADINGDIR) == 0 && |
632 | spec->length >= 2 && | |
916fcbd6 RB |
633 | pattern[spec->length - 1] == '*' && |
634 | pattern[spec->length - 2] == '/') { | |
635 | spec->length -= 2; | |
636 | spec->flags = spec->flags | GIT_ATTR_FNMATCH_LEADINGDIR; | |
637 | /* leave FULLPATH match on, however */ | |
638 | } | |
ee1f0b1a | 639 | |
6069042f | 640 | if (context) { |
90997e40 | 641 | char *slash = strrchr(context, '/'); |
6069042f CMN |
642 | size_t len; |
643 | if (slash) { | |
644 | /* include the slash for easier matching */ | |
645 | len = slash - context + 1; | |
646 | spec->containing_dir = git_pool_strndup(pool, context, len); | |
647 | spec->containing_dir_length = len; | |
648 | } | |
649 | } | |
650 | ||
90997e40 | 651 | spec->pattern = git_pool_strndup(pool, pattern, spec->length); |
a51cd8e6 RB |
652 | |
653 | if (!spec->pattern) { | |
654 | *base = git__next_line(pattern); | |
ae9e29fd | 655 | return -1; |
a51cd8e6 | 656 | } else { |
83bfbdf5 | 657 | /* strip '\' that might have be used for internal whitespace */ |
02a0d651 | 658 | spec->length = git__unescape(spec->pattern); |
7d490872 | 659 | /* TODO: convert remaining '\' into '/' for POSIX ??? */ |
a51cd8e6 RB |
660 | } |
661 | ||
ae9e29fd | 662 | return 0; |
ee1f0b1a RB |
663 | } |
664 | ||
0d32f39e | 665 | static bool parse_optimized_patterns( |
666 | git_attr_fnmatch *spec, | |
667 | git_pool *pool, | |
668 | const char *pattern) | |
669 | { | |
670 | if (!pattern[1] && (pattern[0] == '*' || pattern[0] == '.')) { | |
671 | spec->flags = GIT_ATTR_FNMATCH_MATCH_ALL; | |
672 | spec->pattern = git_pool_strndup(pool, pattern, 1); | |
673 | spec->length = 1; | |
674 | ||
675 | return true; | |
676 | } | |
677 | ||
678 | return false; | |
679 | } | |
680 | ||
ee1f0b1a RB |
681 | static int sort_by_hash_and_name(const void *a_raw, const void *b_raw) |
682 | { | |
683 | const git_attr_name *a = a_raw; | |
684 | const git_attr_name *b = b_raw; | |
685 | ||
686 | if (b->name_hash < a->name_hash) | |
687 | return 1; | |
688 | else if (b->name_hash > a->name_hash) | |
689 | return -1; | |
690 | else | |
691 | return strcmp(b->name, a->name); | |
692 | } | |
693 | ||
bd370b14 | 694 | static void git_attr_assignment__free(git_attr_assignment *assign) |
73b51450 | 695 | { |
19fa2bc1 RB |
696 | /* name and value are stored in a git_pool associated with the |
697 | * git_attr_file, so they do not need to be freed here | |
698 | */ | |
73b51450 | 699 | assign->name = NULL; |
19fa2bc1 | 700 | assign->value = NULL; |
73b51450 RB |
701 | git__free(assign); |
702 | } | |
703 | ||
bd370b14 RB |
704 | static int merge_assignments(void **old_raw, void *new_raw) |
705 | { | |
706 | git_attr_assignment **old = (git_attr_assignment **)old_raw; | |
707 | git_attr_assignment *new = (git_attr_assignment *)new_raw; | |
708 | ||
709 | GIT_REFCOUNT_DEC(*old, git_attr_assignment__free); | |
710 | *old = new; | |
711 | return GIT_EEXISTS; | |
712 | } | |
713 | ||
73b51450 RB |
714 | int git_attr_assignment__parse( |
715 | git_repository *repo, | |
19fa2bc1 | 716 | git_pool *pool, |
ee1f0b1a RB |
717 | git_vector *assigns, |
718 | const char **base) | |
719 | { | |
ae9e29fd | 720 | int error; |
ee1f0b1a RB |
721 | const char *scan = *base; |
722 | git_attr_assignment *assign = NULL; | |
723 | ||
724 | assert(assigns && !assigns->length); | |
725 | ||
22b6b82f | 726 | git_vector_set_cmp(assigns, sort_by_hash_and_name); |
bd370b14 | 727 | |
ae9e29fd | 728 | while (*scan && *scan != '\n') { |
ee1f0b1a RB |
729 | const char *name_start, *value_start; |
730 | ||
731 | /* skip leading blanks */ | |
0f49200c | 732 | while (git__isspace(*scan) && *scan != '\n') scan++; |
ee1f0b1a RB |
733 | |
734 | /* allocate assign if needed */ | |
735 | if (!assign) { | |
736 | assign = git__calloc(1, sizeof(git_attr_assignment)); | |
ae9e29fd | 737 | GITERR_CHECK_ALLOC(assign); |
bd370b14 | 738 | GIT_REFCOUNT_INC(assign); |
ee1f0b1a RB |
739 | } |
740 | ||
741 | assign->name_hash = 5381; | |
0c9eacf3 | 742 | assign->value = git_attr__true; |
ee1f0b1a RB |
743 | |
744 | /* look for magic name prefixes */ | |
745 | if (*scan == '-') { | |
0c9eacf3 | 746 | assign->value = git_attr__false; |
ee1f0b1a RB |
747 | scan++; |
748 | } else if (*scan == '!') { | |
0c9eacf3 | 749 | assign->value = git_attr__unset; /* explicit unspecified state */ |
ee1f0b1a RB |
750 | scan++; |
751 | } else if (*scan == '#') /* comment rest of line */ | |
752 | break; | |
753 | ||
754 | /* find the name */ | |
755 | name_start = scan; | |
0f49200c | 756 | while (*scan && !git__isspace(*scan) && *scan != '=') { |
ee1f0b1a RB |
757 | assign->name_hash = |
758 | ((assign->name_hash << 5) + assign->name_hash) + *scan; | |
759 | scan++; | |
760 | } | |
73b51450 | 761 | if (scan == name_start) { |
ee1f0b1a RB |
762 | /* must have found lone prefix (" - ") or leading = ("=foo") |
763 | * or end of buffer -- advance until whitespace and continue | |
764 | */ | |
0f49200c | 765 | while (*scan && !git__isspace(*scan)) scan++; |
ee1f0b1a RB |
766 | continue; |
767 | } | |
768 | ||
73b51450 | 769 | /* allocate permanent storage for name */ |
19fa2bc1 | 770 | assign->name = git_pool_strndup(pool, name_start, scan - name_start); |
ae9e29fd | 771 | GITERR_CHECK_ALLOC(assign->name); |
73b51450 | 772 | |
ee1f0b1a RB |
773 | /* if there is an equals sign, find the value */ |
774 | if (*scan == '=') { | |
0f49200c | 775 | for (value_start = ++scan; *scan && !git__isspace(*scan); ++scan); |
ee1f0b1a RB |
776 | |
777 | /* if we found a value, allocate permanent storage for it */ | |
778 | if (scan > value_start) { | |
19fa2bc1 | 779 | assign->value = git_pool_strndup(pool, value_start, scan - value_start); |
ae9e29fd | 780 | GITERR_CHECK_ALLOC(assign->value); |
ee1f0b1a RB |
781 | } |
782 | } | |
783 | ||
bd370b14 | 784 | /* expand macros (if given a repo with a macro cache) */ |
0c9eacf3 | 785 | if (repo != NULL && assign->value == git_attr__true) { |
01fed0a8 RB |
786 | git_attr_rule *macro = |
787 | git_attr_cache__lookup_macro(repo, assign->name); | |
73b51450 RB |
788 | |
789 | if (macro != NULL) { | |
790 | unsigned int i; | |
791 | git_attr_assignment *massign; | |
792 | ||
bd370b14 RB |
793 | git_vector_foreach(¯o->assigns, i, massign) { |
794 | GIT_REFCOUNT_INC(massign); | |
73b51450 | 795 | |
bd370b14 RB |
796 | error = git_vector_insert_sorted( |
797 | assigns, massign, &merge_assignments); | |
6f73e026 JG |
798 | if (error < 0 && error != GIT_EEXISTS) { |
799 | git_attr_assignment__free(assign); | |
ae9e29fd | 800 | return error; |
6f73e026 | 801 | } |
73b51450 | 802 | } |
73b51450 | 803 | } |
ee1f0b1a RB |
804 | } |
805 | ||
806 | /* insert allocated assign into vector */ | |
bd370b14 | 807 | error = git_vector_insert_sorted(assigns, assign, &merge_assignments); |
ae9e29fd RB |
808 | if (error < 0 && error != GIT_EEXISTS) |
809 | return error; | |
ee1f0b1a RB |
810 | |
811 | /* clear assign since it is now "owned" by the vector */ | |
812 | assign = NULL; | |
813 | } | |
814 | ||
73b51450 | 815 | if (assign != NULL) |
bd370b14 | 816 | git_attr_assignment__free(assign); |
ee1f0b1a | 817 | |
df743c7d | 818 | *base = git__next_line(scan); |
ee1f0b1a | 819 | |
ae9e29fd | 820 | return (assigns->length == 0) ? GIT_ENOTFOUND : 0; |
ee1f0b1a RB |
821 | } |
822 | ||
c6d2a2c0 | 823 | static void git_attr_rule__clear(git_attr_rule *rule) |
ee1f0b1a RB |
824 | { |
825 | unsigned int i; | |
826 | git_attr_assignment *assign; | |
827 | ||
828 | if (!rule) | |
73b51450 | 829 | return; |
ee1f0b1a | 830 | |
df743c7d RB |
831 | if (!(rule->match.flags & GIT_ATTR_FNMATCH_IGNORE)) { |
832 | git_vector_foreach(&rule->assigns, i, assign) | |
833 | GIT_REFCOUNT_DEC(assign, git_attr_assignment__free); | |
834 | git_vector_free(&rule->assigns); | |
835 | } | |
836 | ||
19fa2bc1 | 837 | /* match.pattern is stored in a git_pool, so no need to free */ |
ee1f0b1a RB |
838 | rule->match.pattern = NULL; |
839 | rule->match.length = 0; | |
ee1f0b1a | 840 | } |
c6d2a2c0 RB |
841 | |
842 | void git_attr_rule__free(git_attr_rule *rule) | |
843 | { | |
844 | git_attr_rule__clear(rule); | |
845 | git__free(rule); | |
846 | } | |
847 | ||
9f779aac ET |
848 | int git_attr_session__init(git_attr_session *session, git_repository *repo) |
849 | { | |
850 | assert(repo); | |
851 | ||
852 | session->key = git_atomic_inc(&repo->attr_session_key); | |
853 | ||
854 | return 0; | |
855 | } | |
d4b1b767 ET |
856 | |
857 | void git_attr_session__free(git_attr_session *session) | |
858 | { | |
859 | if (!session) | |
860 | return; | |
861 | ||
862 | git_buf_free(&session->sysdir); | |
f58cc280 | 863 | git_buf_free(&session->tmp); |
d4b1b767 ET |
864 | |
865 | memset(session, 0, sizeof(git_attr_session)); | |
866 | } |