]>
Commit | Line | Data |
---|---|---|
2e3d4b96 | 1 | /* |
359fc2d2 | 2 | * Copyright (C) the libgit2 contributors. All rights reserved. |
2e3d4b96 RB |
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 | ||
d2ce27dd | 8 | #include "git2/pathspec.h" |
2e3d4b96 | 9 | #include "pathspec.h" |
7bf87ab6 | 10 | #include "buf_text.h" |
2e3d4b96 | 11 | #include "attr_file.h" |
d2ce27dd RB |
12 | #include "iterator.h" |
13 | #include "repository.h" | |
14 | #include "index.h" | |
2e3d4b96 RB |
15 | |
16 | /* what is the common non-wildcard prefix for all items in the pathspec */ | |
17 | char *git_pathspec_prefix(const git_strarray *pathspec) | |
18 | { | |
19 | git_buf prefix = GIT_BUF_INIT; | |
20 | const char *scan; | |
21 | ||
22 | if (!pathspec || !pathspec->count || | |
7bf87ab6 | 23 | git_buf_text_common_prefix(&prefix, pathspec) < 0) |
2e3d4b96 RB |
24 | return NULL; |
25 | ||
26 | /* diff prefix will only be leading non-wildcards */ | |
27 | for (scan = prefix.ptr; *scan; ++scan) { | |
28 | if (git__iswildcard(*scan) && | |
29 | (scan == prefix.ptr || (*(scan - 1) != '\\'))) | |
30 | break; | |
31 | } | |
32 | git_buf_truncate(&prefix, scan - prefix.ptr); | |
33 | ||
34 | if (prefix.size <= 0) { | |
35 | git_buf_free(&prefix); | |
36 | return NULL; | |
37 | } | |
38 | ||
7bf87ab6 | 39 | git_buf_text_unescape(&prefix); |
2e3d4b96 RB |
40 | |
41 | return git_buf_detach(&prefix); | |
42 | } | |
43 | ||
44 | /* is there anything in the spec that needs to be filtered on */ | |
0d32f39e | 45 | bool git_pathspec_is_empty(const git_strarray *pathspec) |
2e3d4b96 | 46 | { |
0d32f39e | 47 | size_t i; |
2e3d4b96 | 48 | |
0d32f39e | 49 | if (pathspec == NULL) |
2e3d4b96 RB |
50 | return true; |
51 | ||
0d32f39e | 52 | for (i = 0; i < pathspec->count; ++i) { |
53 | const char *str = pathspec->strings[i]; | |
54 | ||
55 | if (str && str[0]) | |
56 | return false; | |
57 | } | |
58 | ||
2e3d4b96 RB |
59 | return true; |
60 | } | |
61 | ||
62 | /* build a vector of fnmatch patterns to evaluate efficiently */ | |
d2ce27dd | 63 | int git_pathspec__vinit( |
2e3d4b96 RB |
64 | git_vector *vspec, const git_strarray *strspec, git_pool *strpool) |
65 | { | |
66 | size_t i; | |
67 | ||
68 | memset(vspec, 0, sizeof(*vspec)); | |
69 | ||
0d32f39e | 70 | if (git_pathspec_is_empty(strspec)) |
2e3d4b96 RB |
71 | return 0; |
72 | ||
73 | if (git_vector_init(vspec, strspec->count, NULL) < 0) | |
74 | return -1; | |
75 | ||
76 | for (i = 0; i < strspec->count; ++i) { | |
77 | int ret; | |
78 | const char *pattern = strspec->strings[i]; | |
79 | git_attr_fnmatch *match = git__calloc(1, sizeof(git_attr_fnmatch)); | |
80 | if (!match) | |
81 | return -1; | |
82 | ||
83 | match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE; | |
84 | ||
85 | ret = git_attr_fnmatch__parse(match, strpool, NULL, &pattern); | |
86 | if (ret == GIT_ENOTFOUND) { | |
87 | git__free(match); | |
88 | continue; | |
89 | } else if (ret < 0) | |
90 | return ret; | |
91 | ||
92 | if (git_vector_insert(vspec, match) < 0) | |
93 | return -1; | |
94 | } | |
95 | ||
96 | return 0; | |
97 | } | |
98 | ||
99 | /* free data from the pathspec vector */ | |
d2ce27dd | 100 | void git_pathspec__vfree(git_vector *vspec) |
2e3d4b96 RB |
101 | { |
102 | git_attr_fnmatch *match; | |
103 | unsigned int i; | |
104 | ||
105 | git_vector_foreach(vspec, i, match) { | |
106 | git__free(match); | |
107 | vspec->contents[i] = NULL; | |
108 | } | |
109 | ||
110 | git_vector_free(vspec); | |
111 | } | |
112 | ||
d2ce27dd RB |
113 | struct pathspec_match_context { |
114 | int fnmatch_flags; | |
115 | int (*strcomp)(const char *, const char *); | |
116 | int (*strncomp)(const char *, const char *, size_t); | |
117 | }; | |
118 | ||
119 | static void pathspec_match_context_init( | |
120 | struct pathspec_match_context *ctxt, | |
121 | bool disable_fnmatch, | |
122 | bool casefold) | |
123 | { | |
124 | if (disable_fnmatch) | |
125 | ctxt->fnmatch_flags = -1; | |
126 | else if (casefold) | |
127 | ctxt->fnmatch_flags = FNM_CASEFOLD; | |
128 | else | |
129 | ctxt->fnmatch_flags = 0; | |
130 | ||
131 | if (casefold) { | |
132 | ctxt->strcomp = git__strcasecmp; | |
133 | ctxt->strncomp = git__strncasecmp; | |
134 | } else { | |
135 | ctxt->strcomp = git__strcmp; | |
136 | ctxt->strncomp = git__strncmp; | |
137 | } | |
138 | } | |
139 | ||
140 | static int pathspec_match_one( | |
141 | const git_attr_fnmatch *match, | |
142 | struct pathspec_match_context *ctxt, | |
143 | const char *path) | |
144 | { | |
145 | int result = (match->flags & GIT_ATTR_FNMATCH_MATCH_ALL) ? 0 : FNM_NOMATCH; | |
146 | ||
147 | if (result == FNM_NOMATCH) | |
148 | result = ctxt->strcomp(match->pattern, path) ? FNM_NOMATCH : 0; | |
149 | ||
150 | if (ctxt->fnmatch_flags >= 0 && result == FNM_NOMATCH) | |
151 | result = p_fnmatch(match->pattern, path, ctxt->fnmatch_flags); | |
152 | ||
153 | /* if we didn't match, look for exact dirname prefix match */ | |
154 | if (result == FNM_NOMATCH && | |
155 | (match->flags & GIT_ATTR_FNMATCH_HASWILD) == 0 && | |
156 | ctxt->strncomp(path, match->pattern, match->length) == 0 && | |
157 | path[match->length] == '/') | |
158 | result = 0; | |
159 | ||
160 | if (result == 0) | |
161 | return (match->flags & GIT_ATTR_FNMATCH_NEGATIVE) ? 0 : 1; | |
162 | return -1; | |
163 | } | |
164 | ||
2e3d4b96 | 165 | /* match a path against the vectorized pathspec */ |
d2ce27dd RB |
166 | bool git_pathspec__match( |
167 | const git_vector *vspec, | |
943700ec | 168 | const char *path, |
169 | bool disable_fnmatch, | |
170 | bool casefold, | |
d2ce27dd RB |
171 | const char **matched_pathspec, |
172 | size_t *matched_at) | |
2e3d4b96 | 173 | { |
943700ec | 174 | size_t i; |
d2ce27dd RB |
175 | const git_attr_fnmatch *match; |
176 | struct pathspec_match_context ctxt; | |
2e3d4b96 | 177 | |
943700ec | 178 | if (matched_pathspec) |
179 | *matched_pathspec = NULL; | |
d2ce27dd RB |
180 | if (matched_at) |
181 | *matched_at = GIT_PATHSPEC_NOMATCH; | |
943700ec | 182 | |
2e3d4b96 RB |
183 | if (!vspec || !vspec->length) |
184 | return true; | |
185 | ||
d2ce27dd | 186 | pathspec_match_context_init(&ctxt, disable_fnmatch, casefold); |
2e3d4b96 | 187 | |
d2ce27dd RB |
188 | git_vector_foreach(vspec, i, match) { |
189 | int result = pathspec_match_one(match, &ctxt, path); | |
190 | ||
191 | if (result >= 0) { | |
192 | if (matched_pathspec) | |
193 | *matched_pathspec = match->pattern; | |
194 | if (matched_at) | |
195 | *matched_at = i; | |
196 | ||
197 | return (result != 0); | |
198 | } | |
2e3d4b96 RB |
199 | } |
200 | ||
d2ce27dd RB |
201 | return false; |
202 | } | |
1fed6b07 | 203 | |
2e3d4b96 | 204 | |
d2ce27dd RB |
205 | int git_pathspec__init(git_pathspec *ps, const git_strarray *paths) |
206 | { | |
207 | int error = 0; | |
2e3d4b96 | 208 | |
d2ce27dd | 209 | memset(ps, 0, sizeof(*ps)); |
2e3d4b96 | 210 | |
d2ce27dd RB |
211 | ps->prefix = git_pathspec_prefix(paths); |
212 | ||
213 | if ((error = git_pool_init(&ps->pool, 1, 0)) < 0 || | |
214 | (error = git_pathspec__vinit(&ps->pathspec, paths, &ps->pool)) < 0) | |
215 | git_pathspec__clear(ps); | |
216 | ||
217 | return error; | |
218 | } | |
219 | ||
220 | void git_pathspec__clear(git_pathspec *ps) | |
221 | { | |
222 | git__free(ps->prefix); | |
223 | git_pathspec__vfree(&ps->pathspec); | |
224 | git_pool_clear(&ps->pool); | |
225 | memset(ps, 0, sizeof(*ps)); | |
226 | } | |
227 | ||
228 | int git_pathspec_new(git_pathspec **out, const git_strarray *pathspec) | |
229 | { | |
230 | int error = 0; | |
231 | git_pathspec *ps = git__malloc(sizeof(git_pathspec)); | |
232 | GITERR_CHECK_ALLOC(ps); | |
233 | ||
234 | if ((error = git_pathspec__init(ps, pathspec)) < 0) { | |
235 | git__free(ps); | |
236 | return error; | |
237 | } | |
238 | ||
239 | GIT_REFCOUNT_INC(ps); | |
240 | *out = ps; | |
241 | return 0; | |
242 | } | |
243 | ||
244 | static void pathspec_free(git_pathspec *ps) | |
245 | { | |
246 | git_pathspec__clear(ps); | |
247 | git__free(ps); | |
248 | } | |
249 | ||
250 | void git_pathspec_free(git_pathspec *ps) | |
251 | { | |
252 | if (!ps) | |
253 | return; | |
254 | GIT_REFCOUNT_DEC(ps, pathspec_free); | |
255 | } | |
943700ec | 256 | |
d2ce27dd RB |
257 | int git_pathspec_matches_path( |
258 | const git_pathspec *ps, uint32_t flags, const char *path) | |
259 | { | |
260 | bool no_fnmatch = (flags & GIT_PATHSPEC_NO_GLOB) != 0; | |
261 | bool casefold = (flags & GIT_PATHSPEC_IGNORE_CASE) != 0; | |
262 | ||
263 | assert(ps && path); | |
264 | ||
265 | return (0 != git_pathspec__match( | |
266 | &ps->pathspec, path, no_fnmatch, casefold, NULL, NULL)); | |
267 | } | |
268 | ||
269 | static void pathspec_match_free(git_pathspec_match_list *m) | |
270 | { | |
271 | git_pathspec_free(m->pathspec); | |
272 | m->pathspec = NULL; | |
273 | ||
274 | git_array_clear(m->matches); | |
275 | git_array_clear(m->failures); | |
276 | git_pool_clear(&m->pool); | |
277 | git__free(m); | |
278 | } | |
279 | ||
280 | static git_pathspec_match_list *pathspec_match_alloc(git_pathspec *ps) | |
281 | { | |
282 | git_pathspec_match_list *m = git__calloc(1, sizeof(git_pathspec_match_list)); | |
283 | ||
284 | if (m != NULL && git_pool_init(&m->pool, 1, 0) < 0) { | |
285 | pathspec_match_free(m); | |
286 | m = NULL; | |
287 | } | |
288 | ||
289 | /* need to keep reference to pathspec and increment refcount because | |
290 | * failures array stores pointers to the pattern strings of the | |
291 | * pathspec that had no matches | |
292 | */ | |
293 | GIT_REFCOUNT_INC(ps); | |
294 | m->pathspec = ps; | |
295 | ||
296 | return m; | |
297 | } | |
298 | ||
299 | GIT_INLINE(void) pathspec_mark_pattern(uint8_t *used, size_t pos, size_t *ct) | |
300 | { | |
301 | if (!used[pos]) { | |
302 | used[pos] = 1; | |
303 | (*ct)++; | |
304 | } | |
305 | } | |
306 | ||
307 | static int pathspec_match_from_iterator( | |
308 | git_pathspec_match_list **out, | |
309 | git_iterator *iter, | |
310 | uint32_t flags, | |
311 | git_pathspec *ps) | |
312 | { | |
313 | int error = 0; | |
a8b5f116 | 314 | git_pathspec_match_list *m = NULL; |
d2ce27dd RB |
315 | const git_index_entry *entry = NULL; |
316 | struct pathspec_match_context ctxt; | |
317 | git_vector *patterns = &ps->pathspec; | |
318 | bool find_failures = (flags & GIT_PATHSPEC_FIND_FAILURES) != 0; | |
319 | bool failures_only = (flags & GIT_PATHSPEC_FAILURES_ONLY) != 0; | |
320 | size_t pos, used_ct = 0, found_files = 0; | |
321 | git_index *index = NULL; | |
322 | uint8_t *used_patterns = NULL; | |
323 | char **file; | |
324 | ||
a8b5f116 RB |
325 | if (out) { |
326 | *out = m = pathspec_match_alloc(ps); | |
327 | GITERR_CHECK_ALLOC(m); | |
328 | } else { | |
329 | failures_only = true; | |
330 | find_failures = false; | |
331 | } | |
d2ce27dd RB |
332 | |
333 | if ((error = git_iterator_reset(iter, ps->prefix, ps->prefix)) < 0) | |
334 | goto done; | |
335 | ||
336 | if (patterns->length > 0) { | |
337 | used_patterns = git__calloc(patterns->length, sizeof(uint8_t)); | |
338 | GITERR_CHECK_ALLOC(used_patterns); | |
339 | } | |
340 | ||
341 | if (git_iterator_type(iter) == GIT_ITERATOR_TYPE_WORKDIR && | |
342 | (error = git_repository_index__weakptr( | |
343 | &index, git_iterator_owner(iter))) < 0) | |
344 | goto done; | |
345 | ||
346 | pathspec_match_context_init(&ctxt, | |
347 | (flags & GIT_PATHSPEC_NO_GLOB) != 0, git_iterator_ignore_case(iter)); | |
348 | ||
349 | while (!(error = git_iterator_advance(&entry, iter))) { | |
350 | int result = -1; | |
351 | ||
352 | for (pos = 0; pos < patterns->length; ++pos) { | |
353 | const git_attr_fnmatch *pat = git_vector_get(patterns, pos); | |
354 | ||
355 | result = pathspec_match_one(pat, &ctxt, entry->path); | |
356 | if (result >= 0) | |
357 | break; | |
358 | } | |
359 | ||
360 | /* no matches for this path */ | |
361 | if (result < 0) | |
362 | continue; | |
363 | ||
364 | /* if result was a negative pattern match, then don't list file */ | |
365 | if (!result) { | |
366 | pathspec_mark_pattern(used_patterns, pos, &used_ct); | |
367 | continue; | |
368 | } | |
369 | ||
370 | /* check if path is untracked and ignored */ | |
371 | if (index != NULL && | |
372 | git_iterator_current_is_ignored(iter) && | |
373 | git_index__find(NULL, index, entry->path, GIT_INDEX_STAGE_ANY) < 0) | |
374 | continue; | |
375 | ||
376 | /* mark the matched pattern as used */ | |
377 | pathspec_mark_pattern(used_patterns, pos, &used_ct); | |
378 | ++found_files; | |
379 | ||
380 | /* if find_failures is on, check if any later patterns also match */ | |
381 | if (find_failures && used_ct < patterns->length) { | |
382 | for (++pos; pos < patterns->length; ++pos) { | |
383 | const git_attr_fnmatch *pat = git_vector_get(patterns, pos); | |
384 | if (used_patterns[pos]) | |
385 | continue; | |
386 | ||
387 | if (pathspec_match_one(pat, &ctxt, entry->path) > 0) | |
388 | pathspec_mark_pattern(used_patterns, pos, &used_ct); | |
389 | } | |
390 | } | |
391 | ||
392 | /* if only looking at failures, exit early or just continue */ | |
a8b5f116 | 393 | if (failures_only || !out) { |
d2ce27dd RB |
394 | if (used_ct == patterns->length) |
395 | break; | |
396 | continue; | |
397 | } | |
398 | ||
399 | /* insert matched path into matches array */ | |
400 | if ((file = git_array_alloc(m->matches)) == NULL || | |
401 | (*file = git_pool_strdup(&m->pool, entry->path)) == NULL) { | |
402 | error = -1; | |
403 | goto done; | |
943700ec | 404 | } |
2e3d4b96 RB |
405 | } |
406 | ||
d2ce27dd RB |
407 | if (error < 0 && error != GIT_ITEROVER) |
408 | goto done; | |
409 | error = 0; | |
410 | ||
411 | /* insert patterns that had no matches into failures array */ | |
412 | if (find_failures && used_ct < patterns->length) { | |
413 | for (pos = 0; pos < patterns->length; ++pos) { | |
414 | const git_attr_fnmatch *pat = git_vector_get(patterns, pos); | |
415 | if (used_patterns[pos]) | |
416 | continue; | |
417 | ||
418 | if ((file = git_array_alloc(m->failures)) == NULL || | |
419 | (*file = git_pool_strdup(&m->pool, pat->pattern)) == NULL) { | |
420 | error = -1; | |
421 | goto done; | |
422 | } | |
423 | } | |
424 | } | |
425 | ||
426 | /* if every pattern failed to match, then we have failed */ | |
427 | if ((flags & GIT_PATHSPEC_NO_MATCH_ERROR) != 0 && !found_files) { | |
428 | giterr_set(GITERR_INVALID, "No matching files were found"); | |
429 | error = GIT_ENOTFOUND; | |
430 | } | |
431 | ||
432 | done: | |
433 | git__free(used_patterns); | |
434 | ||
435 | if (error < 0) { | |
436 | pathspec_match_free(m); | |
a8b5f116 | 437 | if (out) *out = NULL; |
d2ce27dd RB |
438 | } |
439 | ||
440 | return error; | |
2e3d4b96 RB |
441 | } |
442 | ||
d2ce27dd RB |
443 | static git_iterator_flag_t pathspec_match_iter_flags(uint32_t flags) |
444 | { | |
445 | git_iterator_flag_t f = 0; | |
446 | ||
447 | if ((flags & GIT_PATHSPEC_IGNORE_CASE) != 0) | |
448 | f |= GIT_ITERATOR_IGNORE_CASE; | |
449 | else if ((flags & GIT_PATHSPEC_USE_CASE) != 0) | |
450 | f |= GIT_ITERATOR_DONT_IGNORE_CASE; | |
e91f9a8f | 451 | |
d2ce27dd RB |
452 | return f; |
453 | } | |
454 | ||
455 | int git_pathspec_match_workdir( | |
456 | git_pathspec_match_list **out, | |
457 | git_repository *repo, | |
458 | uint32_t flags, | |
459 | git_pathspec *ps) | |
e91f9a8f RB |
460 | { |
461 | int error = 0; | |
d2ce27dd RB |
462 | git_iterator *iter; |
463 | ||
a8b5f116 | 464 | assert(repo); |
e91f9a8f | 465 | |
d2ce27dd RB |
466 | if (!(error = git_iterator_for_workdir( |
467 | &iter, repo, pathspec_match_iter_flags(flags), NULL, NULL))) { | |
e91f9a8f | 468 | |
d2ce27dd | 469 | error = pathspec_match_from_iterator(out, iter, flags, ps); |
e91f9a8f | 470 | |
d2ce27dd RB |
471 | git_iterator_free(iter); |
472 | } | |
e91f9a8f RB |
473 | |
474 | return error; | |
475 | } | |
476 | ||
d2ce27dd RB |
477 | int git_pathspec_match_index( |
478 | git_pathspec_match_list **out, | |
479 | git_index *index, | |
480 | uint32_t flags, | |
481 | git_pathspec *ps) | |
482 | { | |
483 | int error = 0; | |
484 | git_iterator *iter; | |
485 | ||
a8b5f116 | 486 | assert(index); |
d2ce27dd RB |
487 | |
488 | if (!(error = git_iterator_for_index( | |
489 | &iter, index, pathspec_match_iter_flags(flags), NULL, NULL))) { | |
490 | ||
491 | error = pathspec_match_from_iterator(out, iter, flags, ps); | |
492 | ||
493 | git_iterator_free(iter); | |
494 | } | |
495 | ||
496 | return error; | |
497 | } | |
498 | ||
499 | int git_pathspec_match_tree( | |
500 | git_pathspec_match_list **out, | |
501 | git_tree *tree, | |
502 | uint32_t flags, | |
503 | git_pathspec *ps) | |
504 | { | |
505 | int error = 0; | |
506 | git_iterator *iter; | |
507 | ||
a8b5f116 | 508 | assert(tree); |
d2ce27dd RB |
509 | |
510 | if (!(error = git_iterator_for_tree( | |
511 | &iter, tree, pathspec_match_iter_flags(flags), NULL, NULL))) { | |
512 | ||
513 | error = pathspec_match_from_iterator(out, iter, flags, ps); | |
514 | ||
515 | git_iterator_free(iter); | |
516 | } | |
517 | ||
518 | return error; | |
519 | } | |
520 | ||
521 | void git_pathspec_match_list_free(git_pathspec_match_list *m) | |
522 | { | |
523 | pathspec_match_free(m); | |
524 | } | |
525 | ||
526 | size_t git_pathspec_match_list_entrycount( | |
527 | const git_pathspec_match_list *m) | |
528 | { | |
529 | return git_array_size(m->matches); | |
530 | } | |
531 | ||
532 | const char *git_pathspec_match_list_entry( | |
533 | const git_pathspec_match_list *m, size_t pos) | |
534 | { | |
535 | char **entry = git_array_get(m->matches, pos); | |
536 | return entry ? *entry : NULL; | |
537 | } | |
538 | ||
539 | size_t git_pathspec_match_list_failed_entrycount( | |
540 | const git_pathspec_match_list *m) | |
541 | { | |
542 | return git_array_size(m->failures); | |
543 | } | |
544 | ||
545 | const char * git_pathspec_match_list_failed_entry( | |
546 | const git_pathspec_match_list *m, size_t pos) | |
e91f9a8f | 547 | { |
d2ce27dd RB |
548 | char **entry = git_array_get(m->failures, pos); |
549 | return entry ? *entry : NULL; | |
e91f9a8f | 550 | } |