1use std::fmt;
13use std::path::{Path, PathBuf};
14
15use serde::{Deserialize, Serialize};
16
17use crate::errors::ConfigError;
18use crate::model::names::{GitRef, ProjectName, TagGlob, TagName};
19use crate::store::SnapshotStore;
20use crate::versions::version_cmp;
21
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
23#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
24pub struct Pin {
25 pub project: ProjectName,
26 pub tag: TagName,
27}
28
29impl Pin {
30 pub fn new(project: ProjectName, tag: TagName) -> Self {
31 Self { project, tag }
32 }
33}
34
35impl fmt::Display for Pin {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(f, "{}@{}", self.project, self.tag)
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
43#[serde(rename_all = "snake_case")]
44pub enum PinSelect {
45 Latest,
46 Oldest,
47}
48
49impl PinSelect {
50 pub fn pick<'a, I>(self, tags: I) -> Option<&'a TagName>
52 where
53 I: IntoIterator<Item = &'a TagName>,
54 {
55 let iter = tags.into_iter();
56 match self {
58 Self::Latest => iter.min_by(|a, b| version_cmp(a.as_str(), b.as_str())),
59 Self::Oldest => iter.max_by(|a, b| version_cmp(a.as_str(), b.as_str())),
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
65#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
66#[serde(untagged)]
67pub enum PinSpec {
68 Literal {
69 project: ProjectName,
70 tag: TagName,
71 },
72 Pattern {
73 project: ProjectName,
74 #[serde(rename = "tag_pattern")]
75 pattern: TagGlob,
76 select: PinSelect,
77 },
78 SelfRef {
84 project: ProjectName,
85 git_ref: GitRef,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 repo_dir_path: Option<PathBuf>,
88 },
89}
90
91impl PinSpec {
92 pub fn literal(project: ProjectName, tag: TagName) -> Self {
93 Self::Literal { project, tag }
94 }
95
96 pub fn pattern(project: ProjectName, pattern: TagGlob, select: PinSelect) -> Self {
97 Self::Pattern {
98 project,
99 pattern,
100 select,
101 }
102 }
103
104 pub fn project(&self) -> &ProjectName {
105 match self {
106 Self::Literal { project, .. }
107 | Self::Pattern { project, .. }
108 | Self::SelfRef { project, .. } => project,
109 }
110 }
111
112 pub fn is_self(&self) -> bool {
113 matches!(self, Self::SelfRef { .. })
114 }
115
116 pub fn resolve<M>(&self, store: &SnapshotStore<M>) -> Result<Pin, ConfigError> {
120 match self {
121 Self::Literal { project, tag } => Ok(Pin::new(project.clone(), tag.clone())),
122 Self::Pattern {
123 project,
124 pattern,
125 select,
126 } => {
127 let stored =
128 store
129 .list_tags(project)
130 .map_err(|e| ConfigError::PinPatternStore {
131 project: project.to_string(),
132 pattern: pattern.to_string(),
133 detail: e.to_string(),
134 })?;
135 let chosen = select
136 .pick(stored.iter().filter(|t| pattern.matches(t)))
137 .ok_or_else(|| ConfigError::PinPatternNoMatch {
138 project: project.to_string(),
139 pattern: pattern.to_string(),
140 })?;
141 Ok(Pin::new(project.clone(), chosen.clone()))
142 }
143 Self::SelfRef {
144 project, git_ref, ..
145 } => Err(ConfigError::SelfPinNeedsRepoDirPath {
146 project: project.to_string(),
147 git_ref: git_ref.to_string(),
148 }),
149 }
150 }
151
152 pub fn self_repo_override(&self) -> Option<&Path> {
156 match self {
157 Self::SelfRef { repo_dir_path, .. } => repo_dir_path.as_deref(),
158 _ => None,
159 }
160 }
161}
162
163pub fn resolve_all<M>(
166 specs: &[PinSpec],
167 store: &SnapshotStore<M>,
168) -> Result<Vec<Pin>, ConfigError> {
169 specs.iter().map(|s| s.resolve(store)).collect()
170}
171
172impl fmt::Display for PinSpec {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 match self {
175 Self::Literal { project, tag } => write!(f, "{project}@{tag}"),
176 Self::Pattern {
177 project,
178 pattern,
179 select,
180 } => {
181 let select_label = match select {
182 PinSelect::Latest => "latest",
183 PinSelect::Oldest => "oldest",
184 };
185 write!(f, "{project}@{pattern} ({select_label})")
186 }
187 Self::SelfRef {
188 project, git_ref, ..
189 } => write!(f, "{project}@{git_ref} (self)"),
190 }
191 }
192}