1pub mod config;
2pub mod detect;
3pub mod git;
4pub mod graph;
5pub mod resolvers;
6pub mod runner;
7pub mod types;
8
9use anyhow::{Context, Result};
10use std::collections::{HashMap, HashSet};
11use std::path::Path;
12use tracing::debug;
13
14use types::{AffectedResult, ExplainEntry, ExplainReason};
15
16pub fn find_affected(root: &Path, base_ref: &str) -> Result<AffectedResult> {
19 find_affected_with_options(root, base_ref, false, None, None)
20}
21
22pub fn find_affected_with_options(
28 root: &Path,
29 base_ref: &str,
30 explain: bool,
31 filter: Option<&str>,
32 skip: Option<&str>,
33) -> Result<AffectedResult> {
34 debug!(
35 "Finding affected packages at {} with base ref '{}'",
36 root.display(),
37 base_ref
38 );
39
40 let config = config::Config::load(root)?;
41
42 let resolver = resolvers::detect_resolver(root)?;
44 debug!("Using resolver for ecosystem: {}", resolver.ecosystem());
45
46 let project_graph = resolver
48 .resolve(root)
49 .context("Failed to resolve project graph")?;
50 debug!("Resolved {} packages", project_graph.packages.len());
51
52 let git_diff = git::changed_files(root, base_ref).context("Failed to compute git diff")?;
54 debug!(
55 "{} files changed since {}",
56 git_diff.changed_files.len(),
57 base_ref
58 );
59
60 let mut changed_packages = HashSet::new();
62 let mut changed_files_per_package: HashMap<types::PackageId, Vec<String>> = HashMap::new();
63 for file in &git_diff.changed_files {
64 let file_str = file.to_str().unwrap_or("");
65 if config.is_ignored(file_str) {
66 debug!("Ignoring file: {}", file_str);
67 continue;
68 }
69 if let Some(pkg_id) = resolver.package_for_file(&project_graph, file) {
70 changed_packages.insert(pkg_id.clone());
71 if explain {
72 changed_files_per_package
73 .entry(pkg_id)
74 .or_default()
75 .push(file_str.to_string());
76 }
77 }
78 }
79 debug!("{} packages directly changed", changed_packages.len());
80
81 let dep_graph = graph::DepGraph::from_project_graph(&project_graph);
83 let affected = dep_graph.affected_by(&changed_packages);
84 debug!(
85 "{} packages affected (including transitive)",
86 affected.len()
87 );
88
89 let explanations = if explain {
91 let chains = dep_graph.explain_affected(&changed_packages, &affected);
92 let mut entries: Vec<ExplainEntry> = chains
93 .into_iter()
94 .map(|(pkg_id, chain)| {
95 let reason = if changed_packages.contains(&pkg_id) {
96 ExplainReason::DirectlyChanged {
97 files: changed_files_per_package
98 .get(&pkg_id)
99 .cloned()
100 .unwrap_or_default(),
101 }
102 } else {
103 ExplainReason::TransitivelyAffected {
104 chain: chain.into_iter().map(|p| p.0).collect(),
105 }
106 };
107 ExplainEntry {
108 package: pkg_id.0,
109 reason,
110 }
111 })
112 .collect();
113 entries.sort_by(|a, b| a.package.cmp(&b.package));
114 Some(entries)
115 } else {
116 None
117 };
118
119 let mut affected_names: Vec<String> = affected.into_iter().map(|p| p.0).collect();
121 affected_names.sort();
122
123 if let Some(filter_pattern) = filter {
125 let pat = glob::Pattern::new(filter_pattern)
126 .with_context(|| format!("Invalid filter pattern '{filter_pattern}'"))?;
127 debug!("Applying filter pattern: {}", filter_pattern);
128 affected_names.retain(|name| pat.matches(name));
129 }
130
131 if let Some(skip_pattern) = skip {
133 let pat = glob::Pattern::new(skip_pattern)
134 .with_context(|| format!("Invalid skip pattern '{skip_pattern}'"))?;
135 debug!("Applying skip pattern: {}", skip_pattern);
136 affected_names.retain(|name| !pat.matches(name));
137 }
138
139 Ok(AffectedResult {
140 affected: affected_names,
141 base: base_ref.to_string(),
142 changed_files: git_diff.changed_files.len(),
143 total_packages: project_graph.packages.len(),
144 explanations,
145 })
146}
147
148pub fn find_merge_base(root: &Path, branch: &str) -> Result<String> {
151 debug!("Finding merge-base with branch '{}'", branch);
152 git::merge_base(root, branch)
153}
154
155pub fn resolve_project(root: &Path) -> Result<(Box<dyn resolvers::Resolver>, types::ProjectGraph)> {
158 let resolver = resolvers::detect_resolver(root)?;
159 let graph = resolver
160 .resolve(root)
161 .context("Failed to resolve project graph")?;
162 Ok((resolver, graph))
163}