OXYGENHUB
# 07eb99be126e oxygen / crates / oxy-core / src / state.rs
30.1 KiB · Rust · last changed Commit and status no longer slow down with the number of heads 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
    /// sorted. One walk down from all of them at once: whatever the walk
420
    /// reaches is below some other element. Linear in the changes reached,
421
    /// where comparing every pair's closures was quadratic in the number
422
    /// of heads, which a bookmark of independent changes has many of.
423
    pub fn maximal(&self, hashes: &[ChangeHash]) -> Vec<ChangeHash> {
424
        let set: BTreeSet<ChangeHash> = hashes.iter().copied().collect();
425
        let mut below: HashSet<ChangeHash> = HashSet::new();
426
        let mut stack: Vec<ChangeHash> = set
427
            .iter()
428
            .filter_map(|h| self.changes.get(h))
429
            .flat_map(|c| c.deps.iter().copied())
430
            .collect();
431
        while let Some(d) = stack.pop() {
432
            if below.insert(d) {
433
                if let Some(c) = self.changes.get(&d) {
434
                    stack.extend(c.deps.iter().copied());
435
                }
436
            }
437
        }
438
        set.into_iter().filter(|h| !below.contains(h)).collect()
439
    }
440
441
    pub fn materialize_file(&self, node: &NodeId) -> Materialized {
442
        self.files
443
            .get(node)
444
            .map(|g| g.materialize(self))
445
            .unwrap_or_default()
446
    }
447
448
    /// Live lines of a text node in materialized order.
449
    pub fn live_lines(&self, node: &NodeId) -> Vec<(AtomKey, Vec<u8>)> {
450
        let Some(g) = self.files.get(node) else {
451
            return vec![];
452
        };
453
        let m = g.materialize(self);
454
        m.live_atoms()
455
            .into_iter()
456
            .map(|a| (a, g.content(&a).unwrap_or_default().to_vec()))
457
            .collect()
458
    }
459
460
    /// Live blobs of an opaque node: every `new` that no later `Replace` retired.
461
    pub fn live_blobs(&self, node: &NodeId) -> Vec<ObjectRef> {
462
        let Some(rs) = self.replaces.get(node) else {
463
            return vec![];
464
        };
465
        let olds: HashSet<ObjectRef> = rs.iter().filter_map(|(_, o, _)| *o).collect();
466
        let mut live: Vec<ObjectRef> = rs
467
            .iter()
468
            .filter_map(|(_, _, n)| *n)
469
            .filter(|n| !olds.contains(n))
470
            .collect();
471
        live.sort();
472
        live.dedup();
473
        live
474
    }
475
476
    /// Short label for conflict markers: `changeid-prefix "message"`.
477
    pub fn label(&self, h: &ChangeHash) -> String {
478
        match self.changes.get(h) {
479
            Some(c) => format!(
480
                "{} \"{}\"",
481
                c.change_id.short(8),
482
                c.message.lines().next().unwrap_or("")
483
            ),
484
            None => format!("#{}", h.short(12)),
485
        }
486
    }
487
488
    /// For resolving a conflicted text node (spec §9.6): the live atoms that
489
    /// are not part of any conflict region in order, every live atom that is
490
    /// part of a region, and every change that contributed to a region.
491
    /// The changes that introduced the live blobs of an opaque node, for a
492
    /// replace conflict (spec §9.3).
493
    pub fn blob_contributors(&self, node: &NodeId, blobs: &[ObjectRef]) -> Vec<ChangeHash> {
494
        let mut out: Vec<ChangeHash> = Vec::new();
495
        for (h, _, new) in self.replaces.get(node).map(|v| v.as_slice()).unwrap_or(&[]) {
496
            if new.is_some_and(|b| blobs.contains(&b)) && !out.contains(h) {
497
                out.push(*h);
498
            }
499
        }
500
        out.sort();
501
        out
502
    }
