1use std::path::PathBuf;
2use std::process::Command;
3
4use crate::config::RuntimeOptionOverrides;
5use crate::context;
6use crate::{
7 ActionPlan, Config, Error, ExecuteOptions, Executor, InitScriptDiscovery, OutputEvent,
8 Reporter, Result, Worktree, WorktreeOptions,
9};
10
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct RunOptions {
14 pub cwd: Option<PathBuf>,
16 pub root: Option<PathBuf>,
18 pub config: Option<PathBuf>,
20 pub no_init_script: bool,
22 pub strict: bool,
24 pub force: bool,
26 pub dry_run: bool,
28 pub skip_commands: bool,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum RunAction {
35 MissingConfig,
37 RootWorktreeSkipped,
39 WouldRunInitScript {
41 path: PathBuf,
43 },
44 RanInitScript {
46 path: PathBuf,
48 },
49 ConfigDetected {
51 path: PathBuf,
53 },
54 ConfigApplied {
56 path: PathBuf,
58 },
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct RunReport {
64 pub context: Worktree,
66 pub action: RunAction,
68}
69
70pub fn run(options: RunOptions, reporter: &mut dyn Reporter) -> Result<RunReport> {
82 let env_options = RuntimeOptionOverrides::from_env()?;
83 let pre_config_strict = env_options.pre_config_strict(options.strict);
84 let context = context::resolve(&WorktreeOptions {
85 cwd: options.cwd.clone(),
86 root: options.root.clone(),
87 })?;
88
89 if context.root_path == context.worktree_path {
90 report(reporter, OutputEvent::RootWorktreeDetected)?;
91
92 if pre_config_strict {
93 return Err(Error::RootWorktreeStrict);
94 }
95
96 return Ok(RunReport {
97 context,
98 action: RunAction::RootWorktreeSkipped,
99 });
100 }
101
102 if options.config.is_none() && !options.no_init_script {
103 let scripts = InitScriptDiscovery::discover(&context);
104
105 for path in scripts.ignored {
106 report(reporter, OutputEvent::IgnoredInitScript { path })?;
107 }
108
109 if let Some(path) = scripts.executable {
110 return run_init_script(path, context, &options, reporter);
111 }
112 }
113
114 match Config::discover_path(&context, options.config.as_deref())? {
115 Some(path) => {
116 report(reporter, OutputEvent::ConfigDetected { path: path.clone() })?;
117 let config = Config::load(&path, &context)?;
118 let plan_options = env_options.resolve(&config.options, options.strict);
119 let plan = ActionPlan::from_manifest(&path, &config, &context, plan_options.into())?;
120 Executor::new(ExecuteOptions {
121 strict: plan_options.strict,
122 force: options.force,
123 dry_run: options.dry_run,
124 skip_commands: options.skip_commands,
125 })
126 .execute(&plan, reporter)?;
127
128 Ok(RunReport {
129 context,
130 action: RunAction::ConfigApplied { path },
131 })
132 }
133 None => {
134 report(reporter, OutputEvent::NoConfigDetected)?;
135
136 if pre_config_strict {
137 Err(Error::NoConfigDetectedStrict)
138 } else {
139 Ok(RunReport {
140 context,
141 action: RunAction::MissingConfig,
142 })
143 }
144 }
145 }
146}
147
148fn run_init_script(
149 path: PathBuf,
150 context: Worktree,
151 options: &RunOptions,
152 reporter: &mut dyn Reporter,
153) -> Result<RunReport> {
154 if options.dry_run {
155 report(
156 reporter,
157 OutputEvent::WouldRunInitScript {
158 path: path.clone(),
159 root_path: context.root_path.clone(),
160 },
161 )?;
162
163 return Ok(RunReport {
164 context,
165 action: RunAction::WouldRunInitScript { path },
166 });
167 }
168
169 report(reporter, OutputEvent::RunInitScript { path: path.clone() })?;
170
171 let status = Command::new(&path)
172 .arg(&context.root_path)
173 .current_dir(&context.worktree_path)
174 .envs(&context.environment)
175 .status()
176 .map_err(|source| Error::ScriptIo {
177 path: path.clone(),
178 source,
179 })?;
180
181 if !status.success() {
182 return Err(Error::ScriptFailed { path, status });
183 }
184
185 Ok(RunReport {
186 context,
187 action: RunAction::RanInitScript { path },
188 })
189}
190
191fn report(reporter: &mut dyn Reporter, event: OutputEvent) -> Result<()> {
192 reporter
193 .report(event)
194 .map_err(|source| Error::Output { source })
195}