OXYGENHUB
# 8f10a07a1dbe oxygen / crates / oxy-core / src / state.rs
23.3 KiB · Rust · last changed Checkpoints: serialized states per bookmark, automatic every core.checkpoint-interval changes, oxy checkpoint, checkpoint-accelerated load_state; states keep change metadata only 1 month ago source blame history raw
ff0af1ee unknown 1
//! A state: a dependency-closed set of changes, applied to a tree graph and
2
//! per-node file graphs (spec §8.3), with materialization (spec §9.5, §11).
3
4
use crate::graph::{AtomKey, DepOracle, FileGraph, Materialized, Vertex};
5
use crate::hash::{ChangeHash, ObjectRef};
6
use crate::ids::NodeId;
252e8cb2 Andy McCall ↶ 7
use crate::cbor::{self, Value};
8
use crate::hash::KeyId;
9
use crate::objects::{self as o, AtomRef, Change, ChangeKind, Edit, Hlc, Mode, NodeKind, ObjectError, TreeOp};
ff0af1ee unknown 10
use crate::tree::{TreeGraph, TreeView};
11
use std::cell::RefCell;
12
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
13
use std::rc::Rc;
14
15
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
16
pub enum StateError {
17
    #[error("change {1} depends on {0}, which is not in the state")]
18
    MissingDep(ChangeHash, ChangeHash),
19
}
20
55e5a16a unknown ↶ 21
/// One `Replace` edit: `(change, old blob, new blob)`.
22
type ReplaceRecord = (ChangeHash, Option<ObjectRef>, Option<ObjectRef>);
23
252e8cb2 Andy McCall ↶ 24
/// What a state remembers about a change: everything but its operations,
25
/// which live in the graphs once applied (and in the store).
26
#[derive(Clone, Debug, PartialEq, Eq)]
27
pub struct ChangeMeta {
28
    pub change_id: crate::ids::ChangeId,
29
    pub deps: Vec<ChangeHash>,
30
    pub author: KeyId,
31
    pub hlc: Hlc,
32
    pub message: String,
33
    pub kind: ChangeKind,
34
}
35
36
impl ChangeMeta {
37
    pub fn of(c: &Change) -> ChangeMeta {
38
        ChangeMeta { change_id: c.change_id, deps: c.deps.clone(), author: c.author, hlc: c.hlc, message: c.message.clone(), kind: c.kind }
39
    }
40
}
41
c3069537 Andy McCall ↶ 42
#[derive(Default, Clone)]
ff0af1ee unknown 43
pub struct State {
252e8cb2 Andy McCall ↶ 44
    changes: HashMap<ChangeHash, ChangeMeta>,
ff0af1ee unknown 45
    tree: TreeGraph,
46
    files: HashMap<NodeId, FileGraph>,
55e5a16a unknown ↶ 47
    replaces: HashMap<NodeId, Vec<ReplaceRecord>>,
ff0af1ee unknown 48
    closure: RefCell<HashMap<ChangeHash, Rc<HashSet<ChangeHash>>>>,
49
}
50
51
#[derive(Clone, Debug, PartialEq, Eq)]
52
pub enum FileContent {
53
    Text { bytes: Vec<u8>, materialized: Materialized },
54
    /// Live blobs; more than one is a replace conflict (spec §9.3).
55
    Opaque { blobs: Vec<ObjectRef> },
56
    Dir,
57
}
58
59
#[derive(Clone, Debug, PartialEq, Eq)]
60
pub struct FileState {
61
    pub node: NodeId,
62
    pub kind: NodeKind,
63
    pub mode: Mode,
64
    pub content: FileContent,
65
}
66
67
impl FileState {
68
    pub fn is_conflicted(&self) -> bool {
69
        match &self.content {
70
            FileContent::Text { materialized, .. } => materialized.is_conflicted(),
71
            FileContent::Opaque { blobs } => blobs.len() > 1,
72
            FileContent::Dir => false,
73
        }
74
    }
75
}
76
20247a03 Andy McCall ↶ 77
/// See `State::conflict_regions`.
78
#[derive(Clone, Debug, Default, PartialEq, Eq)]
79
pub struct ConflictRegions {
80
    pub plain: Vec<(AtomKey, Vec<u8>)>,
81
    pub region_atoms: Vec<AtomKey>,
82
    pub contributors: Vec<ChangeHash>,
83
}
84
ff0af1ee unknown 85
/// The whole tree of a state, materialized.
86
#[derive(Clone, Debug, Default)]
87
pub struct Snapshot {
88
    pub tree: TreeView,
89
    pub files: BTreeMap<String, FileState>,
90
}
91
92
impl Snapshot {
93
    pub fn is_conflicted(&self) -> bool {
94
        self.tree.is_conflicted() || self.files.values().any(|f| f.is_conflicted())
95
    }
96
}
97
98
fn vertex(r: AtomRef) -> Vertex {
99
    match r {
100
        AtomRef::Start => Vertex::Start,
101
        AtomRef::End => Vertex::End,
102
        AtomRef::Atom { change, index } => Vertex::Atom((change, index)),
103
    }
104
}
105
106
/// Replace self references in a tree op with the concrete change hash.
107
pub fn resolve_tree_op(op: &TreeOp, me: ChangeHash) -> TreeOp {
108
    match op {
109
        TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode { node: *node, placement: placement.resolve(me) },
110
        TreeOp::Move { node, old, parent, name } => {
111
            TreeOp::Move { node: *node, old: old.resolve(me), parent: *parent, name: name.clone() }
112
        }
113
        TreeOp::SetMode { node, old, mode } => TreeOp::SetMode { node: *node, old: old.resolve(me), mode: *mode },
114
        add @ TreeOp::AddNode { .. } => add.clone(),
115
    }
116
}
117
118
impl State {
119
    pub fn new() -> Self {
120
        Self::default()
121
    }
122
123
    pub fn len(&self) -> usize {
124
        self.changes.len()
125
    }
126
    pub fn is_empty(&self) -> bool {
127
        self.changes.is_empty()
128
    }
129
    pub fn contains(&self, h: &ChangeHash) -> bool {
130
        self.changes.contains_key(h)
131
    }
252e8cb2 Andy McCall ↶ 132
    pub fn get(&self, h: &ChangeHash) -> Option<&ChangeMeta> {
ff0af1ee unknown 133
        self.changes.get(h)
134
    }
252e8cb2 Andy McCall ↶ 135
    pub fn changes(&self) -> impl Iterator<Item = (&ChangeHash, &ChangeMeta)> {
ff0af1ee unknown 136
        self.changes.iter()
137
    }
138
    pub fn tree(&self) -> &TreeGraph {
139
        &self.tree
140
    }
141
    pub fn file(&self, node: &NodeId) -> Option<&FileGraph> {
142
        self.files.get(node)
143
    }
144
145
    /// Add a change whose dependencies are already present.
146
    pub fn add(&mut self, change: Change) -> Result<ChangeHash, StateError> {
147
        let h = change.hash();
148
        if self.changes.contains_key(&h) {
149
            return Ok(h);
150
        }
151
        for d in &change.deps {
152
            if !self.changes.contains_key(d) {
153
                return Err(StateError::MissingDep(*d, h));
154
            }
155
        }
156
        Ok(self.add_unchecked(change))
157
    }
158
159
    /// Add a change in any order; call `validate` once all are in.
160
    pub fn add_unchecked(&mut self, change: Change) -> ChangeHash {
161
        let h = change.hash();
162
        if self.changes.contains_key(&h) {
163
            return h;
164
        }
165
        self.apply(h, &change);
252e8cb2 Andy McCall ↶ 166
        self.changes.insert(h, ChangeMeta::of(&change));
ff0af1ee unknown 167
        self.closure.borrow_mut().clear();
168
        h
169
    }
170
252e8cb2 Andy McCall ↶ 171
    // ------------------------------------------------------------ checkpoints (spec §11)
172
173
    /// Serialize the whole state: change metadata, tree, file graphs, replaces.
174
    pub fn checkpoint_payload(&self) -> Vec<u8> {
175
        let mut hashes: Vec<&ChangeHash> = self.changes.keys().collect();
176
        hashes.sort();
177
        let changes = Value::Array(
178
            hashes
179
                .into_iter()
180
                .map(|h| {
181
                    let m = &self.changes[h];
182
                    Value::Array(vec![
183
                        Value::Bytes(h.0.to_vec()),
184
                        Value::Bytes(m.change_id.0.to_vec()),
185
                        Value::Array(m.deps.iter().map(|d| Value::Bytes(d.0.to_vec())).collect()),
186
                        Value::Text(m.message.clone()),
187
                        Value::Uint(m.hlc.wall_ms),
188
                        Value::Uint(m.hlc.logical as u64),
189
                        Value::Uint(m.kind.tag()),
190
                        Value::Bytes(m.author.0.to_vec()),
191
                    ])
192
                })
193
                .collect(),
194
        );
195
        let mut nodes: Vec<&NodeId> = self.files.keys().collect();
196
        nodes.sort();
197
        let files = Value::Array(nodes.into_iter().map(|n| Value::Array(vec![Value::Bytes(n.0.to_vec()), self.files[n].to_value()])).collect());
198
        let mut rnodes: Vec<&NodeId> = self.replaces.keys().collect();
199
        rnodes.sort();
200
        let opt = |o: &Option<ObjectRef>| o.map(|r| Value::Bytes(r.0.to_vec())).unwrap_or(Value::Null);
201
        let replaces = Value::Array(
202
            rnodes
203
                .into_iter()
204
                .map(|n| {
205
                    Value::Array(vec![
206
                        Value::Bytes(n.0.to_vec()),
207
                        Value::Array(self.replaces[n].iter().map(|(c, o, nw)| Value::Array(vec![Value::Bytes(c.0.to_vec()), opt(o), opt(nw)])).collect()),
208
                    ])
209
                })
210
                .collect(),
211
        );
212
        cbor::encode(&Value::map([(0, Value::Uint(o::FORMAT_VERSION)), (1, changes), (2, self.tree.to_value()), (3, files), (4, replaces)]))
213
    }
214
215
    pub fn from_checkpoint_payload(bytes: &[u8]) -> Result<State, ObjectError> {
216
        let v = cbor::decode(bytes)?;
217
        let m = o::map(&v, "checkpoint payload")?;
218
        o::check_version(m)?;
219
        let mut st = State::new();
220
        for c in o::array(o::field(m, 1, "changes")?, "changes")? {
221
            let e = o::array(c, "change meta")?;
222
            if e.len() != 8 {
223
                return Err(ObjectError::Schema("change meta: expected 8 fields".into()));
224
            }
225
            let hash = o::oref(&e[0], "hash")?;
226
            let meta = ChangeMeta {
227
                change_id: crate::ids::ChangeId::from_slice(o::bytes(&e[1], "change id")?).ok_or_else(|| ObjectError::Schema("change id: expected 16 bytes".into()))?,
228
                deps: o::array(&e[2], "deps")?.iter().map(|d| o::oref(d, "dep")).collect::<Result<Vec<_>, _>>()?,
229
                message: o::text(&e[3], "message")?.to_owned(),
230
                hlc: Hlc { wall_ms: o::uint(&e[4], "wall")?, logical: o::u32_(&e[5], "logical")? },
231
                kind: ChangeKind::from_tag(o::uint(&e[6], "kind")?)?,
232
                author: o::oref(&e[7], "author")?,
233
            };
234
            st.changes.insert(hash, meta);
235
        }
236
        st.tree = TreeGraph::from_value(o::field(m, 2, "tree")?)?;
237
        for f in o::array(o::field(m, 3, "files")?, "files")? {
238
            let e = o::array(f, "file")?;
239
            if e.len() != 2 {
240
                return Err(ObjectError::Schema("file: expected [node, graph]".into()));
241
            }
242
            let node = NodeId::from_slice(o::bytes(&e[0], "node")?).ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?;
243
            st.files.insert(node, FileGraph::from_value(&e[1])?);
244
        }
245
        let opt = |v: &Value| -> Result<Option<ObjectRef>, ObjectError> { if v.is_null() { Ok(None) } else { o::oref(v, "blob").map(Some) } };
246
        for r in o::array(o::field(m, 4, "replaces")?, "replaces")? {
247
            let e = o::array(r, "replace")?;
248
            if e.len() != 2 {
249
                return Err(ObjectError::Schema("replace: expected [node, records]".into()));
250
            }
251
            let node = NodeId::from_slice(o::bytes(&e[0], "node")?).ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?;
252
            let mut recs = Vec::new();
253
            for rec in o::array(&e[1], "records")? {
254
                let x = o::array(rec, "record")?;
255
                if x.len() != 3 {
256
                    return Err(ObjectError::Schema("record: expected 3 fields".into()));
257
                }
258
                recs.push((o::oref(&x[0], "change")?, opt(&x[1])?, opt(&x[2])?));
259
            }
260
            st.replaces.insert(node, recs);
261
        }
262
        Ok(st)
263
    }
264
ff0af1ee unknown 265
    pub fn validate(&self) -> Result<(), StateError> {
266
        for (h, c) in &self.changes {
267
            for d in &c.deps {
268
                if !self.changes.contains_key(d) {
269
                    return Err(StateError::MissingDep(*d, *h));
270
                }
271
            }
272
        }
273
        Ok(())
274
    }
275
276
    fn apply(&mut self, h: ChangeHash, c: &Change) {
277
        for (i, op) in c.tree_ops.iter().enumerate() {
278
            self.tree.apply(h, i as u32, &resolve_tree_op(op, h));
279
        }
280
        let mut counters: HashMap<NodeId, u32> = HashMap::new();
281
        for e in &c.edits {
282
            match e {
283
                Edit::Insert { node, after, before, lines } => {
284
                    let idx = counters.entry(*node).or_default();
285
                    let first = *idx;
286
                    *idx += lines.len() as u32;
287
                    self.files.entry(*node).or_default().insert(h, first, vertex(after.resolve(h)), vertex(before.resolve(h)), lines);
288
                }
289
                Edit::Delete { node, atoms } => {
290
                    let g = self.files.entry(*node).or_default();
291
                    for r in atoms {
292
                        g.delete(h, &r.resolve(h));
293
                    }
294
                }
295
                Edit::Replace { node, old, new } => {
296
                    self.replaces.entry(*node).or_default().push((h, *old, *new));
297
                }
298
            }
299
        }
300
    }
301
302
    /// Transitive dependencies of `h`, excluding `h` itself.
303
    pub fn closure(&self, h: &ChangeHash) -> Rc<HashSet<ChangeHash>> {
304
        if let Some(c) = self.closure.borrow().get(h) {
305
            return c.clone();
306
        }
307
        let mut out = HashSet::new();
308
        let mut stack: Vec<ChangeHash> = self.changes.get(h).map(|c| c.deps.clone()).unwrap_or_default();
309
        while let Some(d) = stack.pop() {
310
            if out.insert(d) {
311
                if let Some(c) = self.changes.get(&d) {
312
                    stack.extend(c.deps.iter().copied());
313
                }
314
            }
315
        }
316
        let rc = Rc::new(out);
317
        self.closure.borrow_mut().insert(*h, rc.clone());
318
        rc
319
    }
320
321
    /// Changes that no other change depends on directly, sorted.
322
    pub fn heads(&self) -> Vec<ChangeHash> {
323
        let depended: HashSet<&ChangeHash> = self.changes.values().flat_map(|c| c.deps.iter()).collect();
324
        let mut heads: Vec<ChangeHash> = self.changes.keys().filter(|h| !depended.contains(h)).copied().collect();
325
        heads.sort();
326
        heads
327
    }
328
329
    /// Maximal elements among `hashes` (those not in the closure of another).
330
    pub fn maximal(&self, hashes: &[ChangeHash]) -> Vec<ChangeHash> {
331
        let set: BTreeSet<ChangeHash> = hashes.iter().copied().collect();
332
        set.iter().filter(|h| !set.iter().any(|o| o != *h && self.closure(o).contains(h))).copied().collect()
333
    }
334
335
    pub fn materialize_file(&self, node: &NodeId) -> Materialized {
336
        self.files.get(node).map(|g| g.materialize(self)).unwrap_or_default()
337
    }
338
339
    /// Live lines of a text node in materialized order.
340
    pub fn live_lines(&self, node: &NodeId) -> Vec<(AtomKey, Vec<u8>)> {
341
        let Some(g) = self.files.get(node) else { return vec![] };
342
        let m = g.materialize(self);
343
        m.live_atoms().into_iter().map(|a| (a, g.content(&a).unwrap_or_default().to_vec())).collect()
344
    }
345
346
    /// Live blobs of an opaque node: every `new` that no later `Replace` retired.
347
    pub fn live_blobs(&self, node: &NodeId) -> Vec<ObjectRef> {
348
        let Some(rs) = self.replaces.get(node) else { return vec![] };
349
        let olds: HashSet<ObjectRef> = rs.iter().filter_map(|(_, o, _)| *o).collect();
350
        let mut live: Vec<ObjectRef> = rs.iter().filter_map(|(_, _, n)| *n).filter(|n| !olds.contains(n)).collect();
351
        live.sort();
352
        live.dedup();
353
        live
354
    }
355
356
    /// Short label for conflict markers: `changeid-prefix "message"`.
357
    pub fn label(&self, h: &ChangeHash) -> String {
358
        match self.changes.get(h) {
359
            Some(c) => format!("{} \"{}\"", c.change_id.short(8), c.message.lines().next().unwrap_or("")),
360
            None => format!("#{}", h.short(12)),
361
        }
362
    }
363
20247a03 Andy McCall ↶ 364
    /// For resolving a conflicted text node (spec §9.6): the live atoms that
365
    /// are not part of any conflict region in order, every live atom that is
366
    /// part of a region, and every change that contributed to a region.
367
    pub fn conflict_regions(&self, node: &NodeId) -> ConflictRegions {
368
        let mut out = ConflictRegions::default();
369
        let Some(g) = self.files.get(node) else { return out };
370
        let m = g.materialize(self);
371
        let mut contributors = BTreeSet::new();
372
        for seg in &m.segments {
373
            match seg {
374
                crate::graph::Segment::Lines(atoms) => {
375
                    for a in atoms {
376
                        out.plain.push((*a, g.content(a).unwrap_or_default().to_vec()));
377
                    }
378
                }
379
                crate::graph::Segment::Conflict(sides) => {
380
                    for side in sides {
381
                        for a in side {
382
                            out.region_atoms.push(*a);
383
                            contributors.insert(a.0);
384
                        }
385
                    }
386
                }
387
                crate::graph::Segment::Zombie { deleted, inserted } => {
388
                    for a in deleted {
389
                        contributors.insert(a.0);
390
                        contributors.extend(g.deleters(a).copied());
391
                    }
392
                    for a in inserted {
393
                        out.region_atoms.push(*a);
394
                        contributors.insert(a.0);
395
                    }
396
                }
397
            }
398
        }
399
        out.contributors = contributors.into_iter().collect();
400
        out
401
    }
402
ff0af1ee unknown 403
    pub fn render_file(&self, node: &NodeId) -> (Vec<u8>, Materialized) {
404
        let Some(g) = self.files.get(node) else { return (Vec::new(), Materialized::default()) };
405
        let m = g.materialize(self);
406
        let bytes = g.render(&m, &|h| self.label(h));
407
        (bytes, m)
408
    }
409
410
    pub fn materialize(&self) -> Snapshot {
411
        let tree = self.tree.view();
412
        let mut files = BTreeMap::new();
413
        for (path, e) in &tree.entries {
414
            let content = match e.kind {
415
                NodeKind::Dir => FileContent::Dir,
416
                NodeKind::Symlink => FileContent::Opaque { blobs: self.live_blobs(&e.node) },
417
                NodeKind::File if e.mode.opaque => FileContent::Opaque { blobs: self.live_blobs(&e.node) },
418
                NodeKind::File => {
419
                    let (bytes, materialized) = self.render_file(&e.node);
420
                    FileContent::Text { bytes, materialized }
421
                }
422
            };
423
            files.insert(path.clone(), FileState { node: e.node, kind: e.kind, mode: e.mode, content });
424
        }
425
        Snapshot { tree, files }
426
    }
427
}
428
429
impl DepOracle for State {
430
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
431
        a == b || self.closure(a).contains(b)
432
    }
