1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
use std::path::PathBuf;
use std::process::Command;
use crate::context;
use crate::{
ActionPlan, Config, EnvironmentInput, Error, ExecuteOptions, Executor, InitScriptDiscovery,
OutputEvent, Reporter, Result, RuntimePolicy, Worktree, WorktreeOptions,
};
/// Options for running worktree bootstrap.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RunOptions {
/// Directory from which the run starts. Defaults to the process cwd.
pub cwd: Option<PathBuf>,
/// Overrides the root checkout used as the file-operation source.
pub root: Option<PathBuf>,
/// Explicit environment input used for compatibility discovery and options.
pub environment: EnvironmentInput,
/// Uses one specific config file and skips init script discovery.
pub config: Option<PathBuf>,
/// Skips init script discovery and uses declarative config discovery.
pub no_init_script: bool,
/// Fails on missing config and stricter file-operation conflicts.
pub strict: bool,
/// Replaces existing file-operation targets where supported.
pub force: bool,
/// Prints planned work without changing files or running commands.
pub dry_run: bool,
/// Prints detailed file-operation actions instead of compact summaries.
pub verbose: bool,
/// Runs file operations only.
pub skip_commands: bool,
}
/// Completed action for a `treeboot run` invocation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RunAction {
/// No config or executable init script was detected.
MissingConfig,
/// The run started from the root checkout and had no work to do.
RootWorktreeSkipped,
/// An init script would run in dry-run mode.
WouldRunInitScript {
/// Script path.
path: PathBuf,
},
/// An init script was executed.
RanInitScript {
/// Script path.
path: PathBuf,
},
/// A declarative config was detected.
ConfigDetected {
/// Config file path.
path: PathBuf,
},
/// Declarative config file operations were applied.
ConfigApplied {
/// Config file path.
path: PathBuf,
},
}
/// Result summary for a `treeboot run` invocation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RunReport {
/// Runtime context used by the run.
pub context: Worktree,
/// Action taken by the run flow.
pub action: RunAction,
}
/// Runs worktree bootstrap according to the provided options.
///
/// Resolves the worktree context, discovers executable init scripts unless
/// disabled, discovers declarative config files, reports the selected action,
/// and executes an init script when one should run.
///
/// # Errors
///
/// Returns an error if context discovery fails, output reporting fails, an init
/// script cannot be started or exits unsuccessfully, a configured file cannot
/// be read, or strict mode treats a missing config as a failure.
pub fn run(options: RunOptions, reporter: &mut dyn Reporter) -> Result<RunReport> {
let runtime_policy = RuntimePolicy::from_environment(&options.environment, options.strict)?;
let pre_config_strict = runtime_policy.pre_config_strict();
let context = context::resolve(&WorktreeOptions {
cwd: options.cwd.clone(),
root: options.root.clone(),
environment: options.environment.clone(),
})?;
if context.root_path == context.worktree_path {
report(reporter, OutputEvent::RootWorktreeDetected)?;
if pre_config_strict {
return Err(Error::RootWorktreeStrict);
}
return Ok(RunReport {
context,
action: RunAction::RootWorktreeSkipped,
});
}
if options.config.is_none() && !options.no_init_script {
let scripts = InitScriptDiscovery::discover(&context);
for ignored in scripts.ignored {
report(
reporter,
OutputEvent::IgnoredInitScript { path: ignored.path },
)?;
}
if let Some(path) = scripts.executable {
return run_init_script(path, context, &options, reporter);
}
}
match Config::discover_path(&context, options.config.as_deref())? {
Some(path) => {
report(reporter, OutputEvent::ConfigDetected { path: path.clone() })?;
let config = Config::load(&path, &context)?;
let plan_options = runtime_policy.resolve(&config.options);
let strict = plan_options.strict();
let plan = ActionPlan::from_manifest(
&path,
&config,
&context,
plan_options.into_action_plan_options(),
)?;
Executor::new(ExecuteOptions {
strict,
force: options.force,
dry_run: options.dry_run,
verbose: options.verbose,
skip_commands: options.skip_commands,
})
.execute(&plan, reporter)?;
Ok(RunReport {
context,
action: RunAction::ConfigApplied { path },
})
}
None => {
report(reporter, OutputEvent::NoConfigDetected)?;
if pre_config_strict {
Err(Error::NoConfigDetectedStrict)
} else {
Ok(RunReport {
context,
action: RunAction::MissingConfig,
})
}
}
}
}
fn run_init_script(
path: PathBuf,
context: Worktree,
options: &RunOptions,
reporter: &mut dyn Reporter,
) -> Result<RunReport> {
if options.dry_run {
report(
reporter,
OutputEvent::WouldRunInitScript {
path: path.clone(),
root_path: context.root_path.clone(),
},
)?;
return Ok(RunReport {
context,
action: RunAction::WouldRunInitScript { path },
});
}
report(reporter, OutputEvent::RunInitScript { path: path.clone() })?;
let status = Command::new(&path)
.arg(&context.root_path)
.current_dir(&context.worktree_path)
.envs(&context.environment)
.status()
.map_err(|source| Error::ScriptIo {
path: path.clone(),
source,
})?;
if !status.success() {
return Err(Error::ScriptFailed { path, status });
}
Ok(RunReport {
context,
action: RunAction::RanInitScript { path },
})
}
fn report(reporter: &mut dyn Reporter, event: OutputEvent) -> Result<()> {
reporter
.report(event)
.map_err(|source| Error::Output { source })
}