Skip to main content

blockworx_doc/
commit.rs

1//! The commit — the sealed unit of edit, undo, and review: a labeled
2//! batch of ops. The `Rev` and wall time are assigned as the commit is
3//! applied and live outside the payload.
4//! Rationale: `docs/doc-ng-design-notes.md`.
5
6use serde::{Deserialize, Serialize};
7
8use crate::opcode::OpCodes;
9
10#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
11pub struct Commit {
12    /// Semantic label for review/undo grouping: "Moved 15 elements".
13    label: String,
14    /// One gesture. An op's index is its `Seq` in the total write order;
15    /// the `Rev` half is assigned when the commit is applied and lives
16    /// outside the payload.
17    ops: Vec<OpCodes>,
18}
19
20impl Commit {
21    pub fn new(label: String, ops: Vec<OpCodes>) -> Self {
22        Self { label, ops }
23    }
24
25    pub fn label(&self) -> &str {
26        &self.label
27    }
28
29    pub fn ops(&self) -> &[OpCodes] {
30        &self.ops
31    }
32
33    /// The same commit under a different label. Consuming, so a gesture
34    /// that describes itself once its ops are known does not copy them.
35    #[must_use]
36    pub fn relabelled(self, label: String) -> Self {
37        Self { label, ..self }
38    }
39}
40
41#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
42pub enum CommitEnvelope {
43    CommitV1(Commit),
44}
45
46/// Accumulates one gesture's ops into a sealed [`Commit`]. Push order is
47/// preserved verbatim — an op's index is its `Seq` in the total write order,
48/// so the builder's order *is* the merge-relevant order (and a pasted
49/// group's internal stacking). Sealing consumes the builder, so a commit
50/// cannot be extended after the fact, and a gesture that edited nothing
51/// seals to `None` and submits nothing.
52///
53/// Deliberately blind to the document: dropping a *non-edit* (an update
54/// equal to what the document already holds) is the push site's job — the
55/// op emitters can compare against the document, the builder cannot.
56#[derive(Debug)]
57pub struct CommitBuilder {
58    label: String,
59    ops: Vec<OpCodes>,
60}
61
62impl CommitBuilder {
63    /// Start a gesture under its semantic label — a command's stable name
64    /// where a command triggered the edit, a tool-authored string otherwise.
65    pub fn new(label: impl Into<String>) -> Self {
66        Self {
67            label: label.into(),
68            ops: Vec::new(),
69        }
70    }
71
72    /// Append one op; its position becomes its `Seq`.
73    pub fn push(&mut self, op: OpCodes) {
74        self.ops.push(op);
75    }
76
77    /// Append a run of ops in order, as [`Self::push`] would one by one.
78    pub fn extend(&mut self, ops: impl IntoIterator<Item = OpCodes>) {
79        self.ops.extend(ops);
80    }
81
82    /// What the gesture has pushed so far, without sealing. The one reader
83    /// is a gesture that must *see* its own effect before it finishes — a
84    /// solver rider folds these onto a scratch prediction, solves against
85    /// it, and pushes the result here in turn.
86    pub fn ops(&self) -> &[OpCodes] {
87        &self.ops
88    }
89
90    /// Seal the gesture: `None` when nothing was pushed, else the commit
91    /// with the ops exactly as pushed.
92    pub fn seal(self) -> Option<Commit> {
93        (!self.ops.is_empty()).then(|| Commit::new(self.label, self.ops))
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::fixtures::block_id;
101    use crate::opcode::Crud;
102
103    fn delete_block(n: u32) -> OpCodes {
104        OpCodes::Block(block_id(n), Crud::Delete)
105    }
106
107    #[test]
108    fn an_empty_builder_seals_to_no_commit() {
109        assert_eq!(CommitBuilder::new("Idle gesture").seal(), None);
110    }
111
112    /// The mid-gesture read a solver rider takes: everything pushed so far,
113    /// in push order, with the builder still open to receive its answer.
114    #[test]
115    fn ops_reads_the_gesture_so_far_without_closing_it() {
116        let mut builder = CommitBuilder::new("Solved on the way");
117        assert!(
118            builder.ops().is_empty(),
119            "a fresh gesture has pushed nothing"
120        );
121        builder.push(delete_block(1));
122        assert_eq!(builder.ops(), [delete_block(1)]);
123        builder.push(delete_block(2));
124        assert_eq!(builder.ops(), [delete_block(1), delete_block(2)]);
125        let commit = builder.seal().expect("the gesture still seals");
126        assert_eq!(commit.ops(), [delete_block(1), delete_block(2)]);
127    }
128
129    #[test]
130    fn seal_preserves_push_order_as_seq_order() {
131        let pushed = [delete_block(1), delete_block(2), delete_block(3)];
132        let mut builder = CommitBuilder::new("Deleted 3 blocks");
133        builder.push(pushed[0].clone());
134        builder.extend(pushed[1..].iter().cloned());
135        let commit = builder.seal().expect("pushed ops must seal to a commit");
136        assert_eq!(commit.label(), "Deleted 3 blocks");
137        assert_eq!(commit.ops(), pushed);
138    }
139}