1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fs,
4 path::{Path, PathBuf},
5};
6
7use crate::domain::{
8 CodeImpactPathGroups, CodeRepositoryExcludedPath, CodeRepositoryLanguagePreview,
9 CodeRepositoryLargestFile, CodeRepositoryRegistration, CodeRepositoryScopePreview,
10 CodeRepositorySelector,
11};
12
13use super::{
14 CodeIndexError,
15 changes::{GitTreeEntry, tracked_entries},
16 git_bytes, git_object_exists,
17 languages::language_id,
18 parser::dependency_manifest_language_ids,
19 resolve_ref, resolve_tree,
20 source_roots::{NESTED_SOURCE_MARKERS, STRIPPABLE_SOURCE_ROOTS},
21};
22
23const PREVIEW_MAX_EXCLUDED_PATHS: usize = 50;
24const PREVIEW_MAX_LARGEST_FILES: usize = 10;
25const DEFAULT_TEXT_FILE_BUDGET_BYTES: usize = 512 * 1024;
26const DEFAULT_EXCLUDED_SEGMENTS: &[&str] = &[
27 ".git",
28 ".cache",
29 ".next",
30 ".nuxt",
31 ".parcel-cache",
32 ".pytest_cache",
33 ".ruff_cache",
34 ".tox",
35 ".venv",
36 "__pycache__",
37 "build",
38 "coverage",
39 "dist",
40 "node_modules",
41 "out",
42 "target",
43 "third_party",
44 "vendor",
45 "venv",
46];
47const DEFAULT_EXCLUDED_EXTENSIONS: &[&str] = &[
48 "7z", "avif", "bmp", "bz2", "class", "eot", "gif", "gz", "ico", "jar", "jpeg", "jpg", "jsonl",
49 "lockb", "map", "mov", "mp4", "otf", "pdf", "png", "svg", "tar", "tgz", "ttf", "wasm", "webm",
50 "woff", "woff2", "zip", "zst",
51];
52const DEFAULT_EXCLUDED_FILENAMES: &[&str] = &[".relay-knowledgeignore", "uv.lock"];
53const DEFAULT_DISTRIBUTION_SEGMENT: &str = "dist";
54const DEFAULT_DISTRIBUTION_LANGUAGE_SEGMENTS: &[&str] = &[
55 "javascript",
56 "js",
57 "src",
58 "source",
59 "sources",
60 "ts",
61 "typescript",
62];
63const DEFAULT_DISTRIBUTION_RUNTIME_SEGMENTS: &[&str] =
64 &["app", "client", "core", "runtime", "server"];
65const SOURCE_LAYOUT_DISCOVERY_MAX_PATHS: usize = 200_000;
66const SOURCE_LAYOUT_DISCOVERY_MAX_ROOTS: usize = 512;
67const AUTO_SOURCE_SCOPE_FILTERS: &[&str] = &[".", "src", "include", "lib", "Sources"];
68
69pub fn preview_repository_scope(
71 registration: &CodeRepositoryRegistration,
72 selector: &CodeRepositorySelector,
73) -> Result<CodeRepositoryScopePreview, CodeIndexError> {
74 let root = PathBuf::from(®istration.root_path);
75 let commit = resolve_ref(&root, &selector.ref_selector)?;
76 let tree_hash = resolve_tree(&root, &commit)?;
77 let ignore_rules = load_ignore_rules_from_commit(&root, &commit)?;
78 let mut selected_byte_count = 0usize;
79 let mut selected_file_count = 0usize;
80 let mut unsupported_file_count = 0usize;
81 let mut generated_or_heavy_file_count = 0usize;
82 let mut expected_degraded_file_count = 0usize;
83 let mut language_distribution = BTreeMap::<String, (usize, usize)>::new();
84 let mut largest_files = Vec::<CodeRepositoryLargestFile>::new();
85 let mut excluded_paths = Vec::<CodeRepositoryExcludedPath>::new();
86
87 let entries = tracked_entries(&root, &commit)?;
88 let source_layout = discover_source_layout(&entries);
89 for entry in entries {
90 if let Some(reason) = selection_exclusion_reason_with_layout(
91 &entry.path,
92 registration,
93 selector,
94 &ignore_rules,
95 &source_layout,
96 ) {
97 if excluded_paths.len() < PREVIEW_MAX_EXCLUDED_PATHS {
98 excluded_paths.push(CodeRepositoryExcludedPath {
99 path: entry.path,
100 reason,
101 });
102 }
103 continue;
104 }
105 let language = language_id(&entry.path).unwrap_or("unknown");
106 selected_file_count += 1;
107 selected_byte_count = selected_byte_count.saturating_add(entry.byte_count);
108 let bucket = language_distribution
109 .entry(language.to_owned())
110 .or_insert((0, 0));
111 bucket.0 += 1;
112 bucket.1 = bucket.1.saturating_add(entry.byte_count);
113 let is_unsupported = language == "unknown";
114 let is_heavy = entry.byte_count > DEFAULT_TEXT_FILE_BUDGET_BYTES;
115 if is_unsupported {
116 unsupported_file_count += 1;
117 }
118 if is_heavy {
119 generated_or_heavy_file_count += 1;
120 }
121 if is_unsupported || is_heavy {
122 expected_degraded_file_count += 1;
123 }
124 largest_files.push(CodeRepositoryLargestFile {
125 path: entry.path,
126 byte_count: entry.byte_count,
127 });
128 }
129 largest_files.sort_by(|left, right| {
130 right
131 .byte_count
132 .cmp(&left.byte_count)
133 .then_with(|| left.path.cmp(&right.path))
134 });
135 largest_files.truncate(PREVIEW_MAX_LARGEST_FILES);
136
137 Ok(CodeRepositoryScopePreview {
138 repository_id: registration.repository_id.clone(),
139 alias: registration.alias.clone(),
140 requested_ref: selector.ref_selector.clone(),
141 resolved_commit_sha: commit,
142 tree_hash,
143 selected_file_count,
144 selected_byte_count,
145 unsupported_file_count,
146 generated_or_heavy_file_count,
147 expected_degraded_file_count,
148 language_distribution: language_distribution
149 .into_iter()
150 .map(
151 |(language_id, (file_count, byte_count))| CodeRepositoryLanguagePreview {
152 language_id,
153 file_count,
154 byte_count,
155 },
156 )
157 .collect(),
158 largest_files,
159 excluded_paths,
160 })
161}
162
163pub fn partition_changed_paths_for_selector(
165 registration: &CodeRepositoryRegistration,
166 selector: &CodeRepositorySelector,
167 paths: Vec<String>,
168) -> Result<CodeImpactPathGroups, CodeIndexError> {
169 let root = PathBuf::from(®istration.root_path);
170 let commit = resolve_ref(&root, &selector.ref_selector)?;
171 let ignore_rules = load_ignore_rules_from_commit(&root, &commit)?;
172 let entries = tracked_entries(&root, &commit)?;
173 let source_layout = discover_source_layout(&entries);
174 let mut in_scope_changed_paths = Vec::new();
175 let mut out_of_scope_changed_paths = Vec::new();
176 for path in paths {
177 if selection_exclusion_reason_with_layout(
178 &path,
179 registration,
180 selector,
181 &ignore_rules,
182 &source_layout,
183 )
184 .is_none()
185 {
186 in_scope_changed_paths.push(path);
187 } else {
188 out_of_scope_changed_paths.push(path);
189 }
190 }
191 in_scope_changed_paths.sort();
192 in_scope_changed_paths.dedup();
193 out_of_scope_changed_paths.sort();
194 out_of_scope_changed_paths.dedup();
195
196 Ok(CodeImpactPathGroups {
197 in_scope_changed_paths,
198 out_of_scope_changed_paths,
199 })
200}
201
202#[cfg(test)]
203pub(super) fn path_is_selected(
204 path: &str,
205 registration: &CodeRepositoryRegistration,
206 selector: &CodeRepositorySelector,
207) -> bool {
208 let root = Path::new(®istration.root_path);
209 let ignore_rules = load_ignore_rules(root).expect("ignore rules should load in tests");
210
211 path_is_selected_with_rules(path, registration, selector, &ignore_rules)
212}
213
214pub(super) fn path_is_selected_with_rules(
215 path: &str,
216 registration: &CodeRepositoryRegistration,
217 selector: &CodeRepositorySelector,
218 ignore_rules: &[IgnoreRule],
219) -> bool {
220 selection_exclusion_reason(path, registration, selector, ignore_rules).is_none()
221}
222
223pub(super) fn path_is_selected_with_layout(
224 path: &str,
225 registration: &CodeRepositoryRegistration,
226 selector: &CodeRepositorySelector,
227 ignore_rules: &[IgnoreRule],
228 source_layout: &SourceLayoutDiscovery,
229) -> bool {
230 selection_exclusion_reason_with_layout(
231 path,
232 registration,
233 selector,
234 ignore_rules,
235 source_layout,
236 )
237 .is_none()
238}
239
240pub(super) fn selection_exclusion_reason(
241 path: &str,
242 registration: &CodeRepositoryRegistration,
243 selector: &CodeRepositorySelector,
244 ignore_rules: &[IgnoreRule],
245) -> Option<String> {
246 selection_exclusion_reason_with_layout(
247 path,
248 registration,
249 selector,
250 ignore_rules,
251 &SourceLayoutDiscovery::default(),
252 )
253}
254
255pub(super) fn selection_exclusion_reason_with_layout(
256 path: &str,
257 registration: &CodeRepositoryRegistration,
258 selector: &CodeRepositorySelector,
259 ignore_rules: &[IgnoreRule],
260 source_layout: &SourceLayoutDiscovery,
261) -> Option<String> {
262 if !path_scope_allows(path, registration, selector)
263 && !source_layout.extends_path_scope(path, registration, selector)
264 {
265 return Some("outside registered/requested path scope".to_owned());
266 }
267 if !language_filter_allows(path, ®istration.language_filters)
268 || !language_filter_allows(path, &selector.language_filters)
269 {
270 return Some("outside registered/requested language scope".to_owned());
271 }
272 if ignore_rules.iter().any(|rule| rule.matches(path)) {
273 return Some("excluded by .relay-knowledgeignore".to_owned());
274 }
275 if default_source_preset_excludes(path)
276 && !source_layout.keeps_default_excluded_source(path)
277 && !explicit_path_filter_opts_into_default_exclusion(
278 path,
279 registration
280 .path_filters
281 .iter()
282 .chain(selector.path_filters.iter()),
283 )
284 {
285 return Some("excluded by source preset".to_owned());
286 }
287
288 None
289}
290
291#[derive(Debug, Clone, Default, PartialEq, Eq)]
292pub(super) struct SourceLayoutDiscovery {
293 source_roots: BTreeSet<String>,
294}
295
296impl SourceLayoutDiscovery {
297 fn keeps_default_excluded_source(&self, path: &str) -> bool {
298 source_path_has_indexable_content(path)
299 && !path_contains_broad_dependency_segment(path)
300 && self
301 .source_roots
302 .iter()
303 .any(|root| path_matches_filter(path, root))
304 }
305
306 fn extends_path_scope(
307 &self,
308 path: &str,
309 registration: &CodeRepositoryRegistration,
310 selector: &CodeRepositorySelector,
311 ) -> bool {
312 registration_scope_can_discover_source_roots(®istration.path_filters)
313 && selector_path_scope_allows_discovered_root(path, &selector.path_filters)
314 && self.keeps_default_excluded_source(path)
315 }
316}
317
318pub(super) fn discover_source_layout(entries: &[GitTreeEntry]) -> SourceLayoutDiscovery {
319 let mut source_roots = BTreeSet::new();
320 for entry in entries.iter().take(SOURCE_LAYOUT_DISCOVERY_MAX_PATHS) {
321 if !source_path_has_indexable_content(&entry.path)
322 || path_contains_broad_dependency_segment(&entry.path)
323 || default_source_preset_excludes(&entry.path)
324 {
325 continue;
326 }
327 for root in source_layout_roots_for_path(&entry.path) {
328 source_roots.insert(root);
329 if source_roots.len() >= SOURCE_LAYOUT_DISCOVERY_MAX_ROOTS {
330 return SourceLayoutDiscovery { source_roots };
331 }
332 }
333 }
334
335 SourceLayoutDiscovery { source_roots }
336}
337
338pub(super) fn effective_index_path_filters(
339 registration: &CodeRepositoryRegistration,
340 selector: &CodeRepositorySelector,
341 source_layout: &SourceLayoutDiscovery,
342) -> Vec<String> {
343 let mut filters = merged_path_filters(®istration.path_filters, &selector.path_filters);
344 if !registration_scope_can_discover_source_roots(®istration.path_filters) {
345 return filters;
346 }
347 for root in &source_layout.source_roots {
348 if !selector_filter_allows_root(root, &selector.path_filters) {
349 continue;
350 }
351 push_filter_if_uncovered(&mut filters, root);
352 }
353
354 filters
355}
356
357fn source_path_has_indexable_content(path: &str) -> bool {
358 language_id(path).is_some() || dependency_manifest_language_ids(path).is_some()
359}
360
361fn path_contains_broad_dependency_segment(path: &str) -> bool {
362 normalize_path_filter(path)
363 .split('/')
364 .any(|segment| matches!(segment, "vendor" | "third_party" | "node_modules"))
365}
366
367fn registration_scope_can_discover_source_roots(filters: &[String]) -> bool {
368 !filters.is_empty()
369 && filters.iter().all(|filter| {
370 let filter = normalize_path_filter(filter);
371 AUTO_SOURCE_SCOPE_FILTERS.contains(&filter)
372 })
373}
374
375fn selector_path_scope_allows_discovered_root(path: &str, filters: &[String]) -> bool {
376 filters.is_empty()
377 || filters
378 .iter()
379 .any(|filter| path_matches_filter(path, filter))
380}
381
382fn selector_filter_allows_root(root: &str, filters: &[String]) -> bool {
383 filters.is_empty()
384 || filters
385 .iter()
386 .any(|filter| path_matches_filter(root, filter) || path_overlaps_filter(root, filter))
387}
388
389fn merged_path_filters(left: &[String], right: &[String]) -> Vec<String> {
390 let mut merged = Vec::new();
391 for filter in left.iter().chain(right.iter()) {
392 let normalized = normalize_path_filter(filter);
393 if !normalized.is_empty() && !merged.iter().any(|existing| existing == normalized) {
394 merged.push(normalized.to_owned());
395 }
396 }
397
398 merged
399}
400
401fn push_filter_if_uncovered(filters: &mut Vec<String>, root: &str) {
402 if filters
403 .iter()
404 .any(|filter| path_filter_covers(filter, root))
405 {
406 return;
407 }
408 filters.retain(|filter| !path_filter_covers(root, filter));
409 filters.push(root.to_owned());
410}
411
412fn path_filter_covers(filter: &str, path: &str) -> bool {
413 let filter = normalize_path_filter(filter);
414 filter == "." || path_matches_filter(path, filter)
415}
416
417fn source_layout_roots_for_path(path: &str) -> Vec<String> {
418 let path = normalize_path_filter(path);
419 let mut roots = Vec::new();
420 for marker in NESTED_SOURCE_MARKERS {
421 if let Some((prefix, _)) = path.split_once(marker) {
422 push_source_root(&mut roots, format!("{prefix}{marker}"));
423 }
424 }
425 for root in STRIPPABLE_SOURCE_ROOTS {
426 if let Some(suffix) = path.strip_prefix(root) {
427 let mut segments = suffix.split('/').filter(|segment| !segment.is_empty());
428 if let Some(first) = segments.next() {
429 push_source_root(&mut roots, format!("{root}{first}"));
430 } else {
431 push_source_root(&mut roots, root.trim_end_matches('/').to_owned());
432 }
433 }
434 }
435 roots
436}
437
438fn push_source_root(roots: &mut Vec<String>, root: String) {
439 let root = root.trim_end_matches('/').to_owned();
440 if !root.is_empty() && !roots.contains(&root) {
441 roots.push(root);
442 }
443}
444
445pub(super) fn path_scope_allows(
446 path: &str,
447 registration: &CodeRepositoryRegistration,
448 selector: &CodeRepositorySelector,
449) -> bool {
450 path_filter_allows(path, ®istration.path_filters)
451 && path_filter_allows(path, &selector.path_filters)
452}
453
454pub(super) fn path_scope_overlaps(
455 path: &str,
456 registration: &CodeRepositoryRegistration,
457 selector: &CodeRepositorySelector,
458) -> bool {
459 path_filter_overlaps(path, ®istration.path_filters)
460 && path_filter_overlaps(path, &selector.path_filters)
461}
462
463pub(super) fn load_ignore_rules(root: &Path) -> Result<Vec<IgnoreRule>, CodeIndexError> {
464 let path = root.join(".relay-knowledgeignore");
465 let content = match fs::read_to_string(path) {
466 Ok(content) => content,
467 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
468 Err(error) => return Err(error.into()),
469 };
470
471 Ok(parse_ignore_rules(&content))
472}
473
474pub(super) fn load_ignore_rules_from_commit(
475 root: &Path,
476 commit: &str,
477) -> Result<Vec<IgnoreRule>, CodeIndexError> {
478 let object = format!("{commit}:.relay-knowledgeignore");
479 if !git_object_exists(root, &object)? {
480 return Ok(Vec::new());
481 }
482 let content = String::from_utf8(git_bytes(root, ["show", &object])?).map_err(|error| {
483 CodeIndexError::InvalidInput(format!(
484 ".relay-knowledgeignore at {commit} is not valid UTF-8: {}",
485 error.utf8_error()
486 ))
487 })?;
488
489 Ok(parse_ignore_rules(&content))
490}
491
492fn parse_ignore_rules(content: &str) -> Vec<IgnoreRule> {
493 content
494 .lines()
495 .map(str::trim)
496 .filter(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('!'))
497 .map(|line| IgnoreRule {
498 pattern: line.trim_start_matches('/').to_owned(),
499 anchored: line.starts_with('/'),
500 })
501 .collect()
502}
503
504#[derive(Debug, Clone, PartialEq, Eq)]
505pub(super) struct IgnoreRule {
506 pattern: String,
507 anchored: bool,
508}
509
510impl IgnoreRule {
511 fn matches(&self, path: &str) -> bool {
512 let pattern = normalize_path_filter(&self.pattern);
513 let path = normalize_path_filter(path);
514 if pattern.is_empty() {
515 return false;
516 }
517 if let Some(extension) = pattern.strip_prefix("*.") {
518 return if self.anchored {
519 path.rsplit_once('/').is_none()
520 && path
521 .rsplit_once('.')
522 .is_some_and(|(_, path_extension)| path_extension == extension)
523 } else {
524 path.rsplit_once('.')
525 .is_some_and(|(_, path_extension)| path_extension == extension)
526 };
527 }
528 if pattern.contains('/') {
529 return path == pattern || path.starts_with(&format!("{pattern}/"));
530 }
531 if self.anchored {
532 return path == pattern || path.starts_with(&format!("{pattern}/"));
533 }
534 path.split('/').any(|segment| segment == pattern)
535 }
536}
537
538fn path_filter_allows(path: &str, filters: &[String]) -> bool {
539 filters.is_empty()
540 || filters
541 .iter()
542 .any(|filter| path_matches_filter(path, filter))
543}
544
545fn path_filter_overlaps(path: &str, filters: &[String]) -> bool {
546 filters.is_empty()
547 || filters
548 .iter()
549 .any(|filter| path_overlaps_filter(path, filter))
550}
551
552fn language_filter_allows(path: &str, filters: &[String]) -> bool {
553 if filters.is_empty() {
554 return true;
555 }
556 if language_id(path).is_some_and(|language| filters.iter().any(|filter| filter == language)) {
557 return true;
558 }
559 dependency_manifest_language_ids(path).is_some_and(|languages| {
560 languages
561 .iter()
562 .any(|language| filters.iter().any(|filter| filter == language))
563 })
564}
565
566fn default_source_preset_excludes(path: &str) -> bool {
567 let normalized = normalize_path_filter(path);
568 if normalized
569 .rsplit('/')
570 .next()
571 .is_some_and(|file_name| DEFAULT_EXCLUDED_FILENAMES.contains(&file_name))
572 {
573 return true;
574 }
575 if normalized
576 .split('/')
577 .any(|segment| default_excluded_segment_excludes_path(segment, normalized))
578 {
579 return true;
580 }
581 normalized
582 .rsplit_once('.')
583 .map(|(_, extension)| {
584 DEFAULT_EXCLUDED_EXTENSIONS.contains(&extension.to_ascii_lowercase().as_str())
585 })
586 .unwrap_or(false)
587}
588
589fn default_excluded_segment_excludes_path(segment: &str, path: &str) -> bool {
590 DEFAULT_EXCLUDED_SEGMENTS.contains(&segment)
591 && (segment != DEFAULT_DISTRIBUTION_SEGMENT || distribution_segment_excludes_path(path))
592}
593
594fn distribution_segment_excludes_path(path: &str) -> bool {
595 let segments = path.split('/').collect::<Vec<_>>();
596
597 !distribution_runtime_source_path_is_indexable(path, &segments)
598}
599
600fn distribution_runtime_source_path_is_indexable(path: &str, segments: &[&str]) -> bool {
601 language_id(path).is_some()
602 && !path
603 .rsplit('/')
604 .next()
605 .is_some_and(|file_name| file_name.to_ascii_lowercase().contains(".min."))
606 && segments.windows(3).any(|window| {
607 window[0] == DEFAULT_DISTRIBUTION_SEGMENT
608 && DEFAULT_DISTRIBUTION_LANGUAGE_SEGMENTS.contains(&window[1])
609 && DEFAULT_DISTRIBUTION_RUNTIME_SEGMENTS.contains(&window[2])
610 })
611}
612
613fn explicit_path_filter_opts_into_default_exclusion<'a>(
614 path: &str,
615 filters: impl IntoIterator<Item = &'a String>,
616) -> bool {
617 let path_extension = path
618 .rsplit_once('.')
619 .map(|(_, extension)| extension.to_ascii_lowercase());
620 filters.into_iter().any(|filter| {
621 let filter = normalize_path_filter(filter);
622 if filter.is_empty() || filter == "." {
623 return false;
624 }
625 let filter_segments = filter.split('/').collect::<Vec<_>>();
626 let targets_default_exclusion = filter_segments.iter().any(|segment| {
627 DEFAULT_EXCLUDED_SEGMENTS.contains(segment)
628 || DEFAULT_EXCLUDED_FILENAMES.contains(segment)
629 || segment
630 .rsplit_once('.')
631 .map(|(_, ext)| ext.to_ascii_lowercase())
632 .is_some_and(|extension| {
633 DEFAULT_EXCLUDED_EXTENSIONS.contains(&extension.as_str())
634 })
635 });
636 if !targets_default_exclusion {
637 return false;
638 }
639 path_matches_filter(path, filter)
640 || filter.strip_prefix("*.").is_some_and(|extension| {
641 path_extension.as_deref() == Some(&extension.to_ascii_lowercase())
642 })
643 })
644}
645
646fn path_matches_filter(path: &str, filter: &str) -> bool {
647 let path = normalize_path_filter(path);
648 let filter = normalize_path_filter(filter);
649 if filter == "." {
650 return true;
651 }
652 !filter.is_empty() && (path == filter || path.starts_with(&format!("{filter}/")))
653}
654
655fn path_overlaps_filter(path: &str, filter: &str) -> bool {
656 let path = normalize_path_filter(path);
657 let filter = normalize_path_filter(filter);
658 if filter == "." {
659 return true;
660 }
661 !path.is_empty()
662 && !filter.is_empty()
663 && (path == filter
664 || path.starts_with(&format!("{filter}/"))
665 || filter.starts_with(&format!("{path}/")))
666}
667
668fn normalize_path_filter(filter: &str) -> &str {
669 let mut filter = filter.trim_end_matches(['/', '\\']);
670 while let Some(stripped) = filter.strip_prefix("./") {
671 filter = stripped;
672 }
673
674 filter
675}
676
677#[cfg(test)]
678mod tests {
679 use crate::domain::{CodeRepositoryRegistration, CodeRepositorySelector};
680
681 use super::*;
682
683 #[test]
684 fn source_preset_keeps_distribution_runtime_sources_indexable() {
685 assert!(!default_source_preset_excludes(
686 "frontend/dist/js/core/stream.js"
687 ));
688 assert!(!default_source_preset_excludes(
689 "frontend/dist/js/app/bootstrap.js"
690 ));
691 assert!(!default_source_preset_excludes(
692 "web/dist/src/runtime/session.ts"
693 ));
694 assert!(default_source_preset_excludes("dist/bundle.js"));
695 assert!(default_source_preset_excludes(
696 "frontend/dist/js/components/sidebar.js"
697 ));
698 assert!(default_source_preset_excludes(
699 "frontend/dist/js/core/highlight.min.js"
700 ));
701 assert!(default_source_preset_excludes("frontend/dist/css/app.css"));
702 assert!(default_source_preset_excludes(
703 "node_modules/pkg/dist/js/core/index.js"
704 ));
705 }
706
707 #[test]
708 fn explicit_default_exclusion_opt_in_normalizes_extension_case() {
709 let registration = CodeRepositoryRegistration::new(
710 "repo",
711 "alias",
712 "/tmp/repo",
713 vec!["assets/logo.SVG".to_owned()],
714 Vec::new(),
715 )
716 .expect("registration should validate");
717 let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
718 .expect("selector should validate");
719
720 assert!(path_is_selected(
721 "assets/logo.SVG",
722 ®istration,
723 &selector
724 ));
725 }
726
727 #[test]
728 fn default_source_preset_excludes_dataset_dumps_and_uv_lock() {
729 let registration = CodeRepositoryRegistration::new(
730 "repo",
731 "alias",
732 "/tmp/repo",
733 vec![".".to_owned()],
734 Vec::new(),
735 )
736 .expect("registration should validate");
737 let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
738 .expect("selector should validate");
739
740 assert!(!path_is_selected(
741 ".agent_teams/evals/datasets/swebench-verified-full.jsonl",
742 ®istration,
743 &selector
744 ));
745 assert!(!path_is_selected("uv.lock", ®istration, &selector));
746 }
747
748 #[test]
749 fn nonstandard_source_roots_are_selected_without_opt_in() {
750 let registration = CodeRepositoryRegistration::new(
751 "repo",
752 "alias",
753 "/tmp/repo",
754 vec![".".to_owned()],
755 Vec::new(),
756 )
757 .expect("registration should validate");
758 let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
759 .expect("selector should validate");
760
761 for path in [
762 "external_deps/python_sdk/session_client.py",
763 "packages/ui/src/index.ts",
764 "modules/java_sdk/src/main/java/example/SessionClient.java",
765 "plugins/example.com/nonstandard/session/client.go",
766 "Sources/SwiftSdk/SessionClient.swift",
767 "lib/app/controller.rb",
768 ] {
769 assert!(path_is_selected(path, ®istration, &selector), "{path}");
770 }
771 assert!(!path_is_selected(
772 "vendor/pkg/session_client.py",
773 ®istration,
774 &selector
775 ));
776 assert!(!path_is_selected(
777 "third_party/pkg/session_client.py",
778 ®istration,
779 &selector
780 ));
781 }
782
783 #[test]
784 fn explicit_vendor_source_opt_in_stays_supported() {
785 let registration = CodeRepositoryRegistration::new(
786 "repo",
787 "alias",
788 "/tmp/repo",
789 vec![".".to_owned(), "vendor".to_owned()],
790 Vec::new(),
791 )
792 .expect("registration should validate");
793 let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
794 .expect("selector should validate");
795
796 assert!(path_is_selected(
797 "vendor/pkg/session_client.py",
798 ®istration,
799 &selector
800 ));
801 }
802}