OXYGENHUB
# e293c90dca62 oxygen / crates / oxy-core / src / state.rs
28.1 KiB · Rust · last changed Unbound hosted repositories adopt the first pusher's id 1 month ago source blame history raw
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::cbor::{self, Value};
5
use crate::graph::{AtomKey, DepOracle, FileGraph, Materialized, Vertex};
6
use crate::hash::KeyId;
7
use crate::hash::{ChangeHash, ObjectRef};
8
use crate::ids::NodeId;
9
use crate::objects::{
10
    self as o, AtomRef, Change, ChangeKind, Edit, Hlc, Mode, NodeKind, ObjectError, TreeOp,
11
};
12
use crate::tree::{TreeGraph, TreeView};
13
use std::cell::RefCell;
14
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
15
use std::rc::Rc;
16
17
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
18
pub enum StateError {
19
    #[error("change {1} depends on {0}, which is not in the state")]
20
    MissingDep(ChangeHash, ChangeHash),
21
}
22
23
/// One `Replace` edit: `(change, old blob, new blob)`.
24
type ReplaceRecord = (ChangeHash, Option<ObjectRef>, Option<ObjectRef>);
25
26
/// What a state remembers about a change: everything but its operations,
27
/// which live in the graphs once applied (and in the store).
28
#[derive(Clone, Debug, PartialEq, Eq)]
29
pub struct ChangeMeta {
30
    pub change_id: crate::ids::ChangeId,
31
    pub deps: Vec<ChangeHash>,
32
    pub author: KeyId,
33
    pub hlc: Hlc,
34
    pub message: String,
35
    pub kind: ChangeKind,
36
}
37
38
impl ChangeMeta {
39
    pub fn of(c: &Change) -> ChangeMeta {
40
        ChangeMeta {
41
            change_id: c.change_id,
42
            deps: c.deps.clone(),
43
            author: c.author,
44
            hlc: c.hlc,
45
            message: c.message.clone(),
46
            kind: c.kind,
47
        }
48
    }
49
}
50
51
#[derive(Default, Clone)]
52
pub struct State {
53
    changes: HashMap<ChangeHash, ChangeMeta>,
54
    tree: TreeGraph,
55
    files: HashMap<NodeId, FileGraph>,
56
    replaces: HashMap<NodeId, Vec<ReplaceRecord>>,
57
    closure: RefCell<HashMap<ChangeHash, Rc<HashSet<ChangeHash>>>>,
58
}
59
60
#[derive(Clone, Debug, PartialEq, Eq)]
61
pub enum FileContent {
62
    Text {
63
        bytes: Vec<u8>,
64
        materialized: Materialized,
65
    },
66
    /// Live blobs; more than one is a replace conflict (spec §9.3).
67
    Opaque {
68
        blobs: Vec<ObjectRef>,
69
    },
70
    Dir,
71
}
72
73
#[derive(Clone, Debug, PartialEq, Eq)]
74
pub struct FileState {
75
    pub node: NodeId,
76
    pub kind: NodeKind,
77
    pub mode: Mode,
78
    pub content: FileContent,
79
}
80
81
impl FileState {
82
    pub fn is_conflicted(&self) -> bool {
83
        match &self.content {
84
            FileContent::Text { materialized, .. } => materialized.is_conflicted(),
85
            FileContent::Opaque { blobs } => blobs.len() > 1,
86
            FileContent::Dir => false,
87
        }
88
    }
89
}
90
91
/// See `State::conflict_regions`.
92
#[derive(Clone, Debug, Default, PartialEq, Eq)]
93
pub struct ConflictRegions {
94
    pub plain: Vec<(AtomKey, Vec<u8>)>,
95
    pub region_atoms: Vec<AtomKey>,
96
    pub contributors: Vec<ChangeHash>,
97
}
98
99
/// The whole tree of a state, materialized.
100
#[derive(Clone, Debug, Default)]
101
pub struct Snapshot {
102
    pub tree: TreeView,
103
    pub files: BTreeMap<String, FileState>,
104
}
105
106
impl Snapshot {
107
    pub fn is_conflicted(&self) -> bool {
108
        self.tree.is_conflicted() || self.files.values().any(|f| f.is_conflicted())
109
    }
110
}
111
112
fn vertex(r: AtomRef) -> Vertex {
113
    match r {
114
        AtomRef::Start => Vertex::Start,
115
        AtomRef::End => Vertex::End,
116
        AtomRef::Atom { change, index } => Vertex::Atom((change, index)),
117
    }
118
}
119
120
/// Replace self references in a tree op with the concrete change hash.
121
pub fn resolve_tree_op(op: &TreeOp, me: ChangeHash) -> TreeOp {
122
    match op {
123
        TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode {
124
            node: *node,
125
            placement: placement.resolve(me),
126
        },
127
        TreeOp::Move {
128
            node,
129
            old,
130
            parent,
131
            name,
132
        } => TreeOp::Move {
133
            node: *node,
134
            old: old.resolve(me),
135
            parent: *parent,
136
            name: name.clone(),
137
        },
138
        TreeOp::SetMode { node, old, mode } => TreeOp::SetMode {
139
            node: *node,
140
            old: old.resolve(me),
141
            mode: *mode,
142
        },
143
        add @ TreeOp::AddNode { .. } => add.clone(),
144
    }
145
}
146
147
impl State {
148
    pub fn new() -> Self {
149
        Self::default()
150
    }
151
152
    pub fn len(&self) -> usize {
153
        self.changes.len()
154
    }
155
    pub fn is_empty(&self) -> bool {
156
        self.changes.is_empty()
157
    }
158
    pub fn contains(&self, h: &ChangeHash) -> bool {
159
        self.changes.contains_key(h)
160
    }
161
    pub fn get(&self, h: &ChangeHash) -> Option<&ChangeMeta> {
162
        self.changes.get(h)
163
    }
164
    pub fn changes(&self) -> impl Iterator<Item = (&ChangeHash, &ChangeMeta)> {
165
        self.changes.iter()
166
    }
167
    pub fn tree(&self) -> &TreeGraph {
168
        &self.tree
169
    }
170
    pub fn file(&self, node: &NodeId) -> Option<&FileGraph> {
171
        self.files.get(node)
172
    }
173
174
    /// Add a change whose dependencies are already present.
175
    pub fn add(&mut self, change: Change) -> Result<ChangeHash, StateError> {
176
        let h = change.hash();
177
        if self.changes.contains_key(&h) {
178
            return Ok(h);
179
        }
180
        for d in &change.deps {
181
            if !self.changes.contains_key(d) {
182
                return Err(StateError::MissingDep(*d, h));
183
            }
184
        }
185
        Ok(self.add_unchecked(change))
186
    }
187
188
    /// Add a change in any order; call `validate` once all are in.
189
    pub fn add_unchecked(&mut self, change: Change) -> ChangeHash {
190
        let h = change.hash();
191
        if self.changes.contains_key(&h) {
192
            return h;
193
        }
194
        self.apply(h, &change);
195
        self.changes.insert(h, ChangeMeta::of(&change));
196
        self.closure.borrow_mut().clear();
197
        h
198
    }
199
200
    // ------------------------------------------------------------ checkpoints (spec §11)
201
202
    /// Serialize the whole state: change metadata, tree, file graphs, replaces.
203
    pub fn checkpoint_payload(&self) -> Vec<u8> {
204
        let mut hashes: Vec<&ChangeHash> = self.changes.keys().collect();
205
        hashes.sort();
206
        let changes = Value::Array(
207
            hashes
208
                .into_iter()
209
                .map(|h| {
210
                    let m = &self.changes[h];
211
                    Value::Array(vec![
212
                        Value::Bytes(h.0.to_vec()),
213
                        Value::Bytes(m.change_id.0.to_vec()),
214
                        Value::Array(m.deps.iter().map(|d| Value::Bytes(d.0.to_vec())).collect()),
215
                        Value::Text(m.message.clone()),
216
                        Value::Uint(m.hlc.wall_ms),
217
                        Value::Uint(m.hlc.logical as u64),
218
                        Value::Uint(m.kind.tag()),
219
                        Value::Bytes(m.author.0.to_vec()),
220
                    ])
221
                })
222
                .collect(),
223
        );
224
        let mut nodes: Vec<&NodeId> = self.files.keys().collect();
225
        nodes.sort();
226
        let files = Value::Array(
227
            nodes
228
                .into_iter()
229
                .map(|n| Value::Array(vec![Value::Bytes(n.0.to_vec()), self.files[n].to_value()]))
230
                .collect(),
231
        );
232
        let mut rnodes: Vec<&NodeId> = self.replaces.keys().collect();
233
        rnodes.sort();
234
        let opt =
235
            |o: &Option<ObjectRef>| o.map(|r| Value::Bytes(r.0.to_vec())).unwrap_or(Value::Null);
236
        let replaces = Value::Array(
237
            rnodes
238
                .into_iter()
239
                .map(|n| {
240
                    Value::Array(vec![
241
                        Value::Bytes(n.0.to_vec()),
242
                        Value::Array(
243
                            self.replaces[n]
244
                                .iter()
245
                                .map(|(c, o, nw)| {
246
                                    Value::Array(vec![Value::Bytes(c.0.to_vec()), opt(o), opt(nw)])
247
                                })
248
                                .collect(),
249
                        ),
250
                    ])
251
                })
252
                .collect(),
253
        );
254
        cbor::encode(&Value::map([
255
            (0, Value::Uint(o::FORMAT_VERSION)),
256
            (1, changes),
257
            (2, self.tree.to_value()),
258
            (3, files),
259
            (4, replaces),
260
        ]))
261
    }
262
263
    pub fn from_checkpoint_payload(bytes: &[u8]) -> Result<State, ObjectError> {
264
        let v = cbor::decode(bytes)?;
265
        let m = o::map(&v, "checkpoint payload")?;
266
        o::check_version(m)?;
267
        let mut st = State::new();
268
        for c in o::array(o::field(m, 1, "changes")?, "changes")? {
269
            let e = o::array(c, "change meta")?;
270
            if e.len() != 8 {
271
                return Err(ObjectError::Schema("change meta: expected 8 fields".into()));
272
            }
273
            let hash = o::oref(&e[0], "hash")?;
274
            let meta = ChangeMeta {
275
                change_id: crate::ids::ChangeId::from_slice(o::bytes(&e[1], "change id")?)
276
                    .ok_or_else(|| ObjectError::Schema("change id: expected 16 bytes".into()))?,
277
                deps: o::array(&e[2], "deps")?
278
                    .iter()
279
                    .map(|d| o::oref(d, "dep"))
280
                    .collect::<Result<Vec<_>, _>>()?,
281
                message: o::text(&e[3], "message")?.to_owned(),
282
                hlc: Hlc {
283
                    wall_ms: o::uint(&e[4], "wall")?,
284
                    logical: o::u32_(&e[5], "logical")?,
285
                },
286
                kind: ChangeKind::from_tag(o::uint(&e[6], "kind")?)?,
287
                author: o::oref(&e[7], "author")?,
288
            };
289
            st.changes.insert(hash, meta);
290
        }
291
        st.tree = TreeGraph::from_value(o::field(m, 2, "tree")?)?;
292
        for f in o::array(o::field(m, 3, "files")?, "files")? {
293
            let e = o::array(f, "file")?;
294
            if e.len() != 2 {
295
                return Err(ObjectError::Schema("file: expected [node, graph]".into()));
296
            }
297
            let node = NodeId::from_slice(o::bytes(&e[0], "node")?)
298
                .ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?;
299
            st.files.insert(node, FileGraph::from_value(&e[1])?);
300
        }
301
        let opt = |v: &Value| -> Result<Option<ObjectRef>, ObjectError> {
302
            if v.is_null() {
303
                Ok(None)
304
            } else {
305
                o::oref(v, "blob").map(Some)
306
            }
307
        };
308
        for r in o::array(o::field(m, 4, "replaces")?, "replaces")? {
309
            let e = o::array(r, "replace")?;
310
            if e.len() != 2 {
311
                return Err(ObjectError::Schema(
312
                    "replace: expected [node, records]".into(),
313
                ));
314
            }
315
            let node = NodeId::from_slice(o::bytes(&e[0], "node")?)
316
                .ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?;
317
            let mut recs = Vec::new();
318
            for rec in o::array(&e[1], "records")? {
319
                let x = o::array(rec, "record")?;
320
                if x.len() != 3 {
321
                    return Err(ObjectError::Schema("record: expected 3 fields".into()));
322
                }
323
                recs.push((o::oref(&x[0], "change")?, opt(&x[1])?, opt(&x[2])?));
324
            }
325
            st.replaces.insert(node, recs);
326
        }
327
        Ok(st)
328
    }
329
330
    pub fn validate(&self) -> Result<(), StateError> {
331
        for (h, c) in &self.changes {
332
            for d in &c.deps {
333
                if !self.changes.contains_key(d) {
334
                    return Err(StateError::MissingDep(*d, *h));
335
                }
336
            }
337
        }
338
        Ok(())
339
    }
340
341
    fn apply(&mut self, h: ChangeHash, c: &Change) {
342
        for (i, op) in c.tree_ops.iter().enumerate() {
343
            self.tree.apply(h, i as u32, &resolve_tree_op(op, h));
344
        }
345
        let mut counters: HashMap<NodeId, u32> = HashMap::new();
346
        for e in &c.edits {
347
            match e {
348
                Edit::Insert {
349
                    node,
350
                    after,
351
                    before,
352
                    lines,
353
                } => {
354
                    let idx = counters.entry(*node).or_default();
355
                    let first = *idx;
356
                    *idx += lines.len() as u32;
357
                    self.files.entry(*node).or_default().insert(
358
                        h,
359
                        first,
360
                        vertex(after.resolve(h)),
361
                        vertex(before.resolve(h)),
362
                        lines,
363
                    );
364
                }
365
                Edit::Delete { node, atoms } => {
366
                    let g = self.files.entry(*node).or_default();
367
                    for r in atoms {
368
                        g.delete(h, &r.resolve(h));
369
                    }
370
                }
371
                Edit::Replace { node, old, new } => {
372
                    self.replaces
373
                        .entry(*node)
374
                        .or_default()
375
                        .push((h, *old, *new));
376
                }
377
            }
378
        }
379
    }
380
381
    /// Transitive dependencies of `h`, excluding `h` itself.
382
    pub fn closure(&self, h: &ChangeHash) -> Rc<HashSet<ChangeHash>> {
383
        if let Some(c) = self.closure.borrow().get(h) {
384
            return c.clone();
385
        }
386
        let mut out = HashSet::new();
387
        let mut stack: Vec<ChangeHash> = self
388
            .changes
389
            .get(h)
390
            .map(|c| c.deps.clone())
391
            .unwrap_or_default();
392
        while let Some(d) = stack.pop() {
393
            if out.insert(d) {
394
                if let Some(c) = self.changes.get(&d) {
395
                    stack.extend(c.deps.iter().copied());
396
                }
397
            }
398
        }
399
        let rc = Rc::new(out);
400
        self.closure.borrow_mut().insert(*h, rc.clone());
401
        rc
402
    }
403
404
    /// Changes that no other change depends on directly, sorted.
405
    pub fn heads(&self) -> Vec<ChangeHash> {
406
        let depended: HashSet<&ChangeHash> =
407
            self.changes.values().flat_map(|c| c.deps.iter()).collect();
408
        let mut heads: Vec<ChangeHash> = self
409
            .changes
410
            .keys()
411
            .filter(|h| !depended.contains(h))
412
            .copied()
413
            .collect();
414
        heads.sort();
415
        heads
416
    }
417
418
    /// Maximal elements among `hashes` (those not in the closure of another).
419
    pub fn maximal(&self, hashes: &[ChangeHash]) -> Vec<ChangeHash> {
420
        let set: BTreeSet<ChangeHash> = hashes.iter().copied().collect();
421
        set.iter()
422
            .filter(|h| !set.iter().any(|o| o != *h && self.closure(o).contains(h)))
423
            .copied()
424
            .collect()
425
    }
426
427
    pub fn materialize_file(&self, node: &NodeId) -> Materialized {
428
        self.files
429
            .get(node)
430
            .map(|g| g.materialize(self))
431
            .unwrap_or_default()
432
    }
433
434
    /// Live lines of a text node in materialized order.
435
    pub fn live_lines(&self, node: &NodeId) -> Vec<(AtomKey, Vec<u8>)> {
436
        let Some(g) = self.files.get(node) else {
437
            return vec![];
438
        };
439
        let m = g.materialize(self);
440
        m.live_atoms()
441
            .into_iter()
442
            .map(|a| (a, g.content(&a).unwrap_or_default().to_vec()))
443
            .collect()
444
    }
445
446
    /// Live blobs of an opaque node: every `new` that no later `Replace` retired.
447
    pub fn live_blobs(&self, node: &NodeId) -> Vec<ObjectRef> {
448
        let Some(rs) = self.replaces.get(node) else {
449
            return vec![];
450
        };
451
        let olds: HashSet<ObjectRef> = rs.iter().filter_map(|(_, o, _)| *o).collect();
452
        let mut live: Vec<ObjectRef> = rs
453
            .iter()
454
            .filter_map(|(_, _, n)| *n)
455
            .filter(|n| !olds.contains(n))
456
            .collect();
457
        live.sort();
458
        live.dedup();
459
        live
460
    }
461
462
    /// Short label for conflict markers: `changeid-prefix "message"`.
463
    pub fn label(&self, h: &ChangeHash) -> String {
464
        match self.changes.get(h) {
465
            Some(c) => format!(
466
                "{} \"{}\"",
467
                c.change_id.short(8),
468
                c.message.lines().next().unwrap_or("")
469
            ),
470
            None => format!("#{}", h.short(12)),
471
        }
472
    }
473
474
    /// For resolving a conflicted text node (spec §9.6): the live atoms that
475
    /// are not part of any conflict region in order, every live atom that is
476
    /// part of a region, and every change that contributed to a region.
477
    /// The changes that introduced the live blobs of an opaque node, for a
478
    /// replace conflict (spec §9.3).
479
    pub fn blob_contributors(&self, node: &NodeId, blobs: &[ObjectRef]) -> Vec<ChangeHash> {
480
        let mut out: Vec<ChangeHash> = Vec::new();
481
        for (h, _, new) in self.replaces.get(node).map(|v| v.as_slice()).unwrap_or(&[]) {
482
            if new.is_some_and(|b| blobs.contains(&b)) && !out.contains(h) {
483
                out.push(*h);
484
            }
485
        }
486
        out.sort();
487
        out
488
    }
489
490
    pub fn conflict_regions(&self, node: &NodeId) -> ConflictRegions {
491
        let mut out = ConflictRegions::default();
492
        let Some(g) = self.files.get(node) else {
493
            return out;
494
        };
495
        let m = g.materialize(self);
496
        let mut contributors = BTreeSet::new();
497
        for seg in &m.segments {
498
            match seg {
499
                crate::graph::Segment::Lines(atoms) => {
500
                    for a in atoms {
501
                        out.plain
502
                            .push((*a, g.content(a).unwrap_or_default().to_vec()));
503
                    }
504
                }
505
                crate::graph::Segment::Conflict(sides) => {
506
                    for side in sides {
507
                        for a in side {
508
                            out.region_atoms.push(*a);
509
                            contributors.insert(a.0);
510
                        }
511
                    }
512
                }
513
                crate::graph::Segment::Zombie { deleted, inserted } => {
514
                    for a in deleted {
515
                        contributors.insert(a.0);
516
                        contributors.extend(g.deleters(a).copied());
517
                    }
518
                    for a in inserted {
519
                        out.region_atoms.push(*a);
520
                        contributors.insert(a.0);
521
                    }
522
                }
523
            }
524
        }
525
        out.contributors = contributors.into_iter().collect();
526
        out
527
    }
528
529
    pub fn render_file(&self, node: &NodeId) -> (Vec<u8>, Materialized) {
530
        let Some(g) = self.files.get(node) else {
531
            return (Vec::new(), Materialized::default());
532
        };
533
        let m = g.materialize(self);
534
        let bytes = g.render(&m, &|h| self.label(h));
535
        (bytes, m)
536
    }
537
538
    pub fn materialize(&self) -> Snapshot {
539
        let tree = self.tree.view();
540
        let mut files = BTreeMap::new();
541
        for (path, e) in &tree.entries {
542
            let content = match e.kind {
543
                NodeKind::Dir => FileContent::Dir,
544
                NodeKind::Symlink | NodeKind::Subrepo => FileContent::Opaque {
545
                    blobs: self.live_blobs(&e.node),
546
                },
547
                NodeKind::File if e.mode.opaque => FileContent::Opaque {
548
                    blobs: self.live_blobs(&e.node),
549
                },
550
                NodeKind::File => {
551
                    let (bytes, materialized) = self.render_file(&e.node);
552
                    FileContent::Text {
553
                        bytes,
554
                        materialized,
555
                    }
556
                }
557
            };
558
            files.insert(
559
                path.clone(),
560
                FileState {
561
                    node: e.node,
562
                    kind: e.kind,
563
                    mode: e.mode,
564
                    content,
565
                },
566
            );
567
        }
568
        Snapshot { tree, files }
569
    }
570
}
571
572
impl DepOracle for State {
573
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
574
        a == b || self.closure(a).contains(b)
575
    }
