1use std::fmt;
13use std::path::{Path, PathBuf};
14
15use serde::{Deserialize, Serialize};
16
17use crate::errors::ConfigError;
18use crate::model::names::{GitRef, ProjectName, SeriesName, TagGlob, TagName};
19use crate::store::{SnapshotStore, StoreMode};
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: StoreMode>(&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 as_self_pin(&self) -> Option<SelfPin<'_>> {
155 match self {
156 Self::SelfRef {
157 project,
158 git_ref,
159 repo_dir_path,
160 } => Some(SelfPin {
161 project,
162 git_ref,
163 repo_dir_path: repo_dir_path.as_deref(),
164 }),
165 Self::Literal { .. } | Self::Pattern { .. } => None,
166 }
167 }
168}
169
170#[derive(Debug, Clone, Copy)]
173pub struct SelfPin<'a> {
174 pub project: &'a ProjectName,
175 pub git_ref: &'a GitRef,
176 pub repo_dir_path: Option<&'a Path>,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
183pub enum PinSelector {
184 Series(SeriesName),
185 Pin { project: ProjectName, tag: TagName },
186}
187
188impl PinSelector {
189 pub fn series(name: impl Into<SeriesName>) -> Self {
190 Self::Series(name.into())
191 }
192
193 #[must_use]
195 pub fn series_name(&self) -> Option<&SeriesName> {
196 match self {
197 Self::Series(name) => Some(name),
198 Self::Pin { .. } => None,
199 }
200 }
201
202 pub fn pin(project: impl Into<ProjectName>, tag: impl Into<TagName>) -> Self {
203 Self::Pin {
204 project: project.into(),
205 tag: tag.into(),
206 }
207 }
208}
209
210impl From<SeriesName> for PinSelector {
211 fn from(s: SeriesName) -> Self {
212 Self::Series(s)
213 }
214}
215
216impl From<(ProjectName, TagName)> for PinSelector {
217 fn from((project, tag): (ProjectName, TagName)) -> Self {
218 Self::Pin { project, tag }
219 }
220}
221
222pub fn resolve_all<M: StoreMode>(
225 specs: &[PinSpec],
226 store: &SnapshotStore<M>,
227) -> Result<Vec<Pin>, ConfigError> {
228 specs.iter().map(|s| s.resolve(store)).collect()
229}
230
231impl fmt::Display for PinSpec {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 match self {
234 Self::Literal { project, tag } => write!(f, "{project}@{tag}"),
235 Self::Pattern {
236 project,
237 pattern,
238 select,
239 } => {
240 let select_label = match select {
241 PinSelect::Latest => "latest",
242 PinSelect::Oldest => "oldest",
243 };
244 write!(f, "{project}@{pattern} ({select_label})")
245 }
246 Self::SelfRef {
247 project, git_ref, ..
248 } => write!(f, "{project}@{git_ref} (self)"),
249 }
250 }
251}