433
}
434
435
#[cfg(test)]
436
mod tests {
437
    use super::*;
438
    use crate::ids::ChangeId;
439
    use crate::objects::{AtomRange, ChangeKind, Hlc, OpRef};
440
441
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>) -> Change {
442
        let mut c = Change {
443
            change_id: ChangeId::random(),
444
            deps,
445
            author: ObjectRef::ZERO,
446
            hlc: Hlc { wall_ms: 1, logical: 0 },
447
            message: "m".into(),
448
            tree_ops,
449
            edits,
450
            kind: ChangeKind::Normal,
451
        };
452
        c.normalize();
453
        c
454
    }
455
456
    #[test]
457
    fn build_edit_and_materialize() {
458
        let mut st = State::new();
459
        let f = NodeId::random();
460
        let c1 = change(
461
            vec![],
462
            vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "a.txt".into() }],
463
            vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"one\n".to_vec(), b"two\n".to_vec()] }],
464
        );
465
        let h1 = st.add(c1).unwrap();
466
        // Depends on h1: append a line and delete "one".
467
        let c2 = change(
468
            vec![h1],
469
            vec![],
470
            vec![
471
                Edit::Delete { node: f, atoms: vec![AtomRange { change: h1, from: 0, to: 1 }] },
472
                Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"three\n".to_vec()] },
