use crate::candidate_object::CandidateMode;
use crate::candidate_record::CandidateChange as RecordChange;
use crate::candidate_version::{Candidate as LayerCandidate, CandidateVersion};
use crate::dependent_version_content::{
ContentUnavailable, PlanError, TrackedMode, display_path, plan_error_message,
};
use crate::dependent_version_lifecycle::{LifecycleWorkspace, lifecycle_workspace};
use crate::dependent_version_plan::ManifestBump;
use crate::dependent_version_policy::{
Candidate, CandidateChange, DependentFinding, DependentRequest, find_dependent_bumps,
should_plan,
};
use crate::event_path::EventPath;
use crate::diagnostics::EngineFailureCode;
use crate::git_metadata::path_from_git_bytes;
use crate::policy_added_path::{
ADDED_PATH_STAGED_REASON, added_path_message, head_mode, worktree_refusal,
};
use crate::policy_content::{ContentState, Correction, LifecycleContent};
use crate::policy_engine::{PolicyFinding, PolicyOutcome};
use crate::policy_trigger::Trigger;
use crate::repository_facts::RepositoryFacts;
use crate::worktree_identity::worktree_root;
use std::path::{Path, PathBuf};
use std::rc::Rc;
fn clean() -> PolicyOutcome {
return PolicyOutcome::Findings(Vec::new());
}
fn failed(code: EngineFailureCode, message: String) -> PolicyOutcome {
return PolicyOutcome::Failed {
code,
message,
path: None,
};
}
fn planner_candidate(candidate: &LayerCandidate) -> Candidate {
let change: CandidateChange = match candidate.change {
RecordChange::Added => CandidateChange::Added,
RecordChange::Modified => CandidateChange::Modified,
RecordChange::Deleted => CandidateChange::Deleted,
};
return Candidate {
path: candidate.path.clone(),
change,
};
}
fn plan_failure(error: &PlanError) -> PolicyOutcome {
let code: EngineFailureCode = match error {
PlanError::ContentUnavailable(_) => EngineFailureCode::ContentUnavailable,
PlanError::PolicyIncomplete(_) => EngineFailureCode::PolicyIncomplete,
};
return failed(code, plan_error_message(error));
}
fn policy_finding(finding: &DependentFinding) -> PolicyFinding {
return PolicyFinding {
code: finding.code,
message: finding.message.clone(),
path: finding.path.as_deref().map(EventPath::from_git_bytes),
location: None,
fix_available: finding.patch.is_some(),
};
}
fn conflict(path: &str, reason: &str) -> PolicyOutcome {
return failed(
EngineFailureCode::PatchConflict,
added_path_message(path, reason),
);
}
fn unreadable_failure(error: ContentUnavailable) -> PolicyOutcome {
return plan_failure(&PlanError::ContentUnavailable(error));
}
fn is_listed_change(candidate: &&Candidate) -> bool {
return candidate.change != CandidateChange::Deleted;
}
fn candidate_path(candidate: &Candidate) -> Vec<u8> {
return candidate.path.clone();
}
fn correction_of(
bump: &ManifestBump,
selected: bool,
workspace: &mut LifecycleWorkspace<'_>,
root: Option<&Path>,
) -> Result<Correction, PolicyOutcome> {
let mode: CandidateMode = if bump.mode == TrackedMode::Executable {
CandidateMode::Executable
} else {
CandidateMode::Regular
};
let correction: Correction = Correction {
path: bump.path.clone(),
mode,
before: Rc::from(bump.original.as_slice()),
after: Rc::from(bump.replacement.as_slice()),
};
let (false, Some(top_level)) = (selected, root) else {
return Ok(correction);
};
let shown: String = display_path(bump.path.as_slice());
let Some(index) = workspace.index_entry(&bump.path).map_err(unreadable_failure)? else {
return Err(conflict(shown.as_str(), ADDED_PATH_STAGED_REASON));
};
let head = workspace.head_entry(&bump.path).map_err(unreadable_failure)?;
let head_file_mode: CandidateMode = match head_mode(head.as_ref(), &index) {
Ok(found) => found,
Err(reason) => return Err(conflict(shown.as_str(), reason)),
};
let Some(relative) = path_from_git_bytes(&bump.path) else {
return Err(failed(
EngineFailureCode::ContentUnavailable,
format!(
"mono/dependent-version-bump cannot name the worktree copy of {shown} on this system."
),
));
};
let file: PathBuf = top_level.join(relative);
match worktree_refusal(file.as_path(), head_file_mode, bump.original.as_slice()) {
Ok(None) => return Ok(correction),
Ok(Some(reason)) => return Err(conflict(shown.as_str(), reason)),
Err(error) => {
return Err(failed(
EngineFailureCode::ContentUnavailable,
format!(
"mono/dependent-version-bump could not read the worktree copy of {shown}: {error}."
),
));
}
}
}
pub fn check_dependent_version<F: RepositoryFacts>(
content: &mut ContentState,
lifecycle: &LifecycleContent,
facts: &mut F,
trigger: Trigger,
) -> PolicyOutcome {
if trigger == Trigger::PreForward {
return clean();
}
let version: Rc<CandidateVersion> = match content.version(lifecycle, facts) {
Ok(Some(found)) => found,
Ok(None) => return clean(),
Err(outcome) => return outcome,
};
let candidates: Vec<Candidate> = version.candidates().iter().map(planner_candidate).collect();
let request: DependentRequest<'_> = DependentRequest {
trigger,
forwards_commit: false,
candidates: candidates.as_slice(),
};
if !should_plan(&request) {
return clean();
}
let root: Option<PathBuf> = if trigger == Trigger::DirectFix {
match facts.location() {
Ok(location) => worktree_root(&location.identity).map(Path::to_path_buf),
Err(message) => return failed(EngineFailureCode::ContentUnavailable, message),
}
} else {
None
};
let listed: Vec<Vec<u8>> = candidates
.iter()
.filter(is_listed_change)
.map(candidate_path)
.collect();
let Some((store, corrected)) = content.store_with_corrections() else {
return failed(
EngineFailureCode::ContentUnavailable,
String::from(
"mono/dependent-version-bump found no prepared candidates; this is a defect in cli-git.",
),
);
};
let mut workspace: LifecycleWorkspace<'_> =
lifecycle_workspace(store, corrected, listed.as_slice());
let findings: Vec<DependentFinding> = match find_dependent_bumps(&request, &mut workspace) {
Ok(found) => found,
Err(error) => return plan_failure(&error),
};
let mut proposals: Vec<Correction> = Vec::new();
for finding in &findings {
if let (Trigger::DirectFix, Some(bump)) = (trigger, finding.patch.as_ref()) {
let selected: bool = version.candidate_at_path(&bump.path).is_some();
match correction_of(bump, selected, &mut workspace, root.as_deref()) {
Ok(correction) => proposals.push(correction),
Err(outcome) => return outcome,
}
}
}
for correction in proposals {
content.propose(correction);
}
return PolicyOutcome::Findings(findings.iter().map(policy_finding).collect());
}
#[cfg(test)]
#[path = "policy_dependent_version_tests.rs"]
mod tests;