1use globset::{Glob, GlobSet, GlobSetBuilder};
25use serde::{Deserialize, Serialize};
26use std::collections::HashMap;
27use std::path::{Path, PathBuf};
28use thiserror::Error;
29
30pub const CONFIG_FILE_NAME: &str = ".sqry-config.toml";
32
33#[derive(Debug, Error)]
35pub enum ConfigError {
36 #[error("Configuration file not found")]
38 NotFound,
39
40 #[error("Failed to parse config at {0}: {1}")]
42 ParseError(PathBuf, String),
43
44 #[error("Failed to read config at {0}: {1}")]
46 IoError(PathBuf, std::io::Error),
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
54#[serde(default)]
55pub struct ProjectConfig {
56 #[serde(default)]
58 pub ignore: IgnoreConfig,
59
60 #[serde(default)]
62 pub include: IncludeConfig,
63
64 #[serde(default)]
66 pub languages: LanguageConfig,
67
68 #[serde(default)]
70 pub indexing: IndexingConfig,
71
72 #[serde(default)]
74 pub cache: CacheConfig,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
82#[serde(default)]
83pub struct IgnoreConfig {
84 #[serde(default = "default_ignore_patterns")]
94 pub patterns: Vec<String>,
95}
96
97impl Default for IgnoreConfig {
98 fn default() -> Self {
99 Self {
100 patterns: default_ignore_patterns(),
101 }
102 }
103}
104
105fn default_ignore_patterns() -> Vec<String> {
106 vec![
107 "node_modules/**".to_string(),
108 "target/**".to_string(),
109 "dist/**".to_string(),
110 "*.min.js".to_string(),
111 "vendor/**".to_string(),
112 ".git/**".to_string(),
113 "__pycache__/**".to_string(),
114 ".pytest_cache/**".to_string(),
115 ".mypy_cache/**".to_string(),
116 ".tox/**".to_string(),
117 ".venv/**".to_string(),
118 "venv/**".to_string(),
119 ".gradle/**".to_string(),
120 ".idea/**".to_string(),
121 ".vs/**".to_string(),
122 ".vscode/**".to_string(),
123 ]
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
128#[serde(default)]
129pub struct IncludeConfig {
130 #[serde(default)]
134 pub patterns: Vec<String>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
139#[serde(default)]
140pub struct LanguageConfig {
141 #[serde(default)]
145 pub extensions: HashMap<String, String>,
146
147 #[serde(default)]
151 pub files: HashMap<String, String>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
156#[serde(default)]
157pub struct IndexingConfig {
158 #[serde(default = "default_max_file_size")]
162 pub max_file_size: u64,
163
164 #[serde(default = "default_max_depth")]
168 pub max_depth: u32,
169
170 #[serde(default = "default_true")]
174 pub enable_scope_extraction: bool,
175
176 #[serde(default = "default_true")]
180 pub enable_relation_extraction: bool,
181
182 #[serde(default)]
187 pub additional_ignored_dirs: Vec<String>,
188}
189
190impl Default for IndexingConfig {
191 fn default() -> Self {
192 Self {
193 max_file_size: default_max_file_size(),
194 max_depth: default_max_depth(),
195 enable_scope_extraction: true,
196 enable_relation_extraction: true,
197 additional_ignored_dirs: Vec::new(),
198 }
199 }
200}
201
202fn default_max_file_size() -> u64 {
203 10_485_760 }
205
206fn default_max_depth() -> u32 {
207 100
208}
209
210fn default_true() -> bool {
211 true
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
216#[serde(default)]
217pub struct CacheConfig {
218 #[serde(default = "default_cache_directory")]
222 pub directory: String,
223
224 #[serde(default = "default_true")]
228 pub persistent: bool,
229}
230
231impl Default for CacheConfig {
232 fn default() -> Self {
233 Self {
234 directory: default_cache_directory(),
235 persistent: true,
236 }
237 }
238}
239
240fn default_cache_directory() -> String {
241 ".sqry-cache".to_string()
242}
243
244impl ProjectConfig {
245 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
255 let path = path.as_ref();
256
257 let contents = std::fs::read_to_string(path)
258 .map_err(|e| ConfigError::IoError(path.to_path_buf(), e))?;
259
260 toml::from_str(&contents)
261 .map_err(|e| ConfigError::ParseError(path.to_path_buf(), e.to_string()))
262 }
263
264 pub fn load_from_index_root<P: AsRef<Path>>(index_root: P) -> Self {
282 match Self::try_load_from_index_root(index_root.as_ref()) {
283 Ok(config) => config,
284 Err(ConfigError::NotFound) => {
285 Self::default()
287 }
288 Err(ConfigError::ParseError(path, err)) => {
289 log::warn!(
290 "Malformed {} at {}: {}. Using defaults.",
291 CONFIG_FILE_NAME,
292 path.display(),
293 err
294 );
295 Self::default()
296 }
297 Err(ConfigError::IoError(path, err)) => {
298 log::warn!(
299 "Cannot read {} at {}: {}. Using defaults.",
300 CONFIG_FILE_NAME,
301 path.display(),
302 err
303 );
304 Self::default()
305 }
306 }
307 }
308
309 fn try_load_from_index_root(index_root: &Path) -> Result<Self, ConfigError> {
314 let mut current = index_root;
315
316 loop {
317 let config_path = current.join(CONFIG_FILE_NAME);
318
319 if config_path.exists() {
320 return Self::load(&config_path);
321 }
322
323 match current.parent() {
325 Some(parent) if !parent.as_os_str().is_empty() => {
326 current = parent;
327 }
328 _ => break, }
330 }
331
332 Err(ConfigError::NotFound)
333 }
334
335 #[must_use]
340 pub fn effective_ignored_dirs(&self) -> Vec<&str> {
341 use crate::project::path_utils::DEFAULT_IGNORED_DIRS;
342
343 let mut dirs: Vec<&str> = DEFAULT_IGNORED_DIRS.to_vec();
344
345 for dir in &self.indexing.additional_ignored_dirs {
347 dirs.push(dir.as_str());
348 }
349
350 dirs
351 }
352
353 #[must_use]
368 pub fn is_ignored(&self, path: &Path) -> bool {
369 let normalized = normalize_path_for_matching(path);
371
372 let ignore_set = match build_glob_set(&self.ignore.patterns) {
374 Ok(set) => set,
375 Err(e) => {
376 log::warn!("Invalid ignore pattern: {e}");
377 return false;
378 }
379 };
380
381 if ignore_set.is_match(&normalized) {
383 if !self.include.patterns.is_empty() {
385 let include_set = match build_glob_set(&self.include.patterns) {
386 Ok(set) => set,
387 Err(e) => {
388 log::warn!("Invalid include pattern: {e}");
389 return true; }
391 };
392
393 if include_set.is_match(&normalized) {
394 return false; }
396 }
397 return true;
398 }
399
400 false
401 }
402
403 #[must_use]
407 pub fn language_for_path(&self, path: &Path) -> Option<&str> {
408 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
410 for (pattern, lang) in &self.languages.files {
411 if glob_match_filename(pattern, filename) {
412 return Some(lang.as_str());
413 }
414 }
415 }
416
417 if let Some(ext) = path.extension().and_then(|e| e.to_str())
419 && let Some(lang) = self.languages.extensions.get(ext)
420 {
421 return Some(lang.as_str());
422 }
423
424 None
425 }
426}
427
428fn build_glob_set(patterns: &[String]) -> Result<GlobSet, globset::Error> {
441 let mut builder = GlobSetBuilder::new();
442
443 for pattern in patterns {
444 for normalized in normalize_gitignore_pattern(pattern) {
446 let glob = Glob::new(&normalized)?;
447 builder.add(glob);
448 }
449 }
450
451 builder.build()
452}
453
454fn normalize_gitignore_pattern(pattern: &str) -> Vec<String> {
466 if let Some(stripped) = pattern.strip_prefix('/') {
468 return normalize_rooted_pattern(stripped);
469 }
470
471 if pattern.starts_with("**/") {
473 return vec![pattern.to_string()];
474 }
475
476 let pattern_core = pattern
480 .strip_suffix("/**")
481 .or_else(|| pattern.strip_suffix('/'))
482 .unwrap_or(pattern);
483
484 if pattern_core.contains('/') {
486 if pattern.ends_with('/') && !pattern.ends_with("/**") {
489 let dir_name = pattern.trim_end_matches('/');
491 return vec![dir_name.to_string(), format!("{dir_name}/**")];
492 }
493 return vec![pattern.to_string()];
494 }
495
496 if pattern.ends_with("/**") {
499 return vec![format!("**/{pattern}")];
501 }
502
503 if pattern.ends_with('/') {
504 let dir_name = pattern.trim_end_matches('/');
506 return vec![format!("**/{dir_name}"), format!("**/{dir_name}/**")];
507 }
508
509 vec![format!("**/{pattern}")]
511}
512
513fn normalize_rooted_pattern(pattern: &str) -> Vec<String> {
515 if pattern.ends_with("/**") {
517 return vec![pattern.to_string()];
518 }
519
520 if pattern.ends_with('/') {
522 let dir_name = pattern.trim_end_matches('/');
523 return vec![dir_name.to_string(), format!("{dir_name}/**")];
524 }
525
526 let last_segment = pattern.rsplit(['/', '\\']).next().unwrap_or(pattern);
531
532 let has_glob =
534 last_segment.contains('*') || last_segment.contains('?') || last_segment.contains('[');
535
536 if has_glob {
537 return vec![pattern.to_string()];
538 }
539
540 let looks_like_file = if let Some(name) = last_segment.strip_prefix('.') {
548 name.ends_with("ignore") || name.ends_with("rc") || name.ends_with("attributes") || name.ends_with("modules") || (name != "config" && name.ends_with("config"))
560 || name.contains('.') } else if let Some(dot_pos) = last_segment.rfind('.') {
562 dot_pos > 0 && dot_pos < last_segment.len() - 1
564 } else {
565 false
566 };
567
568 if looks_like_file {
569 vec![pattern.to_string()]
571 } else {
572 vec![pattern.to_string(), format!("{pattern}/**")]
574 }
575}
576
577fn glob_match_filename(pattern: &str, filename: &str) -> bool {
581 match Glob::new(pattern) {
583 Ok(glob) => glob.compile_matcher().is_match(filename),
584 Err(_) => pattern == filename,
585 }
586}
587
588fn normalize_path_for_matching(path: &Path) -> String {
594 let path_str = path.to_string_lossy();
596
597 let normalized = path_str.replace('\\', "/");
599
600 normalized
602 .strip_prefix('/')
603 .unwrap_or(&normalized)
604 .to_string()
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use tempfile::TempDir;
611
612 #[test]
613 fn test_default_config() {
614 let config = ProjectConfig::default();
615
616 assert!(!config.ignore.patterns.is_empty());
618 assert!(config.include.patterns.is_empty());
619 assert!(config.languages.extensions.is_empty());
620 assert_eq!(config.indexing.max_file_size, 10_485_760);
621 assert_eq!(config.indexing.max_depth, 100);
622 assert!(config.indexing.enable_scope_extraction);
623 assert!(config.indexing.enable_relation_extraction);
624 assert_eq!(config.cache.directory, ".sqry-cache");
625 assert!(config.cache.persistent);
626 }
627
628 #[test]
629 fn test_load_config_from_file() {
630 let temp = TempDir::new().unwrap();
631 let config_path = temp.path().join(CONFIG_FILE_NAME);
632
633 let toml_content = r#"
634[ignore]
635patterns = ["custom/**", "*.bak"]
636
637[include]
638patterns = ["custom/important/**"]
639
640[languages]
641extensions = { "jsx" = "javascript" }
642files = { "Jenkinsfile" = "groovy" }
643
644[indexing]
645max_file_size = 5242880
646max_depth = 50
647enable_scope_extraction = false
648additional_ignored_dirs = ["my_vendor"]
649
650[cache]
651directory = ".my-cache"
652persistent = false
653"#;
654
655 std::fs::write(&config_path, toml_content).unwrap();
656
657 let config = ProjectConfig::load(&config_path).unwrap();
658
659 assert_eq!(config.ignore.patterns, vec!["custom/**", "*.bak"]);
660 assert_eq!(config.include.patterns, vec!["custom/important/**"]);
661 assert_eq!(
662 config.languages.extensions.get("jsx"),
663 Some(&"javascript".to_string())
664 );
665 assert_eq!(
666 config.languages.files.get("Jenkinsfile"),
667 Some(&"groovy".to_string())
668 );
669 assert_eq!(config.indexing.max_file_size, 5_242_880);
670 assert_eq!(config.indexing.max_depth, 50);
671 assert!(!config.indexing.enable_scope_extraction);
672 assert_eq!(config.indexing.additional_ignored_dirs, vec!["my_vendor"]);
673 assert_eq!(config.cache.directory, ".my-cache");
674 assert!(!config.cache.persistent);
675 }
676
677 #[test]
678 fn test_load_config_ancestor_walk() {
679 let temp = TempDir::new().unwrap();
680
681 let nested = temp.path().join("level1/level2/level3");
683 std::fs::create_dir_all(&nested).unwrap();
684
685 let config_path = temp.path().join("level1").join(CONFIG_FILE_NAME);
687 std::fs::write(
688 &config_path,
689 r"
690[indexing]
691max_depth = 42
692",
693 )
694 .unwrap();
695
696 let config = ProjectConfig::load_from_index_root(&nested);
698 assert_eq!(config.indexing.max_depth, 42);
699 }
700
701 #[test]
702 fn test_load_config_not_found_uses_defaults() {
703 let temp = TempDir::new().unwrap();
704
705 let config = ProjectConfig::load_from_index_root(temp.path());
707
708 assert_eq!(config, ProjectConfig::default());
710 }
711
712 #[test]
713 fn test_partial_config_uses_defaults() {
714 let temp = TempDir::new().unwrap();
715 let config_path = temp.path().join(CONFIG_FILE_NAME);
716
717 std::fs::write(
719 &config_path,
720 r"
721[indexing]
722max_depth = 25
723",
724 )
725 .unwrap();
726
727 let config = ProjectConfig::load(&config_path).unwrap();
728
729 assert_eq!(config.indexing.max_depth, 25);
731
732 assert_eq!(config.indexing.max_file_size, 10_485_760);
734 assert!(config.indexing.enable_scope_extraction);
735 assert_eq!(config.cache.directory, ".sqry-cache");
736 }
737
738 #[test]
739 fn test_effective_ignored_dirs() {
740 let mut config = ProjectConfig::default();
741 config.indexing.additional_ignored_dirs =
742 vec!["my_vendor".to_string(), "artifacts".to_string()];
743
744 let dirs = config.effective_ignored_dirs();
745
746 assert!(dirs.contains(&"node_modules"));
748 assert!(dirs.contains(&"target"));
749
750 assert!(dirs.contains(&"my_vendor"));
752 assert!(dirs.contains(&"artifacts"));
753 }
754
755 #[test]
756 fn test_language_for_path() {
757 let mut config = ProjectConfig::default();
758 config
759 .languages
760 .extensions
761 .insert("jsx".to_string(), "javascript".to_string());
762 config
763 .languages
764 .files
765 .insert("Jenkinsfile".to_string(), "groovy".to_string());
766
767 assert_eq!(
769 config.language_for_path(Path::new("src/App.jsx")),
770 Some("javascript")
771 );
772
773 assert_eq!(
775 config.language_for_path(Path::new("ci/Jenkinsfile")),
776 Some("groovy")
777 );
778
779 assert_eq!(config.language_for_path(Path::new("src/main.rs")), None);
781 }
782
783 #[test]
784 fn test_glob_match_filename() {
785 assert!(glob_match_filename("*.js", "app.js"));
787 assert!(!glob_match_filename("*.js", "app.ts"));
788
789 assert!(glob_match_filename("file?.txt", "file1.txt"));
791 assert!(!glob_match_filename("file?.txt", "file12.txt"));
792
793 assert!(glob_match_filename("Jenkinsfile", "Jenkinsfile"));
795 assert!(!glob_match_filename("Jenkinsfile", "Jenkinsfile.bak"));
796 }
797
798 #[test]
799 fn test_is_ignored_basic() {
800 let config = ProjectConfig::default();
801
802 assert!(config.is_ignored(Path::new("node_modules/foo.js")));
804 assert!(config.is_ignored(Path::new("target/debug/binary")));
805 assert!(config.is_ignored(Path::new(".git/config")));
806 assert!(config.is_ignored(Path::new("__pycache__/module.pyc")));
807
808 assert!(!config.is_ignored(Path::new("src/main.rs")));
810 assert!(!config.is_ignored(Path::new("lib/utils.js")));
811 }
812
813 #[test]
814 fn test_is_ignored_nested_paths() {
815 let config = ProjectConfig::default();
817
818 assert!(config.is_ignored(Path::new("packages/frontend/node_modules/react/index.js")));
820 assert!(config.is_ignored(Path::new("deep/nested/path/node_modules/pkg/lib.js")));
821
822 assert!(config.is_ignored(Path::new("crates/lib/target/release/libfoo.so")));
824 }
825
826 #[test]
827 fn test_is_ignored_absolute_paths() {
828 let config = ProjectConfig::default();
830
831 assert!(config.is_ignored(Path::new("/home/user/project/node_modules/pkg/index.js")));
833 assert!(config.is_ignored(Path::new("/tmp/build/target/debug/app")));
834 assert!(config.is_ignored(Path::new("/var/repo/.git/objects/pack/abc")));
835 }
836
837 #[test]
838 fn test_is_ignored_include_overrides() {
839 let mut config = ProjectConfig::default();
840 config.ignore.patterns = vec!["vendor/**".to_string()];
841 config.include.patterns = vec!["vendor/internal/**".to_string()];
842
843 assert!(config.is_ignored(Path::new("vendor/external/lib.js")));
845 assert!(config.is_ignored(Path::new("vendor/third_party/pkg.py")));
846
847 assert!(!config.is_ignored(Path::new("vendor/internal/core.rs")));
849 assert!(!config.is_ignored(Path::new("vendor/internal/nested/utils.rs")));
850 }
851
852 #[test]
853 fn test_is_ignored_extension_patterns() {
854 let mut config = ProjectConfig::default();
855 config.ignore.patterns = vec!["*.min.js".to_string(), "*.bak".to_string()];
856
857 assert!(config.is_ignored(Path::new("dist/app.min.js")));
859 assert!(config.is_ignored(Path::new("src/old.bak")));
860 assert!(config.is_ignored(Path::new("deeply/nested/file.min.js")));
861
862 assert!(!config.is_ignored(Path::new("src/app.js")));
864 }
865
866 #[test]
867 fn test_normalize_gitignore_pattern() {
868 assert_eq!(
870 normalize_gitignore_pattern("node_modules/**"),
871 vec!["**/node_modules/**"]
872 );
873 assert_eq!(normalize_gitignore_pattern("*.js"), vec!["**/*.js"]);
874 assert_eq!(normalize_gitignore_pattern("target"), vec!["**/target"]);
875
876 assert_eq!(
878 normalize_gitignore_pattern("**/node_modules"),
879 vec!["**/node_modules"]
880 );
881
882 assert_eq!(
884 normalize_gitignore_pattern("/build"),
885 vec!["build", "build/**"]
886 );
887 assert_eq!(normalize_gitignore_pattern("/dist/**"), vec!["dist/**"]);
889
890 assert_eq!(
892 normalize_gitignore_pattern("/config.json"),
893 vec!["config.json"]
894 );
895 assert_eq!(normalize_gitignore_pattern("/*.txt"), vec!["*.txt"]);
896
897 assert_eq!(
899 normalize_gitignore_pattern("/build/"),
900 vec!["build", "build/**"]
901 );
902
903 assert_eq!(normalize_gitignore_pattern("docs/*.md"), vec!["docs/*.md"]);
905 assert_eq!(
906 normalize_gitignore_pattern("src/vendor"),
907 vec!["src/vendor"]
908 );
909
910 assert_eq!(
912 normalize_gitignore_pattern("build/"),
913 vec!["**/build", "**/build/**"]
914 );
915 }
916
917 #[test]
918 fn test_build_glob_set() {
919 let patterns = vec!["node_modules/**".to_string(), "*.min.js".to_string()];
920 let glob_set = build_glob_set(&patterns).unwrap();
921
922 assert!(glob_set.is_match("src/node_modules/pkg/index.js"));
924 assert!(glob_set.is_match("app.min.js"));
925 assert!(glob_set.is_match("dist/bundle.min.js"));
926
927 assert!(!glob_set.is_match("src/main.rs"));
929 assert!(!glob_set.is_match("app.js"));
930 }
931
932 #[test]
933 fn test_rooted_pattern_matches_contents() {
934 let patterns = vec!["/build".to_string()];
936 let glob_set = build_glob_set(&patterns).unwrap();
937
938 assert!(glob_set.is_match("build"));
940 assert!(glob_set.is_match("build/output.log"));
942 assert!(glob_set.is_match("build/subdir/file.txt"));
943
944 assert!(!glob_set.is_match("src/build/output.log"));
946 assert!(!glob_set.is_match("packages/build"));
947 }
948
949 #[test]
950 fn test_slash_containing_patterns_are_root_relative() {
951 let patterns = vec!["docs/*.md".to_string()];
953 let glob_set = build_glob_set(&patterns).unwrap();
954
955 assert!(glob_set.is_match("docs/readme.md"));
957 assert!(glob_set.is_match("docs/api.md"));
958
959 assert!(!glob_set.is_match("packages/foo/docs/readme.md"));
961 assert!(!glob_set.is_match("src/docs/notes.md"));
962 }
963
964 #[test]
965 fn test_simple_patterns_match_anywhere() {
966 let patterns = vec!["*.bak".to_string(), "node_modules".to_string()];
968 let glob_set = build_glob_set(&patterns).unwrap();
969
970 assert!(glob_set.is_match("file.bak"));
972 assert!(glob_set.is_match("src/file.bak"));
973 assert!(glob_set.is_match("deep/nested/path/file.bak"));
974
975 assert!(glob_set.is_match("node_modules"));
977 assert!(glob_set.is_match("packages/frontend/node_modules"));
978 }
979
980 #[test]
981 fn test_dotted_directories_expand_to_contents() {
982 assert_eq!(
987 normalize_gitignore_pattern("/.git"),
988 vec![".git", ".git/**"]
989 );
990 assert_eq!(
991 normalize_gitignore_pattern("/.sqry-cache"),
992 vec![".sqry-cache", ".sqry-cache/**"]
993 );
994 assert_eq!(
995 normalize_gitignore_pattern("/.hidden"),
996 vec![".hidden", ".hidden/**"]
997 );
998 assert_eq!(
1000 normalize_gitignore_pattern("/.config"),
1001 vec![".config", ".config/**"]
1002 );
1003
1004 assert_eq!(
1007 normalize_gitignore_pattern("/.gitconfig"),
1008 vec![".gitconfig"]
1009 );
1010 assert_eq!(
1011 normalize_gitignore_pattern("/.editorconfig"),
1012 vec![".editorconfig"]
1013 );
1014
1015 assert_eq!(
1017 normalize_gitignore_pattern("/.gitignore"),
1018 vec![".gitignore"]
1019 );
1020 assert_eq!(
1021 normalize_gitignore_pattern("/config.json"),
1022 vec!["config.json"]
1023 );
1024 assert_eq!(
1025 normalize_gitignore_pattern("/.env.local"),
1026 vec![".env.local"]
1027 );
1028
1029 let patterns = vec!["/.git".to_string()];
1031 let glob_set = build_glob_set(&patterns).unwrap();
1032
1033 assert!(glob_set.is_match(".git"));
1034 assert!(glob_set.is_match(".git/config"));
1035 assert!(glob_set.is_match(".git/objects/pack/abc123"));
1036 assert!(glob_set.is_match(".git/refs/heads/main"));
1037
1038 assert!(!glob_set.is_match("submodule/.git"));
1040 assert!(!glob_set.is_match("packages/sub/.git/config"));
1041 }
1042
1043 #[test]
1044 fn test_rooted_patterns_with_relative_paths() {
1045 let mut config = ProjectConfig::default();
1049 config.ignore.patterns = vec!["/build".to_string(), "/.git".to_string()];
1050
1051 assert!(config.is_ignored(Path::new("build/output.log")));
1053 assert!(config.is_ignored(Path::new("build")));
1054 assert!(config.is_ignored(Path::new(".git/config")));
1055 assert!(config.is_ignored(Path::new(".git")));
1056
1057 assert!(!config.is_ignored(Path::new("src/build/output.log")));
1059 assert!(!config.is_ignored(Path::new("packages/sub/build")));
1060 assert!(!config.is_ignored(Path::new("submodule/.git")));
1061 }
1062
1063 #[test]
1064 fn test_unrooted_patterns_with_absolute_paths() {
1065 let config = ProjectConfig::default();
1069 assert!(config.is_ignored(Path::new("/home/user/project/node_modules/pkg/index.js")));
1073 assert!(config.is_ignored(Path::new("/tmp/build/target/debug/app")));
1074 assert!(config.is_ignored(Path::new("/var/repo/.git/objects/pack/abc")));
1075
1076 assert!(config.is_ignored(Path::new("node_modules/pkg/index.js")));
1078 assert!(config.is_ignored(Path::new("target/debug/app")));
1079 }
1080
1081 #[test]
1082 fn test_normalize_path_for_matching() {
1083 assert_eq!(
1085 normalize_path_for_matching(Path::new("/home/user/project/src/main.rs")),
1086 "home/user/project/src/main.rs"
1087 );
1088 assert_eq!(
1089 normalize_path_for_matching(Path::new("relative/path/file.rs")),
1090 "relative/path/file.rs"
1091 );
1092 assert_eq!(normalize_path_for_matching(Path::new("/build")), "build");
1093 }
1094}