473
            ],
474
        );
475
        let h2 = st.add(c2).unwrap();
476
        assert_eq!(st.heads(), vec![h2]);
477
        assert!(st.depends_on(&h2, &h1));
478
        assert!(!st.depends_on(&h1, &h2));
479
        let snap = st.materialize();
480
        assert!(!snap.is_conflicted());
481
        let a = &snap.files["a.txt"];
482
        assert!(matches!(&a.content, FileContent::Text { bytes, .. } if bytes == b"two\nthree\n"));
483
        assert_eq!(st.live_lines(&f).len(), 2);
484
    }
485
486
    #[test]
487
    fn missing_dependency_is_rejected() {
488
        let mut st = State::new();
489
        let c = change(vec![ObjectRef([1; 32])], vec![], vec![]);
490
        assert!(matches!(st.add(c), Err(StateError::MissingDep(..))));
491
    }
492
493
    #[test]
494
    fn self_references_and_multi_insert_indexing() {
495
        let mut st = State::new();
496
        let f = NodeId::random();
497
        let me = OpRef { change: ObjectRef::ZERO, index: 0 };
498
        let c1 = change(
499
            vec![],
500
            vec![
501
                TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "x".into() },
502
                TreeOp::SetMode { node: f, old: me, mode: Mode { exec: true, opaque: false } },
503
            ],
504
            vec![
505
                Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec(), b"c\n".to_vec()] },