503
504
    pub fn conflict_regions(&self, node: &NodeId) -> ConflictRegions {
505
        let mut out = ConflictRegions::default();
506
        let Some(g) = self.files.get(node) else {
507
            return out;
508
        };
509
        let m = g.materialize(self);
510
        let mut contributors = BTreeSet::new();
511
        for seg in &m.segments {
512
            match seg {
513
                crate::graph::Segment::Lines(atoms) => {
514
                    for a in atoms {
515
                        out.plain
516
                            .push((*a, g.content(a).unwrap_or_default().to_vec()));
517
                    }
518
                }
519
                crate::graph::Segment::Conflict(sides) => {
520
                    for side in sides {
521
                        for a in side {
522
                            out.region_atoms.push(*a);
523
                            contributors.insert(a.0);
524
                        }
525
                    }
526
                }
527
                crate::graph::Segment::Zombie { deleted, inserted } => {
528
                    for a in deleted {
529
                        contributors.insert(a.0);
530
                        contributors.extend(g.deleters(a).copied());
531
                    }
532
                    for a in inserted {
533
                        out.region_atoms.push(*a);
534
                        contributors.insert(a.0);
535
                    }
536
                }
537
            }
538
        }
539
        out.contributors = contributors.into_iter().collect();
540
        out
541
    }
542
543
    pub fn render_file(&self, node: &NodeId) -> (Vec<u8>, Materialized) {
544
        let Some(g) = self.files.get(node) else {
545
            return (Vec::new(), Materialized::default());
546
        };
547
        let m = g.materialize(self);
548
        let bytes = g.render(&m, &|h| self.label(h));
549
        (bytes, m)
550
    }
551
552
    pub fn materialize(&self) -> Snapshot {
553
        let tree = self.tree.view();
554
        let mut files = BTreeMap::new();
555
        for (path, e) in &tree.entries {
556
            let content = match e.kind {
557
                NodeKind::Dir => FileContent::Dir,
558
                NodeKind::Symlink | NodeKind::Subrepo => FileContent::Opaque {
559
                    blobs: self.live_blobs(&e.node),
560
                },
561
                NodeKind::File if e.mode.opaque => FileContent::Opaque {
562
                    blobs: self.live_blobs(&e.node),
563
                },
564
                NodeKind::File => {
565
                    let (bytes, materialized) = self.render_file(&e.node);
566
                    FileContent::Text {
567
                        bytes,
568
                        materialized,
569
                    }
570
                }
571
            };
572
            files.insert(
573
                path.clone(),
574
                FileState {
575
                    node: e.node,
576
                    kind: e.kind,
577
                    mode: e.mode,
578
                    content,
579
                },
580
            );
581
        }
582
        Snapshot { tree, files }
583
    }
584
}
585
586
impl DepOracle for State {
587
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
588
        a == b || self.closure(a).contains(b)
589
    }
590
}
591
592
#[cfg(test)]
593
mod tests {
594
    use super::*;
595
    use crate::ids::ChangeId;
596
    use crate::objects::{AtomRange, ChangeKind, Hlc, OpRef};
597
598
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>) -> Change {
599
        let mut c = Change {
600
            change_id: ChangeId::random(),
601
            deps,
602
            author: ObjectRef::ZERO,
603
            hlc: Hlc {
604
                wall_ms: 1,
605
                logical: 0,
606
            },
607
            message: "m".into(),
608
            tree_ops,
609
            edits,
610
            kind: ChangeKind::Normal,
611
        };
612
        c.normalize();
613
        c
614
    }
615
616
    #[test]
617
    fn build_edit_and_materialize() {
618
        let mut st = State::new();
619
        let f = NodeId::random();
620
        let c1 = change(
621
            vec![],
622
            vec![TreeOp::AddNode {
623
                node: f,
624
                kind: NodeKind::File,
625
                mode: Mode::TEXT,
626
                parent: NodeId::ROOT,
627
                name: "a.txt".into(),
628
            }],
629
            vec![Edit::Insert {
630
                node: f,
631
                after: AtomRef::Start,
632
                before: AtomRef::End,
633
                lines: vec![b"one\n".to_vec(), b"two\n".to_vec()],
634
            }],
635
        );
636
        let h1 = st.add(c1).unwrap();
637
        // Depends on h1: append a line and delete "one".
638
        let c2 = change(
639
            vec![h1],
640
            vec![],
641
            vec![
642
                Edit::Delete {
643
                    node: f,
644
                    atoms: vec![AtomRange {
645
                        change: h1,
646
                        from: 0,
647
                        to: 1,
648
                    }],
649
                },
650
                Edit::Insert {
651
                    node: f,
652
                    after: AtomRef::Atom {
653
                        change: h1,
654
                        index: 1,
655
                    },
656
                    before: AtomRef::End,
657
                    lines: vec![b"three\n".to_vec()],
658
                },
659
            ],
660
        );
661
        let h2 = st.add(c2).unwrap();
662
        assert_eq!(st.heads(), vec![h2]);
663
        assert!(st.depends_on(&h2, &h1));
664
        assert!(!st.depends_on(&h1, &h2));
665
        let snap = st.materialize();
666
        assert!(!snap.is_conflicted());
667
        let a = &snap.files["a.txt"];
668
        assert!(matches!(&a.content, FileContent::Text { bytes, .. } if bytes == b"two\nthree\n"));
669
        assert_eq!(st.live_lines(&f).len(), 2);
670
    }