576
}
577
578
#[cfg(test)]
579
mod tests {
580
    use super::*;
581
    use crate::ids::ChangeId;
582
    use crate::objects::{AtomRange, ChangeKind, Hlc, OpRef};
583
584
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>) -> Change {
585
        let mut c = Change {
586
            change_id: ChangeId::random(),
587
            deps,
588
            author: ObjectRef::ZERO,
589
            hlc: Hlc {
590
                wall_ms: 1,
591
                logical: 0,
592
            },
593
            message: "m".into(),
594
            tree_ops,
595
            edits,
596
            kind: ChangeKind::Normal,
597
        };
598
        c.normalize();
599
        c
600
    }
601
602
    #[test]
603
    fn build_edit_and_materialize() {
604
        let mut st = State::new();
605
        let f = NodeId::random();
606
        let c1 = change(
607
            vec![],
608
            vec![TreeOp::AddNode {
609
                node: f,
610
                kind: NodeKind::File,
611
                mode: Mode::TEXT,
612
                parent: NodeId::ROOT,
613
                name: "a.txt".into(),
614
            }],
615
            vec![Edit::Insert {
616
                node: f,
617
                after: AtomRef::Start,
618
                before: AtomRef::End,
619
                lines: vec![b"one\n".to_vec(), b"two\n".to_vec()],
620
            }],
621
        );
622
        let h1 = st.add(c1).unwrap();
623
        // Depends on h1: append a line and delete "one".
624
        let c2 = change(
625
            vec![h1],
626
            vec![],
627
            vec![
628
                Edit::Delete {
629
                    node: f,
630
                    atoms: vec![AtomRange {
631
                        change: h1,
632
                        from: 0,
633
                        to: 1,
634
                    }],
635
                },
636
                Edit::Insert {
637
                    node: f,
638
                    after: AtomRef::Atom {
639
                        change: h1,
640
                        index: 1,
641
                    },
642
                    before: AtomRef::End,
643
                    lines: vec![b"three\n".to_vec()],
644
                },
645
            ],
646
        );
647
        let h2 = st.add(c2).unwrap();
648
        assert_eq!(st.heads(), vec![h2]);
649
        assert!(st.depends_on(&h2, &h1));
650
        assert!(!st.depends_on(&h1, &h2));
651
        let snap = st.materialize();
652
        assert!(!snap.is_conflicted());
653
        let a = &snap.files["a.txt"];
654
        assert!(matches!(&a.content, FileContent::Text { bytes, .. } if bytes == b"two\nthree\n"));
655
        assert_eq!(st.live_lines(&f).len(), 2);
656
    }
