use crate::store::{Store, StoreError};
use lex_vcs::{OpId, OpLog, StageTransition};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
pub const DEFAULT_BRANCH: &str = "main";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Branch {
pub name: String,
pub parent: Option<String>,
#[serde(default)]
pub head_op: Option<OpId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub predicate: Option<serde_json::Value>,
#[serde(default)]
pub merges: Vec<MergeRecord>,
pub created_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MergeRecord {
pub src: String,
pub at: u64,
pub merged: usize,
pub conflicts: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct MergeReport {
pub summary: MergeSummary,
pub merged: Vec<MergeEntry>,
pub conflicts: Vec<MergeConflict>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct MergeSummary {
pub total_sigs: usize,
pub clean: usize,
pub conflicts: usize,
pub base: Option<String>,
#[serde(default)]
pub src: String,
#[serde(default)]
pub dst: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct MergeEntry {
pub sig_id: String,
pub stage_id: String,
pub from: &'static str, }
#[derive(Debug, Clone, Serialize)]
pub struct MergeConflict {
pub sig_id: String,
pub kind: &'static str,
pub base: Option<String>,
pub src: Option<String>,
pub dst: Option<String>,
}
impl Store {
fn branches_dir(&self) -> PathBuf { self.root().join("branches") }
fn branch_path(&self, name: &str) -> PathBuf {
self.branches_dir().join(format!("{name}.json"))
}
fn current_branch_path(&self) -> PathBuf {
self.root().join("current_branch")
}
pub fn current_branch(&self) -> String {
match fs::read_to_string(self.current_branch_path()) {
Ok(s) => s.trim().to_string(),
Err(_) => DEFAULT_BRANCH.to_string(),
}
}
pub fn set_current_branch(&self, name: &str) -> Result<(), StoreError> {
if name != DEFAULT_BRANCH && self.get_branch(name)?.is_none() {
return Err(StoreError::UnknownBranch(name.into()));
}
fs::write(self.current_branch_path(), name)?;
Ok(())
}
pub fn list_branches(&self) -> Result<Vec<String>, StoreError> {
let mut out: Vec<String> = vec![DEFAULT_BRANCH.into()];
let dir = self.branches_dir();
if !dir.exists() { return Ok(out); }
for entry in fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|e| e == "json") {
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
if name != DEFAULT_BRANCH { out.push(name.to_string()); }
}
}
}
out.sort();
Ok(out)
}
pub fn get_branch(&self, name: &str) -> Result<Option<Branch>, StoreError> {
let path = self.branch_path(name);
if !path.exists() { return Ok(None); }
let raw = fs::read_to_string(&path)?;
let b: Branch = serde_json::from_str(&raw)?;
Ok(Some(b))
}
pub fn branch_head(&self, name: &str) -> Result<BTreeMap<String, String>, StoreError> {
let b = match self.get_branch(name)? {
Some(b) => b,
None if name == DEFAULT_BRANCH => return Ok(BTreeMap::new()),
None => return Err(StoreError::UnknownBranch(name.into())),
};
let Some(head) = b.head_op else { return Ok(BTreeMap::new()); };
let log = OpLog::open(self.root())?;
let mut map = BTreeMap::new();
for rec in log.walk_forward(&head, None)? {
apply_transition(&mut map, &rec.produces);
}
Ok(map)
}
pub fn branch_log(&self, name: &str) -> Result<Vec<MergeRecord>, StoreError> {
match self.get_branch(name)? {
Some(b) => Ok(b.merges),
None if name == DEFAULT_BRANCH => Ok(Vec::new()),
None => Err(StoreError::UnknownBranch(name.into())),
}
}
pub fn create_branch(&self, name: &str, from: &str) -> Result<(), StoreError> {
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(StoreError::InvalidTransition(
format!("branch name `{name}` rejected (empty or path-like)")));
}
if self.branch_path(name).exists() {
return Err(StoreError::InvalidTransition(
format!("branch `{name}` already exists")));
}
let head_op = self.get_branch(from)?.and_then(|b| b.head_op);
fs::create_dir_all(self.branches_dir())?;
let b = Branch {
name: name.into(),
parent: Some(from.into()),
head_op,
predicate: None,
merges: Vec::new(),
created_at: now(),
};
fs::write(self.branch_path(name), serde_json::to_string_pretty(&b)?)?;
Ok(())
}
pub fn create_predicate_branch(
&self,
name: &str,
predicate: serde_json::Value,
) -> Result<(), StoreError> {
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(StoreError::InvalidTransition(
format!("branch name `{name}` rejected (empty or path-like)")));
}
if self.branch_path(name).exists() {
return Err(StoreError::InvalidTransition(
format!("branch `{name}` already exists")));
}
fs::create_dir_all(self.branches_dir())?;
let b = Branch {
name: name.into(),
parent: None,
head_op: None,
predicate: Some(predicate),
merges: Vec::new(),
created_at: now(),
};
fs::write(self.branch_path(name), serde_json::to_string_pretty(&b)?)?;
Ok(())
}
pub fn delete_branch(&self, name: &str) -> Result<(), StoreError> {
if name == DEFAULT_BRANCH {
return Err(StoreError::InvalidTransition(
"cannot delete the default branch".into()));
}
if self.current_branch() == name {
return Err(StoreError::InvalidTransition(format!(
"cannot delete `{name}`; check out another branch first")));
}
let path = self.branch_path(name);
if !path.exists() {
return Err(StoreError::UnknownBranch(name.into()));
}
fs::remove_file(path)?;
Ok(())
}
pub(crate) fn set_branch_head_op(
&self,
name: &str,
head_op: OpId,
) -> Result<(), StoreError> {
let mut b = match self.get_branch(name)? {
Some(b) => b,
None if name == DEFAULT_BRANCH => Branch {
name: DEFAULT_BRANCH.into(),
parent: None,
head_op: None,
predicate: None,
merges: Vec::new(),
created_at: now(),
},
None => return Err(StoreError::UnknownBranch(name.into())),
};
b.head_op = Some(head_op);
fs::create_dir_all(self.branches_dir())?;
write_branch_atomic(&self.branch_path(name), &b)?;
Ok(())
}
}
fn apply_transition(map: &mut BTreeMap<String, String>, t: &StageTransition) {
match t {
StageTransition::Create { sig_id, stage_id }
| StageTransition::Replace { sig_id, to: stage_id, .. } => {
map.insert(sig_id.clone(), stage_id.clone());
}
StageTransition::Remove { sig_id, .. } => {
map.remove(sig_id);
}
StageTransition::Rename { from, to, body_stage_id } => {
map.remove(from);
map.insert(to.clone(), body_stage_id.clone());
}
StageTransition::ImportOnly => {}
StageTransition::Merge { entries } => {
for (sig, stage) in entries {
match stage {
Some(s) => { map.insert(sig.clone(), s.clone()); }
None => { map.remove(sig); }
}
}
}
}
}
fn write_branch_atomic(path: &std::path::Path, b: &Branch) -> Result<(), StoreError> {
use std::io::Write;
let bytes = serde_json::to_vec_pretty(b)?;
let tmp = path.with_extension("json.tmp");
let mut f = fs::File::create(&tmp)?;
f.write_all(&bytes)?;
f.sync_all()?;
fs::rename(&tmp, path)?;
Ok(())
}
fn now() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
impl Store {
pub fn merge(&self, src: &str, dst: &str) -> Result<MergeReport, StoreError> {
let log = OpLog::open(self.root())?;
let src_head = self.get_branch(src)?.and_then(|b| b.head_op);
let dst_head = match self.get_branch(dst)? {
Some(b) => b.head_op,
None if dst == DEFAULT_BRANCH => None,
None => return Err(StoreError::UnknownBranch(dst.into())),
};
let out = lex_vcs::merge(&log, src_head.as_ref(), dst_head.as_ref())?;
let mut report = MergeReport {
summary: MergeSummary {
base: out.lca.clone(),
src: src.into(),
dst: dst.into(),
..Default::default()
},
merged: Vec::new(),
conflicts: Vec::new(),
};
for o in out.outcomes {
match o {
lex_vcs::MergeOutcome::Both { sig_id, stage_id } => {
if let Some(stage_id) = stage_id {
report.merged.push(MergeEntry { sig_id, stage_id, from: "both" });
}
}
lex_vcs::MergeOutcome::Src { sig_id, stage_id } => {
if let Some(stage_id) = stage_id {
report.merged.push(MergeEntry { sig_id, stage_id, from: "src" });
}
}
lex_vcs::MergeOutcome::Dst { sig_id, stage_id } => {
if let Some(stage_id) = stage_id {
report.merged.push(MergeEntry { sig_id, stage_id, from: "dst" });
}
}
lex_vcs::MergeOutcome::Conflict { sig_id, kind, base, src, dst } => {
let kind: &'static str = match kind {
lex_vcs::ConflictKind::ModifyModify => "modify-modify",
lex_vcs::ConflictKind::ModifyDelete => "modify-delete",
lex_vcs::ConflictKind::DeleteModify => "delete-modify",
lex_vcs::ConflictKind::AddAdd => "add-add",
};
report.conflicts.push(MergeConflict {
sig_id, kind, base, src, dst,
});
}
}
}
report.summary.clean = report.merged.len();
report.summary.conflicts = report.conflicts.len();
report.summary.total_sigs = report.merged.len() + report.conflicts.len();
Ok(report)
}
pub fn commit_merge(&self, dst: &str, report: &MergeReport) -> Result<(), StoreError> {
if !report.conflicts.is_empty() {
return Err(StoreError::InvalidTransition(format!(
"{} conflicts; resolve before committing", report.conflicts.len())));
}
let dst_head_map = self.branch_head(dst)?;
let mut entries: BTreeMap<String, Option<String>> = BTreeMap::new();
for m in &report.merged {
let cur = dst_head_map.get(&m.sig_id);
if cur != Some(&m.stage_id) {
entries.insert(m.sig_id.clone(), Some(m.stage_id.clone()));
}
}
let src_head = self.get_branch(&report.summary.src)?.and_then(|b| b.head_op);
let dst_head_op = self.get_branch(dst)?.and_then(|b| b.head_op);
match (src_head.clone(), dst_head_op.clone()) {
(Some(s), None) => {
self.set_branch_head_op(dst, s)?;
}
(Some(s), Some(d)) if s == d => { }
(Some(s), Some(d)) => {
let op = lex_vcs::Operation::new(
lex_vcs::OperationKind::Merge { resolved: entries.len() },
[s, d],
);
let t = lex_vcs::StageTransition::Merge { entries };
let _ = self.apply_operation(dst, op, t)?;
}
(None, _) => { }
}
let mut b = self.get_branch(dst)?
.ok_or_else(|| StoreError::UnknownBranch(dst.into()))?;
if !report.summary.src.is_empty() {
b.merges.push(MergeRecord {
src: report.summary.src.clone(),
at: now(),
merged: report.merged.len(),
conflicts: 0,
});
write_branch_atomic(&self.branch_path(dst), &b)?;
}
Ok(())
}
}