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