657
658
    #[test]
659
    fn missing_dependency_is_rejected() {
660
        let mut st = State::new();
661
        let c = change(vec![ObjectRef([1; 32])], vec![], vec![]);
662
        assert!(matches!(st.add(c), Err(StateError::MissingDep(..))));
663
    }
664
665
    #[test]
666
    fn self_references_and_multi_insert_indexing() {
667
        let mut st = State::new();
668
        let f = NodeId::random();
669
        let me = OpRef {
670
            change: ObjectRef::ZERO,
671
            index: 0,
672
        };
673
        let c1 = change(
674
            vec![],
675
            vec![
676
                TreeOp::AddNode {
677
                    node: f,
678
                    kind: NodeKind::File,
679
                    mode: Mode::TEXT,
680
                    parent: NodeId::ROOT,
681
                    name: "x".into(),
682
                },
683
                TreeOp::SetMode {
684
                    node: f,
685
                    old: me,
686
                    mode: Mode {
687
                        exec: true,
688
                        opaque: false,
689
                    },
690
                },
691
            ],
692
            vec![
693
                Edit::Insert {
694
                    node: f,
695
                    after: AtomRef::Start,
696
                    before: AtomRef::End,
697
                    lines: vec![b"a\n".to_vec(), b"c\n".to_vec()],
698
                },
699
                // Insert between the two lines just inserted, using self references.
700
                Edit::Insert {
701
                    node: f,
702
                    after: AtomRef::Atom {
703
                        change: ObjectRef::ZERO,
704
                        index: 0,
705
                    },
706
                    before: AtomRef::Atom {
707
                        change: ObjectRef::ZERO,
708
                        index: 1,
709
                    },
710
                    lines: vec![b"b\n".to_vec()],
711
                },
712
            ],
713
        );
714
        st.add(c1).unwrap();
715
        let snap = st.materialize();
716
        let x = &snap.files["x"];
717
        assert_eq!(
718
            x.mode,
719
            Mode {
720
                exec: true,
721
                opaque: false
722
            }
723
        );
724
        assert!(matches!(&x.content, FileContent::Text { bytes, .. } if bytes == b"a\nb\nc\n"));
725
    }
