guild_cli/commands/
affected.rs1use std::path::Path;
2
3use colored::Colorize;
4
5use crate::affected::{compute_affected, get_changed_files};
6use crate::config::{TargetName, WorkspaceConfig};
7use crate::discovery::{discover_projects, find_workspace_root};
8use crate::error::AffectedError;
9use crate::graph::{ProjectGraph, TaskGraph};
10use crate::runner::{RunResult, TaskRunner};
11
12pub async fn run_affected(
17 cwd: &Path,
18 target: &str,
19 base_branch: &str,
20) -> Result<RunResult, AffectedError> {
21 let target_name: TargetName = target.parse().map_err(|e| AffectedError::InvalidTarget {
23 target: target.to_string(),
24 reason: format!("{e}"),
25 })?;
26
27 let root = find_workspace_root(cwd).map_err(|e| AffectedError::WorkspaceNotFound {
29 path: cwd.to_path_buf(),
30 reason: format!("{e}"),
31 })?;
32
33 let workspace = WorkspaceConfig::from_file(&root.join("guild.toml")).map_err(|e| {
34 AffectedError::ConfigError {
35 path: root.join("guild.toml"),
36 reason: format!("{e}"),
37 }
38 })?;
39
40 let all_projects = discover_projects(&workspace).map_err(|e| AffectedError::ConfigError {
42 path: root.clone(),
43 reason: format!("{e}"),
44 })?;
45
46 if all_projects.is_empty() {
47 return Ok(RunResult {
48 success_count: 0,
49 failure_count: 0,
50 skipped_count: 0,
51 cached_count: 0,
52 task_results: vec![],
53 total_duration: std::time::Duration::ZERO,
54 });
55 }
56
57 let full_project_graph =
59 ProjectGraph::build(all_projects.clone()).map_err(|e| AffectedError::GraphError {
60 reason: format!("{e}"),
61 })?;
62
63 let changed_files = get_changed_files(&root, base_branch)?;
65
66 let affected = compute_affected(&changed_files, &all_projects, &full_project_graph);
68
69 if affected.all.is_empty() {
70 println!(
71 "\n{} No projects affected since '{}'\n",
72 "guild".cyan().bold(),
73 base_branch
74 );
75 return Ok(RunResult {
76 success_count: 0,
77 failure_count: 0,
78 skipped_count: 0,
79 cached_count: 0,
80 task_results: vec![],
81 total_duration: std::time::Duration::ZERO,
82 });
83 }
84
85 let affected_projects: Vec<_> = all_projects
87 .into_iter()
88 .filter(|p| affected.all.contains(p.name()))
89 .collect();
90
91 let affected_graph =
93 ProjectGraph::build(affected_projects).map_err(|e| AffectedError::GraphError {
94 reason: format!("{e}"),
95 })?;
96
97 let task_graph =
99 TaskGraph::build(&affected_graph, &target_name).map_err(|e| AffectedError::GraphError {
100 reason: format!("{e}"),
101 })?;
102
103 let task_count = task_graph.len();
104
105 if task_count == 0 {
106 println!(
107 "{} No affected projects have target '{target}'",
108 "warning:".yellow().bold()
109 );
110 return Ok(RunResult {
111 success_count: 0,
112 failure_count: 0,
113 skipped_count: 0,
114 cached_count: 0,
115 task_results: vec![],
116 total_duration: std::time::Duration::ZERO,
117 });
118 }
119
120 println!(
122 "\n{} Running {} task{} for target '{}' on {} affected project{}\n",
123 "guild".cyan().bold(),
124 task_count,
125 if task_count == 1 { "" } else { "s" },
126 target,
127 affected.all.len(),
128 if affected.all.len() == 1 { "" } else { "s" }
129 );
130
131 if !affected.changed.is_empty() {
133 let changed_names: Vec<String> = affected.changed.iter().map(|n| n.to_string()).collect();
134 println!(" {} Changed: {}", "~".yellow(), changed_names.join(", "));
135 }
136 if !affected.dependents.is_empty() {
137 let dep_names: Vec<String> = affected.dependents.iter().map(|n| n.to_string()).collect();
138 println!(" {} Dependents: {}", "~".cyan(), dep_names.join(", "));
139 }
140 println!();
141
142 let concurrency = std::thread::available_parallelism()
144 .map(|n| n.get())
145 .unwrap_or(4);
146 let runner = TaskRunner::new(concurrency, root);
147
148 let result =
150 runner
151 .run(task_graph, &affected_graph)
152 .await
153 .map_err(|e| AffectedError::GraphError {
154 reason: format!("{e}"),
155 })?;
156
157 print_summary(&result, target);
159
160 Ok(result)
161}
162
163fn print_summary(result: &RunResult, target: &str) {
165 println!();
166
167 let duration_str = format!("{:.2}s", result.total_duration.as_secs_f64());
168
169 if result.is_success() {
170 if result.success_count == 0 {
171 println!(
172 "{} No tasks executed for target '{target}'",
173 "warning:".yellow().bold()
174 );
175 } else {
176 println!(
177 "{} {} {} completed successfully in {}",
178 "Success".green().bold(),
179 result.success_count,
180 if result.success_count == 1 {
181 "task"
182 } else {
183 "tasks"
184 },
185 duration_str
186 );
187 }
188 } else {
189 let mut parts = Vec::new();
190
191 if result.success_count > 0 {
192 parts.push(format!("{} {}", result.success_count, "succeeded".green()));
193 }
194
195 parts.push(format!("{} {}", result.failure_count, "failed".red()));
196
197 if result.skipped_count > 0 {
198 parts.push(format!("{} {}", result.skipped_count, "skipped".yellow()));
199 }
200
201 println!(
202 "{} {} in {}",
203 "Failed".red().bold(),
204 parts.join(", "),
205 duration_str
206 );
207 }
208}