1use super::candidate_object::CandidateMode;
28use super::candidate_record::CandidateChange as RecordChange;
30use super::candidate_version::{Candidate as LayerCandidate, CandidateVersion};
32use super::dependent_version_content::{
35 ContentUnavailable, PlanError, TrackedMode, display_path, plan_error_message,
36};
37use super::dependent_version_lifecycle::{LifecycleWorkspace, lifecycle_workspace};
39use super::dependent_version_plan::ManifestBump;
41use super::dependent_version_policy::{
44 Candidate, CandidateChange, DependentFinding, DependentRequest, find_dependent_bumps,
45 should_plan,
46};
47use super::event_path::EventPath;
50use super::diagnostics::EngineFailureCode;
51use super::git_metadata::path_from_git_bytes;
53use super::policy_added_path::{
55 ADDED_PATH_STAGED_REASON, added_path_message, head_mode, worktree_refusal,
56};
57use super::policy_content::{ContentState, Correction, LifecycleContent};
59use super::policy_engine::{PolicyFinding, PolicyOutcome};
61use super::policy_trigger::Trigger;
63use super::repository_facts::RepositoryFacts;
65use super::worktree_identity::worktree_root;
67use std::path::{Path, PathBuf};
69use std::rc::Rc;
72
73fn clean() -> PolicyOutcome {
83 return PolicyOutcome::Findings(Vec::new());
84}
85
86fn failed(code: EngineFailureCode, message: String) -> PolicyOutcome {
97 return PolicyOutcome::Failed {
98 code,
99 message,
100 path: None,
101 };
102}
103
104fn planner_candidate(candidate: &LayerCandidate) -> Candidate {
114 let change: CandidateChange = match candidate.change {
115 RecordChange::Added => CandidateChange::Added,
116 RecordChange::Modified => CandidateChange::Modified,
117 RecordChange::Deleted => CandidateChange::Deleted,
118 };
119 return Candidate {
120 path: candidate.path.clone(),
121 change,
122 };
123}
124
125fn plan_failure(error: &PlanError) -> PolicyOutcome {
138 let code: EngineFailureCode = match error {
139 PlanError::ContentUnavailable(_) => EngineFailureCode::ContentUnavailable,
140 PlanError::PolicyIncomplete(_) => EngineFailureCode::PolicyIncomplete,
141 };
142 return failed(code, plan_error_message(error));
143}
144
145fn policy_finding(finding: &DependentFinding) -> PolicyFinding {
160 return PolicyFinding {
161 code: finding.code,
162 message: finding.message.clone(),
163 path: finding.path.as_deref().map(EventPath::from_git_bytes),
164 location: None,
165 fix_available: finding.patch.is_some(),
166 };
167}
168
169fn conflict(path: &str, reason: &str) -> PolicyOutcome {
181 return failed(
182 EngineFailureCode::PatchConflict,
183 added_path_message(path, reason),
184 );
185}
186
187fn unreadable_failure(error: ContentUnavailable) -> PolicyOutcome {
194 return plan_failure(&PlanError::ContentUnavailable(error));
195}
196
197fn is_listed_change(candidate: &&Candidate) -> bool {
203 return candidate.change != CandidateChange::Deleted;
204}
205
206fn candidate_path(candidate: &Candidate) -> Vec<u8> {
213 return candidate.path.clone();
214}
215
216fn correction_of(
234 bump: &ManifestBump,
235 selected: bool,
236 workspace: &mut LifecycleWorkspace<'_>,
237 root: Option<&Path>,
238) -> Result<Correction, PolicyOutcome> {
239 let mode: CandidateMode = if bump.mode == TrackedMode::Executable {
240 CandidateMode::Executable
241 } else {
242 CandidateMode::Regular
243 };
244 let correction: Correction = Correction {
245 path: bump.path.clone(),
246 mode,
247 before: Rc::from(bump.original.as_slice()),
248 after: Rc::from(bump.replacement.as_slice()),
249 };
250 let (false, Some(top_level)) = (selected, root) else {
251 return Ok(correction);
252 };
253 let shown: String = display_path(bump.path.as_slice());
254 let Some(index) = workspace.index_entry(&bump.path).map_err(unreadable_failure)? else {
255 return Err(conflict(shown.as_str(), ADDED_PATH_STAGED_REASON));
256 };
257 let head = workspace.head_entry(&bump.path).map_err(unreadable_failure)?;
258 let head_file_mode: CandidateMode = match head_mode(head.as_ref(), &index) {
259 Ok(found) => found,
260 Err(reason) => return Err(conflict(shown.as_str(), reason)),
261 };
262 let Some(relative) = path_from_git_bytes(&bump.path) else {
263 return Err(failed(
264 EngineFailureCode::ContentUnavailable,
265 format!(
266 "mono/dependent-version-bump cannot name the worktree copy of {shown} on this system."
267 ),
268 ));
269 };
270 let file: PathBuf = top_level.join(relative);
271 match worktree_refusal(file.as_path(), head_file_mode, bump.original.as_slice()) {
272 Ok(None) => return Ok(correction),
273 Ok(Some(reason)) => return Err(conflict(shown.as_str(), reason)),
274 Err(error) => {
275 return Err(failed(
276 EngineFailureCode::ContentUnavailable,
277 format!(
278 "mono/dependent-version-bump could not read the worktree copy of {shown}: {error}."
279 ),
280 ));
281 }
282 }
283}
284
285pub fn check_dependent_version<F: RepositoryFacts>(
304 content: &mut ContentState,
305 lifecycle: &LifecycleContent,
306 facts: &mut F,
307 trigger: Trigger,
308) -> PolicyOutcome {
309 if trigger == Trigger::PreForward {
310 return clean();
311 }
312 let version: Rc<CandidateVersion> = match content.version(lifecycle, facts) {
313 Ok(Some(found)) => found,
314 Ok(None) => return clean(),
315 Err(outcome) => return outcome,
316 };
317 let candidates: Vec<Candidate> = version.candidates().iter().map(planner_candidate).collect();
318 let request: DependentRequest<'_> = DependentRequest {
319 trigger,
320 forwards_commit: false,
321 candidates: candidates.as_slice(),
322 };
323 if !should_plan(&request) {
324 return clean();
325 }
326 let root: Option<PathBuf> = if trigger == Trigger::DirectFix {
327 match facts.location() {
328 Ok(location) => worktree_root(&location.identity).map(Path::to_path_buf),
329 Err(message) => return failed(EngineFailureCode::ContentUnavailable, message),
330 }
331 } else {
332 None
333 };
334 let listed: Vec<Vec<u8>> = candidates
335 .iter()
336 .filter(is_listed_change)
337 .map(candidate_path)
338 .collect();
339 let Some((store, corrected)) = content.store_with_corrections() else {
340 return failed(
341 EngineFailureCode::ContentUnavailable,
342 String::from(
343 "mono/dependent-version-bump found no prepared candidates; this is a defect in cli-git.",
344 ),
345 );
346 };
347 let mut workspace: LifecycleWorkspace<'_> =
348 lifecycle_workspace(store, corrected, listed.as_slice());
349 let findings: Vec<DependentFinding> = match find_dependent_bumps(&request, &mut workspace) {
350 Ok(found) => found,
351 Err(error) => return plan_failure(&error),
352 };
353 let mut proposals: Vec<Correction> = Vec::new();
354 for finding in &findings {
355 if let (Trigger::DirectFix, Some(bump)) = (trigger, finding.patch.as_ref()) {
356 let selected: bool = version.candidate_at_path(&bump.path).is_some();
357 match correction_of(bump, selected, &mut workspace, root.as_deref()) {
358 Ok(correction) => proposals.push(correction),
359 Err(outcome) => return outcome,
360 }
361 }
362 }
363 for correction in proposals {
364 content.propose(correction);
365 }
366 return PolicyOutcome::Findings(findings.iter().map(policy_finding).collect());
367}
368
369#[cfg(test)]
371#[path = "policy_dependent_version_tests.rs"]
372mod tests;