runmat_package/source/
git_policy.rs1use crate::{GitRepositoryUrl, GitSelector, GitSourceId, NormalizedRelativePath};
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5pub type GitAcquisitionIntent = super::SourceAcquisitionIntent;
6pub type GitAcquisitionPolicy = super::SourceAcquisitionPolicy;
7pub type GitLockAction = super::SourceLockAction;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(deny_unknown_fields)]
11pub struct GitAcquisitionPlan {
12 pub repository: GitRepositoryUrl,
13 pub selector: GitSelector,
14 pub subdir: NormalizedRelativePath,
15 pub allow_network: bool,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub expected: Option<GitSourceId>,
18 pub lock_action: GitLockAction,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Error)]
22pub enum GitPolicyError {
23 #[error("frozen mode cannot update mutable Git sources")]
24 FrozenUpdate,
25 #[error("locked mode cannot update mutable Git sources")]
26 LockedUpdate,
27 #[error("locked or frozen mode requires an exact Git source in runmat.lock")]
28 MissingLock,
29 #[error("locked Git source does not match the manifest repository or subdirectory")]
30 LockLocatorMismatch,
31 #[error("Git provider returned {found:?}; expected locked source {expected:?}")]
32 LockedSourceMismatch {
33 expected: Box<GitSourceId>,
34 found: Box<GitSourceId>,
35 },
36 #[error("Git provider returned a different repository or subdirectory than requested")]
37 AcquiredLocatorMismatch,
38}
39
40pub fn plan_git_acquisition(
41 repository: GitRepositoryUrl,
42 selector: GitSelector,
43 subdir: NormalizedRelativePath,
44 locked_source: Option<&GitSourceId>,
45 intent: GitAcquisitionIntent,
46 policy: GitAcquisitionPolicy,
47) -> Result<GitAcquisitionPlan, GitPolicyError> {
48 if intent == GitAcquisitionIntent::Update {
49 if policy.frozen {
50 return Err(GitPolicyError::FrozenUpdate);
51 }
52 if policy.locked {
53 return Err(GitPolicyError::LockedUpdate);
54 }
55 }
56 if let Some(locked) = locked_source {
57 if locked.repository != repository || locked.subdir != subdir {
58 return Err(GitPolicyError::LockLocatorMismatch);
59 }
60 }
61 let use_locked = intent != GitAcquisitionIntent::Update && locked_source.is_some();
62 if !use_locked && (policy.locked || policy.frozen) {
63 return Err(GitPolicyError::MissingLock);
64 }
65 if use_locked {
66 let expected = locked_source.cloned().expect("checked locked source");
67 return Ok(GitAcquisitionPlan {
68 repository,
69 selector: GitSelector::Rev {
70 value: expected.commit.hex.clone(),
71 },
72 subdir,
73 allow_network: !policy.offline && !policy.frozen,
74 expected: Some(expected),
75 lock_action: GitLockAction::Preserve,
76 });
77 }
78 Ok(GitAcquisitionPlan {
79 repository,
80 selector,
81 subdir,
82 allow_network: !policy.offline && !policy.frozen,
83 expected: None,
84 lock_action: match intent {
85 GitAcquisitionIntent::Update => GitLockAction::Replace,
86 GitAcquisitionIntent::Execute | GitAcquisitionIntent::Fetch => GitLockAction::Write,
87 },
88 })
89}
90
91pub fn validate_git_acquisition(
92 plan: &GitAcquisitionPlan,
93 acquired: &GitSourceId,
94) -> Result<(), GitPolicyError> {
95 if acquired.repository != plan.repository || acquired.subdir != plan.subdir {
96 return Err(GitPolicyError::AcquiredLocatorMismatch);
97 }
98 if let Some(expected) = &plan.expected {
99 if acquired != expected {
100 return Err(GitPolicyError::LockedSourceMismatch {
101 expected: Box::new(expected.clone()),
102 found: Box::new(acquired.clone()),
103 });
104 }
105 }
106 Ok(())
107}