1use std::borrow::Cow;
9use std::path::{Path, PathBuf};
10
11use crate::crlf::path_gitattribute_value;
12use crate::crlf::AttrRule;
13use crate::error::{Error, Result as LibResult};
14use crate::precompose_config::pathspec_precompose_enabled;
15use crate::unicode_normalization::precompose_utf8_path;
16use crate::wildmatch::{wildmatch, WM_CASEFOLD, WM_PATHNAME};
17
18#[must_use]
21pub fn simple_length(match_str: &str) -> usize {
22 let b = match_str.as_bytes();
23 let mut len = 0usize;
24 for &c in b {
25 if matches!(c, b'*' | b'?' | b'[' | b'\\') {
26 break;
27 }
28 len += 1;
29 }
30 len
31}
32
33#[must_use]
35pub fn has_glob_chars(s: &str) -> bool {
36 simple_length(s) < s.len()
37}
38
39pub fn parse_pathspecs_from_source(data: &[u8], nul_terminated: bool) -> LibResult<Vec<String>> {
48 if nul_terminated {
49 let mut out = Vec::new();
50 for chunk in data.split(|b| *b == 0) {
51 if chunk.is_empty() {
52 continue;
53 }
54 let s = String::from_utf8_lossy(chunk);
55 let t = s.trim();
56 if t.starts_with('"') {
57 return Err(Error::PathError(format!(
58 "pathspec-from-file: line is not NUL terminated: {t}"
59 )));
60 }
61 out.push(t.to_string());
62 }
63 return Ok(out);
64 }
65
66 let text = String::from_utf8_lossy(data);
67 let mut out = Vec::new();
68 for raw in text.split_inclusive('\n') {
69 let line = raw.trim_end_matches('\n').trim_end_matches('\r');
70 if line.is_empty() {
71 continue;
72 }
73 if line.starts_with('"') && line.ends_with('"') && line.len() >= 2 {
74 out.push(unquote_c_style_pathspec_line(line)?);
75 } else {
76 out.push(line.to_string());
77 }
78 }
79 Ok(out)
80}
81
82fn unquote_c_style_pathspec_line(s: &str) -> LibResult<String> {
84 let bytes = s.as_bytes();
85 if bytes.first() != Some(&b'"') || bytes.last() != Some(&b'"') || bytes.len() < 2 {
86 return Err(Error::PathError(format!("invalid C-style quoting: {s}")));
87 }
88
89 let inner = &bytes[1..bytes.len() - 1];
90 let mut out = Vec::with_capacity(inner.len());
91 let mut i = 0;
92 while i < inner.len() {
93 if inner[i] != b'\\' {
94 out.push(inner[i]);
95 i += 1;
96 continue;
97 }
98 i += 1;
99 if i >= inner.len() {
100 return Err(Error::PathError(
101 "invalid escape at end of string".to_string(),
102 ));
103 }
104 match inner[i] {
105 b'\\' => out.push(b'\\'),
106 b'"' => out.push(b'"'),
107 b'a' => out.push(7),
108 b'b' => out.push(8),
109 b'f' => out.push(12),
110 b'n' => out.push(b'\n'),
111 b'r' => out.push(b'\r'),
112 b't' => out.push(b'\t'),
113 b'v' => out.push(11),
114 c if c.is_ascii_digit() => {
115 if i + 2 >= inner.len() {
116 return Err(Error::PathError("truncated octal escape".to_string()));
117 }
118 let oct = std::str::from_utf8(&inner[i..i + 3])
119 .map_err(|_| Error::PathError("invalid octal bytes".to_string()))?;
120 out.push(
121 u8::from_str_radix(oct, 8)
122 .map_err(|_| Error::PathError("invalid octal escape value".to_string()))?,
123 );
124 i += 2;
125 }
126 other => {
127 return Err(Error::PathError(format!(
128 "invalid escape sequence \\{}",
129 char::from(other)
130 )));
131 }
132 }
133 i += 1;
134 }
135 String::from_utf8(out).map_err(|_| Error::PathError("invalid UTF-8 in quoted pathspec".into()))
136}
137
138#[derive(Debug, Clone, Default)]
139struct PathspecMagic {
140 literal: bool,
141 glob: bool,
142 icase: bool,
143 exclude: bool,
144 top: bool,
146 prefix: Option<String>,
147 attr_requirements: Vec<AttrRequirement>,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152enum AttrRequirement {
153 Set(String),
154 Unset(String),
155 Unspecified(String),
156 Value(String, String),
157}
158
159impl AttrRequirement {
160 fn name(&self) -> &str {
161 match self {
162 AttrRequirement::Set(name)
163 | AttrRequirement::Unset(name)
164 | AttrRequirement::Unspecified(name)
165 | AttrRequirement::Value(name, _) => name,
166 }
167 }
168}
169
170fn parse_maybe_bool(v: &str) -> Option<bool> {
171 let s = v.trim().to_ascii_lowercase();
172 match s.as_str() {
173 "true" | "yes" | "on" | "1" => Some(true),
174 "false" | "no" | "off" | "0" => Some(false),
175 _ => None,
176 }
177}
178
179fn git_env_bool(key: &str, default: bool) -> bool {
180 match std::env::var(key) {
181 Ok(v) => parse_maybe_bool(&v).unwrap_or(default),
182 Err(_) => default,
183 }
184}
185
186fn literal_global() -> bool {
187 git_env_bool("GIT_LITERAL_PATHSPECS", false)
188}
189
190#[must_use]
192pub fn literal_pathspecs_enabled() -> bool {
193 literal_global()
194}
195
196fn glob_global() -> bool {
197 git_env_bool("GIT_GLOB_PATHSPECS", false)
198}
199
200fn noglob_global() -> bool {
201 git_env_bool("GIT_NOGLOB_PATHSPECS", false)
202}
203
204fn icase_global() -> bool {
205 git_env_bool("GIT_ICASE_PATHSPECS", false)
206}
207
208pub fn validate_global_pathspec_flags() -> Result<(), String> {
212 let lit = literal_global();
213 let glob = glob_global();
214 let noglob = noglob_global();
215 let icase = icase_global();
216
217 if glob && noglob {
218 return Err("global 'glob' and 'noglob' pathspec settings are incompatible".to_string());
219 }
220 if lit && (glob || noglob || icase) {
221 return Err(
222 "global 'literal' pathspec setting is incompatible with all other global pathspec settings"
223 .to_string(),
224 );
225 }
226 Ok(())
227}
228
229fn is_valid_attr_name(name: &str) -> bool {
230 !name.is_empty()
231 && name
232 .bytes()
233 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'))
234}
235
236fn split_attr_expr(expr: &str) -> Result<Vec<String>, String> {
237 let mut parts = Vec::new();
238 let mut cur = String::new();
239 let mut in_value = false;
240 let mut escaped = false;
241
242 for ch in expr.chars() {
243 if escaped {
244 if ch.is_ascii_whitespace() {
245 return Err(
246 "Escape character '\\' not allowed as last character in attr value".to_string(),
247 );
248 }
249 if ch != ',' {
250 return Err("Escape character '\\' not allowed for value matching".to_string());
251 }
252 cur.push(ch);
253 escaped = false;
254 continue;
255 }
256 if in_value && ch == '\\' {
257 escaped = true;
258 continue;
259 }
260 if ch == '=' {
261 in_value = true;
262 cur.push(ch);
263 continue;
264 }
265 if ch.is_ascii_whitespace() {
266 if !cur.is_empty() {
267 parts.push(cur);
268 cur = String::new();
269 }
270 in_value = false;
271 continue;
272 }
273 cur.push(ch);
274 }
275
276 if escaped {
277 return Err(
278 "Escape character '\\' not allowed as last character in attr value".to_string(),
279 );
280 }
281 if !cur.is_empty() {
282 parts.push(cur);
283 }
284 Ok(parts)
285}
286
287fn parse_attr_requirements(expr: &str) -> Result<Vec<AttrRequirement>, String> {
288 if expr.trim().is_empty() {
289 return Err("empty attr magic is invalid".to_string());
290 }
291 let mut out = Vec::new();
292 for token in split_attr_expr(expr)? {
293 if let Some(name) = token.strip_prefix('-') {
294 if name.contains('=') {
295 return Err("invalid attribute name".to_string());
296 }
297 if !is_valid_attr_name(name) {
298 return Err(format!("{name} is not a valid attribute name"));
299 }
300 out.push(AttrRequirement::Unset(name.to_string()));
301 } else if let Some(name) = token.strip_prefix('!') {
302 if name.contains('=') {
303 return Err("invalid attribute name".to_string());
304 }
305 if !is_valid_attr_name(name) {
306 return Err(format!("{name} is not a valid attribute name"));
307 }
308 out.push(AttrRequirement::Unspecified(name.to_string()));
309 } else if let Some((name, value)) = token.split_once('=') {
310 if !is_valid_attr_name(name) {
311 return Err(format!("{name} is not a valid attribute name"));
312 }
313 if value.is_empty() {
314 return Err("empty attribute value is not allowed".to_string());
315 }
316 out.push(AttrRequirement::Value(name.to_string(), value.to_string()));
317 } else {
318 if !is_valid_attr_name(&token) {
319 return Err(format!("{token} is not a valid attribute name"));
320 }
321 out.push(AttrRequirement::Set(token));
322 }
323 }
324 if out.is_empty() {
325 return Err("empty attr magic is invalid".to_string());
326 }
327 Ok(out)
328}
329
330pub fn validate_attr_pathspecs(specs: &[String]) -> Result<(), String> {
335 for spec in specs {
336 if literal_global() || !spec.starts_with(":(") {
337 continue;
338 }
339 let Some(rest) = spec.strip_prefix(":(") else {
340 continue;
341 };
342 let Some(close) = rest.find(')') else {
343 continue;
344 };
345 let magic_part = &rest[..close];
346 let mut attr_count = 0usize;
347 for token in split_long_magic_tokens(magic_part) {
348 let Some(expr) = token.trim().strip_prefix("attr:") else {
349 continue;
350 };
351 attr_count += 1;
352 if attr_count > 1 {
353 return Err("Only one 'attr:' specification is allowed.".to_string());
354 }
355 parse_attr_requirements(expr)?;
356 }
357 }
358 Ok(())
359}
360
361fn split_long_magic_tokens(magic_part: &str) -> Vec<String> {
362 let mut tokens = Vec::new();
363 let mut cur = String::new();
364 let mut escaped = false;
365 for ch in magic_part.chars() {
366 if escaped {
367 cur.push('\\');
368 cur.push(ch);
369 escaped = false;
370 continue;
371 }
372 if ch == '\\' {
373 escaped = true;
374 continue;
375 }
376 if ch == ',' {
377 tokens.push(cur.trim().to_string());
378 cur.clear();
379 continue;
380 }
381 cur.push(ch);
382 }
383 if escaped {
384 cur.push('\\');
385 }
386 tokens.push(cur.trim().to_string());
387 tokens
388}
389
390fn parse_long_magic(rest_after_paren: &str) -> Option<(PathspecMagic, &str)> {
391 let close = rest_after_paren.find(')')?;
392 let magic_part = &rest_after_paren[..close];
393 let tail = &rest_after_paren[close + 1..];
394 let mut magic = PathspecMagic::default();
395 for raw in split_long_magic_tokens(magic_part) {
396 let token = raw.trim();
397 if token.is_empty() {
398 continue;
399 }
400 if let Some(p) = token.strip_prefix("prefix:") {
401 magic.prefix = Some(p.to_string());
402 continue;
403 }
404 if let Some(expr) = token.strip_prefix("attr:") {
405 if let Ok(reqs) = parse_attr_requirements(expr) {
406 magic.attr_requirements = reqs;
407 }
408 continue;
409 }
410 if token.eq_ignore_ascii_case("literal") {
411 magic.literal = true;
412 } else if token.eq_ignore_ascii_case("glob") {
413 magic.glob = true;
414 } else if token.eq_ignore_ascii_case("icase") {
415 magic.icase = true;
416 } else if token.eq_ignore_ascii_case("exclude") {
417 magic.exclude = true;
418 } else if token.eq_ignore_ascii_case("top") {
419 magic.top = true;
420 }
421 }
422 Some((magic, tail))
423}
424
425fn parse_short_magic(elem: &str) -> (PathspecMagic, &str) {
427 let bytes = elem.as_bytes();
428 let mut i = 1usize;
429 let mut magic = PathspecMagic::default();
430 while i < bytes.len() && bytes[i] != b':' {
431 let ch = bytes[i];
432 if ch == b'^' {
433 magic.exclude = true;
434 i += 1;
435 continue;
436 }
437 let is_magic = match ch {
438 b'!' => {
439 magic.exclude = true;
440 true
441 }
442 b'/' => {
443 magic.top = true;
444 true
445 } _ => false,
447 };
448 if is_magic {
449 i += 1;
450 continue;
451 }
452 break;
453 }
454 if i < bytes.len() && bytes[i] == b':' {
455 i += 1;
456 }
457 (magic, &elem[i..])
458}
459
460fn parse_element_magic(elem: &str) -> (PathspecMagic, &str) {
462 if !elem.starts_with(':') || literal_global() {
463 return (PathspecMagic::default(), elem);
464 }
465 if let Some(rest) = elem.strip_prefix(":(") {
466 return parse_long_magic(rest).unwrap_or((PathspecMagic::default(), elem));
467 }
468 parse_short_magic(elem)
469}
470
471fn combine_magic(element: PathspecMagic) -> PathspecMagic {
472 let mut m = element;
473 if literal_global() {
474 m.literal = true;
475 }
476 if glob_global() && !m.literal {
477 m.glob = true;
478 }
479 if icase_global() {
480 m.icase = true;
481 }
482 if noglob_global() && !m.glob {
483 m.literal = true;
484 }
485 m
486}
487
488fn strip_top_magic(mut pattern: &str) -> &str {
489 if let Some(r) = pattern.strip_prefix(":/") {
490 pattern = r;
491 }
492 pattern
493}
494
495#[must_use]
500pub fn bloom_lookup_prefix_with_cwd(
501 spec: &str,
502 cwd_from_repo_root: Option<&str>,
503) -> Option<String> {
504 let (elem_magic, raw_pattern) = parse_element_magic(spec);
505 let magic = combine_magic(elem_magic);
506 if magic.exclude || magic.icase {
507 return None;
508 }
509 let pattern = strip_top_magic(raw_pattern);
510 if pattern.is_empty() {
511 return None;
512 }
513 let combined = if magic.top {
514 let cwd = cwd_from_repo_root.unwrap_or("").trim_end_matches('/');
515 if cwd.is_empty() {
516 pattern.to_string()
517 } else {
518 format!("{cwd}/{pattern}")
519 }
520 } else {
521 pattern.to_string()
522 };
523 let pattern = combined.as_str();
524 let mut len = simple_length(pattern);
525 if len != pattern.len() {
526 while len > 0 && pattern.as_bytes()[len - 1] != b'/' {
527 len -= 1;
528 }
529 }
530 while len > 0 && pattern.as_bytes()[len - 1] == b'/' {
531 len -= 1;
532 }
533 if len == 0 {
534 return None;
535 }
536 Some(combined[..len].to_string())
537}
538
539#[must_use]
540pub fn bloom_lookup_prefix(spec: &str) -> Option<String> {
541 bloom_lookup_prefix_with_cwd(spec, None)
542}
543
544#[must_use]
546pub fn pathspecs_allow_bloom(specs: &[String]) -> bool {
547 specs.iter().all(|s| {
548 !s.is_empty() && !pathspec_is_exclude(s) && bloom_lookup_prefix_with_cwd(s, None).is_some()
549 })
550}
551
552#[must_use]
557pub fn path_allowed_by_pathspec_list(specs: &[String], path: &str) -> bool {
558 let mut has_positive = false;
559 let mut positive_match = false;
560 for s in specs {
561 let (elem, raw_pattern) = parse_element_magic(s);
562 let magic = combine_magic(elem);
563 if magic.exclude {
564 if path_matches_pathspec_tail(raw_pattern, path, magic) {
565 return false;
566 }
567 continue;
568 }
569 has_positive = true;
570 if pathspec_matches(s, path) {
571 positive_match = true;
572 }
573 }
574 !has_positive || positive_match
575}
576
577#[must_use]
579pub fn pathspec_contributes_match(spec: &str, path: &str) -> bool {
580 pathspec_matches(spec, path) || pathspec_exclude_matches(spec, path)
581}
582
583fn path_matches_pathspec_tail(raw_pattern: &str, path: &str, magic: PathspecMagic) -> bool {
584 if magic.literal && magic.glob {
585 return false;
586 }
587 let pattern = strip_top_magic(raw_pattern);
588 let path_for_match = if let Some(prefix) = magic.prefix.as_deref() {
589 if !path.starts_with(prefix) {
590 return false;
591 }
592 &path[prefix.len()..]
593 } else {
594 path
595 };
596 pathspec_matches_tail(pattern, path_for_match, magic)
597}
598
599#[must_use]
604pub fn pathspec_matches(spec: &str, path: &str) -> bool {
605 matches_pathspec(spec, path)
606}
607
608#[must_use]
610pub fn pathspec_is_exclude(spec: &str) -> bool {
611 let (elem_magic, _) = parse_element_magic(spec);
612 combine_magic(elem_magic).exclude
613}
614
615#[must_use]
620pub fn pathspec_wants_descent_into_tree(spec: &str, full_name: &str) -> bool {
621 if pathspec_is_exclude(spec) {
622 return false;
623 }
624 let (elem_magic, raw_pattern) = parse_element_magic(spec);
625 let magic = combine_magic(elem_magic);
626 if magic.exclude {
627 return false;
628 }
629 let pattern = strip_top_magic(raw_pattern);
630 let pattern = pattern.strip_prefix("./").unwrap_or(pattern);
631 if pattern.is_empty() || pattern == "." {
632 return true;
633 }
634 let dir_prefix = format!("{full_name}/");
635 if pattern.starts_with(&dir_prefix) {
636 return true;
637 }
638 let probe = format!("{full_name}/.__grit_ls_tree_probe__");
639 matches_ls_tree_pathspec(spec, &probe, 0o100644, &[])
640}
641
642#[must_use]
645pub fn matches_pathspec_set_for_object_ls_tree(
646 specs: &[String],
647 path: &str,
648 mode: u32,
649 attr_rules: &[AttrRule],
650) -> bool {
651 if specs.is_empty() {
652 return true;
653 }
654 let mut positives: Vec<&str> = Vec::new();
655 let mut excludes: Vec<&str> = Vec::new();
656 for s in specs {
657 if pathspec_is_exclude(s) {
658 excludes.push(s.as_str());
659 } else {
660 positives.push(s.as_str());
661 }
662 }
663 let positive_ok = if positives.is_empty() {
664 true
665 } else {
666 positives
667 .iter()
668 .any(|s| matches_ls_tree_pathspec(s, path, mode, attr_rules))
669 };
670 if !positive_ok {
671 return false;
672 }
673 for ex in excludes {
674 if matches_ls_tree_pathspec(ex, path, mode, attr_rules) {
675 return false;
676 }
677 }
678 true
679}
680
681#[must_use]
684pub fn matches_pathspec_set_for_object(
685 specs: &[String],
686 path: &str,
687 mode: u32,
688 attr_rules: &[AttrRule],
689) -> bool {
690 if specs.is_empty() {
691 return true;
692 }
693 let mut positives: Vec<&str> = Vec::new();
694 let mut excludes: Vec<&str> = Vec::new();
695 for s in specs {
696 if pathspec_is_exclude(s) {
697 excludes.push(s.as_str());
698 } else {
699 positives.push(s.as_str());
700 }
701 }
702 let positive_ok = if positives.is_empty() {
703 true
704 } else {
705 positives
706 .iter()
707 .any(|s| matches_pathspec_for_object(s, path, mode, attr_rules))
708 };
709 if !positive_ok {
710 return false;
711 }
712 for ex in excludes {
713 if matches_pathspec_for_object(ex, path, mode, attr_rules) {
714 return false;
715 }
716 }
717 true
718}
719
720#[must_use]
722pub fn pathspec_has_top(spec: &str) -> bool {
723 let (elem_magic, _) = parse_element_magic(spec);
724 combine_magic(elem_magic).top
725}
726
727fn pathspec_match_one_positive(path: &str, magic: PathspecMagic, raw_pattern: &str) -> bool {
728 if magic.literal && magic.glob {
729 return false;
730 }
731 let pattern = strip_top_magic(raw_pattern);
732 let path_for_match = if let Some(prefix) = magic.prefix.as_deref() {
733 if !path.starts_with(prefix) {
734 return false;
735 }
736 &path[prefix.len()..]
737 } else {
738 path
739 };
740 pathspec_matches_tail(pattern, path_for_match, magic)
741}
742
743fn attr_requirements_match(
744 requirements: &[AttrRequirement],
745 attr_rules: &[AttrRule],
746 path: &str,
747 is_dir: bool,
748 mode: u32,
749) -> bool {
750 requirements.iter().all(|req| {
751 let value = if req.name() == "builtin_objectmode" {
752 if mode == 0 {
753 None
754 } else {
755 Some(format!("{mode:06o}"))
756 }
757 } else {
758 path_gitattribute_value(attr_rules, path, is_dir, req.name())
759 };
760 match req {
761 AttrRequirement::Set(_) => value.as_deref() == Some("set"),
762 AttrRequirement::Unset(_) => value.as_deref() == Some("unset"),
763 AttrRequirement::Unspecified(_) => value.is_none(),
764 AttrRequirement::Value(_, expected) => value.as_deref() == Some(expected.as_str()),
765 }
766 })
767}
768
769fn matches_pathspec_element_with_context(
770 spec: &str,
771 path: &str,
772 ctx: PathspecMatchContext,
773) -> bool {
774 let (elem_magic, raw_pattern) = parse_element_magic(spec);
775 let magic = combine_magic(elem_magic);
776 if magic.exclude {
777 return false;
778 }
779 if magic.literal && magic.glob {
780 return false;
781 }
782 if !magic.attr_requirements.is_empty() {
783 return false;
784 }
785 if magic.literal || magic.glob || magic.icase {
786 return pathspec_matches(spec, path);
787 }
788 let pattern = strip_top_magic(raw_pattern);
789 let path_for_match = if let Some(prefix) = magic.prefix.as_deref() {
790 if !path.starts_with(prefix) {
791 return false;
792 }
793 &path[prefix.len()..]
794 } else {
795 path
796 };
797 matches_pathspec_with_context(pattern, path_for_match, ctx)
798}
799
800fn pathspec_exclude_element_matches_with_context(
801 spec: &str,
802 path: &str,
803 ctx: PathspecMatchContext,
804) -> bool {
805 let (elem_magic, raw_pattern) = parse_element_magic(spec);
806 let mut magic = combine_magic(elem_magic);
807 if !magic.exclude {
808 return false;
809 }
810 magic.exclude = false;
811 if magic.literal && magic.glob {
812 return false;
813 }
814 if !magic.attr_requirements.is_empty() {
815 return false;
818 }
819 if magic.literal || magic.glob || magic.icase {
820 return pathspec_match_one_positive(path, magic, raw_pattern);
821 }
822 let pattern = strip_top_magic(raw_pattern);
823 let path_for_match = if let Some(prefix) = magic.prefix.as_deref() {
824 if !path.starts_with(prefix) {
825 return false;
826 }
827 &path[prefix.len()..]
828 } else {
829 path
830 };
831 matches_pathspec_with_context(pattern, path_for_match, ctx)
832}
833
834#[must_use]
837pub fn pathspec_exclude_matches(spec: &str, path: &str) -> bool {
838 pathspec_exclude_element_matches_with_context(spec, path, PathspecMatchContext::default())
839}
840
841#[must_use]
846pub fn extend_pathspec_list_implicit_cwd(
847 specs: &[String],
848 cwd_from_repo_root: Option<&str>,
849) -> Vec<String> {
850 if specs.is_empty() {
851 return specs.to_vec();
852 }
853 if !specs.iter().all(|s| pathspec_is_exclude(s)) {
854 return specs.to_vec();
855 }
856 let any_top = specs.iter().any(|s| pathspec_has_top(s));
857 if any_top {
858 return specs.to_vec();
859 }
860 let Some(cwd) = cwd_from_repo_root.map(str::trim).filter(|s| !s.is_empty()) else {
861 return specs.to_vec();
862 };
863 let cwd = cwd.trim_end_matches('/');
864 if cwd.is_empty() {
865 return specs.to_vec();
866 }
867 let mut out = Vec::with_capacity(specs.len() + 1);
868 out.push(format!("{cwd}/"));
869 out.extend_from_slice(specs);
870 out
871}
872
873#[must_use]
877pub fn matches_pathspec_list(path: &str, specs: &[String]) -> bool {
878 matches_pathspec_list_with_context(path, specs, PathspecMatchContext::default())
879}
880
881#[must_use]
883pub fn matches_pathspec_list_with_context(
884 path: &str,
885 specs: &[String],
886 ctx: PathspecMatchContext,
887) -> bool {
888 if specs.is_empty() {
889 return true;
890 }
891 let has_exclude = specs.iter().any(|s| pathspec_is_exclude(s));
892 let positive_specs: Vec<&String> = specs.iter().filter(|s| !pathspec_is_exclude(s)).collect();
893 let positive = if positive_specs.is_empty() {
894 true
895 } else {
896 positive_specs
897 .iter()
898 .any(|s| matches_pathspec_element_with_context(s, path, ctx))
899 };
900 if !positive {
901 return false;
902 }
903 if !has_exclude {
904 return true;
905 }
906 let excluded = specs.iter().any(|s| {
907 pathspec_is_exclude(s) && pathspec_exclude_element_matches_with_context(s, path, ctx)
908 });
909 !excluded
910}
911
912#[must_use]
914pub fn matches_pathspec_list_for_object(
915 path: &str,
916 mode: u32,
917 attr_rules: &[AttrRule],
918 specs: &[String],
919) -> bool {
920 if specs.is_empty() {
921 return true;
922 }
923 let has_exclude = specs.iter().any(|s| pathspec_is_exclude(s));
924 let positive_specs: Vec<&String> = specs.iter().filter(|s| !pathspec_is_exclude(s)).collect();
925 let positive = if positive_specs.is_empty() {
926 true
927 } else {
928 positive_specs
929 .iter()
930 .any(|s| matches_pathspec_for_object(s, path, mode, attr_rules))
931 };
932 if !positive {
933 return false;
934 }
935 if !has_exclude {
936 return true;
937 }
938 let excluded = specs.iter().any(|s| {
939 pathspec_is_exclude(s) && matches_pathspec_exclude_for_object(s, path, mode, attr_rules)
940 });
941 !excluded
942}
943
944fn matches_pathspec_exclude_for_object(
945 spec: &str,
946 path: &str,
947 mode: u32,
948 attr_rules: &[AttrRule],
949) -> bool {
950 let (elem_magic, raw_pattern) = parse_element_magic(spec);
951 let mut magic = combine_magic(elem_magic);
952 if !magic.exclude {
953 return false;
954 }
955 magic.exclude = false;
956 if magic.literal && magic.glob {
957 return false;
958 }
959 let ctx = context_from_mode_bits(mode);
960 let is_dir_for_attr = path.ends_with('/') || ctx.is_directory || ctx.is_git_submodule;
961 if !magic.attr_requirements.is_empty()
962 && !attr_requirements_match(
963 &magic.attr_requirements,
964 attr_rules,
965 path,
966 is_dir_for_attr,
967 mode,
968 )
969 {
970 return false;
971 }
972 let pattern = strip_top_magic(raw_pattern);
973 let path_for_match = if let Some(prefix) = magic.prefix.as_deref() {
974 if !path.starts_with(prefix) {
975 return false;
976 }
977 &path[prefix.len()..]
978 } else {
979 path
980 };
981 if magic.literal || magic.glob || magic.icase {
982 pathspec_matches_tail(pattern, path_for_match, magic)
983 } else {
984 matches_pathspec_with_context(pattern, path_for_match, ctx)
985 }
986}
987
988fn pathspec_matches_tail(pattern: &str, path: &str, magic: PathspecMagic) -> bool {
989 if pattern.is_empty() {
990 return true;
991 }
992
993 let flags = if magic.icase { WM_CASEFOLD } else { 0 };
994
995 if magic.literal {
996 return literal_prefix_match(pattern, path);
997 }
998
999 let wm_flags = if magic.glob {
1000 flags | WM_PATHNAME
1001 } else {
1002 flags
1003 };
1004
1005 let pattern_bytes = pattern.as_bytes();
1006 let path_bytes = path.as_bytes();
1007 let simple = simple_length(pattern);
1008
1009 if ps_str_eq(pattern, path, magic.icase) {
1013 return true;
1014 }
1015 if simple == pattern.len() {
1016 if let Some(prefix) = pattern.strip_suffix('/') {
1017 if ps_str_eq(prefix, path, magic.icase) {
1018 return true;
1019 }
1020 let prefix_slash = format!("{prefix}/");
1021 if path_starts_with(path, &prefix_slash, magic.icase) {
1022 return true;
1023 }
1024 } else {
1025 let prefix_slash = format!("{pattern}/");
1026 if path_starts_with(path, &prefix_slash, magic.icase) {
1027 return true;
1028 }
1029 }
1030 }
1031
1032 if magic.glob && !path.contains('/') && pattern.starts_with("**/") {
1034 if wildmatch(pattern_bytes, path_bytes, wm_flags) {
1035 return true;
1036 }
1037 if let Some(suffix) = pattern.strip_prefix("**/") {
1038 if wildmatch(suffix.as_bytes(), path_bytes, wm_flags) {
1039 return true;
1040 }
1041 }
1042 }
1043
1044 if simple < pattern.len() {
1046 if path_bytes.len() < simple {
1047 return false;
1048 }
1049 let path_lit = &path_bytes[..simple];
1050 let pat_lit = &pattern_bytes[..simple];
1051 let same = if magic.icase {
1052 path_lit.eq_ignore_ascii_case(pat_lit)
1053 } else {
1054 path_lit == pat_lit
1055 };
1056 if !same {
1057 return false;
1058 }
1059 let pat_rest = &pattern[simple..];
1060 let path_rest = &path[simple..];
1061 return wildmatch(pat_rest.as_bytes(), path_rest.as_bytes(), wm_flags);
1062 }
1063
1064 ps_str_eq(pattern, path, magic.icase)
1065 || path_starts_with(path, &format!("{pattern}/"), magic.icase)
1066}
1067
1068fn ps_str_eq(a: &str, b: &str, icase: bool) -> bool {
1069 if icase {
1070 a.eq_ignore_ascii_case(b)
1071 } else {
1072 a == b
1073 }
1074}
1075
1076fn path_starts_with(path: &str, prefix: &str, icase: bool) -> bool {
1077 if icase {
1078 path.get(..prefix.len())
1079 .is_some_and(|head| head.eq_ignore_ascii_case(prefix))
1080 } else {
1081 path.starts_with(prefix)
1082 }
1083}
1084
1085fn literal_prefix_match(pattern: &str, path: &str) -> bool {
1086 if let Some(prefix) = pattern.strip_suffix('/') {
1087 return path == prefix || path.starts_with(&format!("{prefix}/"));
1088 }
1089 path == pattern || path.starts_with(&format!("{pattern}/"))
1090}
1091
1092fn ls_tree_literal_match(pattern: &str, path: &str, ctx: PathspecMatchContext) -> bool {
1094 if let Some(prefix) = pattern.strip_suffix('/') {
1095 if path.starts_with(&format!("{prefix}/")) {
1096 return true;
1097 }
1098 if path == prefix {
1099 return ctx.is_directory || ctx.is_git_submodule;
1100 }
1101 return false;
1102 }
1103 path == pattern || path.starts_with(&format!("{pattern}/"))
1104}
1105
1106#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1111pub struct PathspecMatchContext {
1112 pub is_directory: bool,
1114 pub is_git_submodule: bool,
1116}
1117
1118#[must_use]
1124pub fn matches_pathspec(spec: &str, path: &str) -> bool {
1125 matches_pathspec_with_context(spec, path, PathspecMatchContext::default())
1126}
1127
1128#[must_use]
1132pub fn matches_pathspec_with_context(spec: &str, path: &str, ctx: PathspecMatchContext) -> bool {
1133 let spec_nfc: Cow<'_, str> = if pathspec_precompose_enabled() {
1134 precompose_utf8_path(spec)
1135 } else {
1136 Cow::Borrowed(spec)
1137 };
1138 let path_nfc: Cow<'_, str> = if pathspec_precompose_enabled() {
1139 precompose_utf8_path(path)
1140 } else {
1141 Cow::Borrowed(path)
1142 };
1143 let spec = spec_nfc.as_ref();
1144 let path = path_nfc.as_ref();
1145
1146 let trimmed = spec.strip_prefix("./").unwrap_or(spec);
1147 if trimmed == "." || trimmed.is_empty() {
1148 return true;
1149 }
1150
1151 let (elem_magic, raw_pattern) = parse_element_magic(trimmed);
1152 let magic = combine_magic(elem_magic);
1153
1154 if magic.literal && magic.glob {
1155 return false;
1156 }
1157 if magic.exclude {
1158 return false;
1159 }
1160
1161 let pattern = strip_top_magic(raw_pattern);
1162 let path_for_match = if let Some(prefix) = magic.prefix.as_deref() {
1163 if !path.starts_with(prefix) {
1164 return false;
1165 }
1166 &path[prefix.len()..]
1167 } else {
1168 path
1169 };
1170
1171 if magic.literal {
1172 if let Some(prefix) = pattern.strip_suffix('/') {
1173 if path_for_match.starts_with(&format!("{prefix}/")) {
1174 return true;
1175 }
1176 if path_for_match == prefix {
1177 return ctx.is_directory || ctx.is_git_submodule;
1178 }
1179 return false;
1180 }
1181 return path_for_match == pattern || path_for_match.starts_with(&format!("{pattern}/"));
1182 }
1183
1184 if let Some(prefix) = pattern.strip_suffix('/') {
1186 if simple_length(pattern) == pattern.len() {
1187 if path_for_match.starts_with(&format!("{prefix}/")) {
1188 return true;
1189 }
1190 if path_for_match == prefix {
1191 return ctx.is_directory || ctx.is_git_submodule;
1192 }
1193 return false;
1194 }
1195 }
1196
1197 if pathspec_matches_tail(pattern, path_for_match, magic) {
1198 return true;
1199 }
1200
1201 if (ctx.is_directory || ctx.is_git_submodule)
1202 && !path_for_match.is_empty()
1203 && pattern.len() > path_for_match.len()
1204 && pattern.as_bytes().get(path_for_match.len()) == Some(&b'/')
1205 && pattern.starts_with(path_for_match)
1206 && simple_length(pattern) < pattern.len()
1207 {
1208 return true;
1209 }
1210
1211 false
1212}
1213
1214#[must_use]
1216pub fn context_from_mode_octal(mode: &str) -> PathspecMatchContext {
1217 let Ok(bits) = u32::from_str_radix(mode, 8) else {
1218 return PathspecMatchContext::default();
1219 };
1220 context_from_mode_bits(bits)
1221}
1222
1223#[must_use]
1225pub fn context_from_mode_bits(mode: u32) -> PathspecMatchContext {
1226 let ty = mode & 0o170000;
1227 PathspecMatchContext {
1228 is_directory: ty == 0o040000,
1229 is_git_submodule: ty == 0o160000,
1230 }
1231}
1232
1233#[must_use]
1239pub fn matches_ls_tree_pathspec(
1240 spec: &str,
1241 path: &str,
1242 mode: u32,
1243 attr_rules: &[AttrRule],
1244) -> bool {
1245 let (elem_magic, raw_pattern) = parse_element_magic(spec);
1246 let mut magic = combine_magic(elem_magic);
1247 magic.exclude = false;
1248
1249 if magic.literal && magic.glob {
1250 return false;
1251 }
1252
1253 let ctx = context_from_mode_bits(mode);
1254 let is_dir_for_attr = path.ends_with('/') || ctx.is_directory || ctx.is_git_submodule;
1255
1256 if !magic.attr_requirements.is_empty()
1257 && !attr_requirements_match(
1258 &magic.attr_requirements,
1259 attr_rules,
1260 path,
1261 is_dir_for_attr,
1262 mode,
1263 )
1264 {
1265 return false;
1266 }
1267
1268 let pattern = strip_top_magic(raw_pattern);
1269 let path_for_match = if let Some(prefix) = magic.prefix.as_deref() {
1270 if !path.starts_with(prefix) {
1271 return false;
1272 }
1273 &path[prefix.len()..]
1274 } else {
1275 path
1276 };
1277
1278 if magic.literal || magic.glob || magic.icase {
1279 return pathspec_matches_tail(pattern, path_for_match, magic);
1280 }
1281
1282 let spec_nfc: Cow<'_, str> = if pathspec_precompose_enabled() {
1283 precompose_utf8_path(pattern)
1284 } else {
1285 Cow::Borrowed(pattern)
1286 };
1287 let path_nfc: Cow<'_, str> = if pathspec_precompose_enabled() {
1288 precompose_utf8_path(path_for_match)
1289 } else {
1290 Cow::Borrowed(path_for_match)
1291 };
1292 let pattern = spec_nfc.as_ref();
1293 let path = path_nfc.as_ref();
1294
1295 let trimmed = pattern.strip_prefix("./").unwrap_or(pattern);
1296 if trimmed == "." || trimmed.is_empty() {
1297 return true;
1298 }
1299
1300 let uses_star_or_question = trimmed.contains('*') || trimmed.contains('?');
1301 if !uses_star_or_question {
1302 return ls_tree_literal_match(trimmed, path, ctx);
1303 }
1304
1305 let nwl = simple_length(trimmed);
1306 let flags = 0u32;
1307 if nwl == trimmed.len() {
1308 return wildmatch(trimmed.as_bytes(), path.as_bytes(), flags);
1309 }
1310 let lit = trimmed.as_bytes().get(..nwl).unwrap_or_default();
1311 let path_b = path.as_bytes();
1312 if path_b.len() < nwl {
1313 return false;
1314 }
1315 if &path_b[..nwl] != lit {
1316 return false;
1317 }
1318 let pat_rest = &trimmed[nwl..];
1319 let path_rest = &path[nwl..];
1320 wildmatch(pat_rest.as_bytes(), path_rest.as_bytes(), flags)
1321}
1322
1323#[must_use]
1328pub fn matches_pathspec_for_object(
1329 spec: &str,
1330 path: &str,
1331 mode: u32,
1332 attr_rules: &[AttrRule],
1333) -> bool {
1334 let (elem_magic, raw_pattern) = parse_element_magic(spec);
1335 let mut magic = combine_magic(elem_magic);
1336 magic.exclude = false;
1337
1338 if magic.literal && magic.glob {
1339 return false;
1340 }
1341
1342 let ctx = context_from_mode_bits(mode);
1343 let is_dir_for_attr = path.ends_with('/') || ctx.is_directory || ctx.is_git_submodule;
1344
1345 if !magic.attr_requirements.is_empty()
1346 && !attr_requirements_match(
1347 &magic.attr_requirements,
1348 attr_rules,
1349 path,
1350 is_dir_for_attr,
1351 mode,
1352 )
1353 {
1354 return false;
1355 }
1356
1357 let pattern = strip_top_magic(raw_pattern);
1358 let path_for_match = if let Some(prefix) = magic.prefix.as_deref() {
1359 if !path.starts_with(prefix) {
1360 return false;
1361 }
1362 &path[prefix.len()..]
1363 } else {
1364 path
1365 };
1366 if magic.literal || magic.glob || magic.icase {
1367 pathspec_matches_tail(pattern, path_for_match, magic)
1368 } else {
1369 matches_pathspec_with_context(pattern, path_for_match, ctx)
1370 }
1371}
1372
1373#[must_use]
1376pub fn wildmatch_flags_icase_glob(icase: bool, glob: bool) -> u32 {
1377 let mut f = if glob { WM_PATHNAME } else { 0 };
1378 if icase {
1379 f |= WM_CASEFOLD;
1380 }
1381 f
1382}
1383
1384#[derive(Debug, Clone)]
1386pub struct PathOutsideRepository {
1387 pub elt: String,
1389 pub path: String,
1391 pub work_tree: PathBuf,
1393}
1394
1395impl std::fmt::Display for PathOutsideRepository {
1396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1397 write!(
1398 f,
1399 "fatal: {}: '{}' is outside repository at '{}'",
1400 self.elt,
1401 self.path,
1402 self.work_tree.display()
1403 )
1404 }
1405}
1406
1407pub fn resolve_magic_pathspec(spec: &str, cwd_prefix: &str) -> Option<String> {
1413 if !spec.starts_with(":(") {
1414 return None;
1415 }
1416 let close_idx = spec.find(')')?;
1417 let magic_prefix = &spec[..=close_idx];
1418 let tail = &spec[close_idx + 1..];
1419 Some(resolve_magic_pathspec_parts(magic_prefix, tail, cwd_prefix))
1420}
1421
1422fn resolve_magic_pathspec_parts(magic_prefix: &str, tail: &str, cwd_prefix: &str) -> String {
1423 if has_magic_prefix_token(magic_prefix) {
1424 return format!("{magic_prefix}{tail}");
1425 }
1426
1427 if let Some(rooted_tail) = tail.strip_prefix('/') {
1428 return format!("{magic_prefix}{}", normalize_relative_path_str(rooted_tail));
1429 }
1430
1431 let combined = if cwd_prefix.is_empty() {
1432 normalize_relative_path_str(tail)
1433 } else {
1434 normalize_relative_path_str(&format!("{cwd_prefix}{tail}"))
1435 };
1436
1437 let cwd_base = normalize_relative_path_str(cwd_prefix.trim_end_matches('/'));
1438 if !cwd_base.is_empty()
1439 && (combined == cwd_base || combined.starts_with(&format!("{cwd_base}/")))
1440 {
1441 let after_base = combined
1442 .strip_prefix(&cwd_base)
1443 .unwrap_or(combined.as_str());
1444 let remainder = after_base.strip_prefix('/').unwrap_or(after_base);
1445 let magic_with_prefix = inject_magic_prefix_token(magic_prefix, &format!("{cwd_base}/"));
1446 return format!("{magic_with_prefix}{remainder}");
1447 }
1448
1449 format!("{magic_prefix}{combined}")
1450}
1451
1452fn has_magic_prefix_token(magic_prefix: &str) -> bool {
1453 let Some(inner) = magic_prefix
1454 .strip_prefix(":(")
1455 .and_then(|s| s.strip_suffix(')'))
1456 else {
1457 return false;
1458 };
1459 inner
1460 .split(',')
1461 .map(str::trim)
1462 .any(|token| token.starts_with("prefix:"))
1463}
1464
1465fn inject_magic_prefix_token(magic_prefix: &str, prefix: &str) -> String {
1466 let Some(inner) = magic_prefix
1467 .strip_prefix(":(")
1468 .and_then(|s| s.strip_suffix(')'))
1469 else {
1470 return magic_prefix.to_string();
1471 };
1472 if inner.trim().is_empty() {
1473 format!(":(prefix:{prefix})")
1474 } else {
1475 format!(":({inner},prefix:{prefix})")
1476 }
1477}
1478
1479fn normalize_relative_path_str(path: &str) -> String {
1480 let mut parts: Vec<String> = Vec::new();
1481 for component in std::path::Path::new(path).components() {
1482 match component {
1483 std::path::Component::CurDir => {}
1484 std::path::Component::ParentDir => {
1485 parts.pop();
1486 }
1487 std::path::Component::Normal(seg) => {
1488 parts.push(seg.to_string_lossy().to_string());
1489 }
1490 std::path::Component::RootDir | std::path::Component::Prefix(_) => {}
1491 }
1492 }
1493 parts.join("/")
1494}
1495
1496#[must_use]
1498pub fn pathdiff(cwd: &Path, work_tree: &Path) -> Option<String> {
1499 let cwd_canon = cwd.canonicalize().ok()?;
1500 let wt_canon = work_tree.canonicalize().ok()?;
1501
1502 if cwd_canon == wt_canon {
1503 return None;
1504 }
1505
1506 cwd_canon
1507 .strip_prefix(&wt_canon)
1508 .ok()
1509 .map(|p| p.to_string_lossy().to_string())
1510}
1511
1512fn prepend_cwd_to_short_exclude_pathspec(spec: &str, cwd: &str) -> Option<String> {
1515 let cwd = cwd.trim_end_matches('/');
1516 if cwd.is_empty() {
1517 return None;
1518 }
1519 let bytes = spec.as_bytes();
1520 if bytes.first().copied() != Some(b':') {
1521 return None;
1522 }
1523 if bytes.get(1).copied() == Some(b'/') {
1525 return None;
1526 }
1527 let mut i = 1usize;
1528 while i < bytes.len() && bytes[i] != b':' {
1529 let ch = bytes[i];
1530 if ch == b'^' {
1531 i += 1;
1532 continue;
1533 }
1534 let is_magic = matches!(ch, b'!' | b'/');
1535 if is_magic {
1536 i += 1;
1537 continue;
1538 }
1539 break;
1540 }
1541 if i < bytes.len() && bytes[i] == b':' {
1542 i += 1;
1543 }
1544 let pattern = spec.get(i..)?;
1545 if pattern.is_empty() || pattern.starts_with('/') {
1546 return None;
1547 }
1548 Some(format!("{}{}/{pattern}", &spec[..i], cwd))
1549}
1550
1551#[must_use]
1556pub fn resolve_pathspec(pathspec: &str, work_tree: &Path, prefix: Option<&str>) -> String {
1557 if pathspec == "." {
1560 return match prefix {
1561 Some(p) if !p.is_empty() => p.to_owned(),
1562 _ => ".".to_owned(),
1563 };
1564 }
1565 if pathspec.contains("../") || pathspec.starts_with("../") {
1566 let cwd = std::env::current_dir().unwrap_or_default();
1567 let abs = cwd.join(pathspec);
1568 let wt_canon = work_tree
1569 .canonicalize()
1570 .unwrap_or_else(|_| work_tree.to_path_buf());
1571 let mut parts: Vec<std::ffi::OsString> = Vec::new();
1572 for component in abs.components() {
1573 use std::path::Component;
1574 match component {
1575 Component::ParentDir => {
1576 parts.pop();
1577 }
1578 Component::CurDir => {}
1579 other => parts.push(other.as_os_str().to_os_string()),
1580 }
1581 }
1582 let abs_norm: PathBuf = parts.iter().collect();
1583 if let Ok(rel) = abs_norm.strip_prefix(&wt_canon) {
1584 return rel.to_string_lossy().to_string();
1585 }
1586 }
1587 if Path::new(pathspec).is_absolute() {
1588 let abs = Path::new(pathspec);
1589 let wt_canon = work_tree
1590 .canonicalize()
1591 .unwrap_or_else(|_| work_tree.to_path_buf());
1592 let abs_canon = abs.canonicalize().unwrap_or_else(|_| abs.to_path_buf());
1593 if let Ok(rel) = abs_canon.strip_prefix(&wt_canon) {
1594 return rel.to_string_lossy().to_string();
1595 }
1596 return pathspec.to_owned();
1597 }
1598
1599 if pathspec.starts_with(':') {
1600 if let Some(p) = prefix {
1601 if !p.is_empty() && !literal_pathspecs_enabled() {
1602 let cwd_ps = format!("{}/", p.trim_end_matches('/'));
1603 if pathspec.starts_with(":(") {
1604 if let Some(resolved) = resolve_magic_pathspec(pathspec, &cwd_ps) {
1605 return resolved;
1606 }
1607 return pathspec.to_owned();
1608 }
1609 if pathspec_is_exclude(pathspec) {
1610 if let Some(fixed) = prepend_cwd_to_short_exclude_pathspec(pathspec, p) {
1611 return fixed;
1612 }
1613 }
1614 }
1615 }
1616 if let Some(rest) = pathspec.strip_prefix(":/") {
1617 if rest.starts_with('!') || rest.starts_with('^') {
1619 return pathspec.to_owned();
1620 }
1621 return rest.to_owned();
1622 }
1623 if pathspec.starts_with(":(") {
1626 return pathspec.to_owned();
1627 }
1628 return pathspec.to_owned();
1629 }
1630
1631 match prefix {
1632 Some(p) if !p.is_empty() => {
1633 normalize_relative_path_str(&PathBuf::from(p).join(pathspec).to_string_lossy())
1634 }
1635 _ => pathspec.to_owned(),
1636 }
1637}
1638
1639pub fn resolve_pathspec_in_worktree(
1644 elt: &str,
1645 pathspec: &str,
1646 work_tree: &Path,
1647 prefix: Option<&str>,
1648) -> Result<String, PathOutsideRepository> {
1649 let resolved = resolve_pathspec(pathspec, work_tree, prefix);
1650 if Path::new(&resolved).is_absolute() {
1651 let wt = work_tree
1652 .canonicalize()
1653 .unwrap_or_else(|_| work_tree.to_path_buf());
1654 return Err(PathOutsideRepository {
1655 elt: elt.to_string(),
1656 path: resolved,
1657 work_tree: wt,
1658 });
1659 }
1660 Ok(resolved)
1661}
1662
1663#[must_use]
1667pub fn normalize_worktree_file_path(
1668 file_path: &str,
1669 work_tree: &Path,
1670 prefix: Option<&str>,
1671) -> String {
1672 let resolved = resolve_pathspec(file_path, work_tree, prefix);
1673 if Path::new(&resolved).is_absolute() {
1674 file_path.to_string()
1675 } else {
1676 resolved
1677 }
1678}
1679
1680#[cfg(test)]
1681mod tree_entry_pathspec_tests {
1682 use super::*;
1683
1684 #[test]
1685 fn t6130_bracket_filename_matches_pathspec() {
1686 assert!(matches_pathspec("f[o][o]", "f[o][o]"));
1687 assert!(matches_pathspec(":(glob)f[o][o]", "f[o][o]"));
1688 }
1689
1690 #[test]
1691 fn literal_prefix_and_exact() {
1692 assert!(matches_pathspec("path1", "path1/file1"));
1693 assert!(matches_pathspec_with_context(
1694 "path1/",
1695 "path1/file1",
1696 PathspecMatchContext::default()
1697 ));
1698 assert!(matches_pathspec("file0", "file0"));
1699 assert!(!matches_pathspec("path", "path1/file1"));
1700 }
1701
1702 #[test]
1703 fn ls_tree_bracket_in_name_is_literal_prefix() {
1704 assert!(matches_ls_tree_pathspec(
1705 "a[a]",
1706 "a[a]/three",
1707 0o100644,
1708 &[]
1709 ));
1710 assert!(!matches_pathspec_with_context(
1711 "a[a]",
1712 "a[a]/three",
1713 PathspecMatchContext::default()
1714 ));
1715 }
1716
1717 #[test]
1718 fn wildcards_cross_slash_by_default() {
1719 assert!(matches_pathspec("f*", "file0"));
1720 assert!(matches_pathspec("*file1", "path1/file1"));
1721 assert!(matches_pathspec_with_context(
1722 "path1/f*",
1723 "path1",
1724 PathspecMatchContext {
1725 is_directory: true,
1726 ..Default::default()
1727 }
1728 ));
1729 assert!(matches_pathspec("path1/*file1", "path1/file1"));
1730 }
1731
1732 #[test]
1733 fn glob_double_star_txt_at_repo_root() {
1734 assert!(pathspec_matches(":(glob)**/*.txt", "untracked.txt"));
1735 assert!(pathspec_matches(":(glob)**/*.txt", "d/untracked.txt"));
1736 }
1737
1738 #[test]
1739 fn trailing_slash_directory_only() {
1740 assert!(!matches_pathspec_with_context(
1741 "file0/",
1742 "file0",
1743 PathspecMatchContext::default()
1744 ));
1745 assert!(matches_pathspec_with_context(
1746 "file0/",
1747 "file0",
1748 PathspecMatchContext {
1749 is_directory: true,
1750 ..Default::default()
1751 }
1752 ));
1753 assert!(matches_pathspec_with_context(
1754 "submod/",
1755 "submod",
1756 PathspecMatchContext {
1757 is_git_submodule: true,
1758 ..Default::default()
1759 }
1760 ));
1761 }
1762
1763 #[test]
1764 fn exclude_top_short_magic_subtracts_from_positive() {
1765 let specs = vec!["*".to_string(), ":/!sub2".to_string()];
1766 assert!(matches_pathspec_list("sub/file", &specs));
1767 assert!(!matches_pathspec_list("sub2/file", &specs));
1768 assert!(pathspec_exclude_matches(":/!sub2", "sub2/file"));
1769 }
1770}
1771
1772#[cfg(test)]
1773mod pathspec_list_tests {
1774 use super::*;
1775 use crate::crlf::parse_gitattributes_content;
1776
1777 #[test]
1778 fn exclude_removes_paths_matching_icase_positive() {
1779 let specs = vec![
1780 ":(icase)*.txt".to_string(),
1781 ":(exclude)submodule/subsub/*".to_string(),
1782 ];
1783 assert!(path_allowed_by_pathspec_list(&specs, "submodule/g.txt"));
1784 assert!(!path_allowed_by_pathspec_list(
1785 &specs,
1786 "submodule/subsub/e.txt"
1787 ));
1788 }
1789
1790 #[test]
1791 fn prefixed_attr_exclude_removes_matching_child_path() {
1792 let specs = vec![
1793 "sub".to_string(),
1794 ":(exclude,attr:labelB,prefix:sub/)".to_string(),
1795 ];
1796 let exclude_only = vec![":(exclude,attr:labelB,prefix:sub/)".to_string()];
1797 let attrs = parse_gitattributes_content("fileB labelB\n");
1798 assert!(!matches_pathspec_list_for_object(
1799 "sub/fileB",
1800 0o100644,
1801 &attrs,
1802 &specs,
1803 ));
1804 assert!(!matches_pathspec_list_for_object(
1805 "sub/fileB",
1806 0o100644,
1807 &attrs,
1808 &exclude_only,
1809 ));
1810 }
1811}