671
672
    #[test]
673
    fn missing_dependency_is_rejected() {
674
        let mut st = State::new();
675
        let c = change(vec![ObjectRef([1; 32])], vec![], vec![]);
676
        assert!(matches!(st.add(c), Err(StateError::MissingDep(..))));
677
    }
678
679
    #[test]
680
    fn self_references_and_multi_insert_indexing() {
681
        let mut st = State::new();
682
        let f = NodeId::random();
683
        let me = OpRef {
684
            change: ObjectRef::ZERO,
685
            index: 0,
686
        };
687
        let c1 = change(
688
            vec![],
689
            vec![
690
                TreeOp::AddNode {
691
                    node: f,
692
                    kind: NodeKind::File,
693
                    mode: Mode::TEXT,
694
                    parent: NodeId::ROOT,
695
                    name: "x".into(),
696
                },
697
                TreeOp::SetMode {
698
                    node: f,
699
                    old: me,
700
                    mode: Mode {
701
                        exec: true,
702
                        opaque: false,
703
                    },
704
                },
705
            ],
706
            vec![
707
                Edit::Insert {
708
                    node: f,
709
                    after: AtomRef::Start,
710
                    before: AtomRef::End,
711
                    lines: vec![b"a\n".to_vec(), b"c\n".to_vec()],
712
                },
713
                // Insert between the two lines just inserted, using self references.
714
                Edit::Insert {
715
                    node: f,
716
                    after: AtomRef::Atom {
717
                        change: ObjectRef::ZERO,
718
                        index: 0,
719
                    },
720
                    before: AtomRef::Atom {
721
                        change: ObjectRef::ZERO,
722
                        index: 1,
723
                    },
724
                    lines: vec![b"b\n".to_vec()],
725
                },
726
            ],
727
        );
728
        st.add(c1).unwrap();
729
        let snap = st.materialize();
730
        let x = &snap.files["x"];
731
        assert_eq!(
732
            x.mode,
733
            Mode {
734
                exec: true,
735
                opaque: false
736
            }
737
        );
738
        assert!(matches!(&x.content, FileContent::Text { bytes, .. } if bytes == b"a\nb\nc\n"));
739
    }
740
741
    #[test]
742
    fn opaque_replace_and_conflict() {
743
        let mut st = State::new();
744
        let f = NodeId::random();
745
        let b1 = ObjectRef([1; 32]);
746
        let b2 = ObjectRef([2; 32]);
747
        let b3 = ObjectRef([3; 32]);
748
        let h1 = st
749
            .add(change(
750
                vec![],
751
                vec![TreeOp::AddNode {
752
                    node: f,
753
                    kind: NodeKind::File,
754
                    mode: Mode::OPAQUE,
755
                    parent: NodeId::ROOT,
756
                    name: "img".into(),
757
                }],
758
                vec![Edit::Replace {
759
                    node: f,
760
                    old: None,
761
                    new: Some(b1),
762
                }],
763
            ))
764
            .unwrap();
765
        assert_eq!(st.live_blobs(&f), vec![b1]);
766
        st.add(change(
767
            vec![h1],
768
            vec![],
769
            vec![Edit::Replace {
770
                node: f,
771
                old: Some(b1),
772
                new: Some(b2),
773
            }],
774
        ))
775
        .unwrap();
776
        st.add(change(
777
            vec![h1],
778
            vec![],
779
            vec![Edit::Replace {
780
                node: f,
781
                old: Some(b1),
782
                new: Some(b3),
783
            }],
784
        ))
785
        .unwrap();
786
        let snap = st.materialize();
787
        assert!(snap.is_conflicted());
788
        assert!(
789
            matches!(&snap.files["img"].content, FileContent::Opaque { blobs } if blobs == &vec![b2, b3])
790
        );
791
    }
792
793
    #[test]
