quilt_rs/lineage/
package.rs1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use multihash::Multihash;
5use serde::Deserialize;
6use serde::Serialize;
7use serde::de;
8use serde::ser;
9
10use crate::Error;
11use crate::Res;
12use crate::error::LineageError;
13use crate::lineage::status::UpstreamState;
14use quilt_uri::ManifestUri;
15
16fn multihash_to_str<S: ser::Serializer>(
17 hash: &Multihash<256>,
18 serializer: S,
19) -> Result<S::Ok, S::Error> {
20 let s = hex::encode(hash.to_bytes());
21 serializer.serialize_str(&s)
22}
23
24fn str_to_multihash<'de, D: de::Deserializer<'de>>(
25 deserializer: D,
26) -> Result<Multihash<256>, D::Error> {
27 let s = String::deserialize(deserializer)?;
28 let bytes = hex::decode(s).map_err(de::Error::custom)?;
29 Multihash::from_bytes(&bytes).map_err(de::Error::custom)
30}
31
32#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
34pub struct PathState {
35 pub timestamp: chrono::DateTime<chrono::Utc>,
38 #[serde(
42 serialize_with = "multihash_to_str",
43 deserialize_with = "str_to_multihash"
44 )]
45 pub hash: Multihash<256>,
46}
47
48pub type LineagePaths = BTreeMap<PathBuf, PathState>;
52
53#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
74pub struct CommitState {
75 pub timestamp: chrono::DateTime<chrono::Utc>,
77 pub hash: String,
79 #[serde(default = "Vec::new")]
81 pub prev_hashes: Vec<String>,
82}
83
84#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
86pub struct PackageLineage {
87 pub commit: Option<CommitState>,
89 #[serde(default, rename = "remote", skip_serializing_if = "Option::is_none")]
91 pub remote_uri: Option<ManifestUri>,
92 pub base_hash: String,
94 pub latest_hash: String,
101 #[serde(default = "BTreeMap::new")]
103 pub paths: LineagePaths,
104}
105
106impl From<PackageLineage> for UpstreamState {
107 fn from(lineage: PackageLineage) -> Self {
108 let Some(remote) = lineage.remote_uri.as_ref() else {
109 return Self::Local;
110 };
111 if remote.bucket.is_empty() {
119 return Self::Local;
120 }
121 if remote.hash.is_empty() {
122 return if lineage.latest_hash.is_empty() {
135 Self::Local
136 } else {
137 Self::Diverged
138 };
139 }
140 let behind = lineage.base_hash != lineage.latest_hash;
141 let ahead = lineage.base_hash != lineage.current_hash().unwrap_or_default();
142 match (ahead, behind) {
143 (false, false) => Self::UpToDate,
144 (false, true) => Self::Behind,
145 (true, false) => Self::Ahead,
146 (true, true) => Self::Diverged,
147 }
148 }
149}
150
151impl PackageLineage {
152 pub fn remote(&self) -> Res<&ManifestUri> {
154 self.remote_uri
155 .as_ref()
156 .ok_or(Error::Lineage(LineageError::NoRemote))
157 }
158
159 pub fn remote_mut(&mut self) -> Res<&mut ManifestUri> {
162 self.remote_uri
163 .as_mut()
164 .ok_or(Error::Lineage(LineageError::NoRemote))
165 }
166
167 pub fn from_remote(remote: ManifestUri, latest_hash: String) -> Self {
168 Self {
169 base_hash: remote.hash.clone(),
170 remote_uri: Some(remote),
171 latest_hash,
172 commit: None,
173 paths: BTreeMap::new(),
174 }
175 }
176
177 pub fn current_hash(&self) -> Option<&str> {
178 self.commit
179 .as_ref()
180 .map(|c| c.hash.as_str())
181 .or(self.remote_uri.as_ref().map(|r| r.hash.as_str()))
182 .or(if self.base_hash.is_empty() {
183 None
184 } else {
185 Some(self.base_hash.as_str())
186 })
187 }
188
189 pub fn update_latest(&mut self, manifest_uri: ManifestUri) {
190 let new_latest_hash = manifest_uri.hash;
191 self.latest_hash.clone_from(&new_latest_hash);
192 self.base_hash.clone_from(&new_latest_hash);
193 }
194}
195
196impl From<ManifestUri> for PackageLineage {
197 fn from(uri: ManifestUri) -> Self {
198 Self {
199 base_hash: uri.hash.clone(),
200 remote_uri: Some(uri.clone()),
201 latest_hash: uri.hash.clone(),
202 commit: None,
203 paths: BTreeMap::new(),
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_default_is_local() {
214 assert_eq!(
215 UpstreamState::from(PackageLineage::default()),
216 UpstreamState::Local
217 );
218 }
219
220 #[test]
221 fn test_remote_configured_but_never_pushed_is_local() {
222 let lineage = PackageLineage {
223 remote_uri: Some(ManifestUri {
224 hash: String::new(),
225 bucket: "test-bucket".to_string(),
226 namespace: ("foo", "bar").into(),
227 ..ManifestUri::default()
228 }),
229 ..PackageLineage::default()
230 };
231 assert_eq!(UpstreamState::from(lineage), UpstreamState::Local);
232 }
233
234 #[test]
235 fn test_empty_bucket_is_local_even_with_latest_hash() {
236 let lineage = PackageLineage {
243 remote_uri: Some(ManifestUri {
244 hash: String::new(),
245 bucket: String::new(),
246 namespace: ("foo", "bar").into(),
247 origin: Some("catalog.dev".parse().unwrap()),
248 }),
249 latest_hash: "abc".to_string(),
250 ..PackageLineage::default()
251 };
252 assert_eq!(UpstreamState::from(lineage), UpstreamState::Local);
253 }
254
255 #[test]
256 fn test_foreign_remote_with_latest_is_diverged() {
257 let lineage = PackageLineage {
258 remote_uri: Some(ManifestUri {
259 hash: String::new(),
260 bucket: "test-bucket".to_string(),
261 namespace: ("foo", "bar").into(),
262 origin: Some("catalog.dev".parse().unwrap()),
263 }),
264 latest_hash: "abc".to_string(),
265 ..PackageLineage::default()
266 };
267 assert_eq!(UpstreamState::from(lineage), UpstreamState::Diverged);
268 }
269
270 #[test]
271 fn test_remote_returns_no_remote_error() {
272 let lineage = PackageLineage::default();
273 assert!(matches!(
274 lineage.remote(),
275 Err(Error::Lineage(LineageError::NoRemote))
276 ));
277 }
278
279 #[test]
280 fn test_current_hash_without_remote() {
281 let lineage = PackageLineage::default();
282 assert_eq!(lineage.current_hash(), None);
283 }
284}