Skip to main content

guild_cli/commands/
affected.rs

1use 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
12/// Run a target only on affected projects (changed + their dependents).
13///
14/// Detects which projects have changed since the base branch and runs the target
15/// on those projects plus any projects that transitively depend on them.
16pub async fn run_affected(
17    cwd: &Path,
18    target: &str,
19    base_branch: &str,
20) -> Result<RunResult, AffectedError> {
21    // Parse target name
22    let target_name: TargetName = target.parse().map_err(|e| AffectedError::InvalidTarget {
23        target: target.to_string(),
24        reason: format!("{e}"),
25    })?;
26
27    // Discover workspace
28    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    // Discover all projects
41    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    // Build full project graph
58    let full_project_graph =
59        ProjectGraph::build(all_projects.clone()).map_err(|e| AffectedError::GraphError {
60            reason: format!("{e}"),
61        })?;
62
63    // Get changed files from git
64    let changed_files = get_changed_files(&root, base_branch)?;
65
66    // Compute affected projects
67    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    // Filter projects to only affected ones
86    let affected_projects: Vec<_> = all_projects
87        .into_iter()
88        .filter(|p| affected.all.contains(p.name()))
89        .collect();
90
91    // Build project graph from affected projects only
92    let affected_graph =
93        ProjectGraph::build(affected_projects).map_err(|e| AffectedError::GraphError {
94            reason: format!("{e}"),
95        })?;
96
97    // Build task graph
98    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    // Print header with affected info
121    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    // Show breakdown of changed vs dependents
132    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    // Create runner with reasonable defaults
143    let concurrency = std::thread::available_parallelism()
144        .map(|n| n.get())
145        .unwrap_or(4);
146    let runner = TaskRunner::new(concurrency, root);
147
148    // Execute
149    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
158    print_summary(&result, target);
159
160    Ok(result)
161}
162
163/// Print a summary of the run result.
164fn 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}