Skip to main content

link_cli/version_control/
mod.rs

1//! Optional version-control layer for the Rust link-cli.
2//!
3//! Mirrors the C# `VersionControlDecorator` in
4//! `csharp/Foundation.Data.Doublets.Cli.Library/VersionControlDecorator.cs`.
5//!
6//! Sits above the [`TransactionsDecorator`]
7//! and adds *time travel* ([`checkout`](VersionControlDecorator::checkout)),
8//! *branching* ([`branch`](VersionControlDecorator::branch),
9//! [`switch_branch`](VersionControlDecorator::switch_branch)), and
10//! *tagging* ([`tag`](VersionControlDecorator::tag)) over the transitions
11//! log. Optional — when not instantiated the underlying transactions
12//! decorator behaves identically (R17).
13
14use std::collections::{BTreeMap, HashMap, HashSet};
15use std::path::{Path, PathBuf};
16
17use anyhow::{bail, Result};
18
19use crate::link::Link;
20use crate::link_storage::ChangeObserver;
21use crate::named_types::{NamedTypes, NamedTypesDecorator};
22use crate::transactions::{TransactionHandle, TransactionsDecorator, Transition};
23
24/// Default name of the initial branch (analogous to git's `main`).
25pub const DEFAULT_BRANCH_NAME: &str = "main";
26
27const BRANCH_PREFIX: &str = "__vc:branch:";
28const TAG_PREFIX: &str = "__vc:tag:";
29const CURRENT_PREFIX: &str = "__vc:current=";
30const APPLIED_PREFIX: &str = "__vc:applied=";
31const TRANSITION_PREFIX: &str = "__vc:trans:";
32
33/// Metadata describing one branch in the version-control DAG.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct BranchInfo {
36    pub name: String,
37    pub parent: Option<String>,
38    pub fork_seq: i64,
39    pub head: i64,
40}
41
42impl BranchInfo {
43    pub fn new(name: String, parent: Option<String>, fork_seq: i64, head: i64) -> Self {
44        Self {
45            name,
46            parent,
47            fork_seq,
48            head,
49        }
50    }
51}
52
53/// Decorator that adds *time travel*, *branching*, and *tagging* over the
54/// transitions log produced by a [`TransactionsDecorator`].
55pub struct VersionControlDecorator {
56    transactions: TransactionsDecorator,
57    branches_store: NamedTypesDecorator,
58    branches: HashMap<String, BranchInfo>,
59    tags: BTreeMap<String, i64>,
60    transition_branches: BTreeMap<i64, String>,
61    branch_links: HashMap<String, u32>,
62    tag_links: HashMap<String, u32>,
63    current_branch_link: u32,
64    applied_link: u32,
65    current_branch: String,
66    current_applied: i64,
67    active_transaction: Option<VersionControlTransactionState>,
68    trace: bool,
69}
70
71#[derive(Debug, Clone)]
72struct VersionControlTransactionState {
73    branch_name: String,
74    before_sequence: i64,
75}
76
77impl VersionControlDecorator {
78    pub fn new(
79        transactions: TransactionsDecorator,
80        branches_store: NamedTypesDecorator,
81        trace: bool,
82    ) -> Result<Self> {
83        let mut decorator = Self {
84            transactions,
85            branches_store,
86            branches: HashMap::new(),
87            tags: BTreeMap::new(),
88            transition_branches: BTreeMap::new(),
89            branch_links: HashMap::new(),
90            tag_links: HashMap::new(),
91            current_branch_link: 0,
92            applied_link: 0,
93            current_branch: DEFAULT_BRANCH_NAME.to_string(),
94            current_applied: 0,
95            active_transaction: None,
96            trace,
97        };
98        decorator.recover()?;
99        decorator.ensure_default_branch()?;
100        Ok(decorator)
101    }
102
103    /// Conventional sidecar filename for the version-control store.
104    pub fn make_version_control_database_filename<P: AsRef<Path>>(p: P) -> PathBuf {
105        let path = p.as_ref();
106        let stem = path
107            .file_stem()
108            .and_then(|s| s.to_str())
109            .unwrap_or_default();
110        let name = format!("{stem}.versioncontrol.links");
111        match path.parent() {
112            Some(parent) if !parent.as_os_str().is_empty() => parent.join(name),
113            _ => PathBuf::from(name),
114        }
115    }
116
117    pub fn current_branch(&self) -> &str {
118        &self.current_branch
119    }
120
121    pub fn current_sequence(&self) -> i64 {
122        self.current_applied
123    }
124
125    pub fn list_branches(&self) -> Vec<BranchInfo> {
126        let mut branches: Vec<BranchInfo> = self.branches.values().cloned().collect();
127        branches.sort_by(|a, b| a.name.cmp(&b.name));
128        branches
129    }
130
131    pub fn list_tags(&self) -> BTreeMap<String, i64> {
132        self.tags.clone()
133    }
134
135    pub fn try_get_tag(&self, name: &str) -> Option<i64> {
136        self.tags.get(name).copied()
137    }
138
139    pub fn save(&mut self) -> Result<()> {
140        self.transactions.save()?;
141        self.branches_store.save()?;
142        Ok(())
143    }
144
145    pub fn transactions(&self) -> &TransactionsDecorator {
146        &self.transactions
147    }
148
149    pub fn transactions_mut(&mut self) -> &mut TransactionsDecorator {
150        &mut self.transactions
151    }
152
153    pub fn branches_store(&self) -> &NamedTypesDecorator {
154        &self.branches_store
155    }
156
157    pub fn begin_transaction(&mut self) -> Result<TransactionHandle> {
158        if self.active_transaction.is_some() {
159            bail!("Nested version-control transactions are not supported.");
160        }
161        let before_sequence = self.transactions.last_logged_sequence();
162        let branch_name = self.current_branch.clone();
163        let handle = self.transactions.begin_transaction()?;
164        self.active_transaction = Some(VersionControlTransactionState {
165            branch_name,
166            before_sequence,
167        });
168        Ok(handle)
169    }
170
171    pub fn commit(&mut self) -> Result<()> {
172        let state = self
173            .active_transaction
174            .as_ref()
175            .cloned()
176            .ok_or_else(|| anyhow::anyhow!("No version-control transaction is open."))?;
177        self.transactions.commit()?;
178        self.active_transaction = None;
179        self.attribute_new_transitions_for_branch(state.before_sequence, &state.branch_name)?;
180        Ok(())
181    }
182
183    pub fn rollback(&mut self) -> Result<()> {
184        self.active_transaction
185            .as_ref()
186            .ok_or_else(|| anyhow::anyhow!("No version-control transaction is open."))?;
187        self.transactions.rollback()?;
188        self.active_transaction = None;
189        Ok(())
190    }
191
192    // -- Write API (attribute new transitions to the current branch) ----
193
194    pub fn create(&mut self, source: u32, target: u32) -> Result<u32> {
195        let before_seq = self.transactions.last_logged_sequence();
196        let id = self.transactions.create(source, target)?;
197        if self.active_transaction.is_none() {
198            let branch = self.current_branch.clone();
199            self.attribute_new_transitions_for_branch(before_seq, &branch)?;
200        }
201        Ok(id)
202    }
203
204    pub fn update(&mut self, id: u32, source: u32, target: u32) -> Result<Link> {
205        self.update_observed(id, source, target, &mut |_, _| {})
206    }
207
208    /// [`Self::update`], reporting every change the decorator stack made —
209    /// the deletion of a link merged into its duplicate included.
210    pub fn update_observed(
211        &mut self,
212        id: u32,
213        source: u32,
214        target: u32,
215        observer: ChangeObserver<'_>,
216    ) -> Result<Link> {
217        let before_seq = self.transactions.last_logged_sequence();
218        let result = self
219            .transactions
220            .update_observed(id, source, target, observer)?;
221        if self.active_transaction.is_none() {
222            let branch = self.current_branch.clone();
223            self.attribute_new_transitions_for_branch(before_seq, &branch)?;
224        }
225        Ok(result)
226    }
227
228    pub fn delete(&mut self, id: u32) -> Result<Link> {
229        self.delete_observed(id, &mut |_, _| {})
230    }
231
232    /// [`Self::delete`], reporting every change the decorator stack made —
233    /// cascaded deletions of usages included.
234    pub fn delete_observed(&mut self, id: u32, observer: ChangeObserver<'_>) -> Result<Link> {
235        let before_seq = self.transactions.last_logged_sequence();
236        let result = self.transactions.delete_observed(id, observer)?;
237        if self.active_transaction.is_none() {
238            let branch = self.current_branch.clone();
239            self.attribute_new_transitions_for_branch(before_seq, &branch)?;
240        }
241        Ok(result)
242    }
243
244    pub fn create_and_update(&mut self, source: u32, target: u32) -> Result<u32> {
245        let before_seq = self.transactions.last_logged_sequence();
246        let id = self.transactions.create_and_update(source, target)?;
247        if self.active_transaction.is_none() {
248            let branch = self.current_branch.clone();
249            self.attribute_new_transitions_for_branch(before_seq, &branch)?;
250        }
251        Ok(id)
252    }
253
254    pub fn exists(&self, id: u32) -> bool {
255        self.transactions.exists(id)
256    }
257
258    pub fn get(&self, id: u32) -> Option<&Link> {
259        self.transactions.get(id)
260    }
261
262    pub fn all(&self) -> Vec<&Link> {
263        self.transactions.all()
264    }
265
266    pub fn search(&self, source: u32, target: u32) -> Option<u32> {
267        self.transactions.search(source, target)
268    }
269
270    pub fn get_or_create(&mut self, source: u32, target: u32) -> Result<u32> {
271        if let Some(existing) = self.transactions.search(source, target) {
272            return Ok(existing);
273        }
274        self.create(source, target)
275    }
276
277    pub fn ensure_created(&mut self, id: u32) -> u32 {
278        self.transactions
279            .ensure_created(id)
280            .expect("TransactionsDecorator::ensure_created failed")
281    }
282
283    fn attribute_new_transitions_for_branch(
284        &mut self,
285        before_seq: i64,
286        branch_name: &str,
287    ) -> Result<()> {
288        let after_seq = self.transactions.last_logged_sequence();
289        if after_seq <= before_seq {
290            return Ok(());
291        }
292        for s in (before_seq + 1)..=after_seq {
293            self.transition_branches.insert(s, branch_name.to_string());
294            let marker = format!("{TRANSITION_PREFIX}{s}:branch={branch_name}");
295            self.write_immutable_marker(&marker)?;
296        }
297        if let Some(info) = self.branches.get(branch_name).cloned() {
298            let updated = BranchInfo {
299                head: after_seq,
300                ..info
301            };
302            self.branches
303                .insert(branch_name.to_string(), updated.clone());
304            self.update_branch_link(&updated)?;
305        }
306        if self.current_branch == branch_name {
307            self.current_applied = after_seq;
308            self.set_applied(after_seq)?;
309        }
310        Ok(())
311    }
312
313    // -- Branching ----------------------------------------------------
314
315    pub fn branch(&mut self, name: &str, from: Option<i64>) -> Result<()> {
316        self.ensure_no_open_transaction("branch")?;
317        if name.trim().is_empty() {
318            bail!("Branch name must not be empty.");
319        }
320        if self.branches.contains_key(name) {
321            bail!("Branch '{name}' already exists.");
322        }
323        let parent = self.current_branch.clone();
324        let fork_seq = from.unwrap_or(self.current_applied);
325        if fork_seq < 0 {
326            bail!("Fork point cannot be negative.");
327        }
328        if fork_seq > 0 {
329            let path = self.build_branch_seqs(&parent);
330            if !path.contains(&fork_seq) {
331                bail!("Fork point {fork_seq} is not reachable on branch '{parent}'.",);
332            }
333        }
334        self.create_branch(name, Some(parent), fork_seq, fork_seq)?;
335        self.trace(&format!(
336            "Created branch '{name}' from '{}' at seq {fork_seq}.",
337            self.current_branch
338        ));
339        Ok(())
340    }
341
342    pub fn switch_branch(&mut self, name: &str) -> Result<()> {
343        self.ensure_no_open_transaction("switch_branch")?;
344        if !self.branches.contains_key(name) {
345            bail!("Unknown branch '{name}'.");
346        }
347        let target_path = self.build_branch_seqs(name);
348        self.apply_diff_to(target_path, name)?;
349        self.trace(&format!(
350            "Switched to branch '{name}' at seq {}.",
351            self.current_applied
352        ));
353        Ok(())
354    }
355
356    pub fn checkout(&mut self, sequence: i64) -> Result<()> {
357        self.ensure_no_open_transaction("checkout")?;
358        if sequence < 0 {
359            bail!("Sequence must be non-negative.");
360        }
361        let current = self.current_branch.clone();
362        let path = self.build_branch_seqs(&current);
363        if sequence > 0 && !path.contains(&sequence) {
364            bail!("Sequence {sequence} is not reachable on branch '{current}'.",);
365        }
366        let target_path: Vec<i64> = path.iter().copied().filter(|s| *s <= sequence).collect();
367        self.apply_diff_to(target_path, &current)?;
368        self.trace(&format!(
369            "Checked out seq {sequence} on branch '{current}'.",
370        ));
371        Ok(())
372    }
373
374    pub fn tag(&mut self, name: &str, sequence: Option<i64>) -> Result<()> {
375        self.ensure_no_open_transaction("tag")?;
376        if name.trim().is_empty() {
377            bail!("Tag name must not be empty.");
378        }
379        let seq = sequence.unwrap_or(self.current_applied);
380        if seq < 0 {
381            bail!("Tag sequence must be non-negative.");
382        }
383        self.tags.insert(name.to_string(), seq);
384        self.update_tag_link(name, seq)?;
385        self.trace(&format!("Created tag '{name}' at seq {seq}.",));
386        Ok(())
387    }
388
389    // -- Path / diff helpers -----------------------------------------
390
391    fn apply_diff_to(&mut self, target_path: Vec<i64>, new_branch: &str) -> Result<()> {
392        let current_branch_name = self.current_branch.clone();
393        let current_path: Vec<i64> = self
394            .build_branch_seqs(&current_branch_name)
395            .into_iter()
396            .filter(|s| *s <= self.current_applied)
397            .collect();
398
399        let mut common = 0usize;
400        let max_common = current_path.len().min(target_path.len());
401        while common < max_common && current_path[common] == target_path[common] {
402            common += 1;
403        }
404
405        // Revert transitions that are no longer on the path.
406        let to_revert: Vec<i64> = current_path[common..].iter().rev().copied().collect();
407        for seq in to_revert {
408            if let Some(transition) = self.find_transition(seq) {
409                self.transactions.revert_transition(&transition);
410            }
411        }
412        // Apply transitions that are new on the path.
413        let to_apply: Vec<i64> = target_path[common..].to_vec();
414        for seq in to_apply {
415            if let Some(transition) = self.find_transition(seq) {
416                self.transactions.apply_transition(&transition);
417            }
418        }
419
420        if new_branch != self.current_branch {
421            self.current_branch = new_branch.to_string();
422            self.set_current_branch(new_branch)?;
423        }
424        self.current_applied = target_path.last().copied().unwrap_or(0);
425        self.set_applied(self.current_applied)?;
426        Ok(())
427    }
428
429    fn ensure_no_open_transaction(&self, operation: &str) -> Result<()> {
430        if self.active_transaction.is_some() {
431            bail!("{operation} is not allowed while a version-control transaction is open.");
432        }
433        Ok(())
434    }
435
436    fn build_branch_seqs(&self, branch_name: &str) -> Vec<i64> {
437        let mut visited: HashSet<String> = HashSet::new();
438        self.build_branch_seqs_inner(branch_name, &mut visited)
439    }
440
441    fn build_branch_seqs_inner(
442        &self,
443        branch_name: &str,
444        visited: &mut HashSet<String>,
445    ) -> Vec<i64> {
446        let info = match self.branches.get(branch_name) {
447            Some(info) => info,
448            None => return Vec::new(),
449        };
450        if !visited.insert(branch_name.to_string()) {
451            return Vec::new();
452        }
453        let mut seqs = Vec::new();
454        if let Some(parent_name) = info.parent.as_deref() {
455            if self.branches.contains_key(parent_name) {
456                let mut parent_seqs = self.build_branch_seqs_inner(parent_name, visited);
457                parent_seqs.retain(|s| *s <= info.fork_seq);
458                seqs.extend(parent_seqs);
459            }
460        }
461        let mut own: Vec<i64> = self
462            .transition_branches
463            .iter()
464            .filter(|(s, b)| b.as_str() == branch_name && **s <= info.head)
465            .map(|(s, _)| *s)
466            .collect();
467        own.sort();
468        seqs.extend(own);
469        seqs
470    }
471
472    fn find_transition(&self, sequence: i64) -> Option<Transition> {
473        self.transactions
474            .log()
475            .into_iter()
476            .find(|t| t.sequence == sequence)
477    }
478
479    // -- Persistence helpers -----------------------------------------
480
481    fn ensure_default_branch(&mut self) -> Result<()> {
482        let existing = self.transactions.last_logged_sequence();
483        if !self.branches.contains_key(DEFAULT_BRANCH_NAME) {
484            // Pre-existing transitions are attributed to the default branch.
485            for s in 1..=existing {
486                if let std::collections::btree_map::Entry::Vacant(entry) =
487                    self.transition_branches.entry(s)
488                {
489                    entry.insert(DEFAULT_BRANCH_NAME.to_string());
490                    let marker = format!("{TRANSITION_PREFIX}{s}:branch={DEFAULT_BRANCH_NAME}");
491                    self.write_immutable_marker(&marker)?;
492                }
493            }
494            self.create_branch(DEFAULT_BRANCH_NAME, None, 0, existing)?;
495            self.current_branch = DEFAULT_BRANCH_NAME.to_string();
496            self.current_applied = existing;
497            self.set_current_branch(DEFAULT_BRANCH_NAME)?;
498            self.set_applied(existing)?;
499        } else if self.current_branch_link == 0 {
500            let branch = self.current_branch.clone();
501            self.set_current_branch(&branch)?;
502        }
503        Ok(())
504    }
505
506    fn create_branch(
507        &mut self,
508        name: &str,
509        parent: Option<String>,
510        fork_seq: i64,
511        head: i64,
512    ) -> Result<()> {
513        let info = BranchInfo::new(name.to_string(), parent, fork_seq, head);
514        self.branches.insert(name.to_string(), info.clone());
515        self.update_branch_link(&info)?;
516        Ok(())
517    }
518
519    fn update_branch_link(&mut self, info: &BranchInfo) -> Result<()> {
520        let marker = encode_branch_marker(info);
521        let link = match self.branch_links.get(&info.name).copied() {
522            Some(link) => link,
523            None => {
524                let new_link = self.branches_store.create(0, 0);
525                self.branch_links.insert(info.name.clone(), new_link);
526                new_link
527            }
528        };
529        self.branches_store.set_name(link, &marker)?;
530        Ok(())
531    }
532
533    fn update_tag_link(&mut self, name: &str, seq: i64) -> Result<()> {
534        let marker = format!("{TAG_PREFIX}{name}={seq}");
535        let link = match self.tag_links.get(name).copied() {
536            Some(link) => link,
537            None => {
538                let new_link = self.branches_store.create(0, 0);
539                self.tag_links.insert(name.to_string(), new_link);
540                new_link
541            }
542        };
543        self.branches_store.set_name(link, &marker)?;
544        Ok(())
545    }
546
547    fn set_current_branch(&mut self, name: &str) -> Result<()> {
548        self.current_branch = name.to_string();
549        if self.current_branch_link == 0 {
550            self.current_branch_link = self.branches_store.create(0, 0);
551        }
552        let link = self.current_branch_link;
553        let marker = format!("{CURRENT_PREFIX}{name}");
554        self.branches_store.set_name(link, &marker)?;
555        Ok(())
556    }
557
558    fn set_applied(&mut self, seq: i64) -> Result<()> {
559        if self.applied_link == 0 {
560            self.applied_link = self.branches_store.create(0, 0);
561        }
562        let link = self.applied_link;
563        let marker = format!("{APPLIED_PREFIX}{seq}");
564        self.branches_store.set_name(link, &marker)?;
565        Ok(())
566    }
567
568    fn write_immutable_marker(&mut self, name: &str) -> Result<()> {
569        let link = self.branches_store.create(0, 0);
570        self.branches_store.set_name(link, name)?;
571        Ok(())
572    }
573
574    pub fn recover(&mut self) -> Result<()> {
575        self.branches.clear();
576        self.tags.clear();
577        self.transition_branches.clear();
578        self.branch_links.clear();
579        self.tag_links.clear();
580        self.current_branch = DEFAULT_BRANCH_NAME.to_string();
581        self.current_branch_link = 0;
582        self.applied_link = 0;
583        self.current_applied = 0;
584
585        let links: Vec<Link> = self.branches_store.all().into_iter().copied().collect();
586        for link in &links {
587            let name = match self.branches_store.get_name(link.index)? {
588                Some(value) => value,
589                None => continue,
590            };
591            if name.starts_with(BRANCH_PREFIX) {
592                if let Some(info) = try_decode_branch_marker(&name) {
593                    self.branches.insert(info.name.clone(), info.clone());
594                    self.branch_links.insert(info.name.clone(), link.index);
595                }
596            } else if let Some(rest) = name.strip_prefix(CURRENT_PREFIX) {
597                self.current_branch = rest.to_string();
598                self.current_branch_link = link.index;
599            } else if let Some(rest) = name.strip_prefix(APPLIED_PREFIX) {
600                if let Ok(seq) = rest.parse::<i64>() {
601                    self.current_applied = seq;
602                    self.applied_link = link.index;
603                }
604            } else if let Some(rest) = name.strip_prefix(TAG_PREFIX) {
605                if let Some(eq) = rest.find('=') {
606                    let tag_name = &rest[..eq];
607                    if let Ok(tag_seq) = rest[eq + 1..].parse::<i64>() {
608                        self.tags.insert(tag_name.to_string(), tag_seq);
609                        self.tag_links.insert(tag_name.to_string(), link.index);
610                    }
611                }
612            } else if let Some(rest) = name.strip_prefix(TRANSITION_PREFIX) {
613                if let Some(colon) = rest.find(":branch=") {
614                    if let Ok(seq) = rest[..colon].parse::<i64>() {
615                        let branch_name = &rest[colon + ":branch=".len()..];
616                        self.transition_branches
617                            .insert(seq, branch_name.to_string());
618                    }
619                }
620            }
621        }
622        Ok(())
623    }
624
625    fn trace(&self, message: &str) {
626        if self.trace {
627            eprintln!("[VersionControl] {message}");
628        }
629    }
630}
631
632fn encode_branch_marker(info: &BranchInfo) -> String {
633    let parent = info.parent.as_deref().unwrap_or("");
634    format!(
635        "{BRANCH_PREFIX}{name}:parent={parent}:fork={fork}:head={head}",
636        name = info.name,
637        fork = info.fork_seq,
638        head = info.head,
639    )
640}
641
642fn try_decode_branch_marker(text: &str) -> Option<BranchInfo> {
643    let rest = text.strip_prefix(BRANCH_PREFIX)?;
644    let parent_idx = rest.find(":parent=")?;
645    let name = &rest[..parent_idx];
646    let rest = &rest[parent_idx + ":parent=".len()..];
647    let fork_idx = rest.find(":fork=")?;
648    let parent_text = &rest[..fork_idx];
649    let rest = &rest[fork_idx + ":fork=".len()..];
650    let head_idx = rest.find(":head=")?;
651    let fork_text = &rest[..head_idx];
652    let head_text = &rest[head_idx + ":head=".len()..];
653    let fork: i64 = fork_text.parse().ok()?;
654    let head: i64 = head_text.parse().ok()?;
655    let parent = if parent_text.is_empty() {
656        None
657    } else {
658        Some(parent_text.to_string())
659    };
660    Some(BranchInfo::new(name.to_string(), parent, fork, head))
661}
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666
667    #[test]
668    fn encode_round_trips_through_decode() {
669        let info = BranchInfo::new("feature".into(), Some("main".into()), 5, 9);
670        let text = encode_branch_marker(&info);
671        let decoded = try_decode_branch_marker(&text).unwrap();
672        assert_eq!(info, decoded);
673    }
674
675    #[test]
676    fn make_version_control_database_filename_returns_sibling_path() {
677        let path =
678            VersionControlDecorator::make_version_control_database_filename("/var/data/db.links");
679        assert_eq!(path, PathBuf::from("/var/data/db.versioncontrol.links"));
680    }
681
682    #[test]
683    fn decode_branch_marker_rejects_invalid_input() {
684        assert!(try_decode_branch_marker("not a marker").is_none());
685        assert!(try_decode_branch_marker("__vc:branch:x:parent=:fork=z:head=1").is_none());
686    }
687}