1use crate::store::{Store, StoreError};
10use lex_vcs::{OpId, OpLog, StageTransition};
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13use std::fs;
14use std::path::PathBuf;
15
16pub const DEFAULT_BRANCH: &str = "main";
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct Branch {
20 pub name: String,
21 pub parent: Option<String>,
22 #[serde(default)]
26 pub head_op: Option<OpId>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub predicate: Option<serde_json::Value>,
33 #[serde(default)]
35 pub merges: Vec<MergeRecord>,
36 pub created_at: u64,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40pub struct MergeRecord {
41 pub src: String,
42 pub at: u64,
43 pub merged: usize,
44 pub conflicts: usize,
45}
46
47#[derive(Debug, Clone, Serialize)]
48pub struct MergeReport {
49 pub summary: MergeSummary,
50 pub merged: Vec<MergeEntry>,
51 pub conflicts: Vec<MergeConflict>,
52}
53
54#[derive(Debug, Clone, Serialize, Default)]
55pub struct MergeSummary {
56 pub total_sigs: usize,
57 pub clean: usize,
58 pub conflicts: usize,
59 pub base: Option<String>,
60 #[serde(default)]
61 pub src: String,
62 #[serde(default)]
63 pub dst: String,
64}
65
66#[derive(Debug, Clone, Serialize)]
67pub struct MergeEntry {
68 pub sig_id: String,
69 pub stage_id: String,
70 pub from: &'static str, }
72
73#[derive(Debug, Clone, Serialize)]
74pub struct MergeConflict {
75 pub sig_id: String,
76 pub kind: &'static str,
77 pub base: Option<String>,
78 pub src: Option<String>,
79 pub dst: Option<String>,
80}
81
82impl Store {
83 fn branches_dir(&self) -> PathBuf { self.root().join("branches") }
84 fn branch_path(&self, name: &str) -> PathBuf {
85 self.branches_dir().join(format!("{name}.json"))
86 }
87 fn current_branch_path(&self) -> PathBuf {
88 self.root().join("current_branch")
89 }
90
91 pub fn current_branch(&self) -> String {
92 match fs::read_to_string(self.current_branch_path()) {
93 Ok(s) => s.trim().to_string(),
94 Err(_) => DEFAULT_BRANCH.to_string(),
95 }
96 }
97
98 pub fn set_current_branch(&self, name: &str) -> Result<(), StoreError> {
99 if name != DEFAULT_BRANCH && self.get_branch(name)?.is_none() {
100 return Err(StoreError::UnknownBranch(name.into()));
101 }
102 fs::write(self.current_branch_path(), name)?;
103 Ok(())
104 }
105
106 pub fn list_branches(&self) -> Result<Vec<String>, StoreError> {
107 let mut out: Vec<String> = vec![DEFAULT_BRANCH.into()];
108 let dir = self.branches_dir();
109 if !dir.exists() { return Ok(out); }
110 for entry in fs::read_dir(&dir)? {
111 let entry = entry?;
112 let path = entry.path();
113 if path.extension().is_some_and(|e| e == "json") {
114 if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
115 if name != DEFAULT_BRANCH { out.push(name.to_string()); }
116 }
117 }
118 }
119 out.sort();
120 Ok(out)
121 }
122
123 pub fn get_branch(&self, name: &str) -> Result<Option<Branch>, StoreError> {
124 let path = self.branch_path(name);
125 if !path.exists() { return Ok(None); }
126 let raw = fs::read_to_string(&path)?;
127 let b: Branch = serde_json::from_str(&raw)?;
128 Ok(Some(b))
129 }
130
131 pub fn branch_head(&self, name: &str) -> Result<BTreeMap<String, String>, StoreError> {
145 let b = match self.get_branch(name)? {
146 Some(b) => b,
147 None if name == DEFAULT_BRANCH => return Ok(BTreeMap::new()),
148 None => return Err(StoreError::UnknownBranch(name.into())),
149 };
150 let Some(head) = b.head_op else { return Ok(BTreeMap::new()); };
151 let log = OpLog::open(self.root())?;
152 let mut map = BTreeMap::new();
153 for rec in log.walk_forward(&head, None)? {
154 apply_transition(&mut map, &rec.produces);
155 }
156 Ok(map)
157 }
158
159 pub fn branch_log(&self, name: &str) -> Result<Vec<MergeRecord>, StoreError> {
160 match self.get_branch(name)? {
161 Some(b) => Ok(b.merges),
162 None if name == DEFAULT_BRANCH => Ok(Vec::new()),
163 None => Err(StoreError::UnknownBranch(name.into())),
164 }
165 }
166
167 pub fn create_branch(&self, name: &str, from: &str) -> Result<(), StoreError> {
169 if name.is_empty() || name.contains('/') || name.contains('\\') {
170 return Err(StoreError::InvalidTransition(
171 format!("branch name `{name}` rejected (empty or path-like)")));
172 }
173 if self.branch_path(name).exists() {
174 return Err(StoreError::InvalidTransition(
175 format!("branch `{name}` already exists")));
176 }
177 let head_op = self.get_branch(from)?.and_then(|b| b.head_op);
178 fs::create_dir_all(self.branches_dir())?;
179 let b = Branch {
180 name: name.into(),
181 parent: Some(from.into()),
182 head_op,
183 predicate: None,
184 merges: Vec::new(),
185 created_at: now(),
186 };
187 fs::write(self.branch_path(name), serde_json::to_string_pretty(&b)?)?;
188 Ok(())
189 }
190
191 pub fn create_predicate_branch(
197 &self,
198 name: &str,
199 predicate: serde_json::Value,
200 ) -> Result<(), StoreError> {
201 if name.is_empty() || name.contains('/') || name.contains('\\') {
202 return Err(StoreError::InvalidTransition(
203 format!("branch name `{name}` rejected (empty or path-like)")));
204 }
205 if self.branch_path(name).exists() {
206 return Err(StoreError::InvalidTransition(
207 format!("branch `{name}` already exists")));
208 }
209 fs::create_dir_all(self.branches_dir())?;
210 let b = Branch {
211 name: name.into(),
212 parent: None,
213 head_op: None,
214 predicate: Some(predicate),
215 merges: Vec::new(),
216 created_at: now(),
217 };
218 fs::write(self.branch_path(name), serde_json::to_string_pretty(&b)?)?;
219 Ok(())
220 }
221
222 pub fn delete_branch(&self, name: &str) -> Result<(), StoreError> {
223 if name == DEFAULT_BRANCH {
224 return Err(StoreError::InvalidTransition(
225 "cannot delete the default branch".into()));
226 }
227 if self.current_branch() == name {
228 return Err(StoreError::InvalidTransition(format!(
229 "cannot delete `{name}`; check out another branch first")));
230 }
231 let path = self.branch_path(name);
232 if !path.exists() {
233 return Err(StoreError::UnknownBranch(name.into()));
234 }
235 fs::remove_file(path)?;
236 Ok(())
237 }
238
239 pub(crate) fn set_branch_head_op(
261 &self,
262 name: &str,
263 head_op: OpId,
264 ) -> Result<(), StoreError> {
265 let mut b = match self.get_branch(name)? {
266 Some(b) => b,
267 None if name == DEFAULT_BRANCH => Branch {
268 name: DEFAULT_BRANCH.into(),
269 parent: None,
270 head_op: None,
271 predicate: None,
272 merges: Vec::new(),
273 created_at: now(),
274 },
275 None => return Err(StoreError::UnknownBranch(name.into())),
276 };
277 b.head_op = Some(head_op);
278 fs::create_dir_all(self.branches_dir())?;
279 write_branch_atomic(&self.branch_path(name), &b)?;
280 Ok(())
281 }
282}
283
284fn apply_transition(map: &mut BTreeMap<String, String>, t: &StageTransition) {
287 match t {
288 StageTransition::Create { sig_id, stage_id }
289 | StageTransition::Replace { sig_id, to: stage_id, .. } => {
290 map.insert(sig_id.clone(), stage_id.clone());
291 }
292 StageTransition::Remove { sig_id, .. } => {
293 map.remove(sig_id);
294 }
295 StageTransition::Rename { from, to, body_stage_id } => {
296 map.remove(from);
297 map.insert(to.clone(), body_stage_id.clone());
298 }
299 StageTransition::ImportOnly => {}
300 StageTransition::Merge { entries } => {
301 for (sig, stage) in entries {
302 match stage {
303 Some(s) => { map.insert(sig.clone(), s.clone()); }
304 None => { map.remove(sig); }
305 }
306 }
307 }
308 }
309}
310
311fn write_branch_atomic(path: &std::path::Path, b: &Branch) -> Result<(), StoreError> {
312 use std::io::Write;
313 let bytes = serde_json::to_vec_pretty(b)?;
314 let tmp = path.with_extension("json.tmp");
315 let mut f = fs::File::create(&tmp)?;
316 f.write_all(&bytes)?;
317 f.sync_all()?;
318 fs::rename(&tmp, path)?;
319 Ok(())
320}
321
322fn now() -> u64 {
323 use std::time::{SystemTime, UNIX_EPOCH};
324 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
325}
326
327impl Store {
328 pub fn merge(&self, src: &str, dst: &str) -> Result<MergeReport, StoreError> {
329 let log = OpLog::open(self.root())?;
330 let src_head = self.get_branch(src)?.and_then(|b| b.head_op);
331 let dst_head = match self.get_branch(dst)? {
332 Some(b) => b.head_op,
333 None if dst == DEFAULT_BRANCH => None,
334 None => return Err(StoreError::UnknownBranch(dst.into())),
335 };
336 let out = lex_vcs::merge(&log, src_head.as_ref(), dst_head.as_ref())?;
337
338 let mut report = MergeReport {
339 summary: MergeSummary {
340 base: out.lca.clone(),
341 src: src.into(),
342 dst: dst.into(),
343 ..Default::default()
344 },
345 merged: Vec::new(),
346 conflicts: Vec::new(),
347 };
348 for o in out.outcomes {
349 match o {
350 lex_vcs::MergeOutcome::Both { sig_id, stage_id } => {
351 if let Some(stage_id) = stage_id {
352 report.merged.push(MergeEntry { sig_id, stage_id, from: "both" });
353 }
354 }
355 lex_vcs::MergeOutcome::Src { sig_id, stage_id } => {
356 if let Some(stage_id) = stage_id {
357 report.merged.push(MergeEntry { sig_id, stage_id, from: "src" });
358 }
359 }
360 lex_vcs::MergeOutcome::Dst { sig_id, stage_id } => {
361 if let Some(stage_id) = stage_id {
362 report.merged.push(MergeEntry { sig_id, stage_id, from: "dst" });
363 }
364 }
365 lex_vcs::MergeOutcome::Conflict { sig_id, kind, base, src, dst } => {
366 let kind: &'static str = match kind {
367 lex_vcs::ConflictKind::ModifyModify => "modify-modify",
368 lex_vcs::ConflictKind::ModifyDelete => "modify-delete",
369 lex_vcs::ConflictKind::DeleteModify => "delete-modify",
370 lex_vcs::ConflictKind::AddAdd => "add-add",
371 };
372 report.conflicts.push(MergeConflict {
373 sig_id, kind, base, src, dst,
374 });
375 }
376 }
377 }
378 report.summary.clean = report.merged.len();
379 report.summary.conflicts = report.conflicts.len();
380 report.summary.total_sigs = report.merged.len() + report.conflicts.len();
381 Ok(report)
382 }
383
384 pub fn commit_merge(&self, dst: &str, report: &MergeReport) -> Result<(), StoreError> {
385 if !report.conflicts.is_empty() {
386 return Err(StoreError::InvalidTransition(format!(
387 "{} conflicts; resolve before committing", report.conflicts.len())));
388 }
389 let dst_head_map = self.branch_head(dst)?;
390 let mut entries: BTreeMap<String, Option<String>> = BTreeMap::new();
391 for m in &report.merged {
392 let cur = dst_head_map.get(&m.sig_id);
393 if cur != Some(&m.stage_id) {
394 entries.insert(m.sig_id.clone(), Some(m.stage_id.clone()));
395 }
396 }
397 let src_head = self.get_branch(&report.summary.src)?.and_then(|b| b.head_op);
398 let dst_head_op = self.get_branch(dst)?.and_then(|b| b.head_op);
399
400 match (src_head.clone(), dst_head_op.clone()) {
401 (Some(s), None) => {
403 self.set_branch_head_op(dst, s)?;
404 }
405 (Some(s), Some(d)) if s == d => { }
408 (Some(s), Some(d)) => {
409 let op = lex_vcs::Operation::new(
410 lex_vcs::OperationKind::Merge { resolved: entries.len() },
411 [s, d],
412 );
413 let t = lex_vcs::StageTransition::Merge { entries };
414 let _ = self.apply_operation(dst, op, t)?;
415 }
416 (None, _) => { }
418 }
419
420 let mut b = self.get_branch(dst)?
433 .ok_or_else(|| StoreError::UnknownBranch(dst.into()))?;
434 if !report.summary.src.is_empty() {
435 b.merges.push(MergeRecord {
436 src: report.summary.src.clone(),
437 at: now(),
438 merged: report.merged.len(),
439 conflicts: 0,
440 });
441 write_branch_atomic(&self.branch_path(dst), &b)?;
442 }
443 Ok(())
444 }
445}