506
                // Insert between the two lines just inserted, using self references.
507
                Edit::Insert {
508
                    node: f,
509
                    after: AtomRef::Atom { change: ObjectRef::ZERO, index: 0 },
510
                    before: AtomRef::Atom { change: ObjectRef::ZERO, index: 1 },
511
                    lines: vec![b"b\n".to_vec()],
512
                },
513
            ],
514
        );
515
        st.add(c1).unwrap();
516
        let snap = st.materialize();
517
        let x = &snap.files["x"];
518
        assert_eq!(x.mode, Mode { exec: true, opaque: false });
519
        assert!(matches!(&x.content, FileContent::Text { bytes, .. } if bytes == b"a\nb\nc\n"));
520
    }
521
522
    #[test]
523
    fn opaque_replace_and_conflict() {
524
        let mut st = State::new();
525
        let f = NodeId::random();
526
        let b1 = ObjectRef([1; 32]);
527
        let b2 = ObjectRef([2; 32]);
528
        let b3 = ObjectRef([3; 32]);
529
        let h1 = st.add(change(
530
            vec![],
531
            vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::OPAQUE, parent: NodeId::ROOT, name: "img".into() }],
532
            vec![Edit::Replace { node: f, old: None, new: Some(b1) }],
533
        ))
534
        .unwrap();
