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