2 * Copyright (C) the libgit2 contributors. All rights reserved.
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.
10 #include "git2/pathspec.h"
11 #include "git2/diff.h"
12 #include "attr_file.h"
14 #include "repository.h"
18 #include "wildmatch.h"
20 /* what is the common non-wildcard prefix for all items in the pathspec */
21 char *git_pathspec_prefix(const git_strarray
*pathspec
)
23 git_buf prefix
= GIT_BUF_INIT
;
26 if (!pathspec
|| !pathspec
->count
||
27 git_buf_common_prefix(&prefix
, pathspec
->strings
, pathspec
->count
) < 0)
30 /* diff prefix will only be leading non-wildcards */
31 for (scan
= prefix
.ptr
; *scan
; ++scan
) {
32 if (git__iswildcard(*scan
) &&
33 (scan
== prefix
.ptr
|| (*(scan
- 1) != '\\')))
36 git_buf_truncate(&prefix
, scan
- prefix
.ptr
);
38 if (prefix
.size
<= 0) {
39 git_buf_dispose(&prefix
);
43 git_buf_unescape(&prefix
);
45 return git_buf_detach(&prefix
);
48 /* is there anything in the spec that needs to be filtered on */
49 bool git_pathspec_is_empty(const git_strarray
*pathspec
)
56 for (i
= 0; i
< pathspec
->count
; ++i
) {
57 const char *str
= pathspec
->strings
[i
];
66 /* build a vector of fnmatch patterns to evaluate efficiently */
67 int git_pathspec__vinit(
68 git_vector
*vspec
, const git_strarray
*strspec
, git_pool
*strpool
)
72 memset(vspec
, 0, sizeof(*vspec
));
74 if (git_pathspec_is_empty(strspec
))
77 if (git_vector_init(vspec
, strspec
->count
, NULL
) < 0)
80 for (i
= 0; i
< strspec
->count
; ++i
) {
82 const char *pattern
= strspec
->strings
[i
];
83 git_attr_fnmatch
*match
= git__calloc(1, sizeof(git_attr_fnmatch
));
87 match
->flags
= GIT_ATTR_FNMATCH_ALLOWSPACE
| GIT_ATTR_FNMATCH_ALLOWNEG
;
89 ret
= git_attr_fnmatch__parse(match
, strpool
, NULL
, &pattern
);
90 if (ret
== GIT_ENOTFOUND
) {
98 if (git_vector_insert(vspec
, match
) < 0)
105 /* free data from the pathspec vector */
106 void git_pathspec__vfree(git_vector
*vspec
)
108 git_vector_free_deep(vspec
);
111 struct pathspec_match_context
{
113 int (*strcomp
)(const char *, const char *);
114 int (*strncomp
)(const char *, const char *, size_t);
117 static void pathspec_match_context_init(
118 struct pathspec_match_context
*ctxt
,
119 bool disable_fnmatch
,
123 ctxt
->wildmatch_flags
= -1;
125 ctxt
->wildmatch_flags
= WM_CASEFOLD
;
127 ctxt
->wildmatch_flags
= 0;
130 ctxt
->strcomp
= git__strcasecmp
;
131 ctxt
->strncomp
= git__strncasecmp
;
133 ctxt
->strcomp
= git__strcmp
;
134 ctxt
->strncomp
= git__strncmp
;
138 static int pathspec_match_one(
139 const git_attr_fnmatch
*match
,
140 struct pathspec_match_context
*ctxt
,
143 int result
= (match
->flags
& GIT_ATTR_FNMATCH_MATCH_ALL
) ? 0 : WM_NOMATCH
;
145 if (result
== WM_NOMATCH
)
146 result
= ctxt
->strcomp(match
->pattern
, path
) ? WM_NOMATCH
: 0;
148 if (ctxt
->wildmatch_flags
>= 0 && result
== WM_NOMATCH
)
149 result
= wildmatch(match
->pattern
, path
, ctxt
->wildmatch_flags
);
151 /* if we didn't match, look for exact dirname prefix match */
152 if (result
== WM_NOMATCH
&&
153 (match
->flags
& GIT_ATTR_FNMATCH_HASWILD
) == 0 &&
154 ctxt
->strncomp(path
, match
->pattern
, match
->length
) == 0 &&
155 path
[match
->length
] == '/')
158 /* if we didn't match and this is a negative match, check for exact
159 * match of filename with leading '!'
161 if (result
== WM_NOMATCH
&&
162 (match
->flags
& GIT_ATTR_FNMATCH_NEGATIVE
) != 0 &&
164 ctxt
->strncomp(path
+ 1, match
->pattern
, match
->length
) == 0 &&
165 (!path
[match
->length
+ 1] || path
[match
->length
+ 1] == '/'))
169 return (match
->flags
& GIT_ATTR_FNMATCH_NEGATIVE
) ? 0 : 1;
173 static int git_pathspec__match_at(
175 const git_vector
*vspec
,
176 struct pathspec_match_context
*ctxt
,
180 int result
= GIT_ENOTFOUND
;
182 const git_attr_fnmatch
*match
;
184 git_vector_foreach(vspec
, i
, match
) {
185 if (path0
&& (result
= pathspec_match_one(match
, ctxt
, path0
)) >= 0)
187 if (path1
&& (result
= pathspec_match_one(match
, ctxt
, path1
)) >= 0)
195 /* match a path against the vectorized pathspec */
196 bool git_pathspec__match(
197 const git_vector
*vspec
,
199 bool disable_fnmatch
,
201 const char **matched_pathspec
,
206 struct pathspec_match_context ctxt
;
208 if (matched_pathspec
)
209 *matched_pathspec
= NULL
;
211 *matched_at
= GIT_PATHSPEC_NOMATCH
;
213 if (!vspec
|| !vspec
->length
)
216 pathspec_match_context_init(&ctxt
, disable_fnmatch
, casefold
);
218 result
= git_pathspec__match_at(&pos
, vspec
, &ctxt
, path
, NULL
);
220 if (matched_pathspec
) {
221 const git_attr_fnmatch
*match
= git_vector_get(vspec
, pos
);
222 *matched_pathspec
= match
->pattern
;
233 int git_pathspec__init(git_pathspec
*ps
, const git_strarray
*paths
)
237 memset(ps
, 0, sizeof(*ps
));
239 ps
->prefix
= git_pathspec_prefix(paths
);
241 if ((error
= git_pool_init(&ps
->pool
, 1)) < 0 ||
242 (error
= git_pathspec__vinit(&ps
->pathspec
, paths
, &ps
->pool
)) < 0)
243 git_pathspec__clear(ps
);
248 void git_pathspec__clear(git_pathspec
*ps
)
250 git__free(ps
->prefix
);
251 git_pathspec__vfree(&ps
->pathspec
);
252 git_pool_clear(&ps
->pool
);
253 memset(ps
, 0, sizeof(*ps
));
256 int git_pathspec_new(git_pathspec
**out
, const git_strarray
*pathspec
)
259 git_pathspec
*ps
= git__malloc(sizeof(git_pathspec
));
260 GIT_ERROR_CHECK_ALLOC(ps
);
262 if ((error
= git_pathspec__init(ps
, pathspec
)) < 0) {
267 GIT_REFCOUNT_INC(ps
);
272 static void pathspec_free(git_pathspec
*ps
)
274 git_pathspec__clear(ps
);
278 void git_pathspec_free(git_pathspec
*ps
)
282 GIT_REFCOUNT_DEC(ps
, pathspec_free
);
285 int git_pathspec_matches_path(
286 const git_pathspec
*ps
, uint32_t flags
, const char *path
)
288 bool no_fnmatch
= (flags
& GIT_PATHSPEC_NO_GLOB
) != 0;
289 bool casefold
= (flags
& GIT_PATHSPEC_IGNORE_CASE
) != 0;
292 GIT_ASSERT_ARG(path
);
294 return (0 != git_pathspec__match(
295 &ps
->pathspec
, path
, no_fnmatch
, casefold
, NULL
, NULL
));
298 static void pathspec_match_free(git_pathspec_match_list
*m
)
303 git_pathspec_free(m
->pathspec
);
306 git_array_clear(m
->matches
);
307 git_array_clear(m
->failures
);
308 git_pool_clear(&m
->pool
);
312 static git_pathspec_match_list
*pathspec_match_alloc(
313 git_pathspec
*ps
, int datatype
)
315 git_pathspec_match_list
*m
= git__calloc(1, sizeof(git_pathspec_match_list
));
319 if (git_pool_init(&m
->pool
, 1) < 0)
322 /* need to keep reference to pathspec and increment refcount because
323 * failures array stores pointers to the pattern strings of the
324 * pathspec that had no matches
326 GIT_REFCOUNT_INC(ps
);
328 m
->datatype
= datatype
;
333 GIT_INLINE(size_t) pathspec_mark_pattern(git_bitvec
*used
, size_t pos
)
335 if (!git_bitvec_get(used
, pos
)) {
336 git_bitvec_set(used
, pos
, true);
343 static size_t pathspec_mark_remaining(
345 git_vector
*patterns
,
346 struct pathspec_match_context
*ctxt
,
356 for (; start
< patterns
->length
; ++start
) {
357 const git_attr_fnmatch
*pat
= git_vector_get(patterns
, start
);
359 if (git_bitvec_get(used
, start
))
362 if (path0
&& pathspec_match_one(pat
, ctxt
, path0
) > 0)
363 count
+= pathspec_mark_pattern(used
, start
);
364 else if (path1
&& pathspec_match_one(pat
, ctxt
, path1
) > 0)
365 count
+= pathspec_mark_pattern(used
, start
);
371 static int pathspec_build_failure_array(
372 git_pathspec_string_array_t
*failures
,
373 git_vector
*patterns
,
379 const git_attr_fnmatch
*pat
;
381 for (pos
= 0; pos
< patterns
->length
; ++pos
) {
382 if (git_bitvec_get(used
, pos
))
385 if ((failed
= git_array_alloc(*failures
)) == NULL
)
388 pat
= git_vector_get(patterns
, pos
);
390 if ((*failed
= git_pool_strdup(pool
, pat
->pattern
)) == NULL
)
397 static int pathspec_match_from_iterator(
398 git_pathspec_match_list
**out
,
404 git_pathspec_match_list
*m
= NULL
;
405 const git_index_entry
*entry
= NULL
;
406 struct pathspec_match_context ctxt
;
407 git_vector
*patterns
= &ps
->pathspec
;
408 bool find_failures
= out
&& (flags
& GIT_PATHSPEC_FIND_FAILURES
) != 0;
409 bool failures_only
= !out
|| (flags
& GIT_PATHSPEC_FAILURES_ONLY
) != 0;
410 size_t pos
, used_ct
= 0, found_files
= 0;
411 git_index
*index
= NULL
;
412 git_bitvec used_patterns
;
415 if (git_bitvec_init(&used_patterns
, patterns
->length
) < 0)
419 *out
= m
= pathspec_match_alloc(ps
, PATHSPEC_DATATYPE_STRINGS
);
420 GIT_ERROR_CHECK_ALLOC(m
);
423 if ((error
= git_iterator_reset_range(iter
, ps
->prefix
, ps
->prefix
)) < 0)
426 if (git_iterator_type(iter
) == GIT_ITERATOR_WORKDIR
&&
427 (error
= git_repository_index__weakptr(
428 &index
, git_iterator_owner(iter
))) < 0)
431 pathspec_match_context_init(
432 &ctxt
, (flags
& GIT_PATHSPEC_NO_GLOB
) != 0,
433 git_iterator_ignore_case(iter
));
435 while (!(error
= git_iterator_advance(&entry
, iter
))) {
436 /* search for match with entry->path */
437 int result
= git_pathspec__match_at(
438 &pos
, patterns
, &ctxt
, entry
->path
, NULL
);
440 /* no matches for this path */
444 /* if result was a negative pattern match, then don't list file */
446 used_ct
+= pathspec_mark_pattern(&used_patterns
, pos
);
450 /* check if path is ignored and untracked */
452 git_iterator_current_is_ignored(iter
) &&
453 git_index__find_pos(NULL
, index
, entry
->path
, 0, GIT_INDEX_STAGE_ANY
) < 0)
456 /* mark the matched pattern as used */
457 used_ct
+= pathspec_mark_pattern(&used_patterns
, pos
);
460 /* if find_failures is on, check if any later patterns also match */
461 if (find_failures
&& used_ct
< patterns
->length
)
462 used_ct
+= pathspec_mark_remaining(
463 &used_patterns
, patterns
, &ctxt
, pos
+ 1, entry
->path
, NULL
);
465 /* if only looking at failures, exit early or just continue */
466 if (failures_only
|| !out
) {
467 if (used_ct
== patterns
->length
)
472 /* insert matched path into matches array */
473 if ((file
= (char **)git_array_alloc(m
->matches
)) == NULL
||
474 (*file
= git_pool_strdup(&m
->pool
, entry
->path
)) == NULL
) {
480 if (error
< 0 && error
!= GIT_ITEROVER
)
484 /* insert patterns that had no matches into failures array */
485 if (find_failures
&& used_ct
< patterns
->length
&&
486 (error
= pathspec_build_failure_array(
487 &m
->failures
, patterns
, &used_patterns
, &m
->pool
)) < 0)
490 /* if every pattern failed to match, then we have failed */
491 if ((flags
& GIT_PATHSPEC_NO_MATCH_ERROR
) != 0 && !found_files
) {
492 git_error_set(GIT_ERROR_INVALID
, "no matching files were found");
493 error
= GIT_ENOTFOUND
;
497 git_bitvec_free(&used_patterns
);
500 pathspec_match_free(m
);
501 if (out
) *out
= NULL
;
507 static git_iterator_flag_t
pathspec_match_iter_flags(uint32_t flags
)
509 git_iterator_flag_t f
= 0;
511 if ((flags
& GIT_PATHSPEC_IGNORE_CASE
) != 0)
512 f
|= GIT_ITERATOR_IGNORE_CASE
;
513 else if ((flags
& GIT_PATHSPEC_USE_CASE
) != 0)
514 f
|= GIT_ITERATOR_DONT_IGNORE_CASE
;
519 int git_pathspec_match_workdir(
520 git_pathspec_match_list
**out
,
521 git_repository
*repo
,
526 git_iterator_options iter_opts
= GIT_ITERATOR_OPTIONS_INIT
;
529 GIT_ASSERT_ARG(repo
);
531 iter_opts
.flags
= pathspec_match_iter_flags(flags
);
533 if (!(error
= git_iterator_for_workdir(&iter
, repo
, NULL
, NULL
, &iter_opts
))) {
534 error
= pathspec_match_from_iterator(out
, iter
, flags
, ps
);
535 git_iterator_free(iter
);
541 int git_pathspec_match_index(
542 git_pathspec_match_list
**out
,
548 git_iterator_options iter_opts
= GIT_ITERATOR_OPTIONS_INIT
;
551 GIT_ASSERT_ARG(index
);
553 iter_opts
.flags
= pathspec_match_iter_flags(flags
);
555 if (!(error
= git_iterator_for_index(&iter
, git_index_owner(index
), index
, &iter_opts
))) {
556 error
= pathspec_match_from_iterator(out
, iter
, flags
, ps
);
557 git_iterator_free(iter
);
563 int git_pathspec_match_tree(
564 git_pathspec_match_list
**out
,
570 git_iterator_options iter_opts
= GIT_ITERATOR_OPTIONS_INIT
;
573 GIT_ASSERT_ARG(tree
);
575 iter_opts
.flags
= pathspec_match_iter_flags(flags
);
577 if (!(error
= git_iterator_for_tree(&iter
, tree
, &iter_opts
))) {
578 error
= pathspec_match_from_iterator(out
, iter
, flags
, ps
);
579 git_iterator_free(iter
);
585 int git_pathspec_match_diff(
586 git_pathspec_match_list
**out
,
592 git_pathspec_match_list
*m
= NULL
;
593 struct pathspec_match_context ctxt
;
594 git_vector
*patterns
= &ps
->pathspec
;
595 bool find_failures
= out
&& (flags
& GIT_PATHSPEC_FIND_FAILURES
) != 0;
596 bool failures_only
= !out
|| (flags
& GIT_PATHSPEC_FAILURES_ONLY
) != 0;
597 size_t i
, pos
, used_ct
= 0, found_deltas
= 0;
598 const git_diff_delta
*delta
, **match
;
599 git_bitvec used_patterns
;
601 GIT_ASSERT_ARG(diff
);
603 if (git_bitvec_init(&used_patterns
, patterns
->length
) < 0)
607 *out
= m
= pathspec_match_alloc(ps
, PATHSPEC_DATATYPE_DIFF
);
608 GIT_ERROR_CHECK_ALLOC(m
);
611 pathspec_match_context_init(
612 &ctxt
, (flags
& GIT_PATHSPEC_NO_GLOB
) != 0,
613 git_diff_is_sorted_icase(diff
));
615 git_vector_foreach(&diff
->deltas
, i
, delta
) {
616 /* search for match with delta */
617 int result
= git_pathspec__match_at(
618 &pos
, patterns
, &ctxt
, delta
->old_file
.path
, delta
->new_file
.path
);
620 /* no matches for this path */
624 /* mark the matched pattern as used */
625 used_ct
+= pathspec_mark_pattern(&used_patterns
, pos
);
627 /* if result was a negative pattern match, then don't list file */
633 /* if find_failures is on, check if any later patterns also match */
634 if (find_failures
&& used_ct
< patterns
->length
)
635 used_ct
+= pathspec_mark_remaining(
636 &used_patterns
, patterns
, &ctxt
, pos
+ 1,
637 delta
->old_file
.path
, delta
->new_file
.path
);
639 /* if only looking at failures, exit early or just continue */
640 if (failures_only
|| !out
) {
641 if (used_ct
== patterns
->length
)
646 /* insert matched delta into matches array */
647 if (!(match
= (const git_diff_delta
**)git_array_alloc(m
->matches
))) {
655 /* insert patterns that had no matches into failures array */
656 if (find_failures
&& used_ct
< patterns
->length
&&
657 (error
= pathspec_build_failure_array(
658 &m
->failures
, patterns
, &used_patterns
, &m
->pool
)) < 0)
661 /* if every pattern failed to match, then we have failed */
662 if ((flags
& GIT_PATHSPEC_NO_MATCH_ERROR
) != 0 && !found_deltas
) {
663 git_error_set(GIT_ERROR_INVALID
, "no matching deltas were found");
664 error
= GIT_ENOTFOUND
;
668 git_bitvec_free(&used_patterns
);
671 pathspec_match_free(m
);
672 if (out
) *out
= NULL
;
678 void git_pathspec_match_list_free(git_pathspec_match_list
*m
)
681 pathspec_match_free(m
);
684 size_t git_pathspec_match_list_entrycount(
685 const git_pathspec_match_list
*m
)
687 return m
? git_array_size(m
->matches
) : 0;
690 const char *git_pathspec_match_list_entry(
691 const git_pathspec_match_list
*m
, size_t pos
)
693 if (!m
|| m
->datatype
!= PATHSPEC_DATATYPE_STRINGS
||
694 !git_array_valid_index(m
->matches
, pos
))
697 return *((const char **)git_array_get(m
->matches
, pos
));
700 const git_diff_delta
*git_pathspec_match_list_diff_entry(
701 const git_pathspec_match_list
*m
, size_t pos
)
703 if (!m
|| m
->datatype
!= PATHSPEC_DATATYPE_DIFF
||
704 !git_array_valid_index(m
->matches
, pos
))
707 return *((const git_diff_delta
**)git_array_get(m
->matches
, pos
));
710 size_t git_pathspec_match_list_failed_entrycount(
711 const git_pathspec_match_list
*m
)
713 return m
? git_array_size(m
->failures
) : 0;
716 const char * git_pathspec_match_list_failed_entry(
717 const git_pathspec_match_list
*m
, size_t pos
)
719 char **entry
= m
? git_array_get(m
->failures
, pos
) : NULL
;
721 return entry
? *entry
: NULL
;