535
        assert_eq!(st.live_blobs(&f), vec![b1]);
536
        st.add(change(vec![h1], vec![], vec![Edit::Replace { node: f, old: Some(b1), new: Some(b2) }])).unwrap();
537
        st.add(change(vec![h1], vec![], vec![Edit::Replace { node: f, old: Some(b1), new: Some(b3) }])).unwrap();
538
        let snap = st.materialize();
539
        assert!(snap.is_conflicted());
540
        assert!(matches!(&snap.files["img"].content, FileContent::Opaque { blobs } if blobs == &vec![b2, b3]));
541
    }
542
543
    #[test]
252e8cb2 Andy McCall ↶ 544
    fn checkpoint_payload_round_trip_and_replay() {
545
        let mut st = State::new();
546
        let f = NodeId::random();
547
        let h1 = st.add(change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "a".into() }], vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"1\n".to_vec(), b"2\n".to_vec()] }])).unwrap();
548
        let h2 = st.add(change(vec![h1], vec![], vec![Edit::Delete { node: f, atoms: vec![AtomRange { change: h1, from: 0, to: 1 }] }])).unwrap();
549
        let payload = st.checkpoint_payload();
550
        let back = State::from_checkpoint_payload(&payload).unwrap();
551
        assert_eq!(back.len(), 2);
