mlua_swarm/blueprint/store/
git2_store.rs1use super::git2_blob_store::{extract_msg_field, Git2BlobStore, HeadCommit};
10use super::types::*;
11use super::{blueprint_version, canonical_yaml, BlueprintStore};
12use crate::blueprint::Blueprint;
13use async_trait::async_trait;
14use std::path::Path;
15
16pub struct Git2BlueprintStore {
20 backend: Git2BlobStore,
21}
22
23impl Git2BlueprintStore {
24 pub fn open_or_init(root: impl AsRef<Path>) -> Result<Self, BlueprintStoreError> {
27 Ok(Self {
28 backend: Git2BlobStore::open_or_init(root, "blueprint.yaml")?,
29 })
30 }
31
32 pub fn root(&self) -> &Path {
34 self.backend.root()
35 }
36}
37
38fn build_commit_msg(
39 id: &BlueprintId,
40 version: BlueprintVersion,
41 metadata: &CommitMetadata,
42) -> String {
43 format!(
44 "blueprint update [{id}]\n\n\
45 blueprint_content_hash: {hash}\n\
46 patch_content_hash: {patch}\n\
47 epoch_blueprint_id: {epoch_id}\n\
48 epoch_start_version: {epoch_v}\n\
49 epoch_started_at_ms: {epoch_ts}\n\
50 rationale: {rationale}\n",
51 id = id,
52 hash = version,
53 patch = metadata.patch_hash,
54 epoch_id = metadata.epoch_id.blueprint_id,
55 epoch_v = metadata.epoch_id.start_version,
56 epoch_ts = metadata.epoch_id.started_at_ms,
57 rationale = metadata.rationale,
58 )
59}
60
61fn head_to_traced(
62 id: &BlueprintId,
63 head: HeadCommit,
64) -> Result<Traced<Blueprint>, BlueprintStoreError> {
65 let bp: Blueprint = serde_yaml::from_str(&head.yaml)?;
66 let version = parse_version(&head.commit_msg)
67 .ok_or_else(|| BlueprintStoreError::Other("version not found in commit msg".to_string()))?;
68 let _ = id;
69 let trace = Trace::new(
70 TraceOrigin::Git {
71 commit_hash: head.commit_hash_hex,
72 },
73 version,
74 head.ts_ms,
75 );
76 Ok(Traced::new(bp, trace))
77}
78
79fn parse_version(msg: &str) -> Option<BlueprintVersion> {
80 extract_msg_field(msg, "blueprint_content_hash")
81 .and_then(|hex| ContentHash::from_hex(&hex).ok())
82 .map(BlueprintVersion)
83}
84
85#[async_trait]
86impl BlueprintStore for Git2BlueprintStore {
87 fn name(&self) -> &str {
88 "git2"
89 }
90
91 async fn read_head(&self, id: &BlueprintId) -> Result<Traced<Blueprint>, BlueprintStoreError> {
92 if self.backend.is_archived(id.as_str())? {
93 return Err(BlueprintStoreError::Archived(id.clone()));
94 }
95 let head = self.backend.read_head(id.as_str(), id.clone())?;
96 head_to_traced(id, head)
97 }
98
99 async fn write_new(
100 &self,
101 id: &BlueprintId,
102 new_bp: &Blueprint,
103 parents: &[BlueprintVersion],
104 metadata: CommitMetadata,
105 ) -> Result<BlueprintVersion, BlueprintStoreError> {
106 if self.backend.is_archived(id.as_str())? {
107 return Err(BlueprintStoreError::Archived(id.clone()));
108 }
109 let yaml = canonical_yaml(new_bp)?;
110 let version = blueprint_version(new_bp)?;
111 let msg = build_commit_msg(id, version, &metadata);
112 self.backend
113 .try_write_blob_commit(id.as_str(), &yaml, &msg)?;
114 let _ = parents;
115 Ok(version)
116 }
117
118 async fn read_version(
119 &self,
120 id: &BlueprintId,
121 version: BlueprintVersion,
122 ) -> Result<Traced<Blueprint>, BlueprintStoreError> {
123 let match_line = format!("blueprint_content_hash: {}", version.to_hex());
124 let head = self
125 .backend
126 .find_commit_by_msg(id.as_str(), &match_line, id.clone())
127 .map_err(|e| match e {
128 BlueprintStoreError::Other(_) => BlueprintStoreError::NotFound {
129 id: id.clone(),
130 version,
131 },
132 other => other,
133 })?;
134 head_to_traced(id, head)
135 }
136
137 async fn history(
138 &self,
139 id: &BlueprintId,
140 limit: usize,
141 ) -> Result<Vec<BlueprintVersion>, BlueprintStoreError> {
142 let msgs = self.backend.history_msgs(id.as_str(), limit)?;
143 Ok(msgs.iter().filter_map(|m| parse_version(m)).collect())
144 }
145
146 async fn read_commit_rationale(
147 &self,
148 id: &BlueprintId,
149 version: BlueprintVersion,
150 ) -> Result<Option<String>, BlueprintStoreError> {
151 let match_line = format!("blueprint_content_hash: {}", version.to_hex());
152 match self
153 .backend
154 .find_commit_by_msg(id.as_str(), &match_line, id.clone())
155 {
156 Ok(head) => Ok(extract_msg_field(&head.commit_msg, "rationale")),
157 Err(BlueprintStoreError::HeadEmpty(_)) | Err(BlueprintStoreError::Other(_)) => Ok(None),
158 Err(e) => Err(e),
159 }
160 }
161
162 async fn list_ids(&self) -> Result<Vec<BlueprintId>, BlueprintStoreError> {
163 Ok(self
164 .backend
165 .list_ids(false)?
166 .into_iter()
167 .map(BlueprintId::new)
168 .collect())
169 }
170
171 async fn archive_id(&self, id: &BlueprintId) -> Result<(), BlueprintStoreError> {
172 let ts_ms = std::time::SystemTime::now()
173 .duration_since(std::time::UNIX_EPOCH)
174 .map(|d| d.as_millis() as i64)
175 .unwrap_or(0);
176 self.backend.write_archive_marker(id.as_str(), true, ts_ms)
177 }
178
179 async fn unarchive_id(&self, id: &BlueprintId) -> Result<(), BlueprintStoreError> {
180 let ts_ms = std::time::SystemTime::now()
181 .duration_since(std::time::UNIX_EPOCH)
182 .map(|d| d.as_millis() as i64)
183 .unwrap_or(0);
184 self.backend.write_archive_marker(id.as_str(), false, ts_ms)
185 }
186
187 async fn is_archived(&self, id: &BlueprintId) -> Result<bool, BlueprintStoreError> {
188 self.backend.is_archived(id.as_str())
189 }
190}