726
727
    #[test]
728
    fn opaque_replace_and_conflict() {
729
        let mut st = State::new();
730
        let f = NodeId::random();
731
        let b1 = ObjectRef([1; 32]);
732
        let b2 = ObjectRef([2; 32]);
733
        let b3 = ObjectRef([3; 32]);
734
        let h1 = st
735
            .add(change(
736
                vec![],
737
                vec![TreeOp::AddNode {
738
                    node: f,
739
                    kind: NodeKind::File,
740
                    mode: Mode::OPAQUE,
741
                    parent: NodeId::ROOT,
742
                    name: "img".into(),
743
                }],
744
                vec![Edit::Replace {
745
                    node: f,
746
                    old: None,
747
                    new: Some(b1),
748
                }],
749
            ))
750
            .unwrap();
751
        assert_eq!(st.live_blobs(&f), vec![b1]);
752
        st.add(change(
753
            vec![h1],
754
            vec![],
755
            vec![Edit::Replace {
756
                node: f,
757
                old: Some(b1),
758
                new: Some(b2),
759
            }],
760
        ))
761
        .unwrap();
762
        st.add(change(
763
            vec![h1],
764
            vec![],
765
            vec![Edit::Replace {
766
                node: f,
767
                old: Some(b1),
768
                new: Some(b3),
769
            }],
770
        ))
771
        .unwrap();
772
        let snap = st.materialize();
773
        assert!(snap.is_conflicted());
774
        assert!(
775
            matches!(&snap.files["img"].content, FileContent::Opaque { blobs } if blobs == &vec![b2, b3])
776
        );
777
    }
