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