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