794
    fn checkpoint_payload_round_trip_and_replay() {
795
        let mut st = State::new();
796
        let f = NodeId::random();
797
        let h1 = st
798
            .add(change(
799
                vec![],
800
                vec![TreeOp::AddNode {
801
                    node: f,
802
                    kind: NodeKind::File,
803
                    mode: Mode::TEXT,
804
                    parent: NodeId::ROOT,
805
                    name: "a".into(),
806
                }],
807
                vec![Edit::Insert {
808
                    node: f,
809
                    after: AtomRef::Start,
810
                    before: AtomRef::End,
811
                    lines: vec![b"1\n".to_vec(), b"2\n".to_vec()],
812
                }],
813
            ))
814
            .unwrap();
815
        let h2 = st
816
            .add(change(
817
                vec![h1],
818
                vec![],
819
                vec![Edit::Delete {
820
                    node: f,
821
                    atoms: vec![AtomRange {
822
                        change: h1,
823
                        from: 0,
824
                        to: 1,
825
                    }],
826
                }],
827
            ))
828
            .unwrap();
829
        let payload = st.checkpoint_payload();
830
        let back = State::from_checkpoint_payload(&payload).unwrap();
831
        assert_eq!(back.len(), 2);
832
        assert_eq!(back.heads(), vec![h2]);
833
        assert_eq!(back.get(&h1).unwrap().message, "m");
834
        assert_eq!(back.checkpoint_payload(), payload, "deterministic");
835
        // Replaying a further change onto the restored state equals a full build.
836
        let c3 = change(
837
            vec![h2],
838
            vec![],
839
            vec![Edit::Insert {
840
                node: f,
841
                after: AtomRef::Atom {
842
                    change: h1,
843
                    index: 1,
844
                },
845
                before: AtomRef::End,
846
                lines: vec![b"3\n".to_vec()],
847
            }],
848
        );
849
        let mut full = st.clone();
850
        full.add(c3.clone()).unwrap();
851
        let mut restored = back;
852
        restored.add(c3).unwrap();
853
        let (a, b) = (full.materialize(), restored.materialize());
854
        assert_eq!(
855
            a.files.keys().collect::<Vec<_>>(),
856
            b.files.keys().collect::<Vec<_>>()
857
        );
858
        assert_eq!(a.files["a"].content, b.files["a"].content);
859
        assert!(
860
            matches!(&b.files["a"].content, FileContent::Text { bytes, .. } if bytes == b"2\n3\n")
861
        );
862
        assert!(State::from_checkpoint_payload(b"nonsense").is_err());
863
    }
864
865
    #[test]
866
    fn maximal_elements() {
867
        let mut st = State::new();
868
        let h1 = st.add(change(vec![], vec![], vec![])).unwrap();
869
        let h2 = st.add(change(vec![h1], vec![], vec![])).unwrap();
870
        let h3 = st.add(change(vec![], vec![], vec![])).unwrap();
871
        let mut m = st.maximal(&[h1, h2, h3]);
872
        m.sort();
873
        let mut want = vec![h2, h3];
874
        want.sort();
875
        assert_eq!(m, want);
876
        assert_eq!(st.heads(), want);
877
    }
878
879
    /// The one-walk `maximal` agrees with the definition (not in the
880
    /// closure of another element) on random graphs, with repeated and
881
    /// unknown hashes among the input.
882
    #[test]
883
    fn maximal_matches_its_definition() {
884
        use rand::{Rng, SeedableRng};
885
        let mut rng = rand::rngs::StdRng::seed_from_u64(7);
886
        for _ in 0..20 {
887
            let mut st = State::new();
888
            let mut all: Vec<ChangeHash> = Vec::new();
889
            for _ in 0..60 {
890
                let deps: Vec<ChangeHash> =
891
                    all.iter().copied().filter(|_| rng.gen_bool(0.05)).collect();
892
                all.push(st.add(change(deps, vec![], vec![])).unwrap());
893
            }
894
            let mut pick: Vec<ChangeHash> =
895
                all.iter().copied().filter(|_| rng.gen_bool(0.4)).collect();
896
            pick.extend(pick.clone().into_iter().take(3));
897
            pick.push(ObjectRef([0xee; 32]));
898
            let set: BTreeSet<ChangeHash> = pick.iter().copied().collect();
899
            let want: Vec<ChangeHash> = set
900
                .iter()
901
                .filter(|h| !set.iter().any(|o| o != *h && st.closure(o).contains(h)))
902
                .copied()
903
                .collect();
904
            assert_eq!(st.maximal(&pick), want);
905
            assert_eq!(st.maximal(&all), st.heads());
906
        }
907
    }
908
}