778
779
    #[test]
780
    fn checkpoint_payload_round_trip_and_replay() {
781
        let mut st = State::new();
782
        let f = NodeId::random();
783
        let h1 = st
784
            .add(change(
785
                vec![],
786
                vec![TreeOp::AddNode {
787
                    node: f,
788
                    kind: NodeKind::File,
789
                    mode: Mode::TEXT,
790
                    parent: NodeId::ROOT,
791
                    name: "a".into(),
792
                }],
793
                vec![Edit::Insert {
794
                    node: f,
795
                    after: AtomRef::Start,
796
                    before: AtomRef::End,
797
                    lines: vec![b"1\n".to_vec(), b"2\n".to_vec()],
798
                }],
799
            ))
800
            .unwrap();
801
        let h2 = st
802
            .add(change(
803
                vec![h1],
804
                vec![],
805
                vec![Edit::Delete {
806
                    node: f,
807
                    atoms: vec![AtomRange {
808
                        change: h1,
809
                        from: 0,
810
                        to: 1,
811
                    }],
812
                }],
813
            ))
814
            .unwrap();
815
        let payload = st.checkpoint_payload();
816
        let back = State::from_checkpoint_payload(&payload).unwrap();
817
        assert_eq!(back.len(), 2);
818
        assert_eq!(back.heads(), vec![h2]);
819
        assert_eq!(back.get(&h1).unwrap().message, "m");
820
        assert_eq!(back.checkpoint_payload(), payload, "deterministic");
821
        // Replaying a further change onto the restored state equals a full build.
822
        let c3 = change(
823
            vec![h2],
824
            vec![],
825
            vec![Edit::Insert {
826
                node: f,
827
                after: AtomRef::Atom {
828
                    change: h1,
829
                    index: 1,
830
                },
831
                before: AtomRef::End,
832
                lines: vec![b"3\n".to_vec()],
833
            }],
834
        );
835
        let mut full = st.clone();
836
        full.add(c3.clone()).unwrap();
837
        let mut restored = back;
838
        restored.add(c3).unwrap();
839
        let (a, b) = (full.materialize(), restored.materialize());
840
        assert_eq!(
841
            a.files.keys().collect::<Vec<_>>(),
842
            b.files.keys().collect::<Vec<_>>()
843
        );
844
        assert_eq!(a.files["a"].content, b.files["a"].content);
845
        assert!(
846
            matches!(&b.files["a"].content, FileContent::Text { bytes, .. } if bytes == b"2\n3\n")
847
        );
848
        assert!(State::from_checkpoint_payload(b"nonsense").is_err());
849
    }
850
851
    #[test]
852
    fn maximal_elements() {
853
        let mut st = State::new();
854
        let h1 = st.add(change(vec![], vec![], vec![])).unwrap();
855
        let h2 = st.add(change(vec![h1], vec![], vec![])).unwrap();
856
        let h3 = st.add(change(vec![], vec![], vec![])).unwrap();
857
        let mut m = st.maximal(&[h1, h2, h3]);
858
        m.sort();
859
        let mut want = vec![h2, h3];
860
        want.sort();
861
        assert_eq!(m, want);
862
        assert_eq!(st.heads(), want);
863
    }
864
}