Skip to main content

runmat_package/source/
git_policy.rs

1use 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}