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        let before_seq = self.transactions.last_logged_sequence();
206        let result = self.transactions.update(id, source, target)?;
207        if self.active_transaction.is_none() {
208            let branch = self.current_branch.clone();
209            self.attribute_new_transitions_for_branch(before_seq, &branch)?;
210        }
211        Ok(result)
212    }
213
214    pub fn delete(&mut self, id: u32) -> Result<Link> {
215        self.delete_observed(id, &mut |_, _| {})
216    }
217
218    /// [`Self::delete`], reporting every change the decorator stack made —
219    /// cascaded deletions of usages included.
220    pub fn delete_observed(&mut self, id: u32, observer: ChangeObserver<'_>) -> Result<Link> {
221        let before_seq = self.transactions.last_logged_sequence();
222        let result = self.transactions.delete_observed(id, observer)?;
223        if self.active_transaction.is_none() {
224            let branch = self.current_branch.clone();
225            self.attribute_new_transitions_for_branch(before_seq, &branch)?;
226        }
227        Ok(result)
228    }
229
230    pub fn create_and_update(&mut self, source: u32, target: u32) -> Result<u32> {
231        let before_seq = self.transactions.last_logged_sequence();
232        let id = self.transactions.create_and_update(source, target)?;
233        if self.active_transaction.is_none() {
234            let branch = self.current_branch.clone();
235            self.attribute_new_transitions_for_branch(before_seq, &branch)?;
236        }
237        Ok(id)
238    }
239
240    pub fn exists(&self, id: u32) -> bool {
241        self.transactions.exists(id)
242    }
243
244    pub fn get(&self, id: u32) -> Option<&Link> {
245        self.transactions.get(id)
246    }
247
248    pub fn all(&self) -> Vec<&Link> {
249        self.transactions.all()
250    }
251
252    pub fn search(&self, source: u32, target: u32) -> Option<u32> {
253        self.transactions.search(source, target)
254    }
255
256    pub fn get_or_create(&mut self, source: u32, target: u32) -> Result<u32> {
257        if let Some(existing) = self.transactions.search(source, target) {
258            return Ok(existing);
259        }
260        self.create(source, target)
261    }
262
263    pub fn ensure_created(&mut self, id: u32) -> u32 {
264        self.transactions
265            .ensure_created(id)
266            .expect("TransactionsDecorator::ensure_created failed")
267    }
268
269    fn attribute_new_transitions_for_branch(
270        &mut self,
271        before_seq: i64,
272        branch_name: &str,
273    ) -> Result<()> {
274        let after_seq = self.transactions.last_logged_sequence();
275        if after_seq <= before_seq {
276            return Ok(());
277        }
278        for s in (before_seq + 1)..=after_seq {
279            self.transition_branches.insert(s, branch_name.to_string());
280            let marker = format!("{TRANSITION_PREFIX}{s}:branch={branch_name}");
281            self.write_immutable_marker(&marker)?;
282        }
283        if let Some(info) = self.branches.get(branch_name).cloned() {
284            let updated = BranchInfo {
285                head: after_seq,
286                ..info
287            };
288            self.branches
289                .insert(branch_name.to_string(), updated.clone());
290            self.update_branch_link(&updated)?;
291        }
292        if self.current_branch == branch_name {
293            self.current_applied = after_seq;
294            self.set_applied(after_seq)?;
295        }
296        Ok(())
297    }
298
299    // -- Branching ----------------------------------------------------
300
301    pub fn branch(&mut self, name: &str, from: Option<i64>) -> Result<()> {
302        self.ensure_no_open_transaction("branch")?;
303        if name.trim().is_empty() {
304            bail!("Branch name must not be empty.");
305        }
306        if self.branches.contains_key(name) {
307            bail!("Branch '{name}' already exists.");
308        }
309        let parent = self.current_branch.clone();
310        let fork_seq = from.unwrap_or(self.current_applied);
311        if fork_seq < 0 {
312            bail!("Fork point cannot be negative.");
313        }
314        if fork_seq > 0 {
315            let path = self.build_branch_seqs(&parent);
316            if !path.contains(&fork_seq) {
317                bail!("Fork point {fork_seq} is not reachable on branch '{parent}'.",);
318            }
319        }
320        self.create_branch(name, Some(parent), fork_seq, fork_seq)?;
321        self.trace(&format!(
322            "Created branch '{name}' from '{}' at seq {fork_seq}.",
323            self.current_branch
324        ));
325        Ok(())
326    }
327
328    pub fn switch_branch(&mut self, name: &str) -> Result<()> {
329        self.ensure_no_open_transaction("switch_branch")?;
330        if !self.branches.contains_key(name) {
331            bail!("Unknown branch '{name}'.");
332        }
333        let target_path = self.build_branch_seqs(name);
334        self.apply_diff_to(target_path, name)?;
335        self.trace(&format!(
336            "Switched to branch '{name}' at seq {}.",
337            self.current_applied
338        ));
339        Ok(())
340    }
341
342    pub fn checkout(&mut self, sequence: i64) -> Result<()> {
343        self.ensure_no_open_transaction("checkout")?;
344        if sequence < 0 {
345            bail!("Sequence must be non-negative.");
346        }
347        let current = self.current_branch.clone();
348        let path = self.build_branch_seqs(&current);
349        if sequence > 0 && !path.contains(&sequence) {
350            bail!("Sequence {sequence} is not reachable on branch '{current}'.",);
351        }
352        let target_path: Vec<i64> = path.iter().copied().filter(|s| *s <= sequence).collect();
353        self.apply_diff_to(target_path, &current)?;
354        self.trace(&format!(
355            "Checked out seq {sequence} on branch '{current}'.",
356        ));
357        Ok(())
358    }
359
360    pub fn tag(&mut self, name: &str, sequence: Option<i64>) -> Result<()> {
361        self.ensure_no_open_transaction("tag")?;
362        if name.trim().is_empty() {
363            bail!("Tag name must not be empty.");
364        }
365        let seq = sequence.unwrap_or(self.current_applied);
366        if seq < 0 {
367            bail!("Tag sequence must be non-negative.");
368        }
369        self.tags.insert(name.to_string(), seq);
370        self.update_tag_link(name, seq)?;
371        self.trace(&format!("Created tag '{name}' at seq {seq}.",));
372        Ok(())
373    }
374
375    // -- Path / diff helpers -----------------------------------------
376
377    fn apply_diff_to(&mut self, target_path: Vec<i64>, new_branch: &str) -> Result<()> {
378        let current_branch_name = self.current_branch.clone();
379        let current_path: Vec<i64> = self
380            .build_branch_seqs(&current_branch_name)
381            .into_iter()
382            .filter(|s| *s <= self.current_applied)
383            .collect();
384
385        let mut common = 0usize;
386        let max_common = current_path.len().min(target_path.len());
387        while common < max_common && current_path[common] == target_path[common] {
388            common += 1;
389        }
390
391        // Revert transitions that are no longer on the path.
392        let to_revert: Vec<i64> = current_path[common..].iter().rev().copied().collect();
393        for seq in to_revert {
394            if let Some(transition) = self.find_transition(seq) {
395                self.transactions.revert_transition(&transition);
396            }
397        }
398        // Apply transitions that are new on the path.
399        let to_apply: Vec<i64> = target_path[common..].to_vec();
400        for seq in to_apply {
401            if let Some(transition) = self.find_transition(seq) {
402                self.transactions.apply_transition(&transition);
403            }
404        }
405
406        if new_branch != self.current_branch {
407            self.current_branch = new_branch.to_string();
408            self.set_current_branch(new_branch)?;
409        }
410        self.current_applied = target_path.last().copied().unwrap_or(0);
411        self.set_applied(self.current_applied)?;
412        Ok(())
413    }
414
415    fn ensure_no_open_transaction(&self, operation: &str) -> Result<()> {
416        if self.active_transaction.is_some() {
417            bail!("{operation} is not allowed while a version-control transaction is open.");
418        }
419        Ok(())
420    }
421
422    fn build_branch_seqs(&self, branch_name: &str) -> Vec<i64> {
423        let mut visited: HashSet<String> = HashSet::new();
424        self.build_branch_seqs_inner(branch_name, &mut visited)
425    }
426
427    fn build_branch_seqs_inner(
428        &self,
429        branch_name: &str,
430        visited: &mut HashSet<String>,
431    ) -> Vec<i64> {
432        let info = match self.branches.get(branch_name) {
433            Some(info) => info,
434            None => return Vec::new(),
435        };
436        if !visited.insert(branch_name.to_string()) {
437            return Vec::new();
438        }
439        let mut seqs = Vec::new();
440        if let Some(parent_name) = info.parent.as_deref() {
441            if self.branches.contains_key(parent_name) {
442                let mut parent_seqs = self.build_branch_seqs_inner(parent_name, visited);
443                parent_seqs.retain(|s| *s <= info.fork_seq);
444                seqs.extend(parent_seqs);
445            }
446        }
447        let mut own: Vec<i64> = self
448            .transition_branches
449            .iter()
450            .filter(|(s, b)| b.as_str() == branch_name && **s <= info.head)
451            .map(|(s, _)| *s)
452            .collect();
453        own.sort();
454        seqs.extend(own);
455        seqs
456    }
457
458    fn find_transition(&self, sequence: i64) -> Option<Transition> {
459        self.transactions
460            .log()
461            .into_iter()
462            .find(|t| t.sequence == sequence)
463    }
464
465    // -- Persistence helpers -----------------------------------------
466
467    fn ensure_default_branch(&mut self) -> Result<()> {
468        let existing = self.transactions.last_logged_sequence();
469        if !self.branches.contains_key(DEFAULT_BRANCH_NAME) {
470            // Pre-existing transitions are attributed to the default branch.
471            for s in 1..=existing {
472                if let std::collections::btree_map::Entry::Vacant(entry) =
473                    self.transition_branches.entry(s)
474                {
475                    entry.insert(DEFAULT_BRANCH_NAME.to_string());
476                    let marker = format!("{TRANSITION_PREFIX}{s}:branch={DEFAULT_BRANCH_NAME}");
477                    self.write_immutable_marker(&marker)?;
478                }
479            }
480            self.create_branch(DEFAULT_BRANCH_NAME, None, 0, existing)?;
481            self.current_branch = DEFAULT_BRANCH_NAME.to_string();
482            self.current_applied = existing;
483            self.set_current_branch(DEFAULT_BRANCH_NAME)?;
484            self.set_applied(existing)?;
485        } else if self.current_branch_link == 0 {
486            let branch = self.current_branch.clone();
487            self.set_current_branch(&branch)?;
488        }
489        Ok(())
490    }
491
492    fn create_branch(
493        &mut self,
494        name: &str,
495        parent: Option<String>,
496        fork_seq: i64,
497        head: i64,
498    ) -> Result<()> {
499        let info = BranchInfo::new(name.to_string(), parent, fork_seq, head);
500        self.branches.insert(name.to_string(), info.clone());
501        self.update_branch_link(&info)?;
502        Ok(())
503    }
504
505    fn update_branch_link(&mut self, info: &BranchInfo) -> Result<()> {
506        let marker = encode_branch_marker(info);
507        let link = match self.branch_links.get(&info.name).copied() {
508            Some(link) => link,
509            None => {
510                let new_link = self.branches_store.create(0, 0);
511                self.branch_links.insert(info.name.clone(), new_link);
512                new_link
513            }
514        };
515        self.branches_store.set_name(link, &marker)?;
516        Ok(())
517    }
518
519    fn update_tag_link(&mut self, name: &str, seq: i64) -> Result<()> {
520        let marker = format!("{TAG_PREFIX}{name}={seq}");
521        let link = match self.tag_links.get(name).copied() {
522            Some(link) => link,
523            None => {
524                let new_link = self.branches_store.create(0, 0);
525                self.tag_links.insert(name.to_string(), new_link);
526                new_link
527            }
528        };
529        self.branches_store.set_name(link, &marker)?;
530        Ok(())
531    }
532
533    fn set_current_branch(&mut self, name: &str) -> Result<()> {
534        self.current_branch = name.to_string();
535        if self.current_branch_link == 0 {
536            self.current_branch_link = self.branches_store.create(0, 0);
537        }
538        let link = self.current_branch_link;
539        let marker = format!("{CURRENT_PREFIX}{name}");
540        self.branches_store.set_name(link, &marker)?;
541        Ok(())
542    }
543
544    fn set_applied(&mut self, seq: i64) -> Result<()> {
545        if self.applied_link == 0 {
546            self.applied_link = self.branches_store.create(0, 0);
547        }
548        let link = self.applied_link;
549        let marker = format!("{APPLIED_PREFIX}{seq}");
550        self.branches_store.set_name(link, &marker)?;
551        Ok(())
552    }
553
554    fn write_immutable_marker(&mut self, name: &str) -> Result<()> {
555        let link = self.branches_store.create(0, 0);
556        self.branches_store.set_name(link, name)?;
557        Ok(())
558    }
559
560    pub fn recover(&mut self) -> Result<()> {
561        self.branches.clear();
562        self.tags.clear();
563        self.transition_branches.clear();
564        self.branch_links.clear();
565        self.tag_links.clear();
566        self.current_branch = DEFAULT_BRANCH_NAME.to_string();
567        self.current_branch_link = 0;
568        self.applied_link = 0;
569        self.current_applied = 0;
570
571        let links: Vec<Link> = self.branches_store.all().into_iter().copied().collect();
572        for link in &links {
573            let name = match self.branches_store.get_name(link.index)? {
574                Some(value) => value,
575                None => continue,
576            };
577            if name.starts_with(BRANCH_PREFIX) {
578                if let Some(info) = try_decode_branch_marker(&name) {
579                    self.branches.insert(info.name.clone(), info.clone());
580                    self.branch_links.insert(info.name.clone(), link.index);
581                }
582            } else if let Some(rest) = name.strip_prefix(CURRENT_PREFIX) {
583                self.current_branch = rest.to_string();
584                self.current_branch_link = link.index;
585            } else if let Some(rest) = name.strip_prefix(APPLIED_PREFIX) {
586                if let Ok(seq) = rest.parse::<i64>() {
587                    self.current_applied = seq;
588                    self.applied_link = link.index;
589                }
590            } else if let Some(rest) = name.strip_prefix(TAG_PREFIX) {
591                if let Some(eq) = rest.find('=') {
592                    let tag_name = &rest[..eq];
593                    if let Ok(tag_seq) = rest[eq + 1..].parse::<i64>() {
594                        self.tags.insert(tag_name.to_string(), tag_seq);
595                        self.tag_links.insert(tag_name.to_string(), link.index);
596                    }
597                }
598            } else if let Some(rest) = name.strip_prefix(TRANSITION_PREFIX) {
599                if let Some(colon) = rest.find(":branch=") {
600                    if let Ok(seq) = rest[..colon].parse::<i64>() {
601                        let branch_name = &rest[colon + ":branch=".len()..];
602                        self.transition_branches
603                            .insert(seq, branch_name.to_string());
604                    }
605                }
606            }
607        }
608        Ok(())
609    }
610
611    fn trace(&self, message: &str) {
612        if self.trace {
613            eprintln!("[VersionControl] {message}");
614        }
615    }
616}
617
618fn encode_branch_marker(info: &BranchInfo) -> String {
619    let parent = info.parent.as_deref().unwrap_or("");
620    format!(
621        "{BRANCH_PREFIX}{name}:parent={parent}:fork={fork}:head={head}",
622        name = info.name,
623        fork = info.fork_seq,
624        head = info.head,
625    )
626}
627
628fn try_decode_branch_marker(text: &str) -> Option<BranchInfo> {
629    let rest = text.strip_prefix(BRANCH_PREFIX)?;
630    let parent_idx = rest.find(":parent=")?;
631    let name = &rest[..parent_idx];
632    let rest = &rest[parent_idx + ":parent=".len()..];
633    let fork_idx = rest.find(":fork=")?;
634    let parent_text = &rest[..fork_idx];
635    let rest = &rest[fork_idx + ":fork=".len()..];
636    let head_idx = rest.find(":head=")?;
637    let fork_text = &rest[..head_idx];
638    let head_text = &rest[head_idx + ":head=".len()..];
639    let fork: i64 = fork_text.parse().ok()?;
640    let head: i64 = head_text.parse().ok()?;
641    let parent = if parent_text.is_empty() {
642        None
643    } else {
644        Some(parent_text.to_string())
645    };
646    Some(BranchInfo::new(name.to_string(), parent, fork, head))
647}
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652
653    #[test]
654    fn encode_round_trips_through_decode() {
655        let info = BranchInfo::new("feature".into(), Some("main".into()), 5, 9);
656        let text = encode_branch_marker(&info);
657        let decoded = try_decode_branch_marker(&text).unwrap();
658        assert_eq!(info, decoded);
659    }
660
661    #[test]
662    fn make_version_control_database_filename_returns_sibling_path() {
663        let path =
664            VersionControlDecorator::make_version_control_database_filename("/var/data/db.links");
665        assert_eq!(path, PathBuf::from("/var/data/db.versioncontrol.links"));
666    }
667
668    #[test]
669    fn decode_branch_marker_rejects_invalid_input() {
670        assert!(try_decode_branch_marker("not a marker").is_none());
671        assert!(try_decode_branch_marker("__vc:branch:x:parent=:fork=z:head=1").is_none());
672    }
673}