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
use std::path::PathBuf;
use serde::Serialize;
use crate::context;
use crate::{
ActionPlan, Config, EnvironmentInput, Error, InitScriptDiscovery, Result, RuntimePolicy,
Worktree, WorktreeOptions,
};
/// Options for checking treeboot bootstrap behavior.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CheckOptions {
/// Directory from which the check 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,
}
/// Completed action for a `treeboot check` invocation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CheckAction {
/// No config or executable init script was detected.
MissingConfig,
/// The check started from the root checkout and had no work to validate.
RootWorktreeSkipped,
/// An init script would take precedence.
InitScript {
/// Script path.
path: PathBuf,
},
/// Declarative config was validated.
Config {
/// Config file path.
path: PathBuf,
},
}
/// Result summary for a `treeboot check` invocation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CheckReport {
/// Runtime context used by the check.
pub context: WorktreeSnapshot,
/// Action that was validated.
pub action: CheckAction,
/// Ordered human-readable non-fatal run-validation warnings, such as an
/// include list that matches no source paths. Empty when validation
/// produces no warnings.
pub warnings: Vec<String>,
}
/// Serializable worktree context snapshot for reports.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct WorktreeSnapshot {
/// Source checkout used for file operations.
pub root_path: PathBuf,
/// Current worktree root where targets and commands are anchored.
pub worktree_path: PathBuf,
/// Best-effort default branch name.
pub default_branch: String,
}
impl From<&Worktree> for WorktreeSnapshot {
fn from(context: &Worktree) -> Self {
Self {
root_path: context.root_path.clone(),
worktree_path: context.worktree_path.clone(),
default_branch: context.default_branch.clone(),
}
}
}
/// Checks treeboot bootstrap behavior without side effects.
///
/// # Errors
///
/// Returns an error when context discovery fails, strict mode treats the
/// current state as invalid, config loading fails, or declarative validation
/// fails.
pub fn check(options: CheckOptions) -> Result<CheckReport> {
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 {
if pre_config_strict {
return Err(Error::RootWorktreeStrict);
}
return Ok(CheckReport {
context: WorktreeSnapshot::from(&context),
action: CheckAction::RootWorktreeSkipped,
warnings: Vec::new(),
});
}
if options.config.is_none() && !options.no_init_script {
let scripts = InitScriptDiscovery::discover(&context);
if let Some(path) = scripts.executable {
return Ok(CheckReport {
context: WorktreeSnapshot::from(&context),
action: CheckAction::InitScript { path },
warnings: Vec::new(),
});
}
}
match Config::discover_path(&context, options.config.as_deref())? {
Some(path) => {
let config = Config::load(&path, &context)?;
let plan_options = runtime_policy.resolve(&config.options);
let plan = ActionPlan::from_manifest(
&path,
&config,
&context,
plan_options.into_action_plan_options(),
)?;
Ok(CheckReport {
context: WorktreeSnapshot::from(&context),
action: CheckAction::Config { path },
warnings: plan.warnings().iter().map(ToString::to_string).collect(),
})
}
None => {
if pre_config_strict {
Err(Error::NoConfigDetectedStrict)
} else {
Ok(CheckReport {
context: WorktreeSnapshot::from(&context),
action: CheckAction::MissingConfig,
warnings: Vec::new(),
})
}
}
}
}