552
        assert_eq!(back.heads(), vec![h2]);
553
        assert_eq!(back.get(&h1).unwrap().message, "m");
554
        assert_eq!(back.checkpoint_payload(), payload, "deterministic");
555
        // Replaying a further change onto the restored state equals a full build.
556
        let c3 = change(vec![h2], vec![], vec![Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"3\n".to_vec()] }]);
557
        let mut full = st.clone();
558
        full.add(c3.clone()).unwrap();
559
        let mut restored = back;
560
        restored.add(c3).unwrap();
561
        let (a, b) = (full.materialize(), restored.materialize());
562
        assert_eq!(a.files.keys().collect::<Vec<_>>(), b.files.keys().collect::<Vec<_>>());
563
        assert_eq!(a.files["a"].content, b.files["a"].content);
564
        assert!(matches!(&b.files["a"].content, FileContent::Text { bytes, .. } if bytes == b"2\n3\n"));
565
        assert!(State::from_checkpoint_payload(b"nonsense").is_err());
566
    }
567
568
    #[test]
ff0af1ee unknown 569
    fn maximal_elements() {
570
        let mut st = State::new();
571
        let h1 = st.add(change(vec![], vec![], vec![])).unwrap();
572
        let h2 = st.add(change(vec![h1], vec![], vec![])).unwrap();
573
        let h3 = st.add(change(vec![], vec![], vec![])).unwrap();
574
        let mut m = st.maximal(&[h1, h2, h3]);
575
        m.sort();
576
        let mut want = vec![h2, h3];
577
        want.sort();
578
        assert_eq!(m, want);
579
        assert_eq!(st.heads(), want);
580
    }
581
}