use crate::diagnostics::EngineFailureCode;
use crate::conflict_side_guard::{SideVerdict, judge_conflict_sides};
use crate::discarded_prune::Limits;
use crate::discard_audit::{AuditOutcome, record_audit, record_audit_with_copies};
use crate::effective_target::{EffectiveTarget, classify_effective_target};
use crate::markdown_linter::embedded_linter;
use crate::policy_content::{ContentState, LifecycleContent};
use crate::policy_dependent_version::check_dependent_version;
use crate::policy_engine::{PolicyChecks, PolicyFinding, PolicyOutcome};
use crate::policy_final_newline::check_final_newline;
use crate::policy_forbidden_strings::{
ScannerSettings, ScannerState, check_forbidden_strings, default_scanner_settings,
unloaded_scanner,
};
use crate::policy_markdown::{MarkdownState, check_markdown, markdown_state};
use crate::policy_registry::PolicyId;
use crate::policy_root_context::check_root_context;
use crate::policy_catalog_specifiers::check_catalog_specifiers;
use crate::policy_trigger::Trigger;
use crate::repository_facts::RepositoryFacts;
use crate::repository_location::{RepositoryLocation, effective_directory};
use crate::rule_add_explicit::{BULK_ADD_CODE, decide_add_explicit};
use crate::rule_branch_worktree::{
BRANCH_CREATION_CODE, BranchWorktreeDecision, branch_creation_message, decide_branch_worktree,
};
use crate::rule_linked_worktree::{
GuardedCommand, LINKED_WORKTREE_REQUIRED_CODE, LinkedWorktreeDecision, decide_linked_worktree,
dirty_scope, dirty_worktree_message, resolve_linked_worktree,
};
use crate::rule_require_root::{
NOT_AT_ROOT_CODE, RequireRootDecision, RequireRootFacts, RequireRootVerdict,
decide_require_root, resolve_require_root,
};
use crate::worktree_identity::worktree_root;
use std::ffi::OsString;
use std::path::PathBuf;
pub struct ShippedChecks<F: RepositoryFacts> {
pub can_apply_patches: bool,
pub facts: F,
pub arguments: Vec<OsString>,
pub candidates: LifecycleContent,
pub allowed_worktree_dirs: Vec<PathBuf>,
pub discarded_limits: Limits,
pub content: ContentState,
pub scanner_settings: ScannerSettings,
pub scanner: ScannerState,
pub markdown: MarkdownState,
pub notices: Vec<String>,
}
pub fn shipped_checks<F: RepositoryFacts>(
facts: F,
arguments: Vec<OsString>,
candidates: LifecycleContent,
allowed_worktree_dirs: Vec<PathBuf>,
) -> ShippedChecks<F> {
return ShippedChecks {
can_apply_patches: false,
facts,
arguments,
candidates,
allowed_worktree_dirs,
discarded_limits: Limits::default(),
content: ContentState::new(),
scanner_settings: default_scanner_settings(),
scanner: unloaded_scanner(),
markdown: markdown_state(Box::new(embedded_linter())),
notices: Vec::new(),
};
}
fn fact_unavailable(message: String) -> PolicyOutcome {
return PolicyOutcome::Failed {
code: EngineFailureCode::ContentUnavailable,
message,
path: None,
};
}
fn clean() -> PolicyOutcome {
return PolicyOutcome::Findings(Vec::<PolicyFinding>::new());
}
fn rejected(code: &'static str, message: String) -> PolicyOutcome {
return PolicyOutcome::Findings(vec![PolicyFinding {
code,
message,
path: None,
location: None,
fix_available: false,
}]);
}
fn check_require_root(
facts: &mut dyn RepositoryFacts,
arguments: &[OsString],
trigger: Trigger,
) -> PolicyOutcome {
if trigger == Trigger::PreForward
&& decide_require_root(arguments, &[]) != RequireRootDecision::NeedsRepositoryRoot
{
return clean();
}
let location: RepositoryLocation = match facts.location() {
Ok(found) => found,
Err(message) => return fact_unavailable(message),
};
let Some(root) = worktree_root(&location.identity) else {
return clean();
};
let measured: RequireRootFacts = RequireRootFacts {
effective_directory: effective_directory(root, location.prefix.as_slice()),
repository_root: Some(root.to_path_buf()),
};
match resolve_require_root(&measured) {
RequireRootVerdict::Pass => return clean(),
RequireRootVerdict::NotAtRoot(violation) => {
return rejected(NOT_AT_ROOT_CODE, violation.message);
}
}
}
fn now_milliseconds() -> i64 {
let elapsed: std::time::Duration = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
return i64::try_from(elapsed.as_millis()).unwrap_or(i64::MAX);
}
fn check_linked_worktree(
facts: &mut dyn RepositoryFacts,
arguments: &[OsString],
allowed_worktree_dirs: &[PathBuf],
discarded_limits: Limits,
notices: &mut Vec<String>,
) -> PolicyOutcome {
let (command, moves_head_only): (GuardedCommand, bool) = match decide_linked_worktree(arguments) {
LinkedWorktreeDecision::Pass => return clean(),
LinkedWorktreeDecision::NeedsEffectiveTarget(command) => (command, false),
LinkedWorktreeDecision::WhenDirty(command) => (command, true),
};
let location: RepositoryLocation = match facts.location() {
Ok(found) => found,
Err(message) => return fact_unavailable(message),
};
let target: EffectiveTarget =
classify_effective_target(&location.identity, allowed_worktree_dirs);
let mut conflict_note: String = String::new();
if matches!(command, GuardedCommand::CheckoutPaths | GuardedCommand::RestoreWorktree)
&& matches!(target, EffectiveTarget::MainWorktree | EffectiveTarget::LinkedWorktree)
{
match judge_conflict_sides(&facts.transaction_context(), arguments, now_milliseconds(), discarded_limits) {
SideVerdict::NotApplicable => {}
SideVerdict::Refused(note) => conflict_note = note,
SideVerdict::Allowed { notice, copies } => {
let places: Vec<String> = copies
.iter()
.map(|copy| {
return format!("{} => {}", copy.path, copy.saved_to.as_ref().map_or_else(|| { return String::from("none"); }, |place| { return place.display().to_string(); }));
})
.collect();
record_audit_with_copies(&location.identity, command, AuditOutcome::SavedConflict, arguments, None, places.as_slice());
notices.push(notice);
return clean();
}
SideVerdict::CopyFailed(message) => {
record_audit(&location.identity, command, AuditOutcome::Rejected, arguments, None);
return rejected(LINKED_WORKTREE_REQUIRED_CODE, message);
}
}
}
if moves_head_only {
if target != EffectiveTarget::MainWorktree && target != EffectiveTarget::LinkedWorktree {
return clean();
}
} else if let Some(message) = resolve_linked_worktree(command, target) {
record_audit(&location.identity, command, AuditOutcome::Rejected, arguments, None);
return rejected(LINKED_WORKTREE_REQUIRED_CODE, format!("{message}{conflict_note}"));
}
if target == EffectiveTarget::Allowlisted {
record_audit(&location.identity, command, AuditOutcome::Exempt, arguments, None);
return clean();
}
match facts.worktree_dirty(dirty_scope(command)) {
Ok(true) => {
record_audit(&location.identity, command, AuditOutcome::Rejected, arguments, Some(true));
return rejected(LINKED_WORKTREE_REQUIRED_CODE, format!("{}{conflict_note}", dirty_worktree_message(command)));
}
Ok(false) => {
record_audit(&location.identity, command, AuditOutcome::Allowed, arguments, Some(false));
return clean();
}
Err(message) => return fact_unavailable(message),
}
}
fn check_branch_worktree(facts: &mut dyn RepositoryFacts, arguments: &[OsString]) -> PolicyOutcome {
match decide_branch_worktree(arguments) {
BranchWorktreeDecision::Pass => return clean(),
BranchWorktreeDecision::Creates(command) => {
return rejected(BRANCH_CREATION_CODE, branch_creation_message(command, None));
}
BranchWorktreeDecision::NeedsRemoteGuess { command, target } => {
match facts.remote_guess_creates_branch(target.as_os_str()) {
Err(message) => return fact_unavailable(message),
Ok(false) => return clean(),
Ok(true) => {
return rejected(
BRANCH_CREATION_CODE,
branch_creation_message(command, Some(target.as_os_str())),
);
}
}
}
}
}
fn check_add_explicit(arguments: &[OsString]) -> PolicyOutcome {
match decide_add_explicit(arguments) {
Some(message) => return rejected(BULK_ADD_CODE, message),
None => return clean(),
}
}
impl<F: RepositoryFacts> PolicyChecks for ShippedChecks<F> {
fn check(&mut self, policy: PolicyId, trigger: Trigger) -> PolicyOutcome {
match policy {
PolicyId::RequireRoot => {
return check_require_root(&mut self.facts, self.arguments.as_slice(), trigger);
}
PolicyId::LinkedWorktreeOnly => {
return check_linked_worktree(
&mut self.facts,
self.arguments.as_slice(),
self.allowed_worktree_dirs.as_slice(),
self.discarded_limits,
&mut self.notices,
);
}
PolicyId::BranchWorktreeOnly => {
return check_branch_worktree(&mut self.facts, self.arguments.as_slice());
}
PolicyId::AddExplicit => return check_add_explicit(self.arguments.as_slice()),
PolicyId::FinalNewline => {
return check_final_newline(
&mut self.content,
&self.candidates,
&mut self.facts,
trigger,
self.can_apply_patches,
);
}
PolicyId::ForbiddenRootContext => {
return check_root_context(&mut self.content, &self.candidates, &mut self.facts);
}
PolicyId::MarkdownAutofix => {
return check_markdown(
&mut self.content,
&self.candidates,
&mut self.facts,
&mut self.markdown,
trigger,
);
}
PolicyId::DependentVersionBump => {
return check_dependent_version(
&mut self.content,
&self.candidates,
&mut self.facts,
trigger,
);
}
PolicyId::CatalogSpecifiers => {
return check_catalog_specifiers(
&mut self.content,
&self.candidates,
&mut self.facts,
);
}
PolicyId::ForbiddenStrings => {
return check_forbidden_strings(
&mut self.content,
&self.candidates,
&mut self.facts,
&self.scanner_settings,
&mut self.scanner,
);
}
}
}
}
#[cfg(test)]
#[path = "policy_checks_tests.rs"]
mod tests;