OXYGENHUB
# 6e6668830baf oxygen / crates / oxy-core / src / state.rs
30.8 KiB · Rust · last changed Snapshots use every core 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
        // File graphs are independent; build them in parallel.
293
        {
294
            use rayon::prelude::*;
295
            let graphs = o::array(o::field(m, 3, "files")?, "files")?
296
                .par_iter()
297
                .map(|f| {
298
                    let e = o::array(f, "file")?;
299
                    if e.len() != 2 {
300
                        return Err(ObjectError::Schema("file: expected [node, graph]".into()));
301
                    }
302
                    let node = NodeId::from_slice(o::bytes(&e[0], "node")?)
303
                        .ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?;
304
                    Ok((node, FileGraph::from_value(&e[1])?))
305
                })
306
                .collect::<Result<Vec<_>, ObjectError>>()?;
307
            st.files.extend(graphs);
308
        }
309
        let opt = |v: &Value| -> Result<Option<ObjectRef>, ObjectError> {
310
            if v.is_null() {
311
                Ok(None)
312
            } else {
313
                o::oref(v, "blob").map(Some)
314
            }
315
        };
316
        for r in o::array(o::field(m, 4, "replaces")?, "replaces")? {
317
            let e = o::array(r, "replace")?;
318
            if e.len() != 2 {
319
                return Err(ObjectError::Schema(
320
                    "replace: expected [node, records]".into(),
321
                ));
322
            }
323
            let node = NodeId::from_slice(o::bytes(&e[0], "node")?)
324
                .ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?;
325
            let mut recs = Vec::new();
326
            for rec in o::array(&e[1], "records")? {
327
                let x = o::array(rec, "record")?;
328
                if x.len() != 3 {
329
                    return Err(ObjectError::Schema("record: expected 3 fields".into()));
330
                }
331
                recs.push((o::oref(&x[0], "change")?, opt(&x[1])?, opt(&x[2])?));
332
            }
333
            st.replaces.insert(node, recs);
334
        }
335
        Ok(st)
336
    }
337
338
    pub fn validate(&self) -> Result<(), StateError> {
339
        for (h, c) in &self.changes {
340
            for d in &c.deps {
341
                if !self.changes.contains_key(d) {
342
                    return Err(StateError::MissingDep(*d, *h));
343
                }
344
            }
345
        }
346
        Ok(())
347
    }
348
349
    fn apply(&mut self, h: ChangeHash, c: &Change) {
350
        for (i, op) in c.tree_ops.iter().enumerate() {
351
            self.tree.apply(h, i as u32, &resolve_tree_op(op, h));
352
        }
353
        let mut counters: HashMap<NodeId, u32> = HashMap::new();
354
        for e in &c.edits {
355
            match e {
356
                Edit::Insert {
357
                    node,
358
                    after,
359
                    before,
360
                    lines,
361
                } => {
362
                    let idx = counters.entry(*node).or_default();
363
                    let first = *idx;
364
                    *idx += lines.len() as u32;
365
                    self.files.entry(*node).or_default().insert(
366
                        h,
367
                        first,
368
                        vertex(after.resolve(h)),
369
                        vertex(before.resolve(h)),
370
                        lines,
371
                    );
372
                }
373
                Edit::Delete { node, atoms } => {
374
                    let g = self.files.entry(*node).or_default();
375
                    for r in atoms {
376
                        g.delete(h, &r.resolve(h));
377
                    }
378
                }
379
                Edit::Replace { node, old, new } => {
380
                    self.replaces
381
                        .entry(*node)
382
                        .or_default()
383
                        .push((h, *old, *new));
384
                }
385
            }
386
        }
387
    }
388
389
    /// Transitive dependencies of `h`, excluding `h` itself.
390
    pub fn closure(&self, h: &ChangeHash) -> Rc<HashSet<ChangeHash>> {
391
        if let Some(c) = self.closure.borrow().get(h) {
392
            return c.clone();
393
        }
394
        let mut out = HashSet::new();
395
        let mut stack: Vec<ChangeHash> = self
396
            .changes
397
            .get(h)
398
            .map(|c| c.deps.clone())
399
            .unwrap_or_default();
400
        while let Some(d) = stack.pop() {
401
            if out.insert(d) {
402
                if let Some(c) = self.changes.get(&d) {
403
                    stack.extend(c.deps.iter().copied());
404
                }
405
            }
406
        }
407
        let rc = Rc::new(out);
408
        self.closure.borrow_mut().insert(*h, rc.clone());
409
        rc
410
    }
411
412
    /// Changes that no other change depends on directly, sorted.
413
    pub fn heads(&self) -> Vec<ChangeHash> {
414
        let depended: HashSet<&ChangeHash> =
415
            self.changes.values().flat_map(|c| c.deps.iter()).collect();
416
        let mut heads: Vec<ChangeHash> = self
417
            .changes
418
            .keys()
419
            .filter(|h| !depended.contains(h))
420
            .copied()
421
            .collect();
422
        heads.sort();
423
        heads
424
    }
425
426
    /// Maximal elements among `hashes` (those not in the closure of another),
427
    /// sorted. One walk down from all of them at once: whatever the walk
428
    /// reaches is below some other element. Linear in the changes reached,
429
    /// where comparing every pair's closures was quadratic in the number
430
    /// of heads, which a bookmark of independent changes has many of.
431
    pub fn maximal(&self, hashes: &[ChangeHash]) -> Vec<ChangeHash> {
432
        let set: BTreeSet<ChangeHash> = hashes.iter().copied().collect();
433
        let mut below: HashSet<ChangeHash> = HashSet::new();
434
        let mut stack: Vec<ChangeHash> = set
435
            .iter()
436
            .filter_map(|h| self.changes.get(h))
437
            .flat_map(|c| c.deps.iter().copied())
438
            .collect();
439
        while let Some(d) = stack.pop() {
440
            if below.insert(d) {
441
                if let Some(c) = self.changes.get(&d) {
442
                    stack.extend(c.deps.iter().copied());
443
                }
444
            }
445
        }
446
        set.into_iter().filter(|h| !below.contains(h)).collect()
447
    }
448
449
    pub fn materialize_file(&self, node: &NodeId) -> Materialized {
450
        self.files
451
            .get(node)
452
            .map(|g| g.materialize(self))
453
            .unwrap_or_default()
454
    }
455
456
    /// Live lines of a text node in materialized order.
457
    pub fn live_lines(&self, node: &NodeId) -> Vec<(AtomKey, Vec<u8>)> {
458
        let Some(g) = self.files.get(node) else {
459
            return vec![];
460
        };
461
        let m = g.materialize(self);
462
        m.live_atoms()
463
            .into_iter()
464
            .map(|a| (a, g.content(&a).unwrap_or_default().to_vec()))
465
            .collect()
466
    }
467
468
    /// Live blobs of an opaque node: every `new` that no later `Replace` retired.
469
    pub fn live_blobs(&self, node: &NodeId) -> Vec<ObjectRef> {
470
        let Some(rs) = self.replaces.get(node) else {
471
            return vec![];
472
        };
473
        let olds: HashSet<ObjectRef> = rs.iter().filter_map(|(_, o, _)| *o).collect();
474
        let mut live: Vec<ObjectRef> = rs
475
            .iter()
476
            .filter_map(|(_, _, n)| *n)
477
            .filter(|n| !olds.contains(n))
478
            .collect();
479
        live.sort();
480
        live.dedup();
481
        live
482
    }
483
484
    /// Short label for conflict markers: `changeid-prefix "message"`.
485
    pub fn label(&self, h: &ChangeHash) -> String {
486
        match self.changes.get(h) {
487
            Some(c) => format!(
488
                "{} \"{}\"",
489
                c.change_id.short(8),
490
                c.message.lines().next().unwrap_or("")
491
            ),
492
            None => format!("#{}", h.short(12)),
493
        }
494
    }
495
496
    /// For resolving a conflicted text node (spec §9.6): the live atoms that
497
    /// are not part of any conflict region in order, every live atom that is
498
    /// part of a region, and every change that contributed to a region.
499
    /// The changes that introduced the live blobs of an opaque node, for a
500
    /// replace conflict (spec §9.3).
501
    pub fn blob_contributors(&self, node: &NodeId, blobs: &[ObjectRef]) -> Vec<ChangeHash> {
502
        let mut out: Vec<ChangeHash> = Vec::new();
503
        for (h, _, new) in self.replaces.get(node).map(|v| v.as_slice()).unwrap_or(&[]) {
504
            if new.is_some_and(|b| blobs.contains(&b)) && !out.contains(h) {
505
                out.push(*h);
506
            }
507
        }
508
        out.sort();
509
        out
510
    }
511
512
    pub fn conflict_regions(&self, node: &NodeId) -> ConflictRegions {
513
        let mut out = ConflictRegions::default();
514
        let Some(g) = self.files.get(node) else {
515
            return out;
516
        };
517
        let m = g.materialize(self);
518
        let mut contributors = BTreeSet::new();
519
        for seg in &m.segments {
520
            match seg {
521
                crate::graph::Segment::Lines(atoms) => {
522
                    for a in atoms {
523
                        out.plain
524
                            .push((*a, g.content(a).unwrap_or_default().to_vec()));
525
                    }
526
                }
527
                crate::graph::Segment::Conflict(sides) => {
528
                    for side in sides {
529
                        for a in side {
530
                            out.region_atoms.push(*a);
531
                            contributors.insert(a.0);
532
                        }
533
                    }
534
                }
535
                crate::graph::Segment::Zombie { deleted, inserted } => {
536
                    for a in deleted {
537
                        contributors.insert(a.0);
538
                        contributors.extend(g.deleters(a).copied());
539
                    }
540
                    for a in inserted {
541
                        out.region_atoms.push(*a);
542
                        contributors.insert(a.0);
543
                    }
544
                }
545
            }
546
        }
547
        out.contributors = contributors.into_iter().collect();
548
        out
549
    }
550
551
    pub fn render_file(&self, node: &NodeId) -> (Vec<u8>, Materialized) {
552
        let Some(g) = self.files.get(node) else {
553
            return (Vec::new(), Materialized::default());
554
        };
555
        let m = g.materialize(self);
556
        let bytes = g.render(&m, &|h| self.label(h));
557
        (bytes, m)
558
    }
559
560
    /// Every file rendered, in parallel: files are independent, and
561
    /// rendering them is most of a snapshot's cost in a large tree.
562
    pub fn materialize(&self) -> Snapshot {
563
        use rayon::prelude::*;
564
        let tree = self.tree.view();
565
        let entries: Vec<(&String, &crate::tree::TreeEntry)> = tree.entries.iter().collect();
566
        let rendered: Vec<(String, FileState)> = entries
567
            .par_iter()
568
            .map(|(path, e)| (path.to_string(), self.file_state(e)))
569
            .collect();
570
        let files: BTreeMap<String, FileState> = rendered.into_iter().collect();
571
        Snapshot { tree, files }
572
    }
573
574
    fn file_state(&self, e: &crate::tree::TreeEntry) -> FileState {
575
        {
576
            let content = match e.kind {
577
                NodeKind::Dir => FileContent::Dir,
578
                NodeKind::Symlink | NodeKind::Subrepo => FileContent::Opaque {
579
                    blobs: self.live_blobs(&e.node),
580
                },
581
                NodeKind::File if e.mode.opaque => FileContent::Opaque {
582
                    blobs: self.live_blobs(&e.node),
583
                },
584
                NodeKind::File => {
585
                    let (bytes, materialized) = self.render_file(&e.node);
586
                    FileContent::Text {
587
                        bytes,
588
                        materialized,
589
                    }
590
                }
591
            };
592
            FileState {
593
                node: e.node,
594
                kind: e.kind,
595
                mode: e.mode,
596
                content,
597
            }
598
        }
599
    }
600
}
601
602
impl DepOracle for State {
603
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
604
        a == b || self.closure(a).contains(b)
605
    }
606
}
607
608
#[cfg(test)]
609
mod tests {
610
    use super::*;
611
    use crate::ids::ChangeId;
612
    use crate::objects::{AtomRange, ChangeKind, Hlc, OpRef};
613
614
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>) -> Change {
615
        let mut c = Change {
616
            change_id: ChangeId::random(),
617
            deps,
618
            author: ObjectRef::ZERO,
619
            hlc: Hlc {
620
                wall_ms: 1,
621
                logical: 0,
622
            },
623
            message: "m".into(),
624
            tree_ops,
625
            edits,
626
            kind: ChangeKind::Normal,
627
        };
628
        c.normalize();
629
        c
630
    }
631
632
    #[test]
633
    fn build_edit_and_materialize() {
634
        let mut st = State::new();
635
        let f = NodeId::random();
636
        let c1 = change(
637
            vec![],
638
            vec![TreeOp::AddNode {
639
                node: f,
640
                kind: NodeKind::File,
641
                mode: Mode::TEXT,
642
                parent: NodeId::ROOT,
643
                name: "a.txt".into(),
644
            }],
645
            vec![Edit::Insert {
646
                node: f,
647
                after: AtomRef::Start,
648
                before: AtomRef::End,
649
                lines: vec![b"one\n".to_vec(), b"two\n".to_vec()],
650
            }],
651
        );
652
        let h1 = st.add(c1).unwrap();
653
        // Depends on h1: append a line and delete "one".
654
        let c2 = change(
655
            vec![h1],
656
            vec![],
657
            vec![
658
                Edit::Delete {
659
                    node: f,
660
                    atoms: vec![AtomRange {
661
                        change: h1,
662
                        from: 0,
663
                        to: 1,
664
                    }],
665
                },
666
                Edit::Insert {
667
                    node: f,
668
                    after: AtomRef::Atom {
669
                        change: h1,
670
                        index: 1,
671
                    },
672
                    before: AtomRef::End,
673
                    lines: vec![b"three\n".to_vec()],
674
                },
675
            ],
676
        );
677
        let h2 = st.add(c2).unwrap();
678
        assert_eq!(st.heads(), vec![h2]);
679
        assert!(st.depends_on(&h2, &h1));
680
        assert!(!st.depends_on(&h1, &h2));
681
        let snap = st.materialize();
682
        assert!(!snap.is_conflicted());
683
        let a = &snap.files["a.txt"];
684
        assert!(matches!(&a.content, FileContent::Text { bytes, .. } if bytes == b"two\nthree\n"));
685
        assert_eq!(st.live_lines(&f).len(), 2);
686
    }
687
688
    #[test]
689
    fn missing_dependency_is_rejected() {
690
        let mut st = State::new();
691
        let c = change(vec![ObjectRef([1; 32])], vec![], vec![]);
692
        assert!(matches!(st.add(c), Err(StateError::MissingDep(..))));
693
    }
694
695
    #[test]
696
    fn self_references_and_multi_insert_indexing() {
697
        let mut st = State::new();
698
        let f = NodeId::random();
699
        let me = OpRef {
700
            change: ObjectRef::ZERO,
701
            index: 0,
702
        };
703
        let c1 = change(
704
            vec![],
705
            vec![
706
                TreeOp::AddNode {
707
                    node: f,
708
                    kind: NodeKind::File,
709
                    mode: Mode::TEXT,
710
                    parent: NodeId::ROOT,
711
                    name: "x".into(),
712
                },
713
                TreeOp::SetMode {
714
                    node: f,
715
                    old: me,
716
                    mode: Mode {
717
                        exec: true,
718
                        opaque: false,
719
                    },
720
                },
721
            ],
722
            vec![
723
                Edit::Insert {
724
                    node: f,
725
                    after: AtomRef::Start,
726
                    before: AtomRef::End,
727
                    lines: vec![b"a\n".to_vec(), b"c\n".to_vec()],
728
                },
729
                // Insert between the two lines just inserted, using self references.
730
                Edit::Insert {
731
                    node: f,
732
                    after: AtomRef::Atom {
733
                        change: ObjectRef::ZERO,
734
                        index: 0,
735
                    },
736
                    before: AtomRef::Atom {
737
                        change: ObjectRef::ZERO,
738
                        index: 1,
739
                    },
740
                    lines: vec![b"b\n".to_vec()],
741
                },
742
            ],
743
        );
744
        st.add(c1).unwrap();
745
        let snap = st.materialize();
746
        let x = &snap.files["x"];
747
        assert_eq!(
748
            x.mode,
749
            Mode {
750
                exec: true,
751
                opaque: false
752
            }
753
        );
754
        assert!(matches!(&x.content, FileContent::Text { bytes, .. } if bytes == b"a\nb\nc\n"));
755
    }
756
757
    #[test]
758
    fn opaque_replace_and_conflict() {
759
        let mut st = State::new();
760
        let f = NodeId::random();
761
        let b1 = ObjectRef([1; 32]);
762
        let b2 = ObjectRef([2; 32]);
763
        let b3 = ObjectRef([3; 32]);
764
        let h1 = st
765
            .add(change(
766
                vec![],
767
                vec![TreeOp::AddNode {
768
                    node: f,
769
                    kind: NodeKind::File,
770
                    mode: Mode::OPAQUE,
771
                    parent: NodeId::ROOT,
772
                    name: "img".into(),
773
                }],
774
                vec![Edit::Replace {
775
                    node: f,
776
                    old: None,
777
                    new: Some(b1),
778
                }],
779
            ))
780
            .unwrap();
781
        assert_eq!(st.live_blobs(&f), vec![b1]);
782
        st.add(change(
783
            vec![h1],
784
            vec![],
785
            vec![Edit::Replace {
786
                node: f,
787
                old: Some(b1),
788
                new: Some(b2),
789
            }],
790
        ))
791
        .unwrap();
792
        st.add(change(
793
            vec![h1],
794
            vec![],
795
            vec![Edit::Replace {
796
                node: f,
797
                old: Some(b1),
798
                new: Some(b3),
799
            }],
800
        ))
801
        .unwrap();
802
        let snap = st.materialize();
803
        assert!(snap.is_conflicted());
804
        assert!(
805
            matches!(&snap.files["img"].content, FileContent::Opaque { blobs } if blobs == &vec![b2, b3])
806
        );
807
    }
808
809
    #[test]
810
    fn checkpoint_payload_round_trip_and_replay() {
811
        let mut st = State::new();
812
        let f = NodeId::random();
813
        let h1 = st
814
            .add(change(
815
                vec![],
816
                vec![TreeOp::AddNode {
817
                    node: f,
818
                    kind: NodeKind::File,
819
                    mode: Mode::TEXT,
820
                    parent: NodeId::ROOT,
821
                    name: "a".into(),
822
                }],
823
                vec![Edit::Insert {
824
                    node: f,
825
                    after: AtomRef::Start,
826
                    before: AtomRef::End,
827
                    lines: vec![b"1\n".to_vec(), b"2\n".to_vec()],
828
                }],
829
            ))
830
            .unwrap();
831
        let h2 = st
832
            .add(change(
833
                vec![h1],
834
                vec![],
835
                vec![Edit::Delete {
836
                    node: f,
837
                    atoms: vec![AtomRange {
838
                        change: h1,
839
                        from: 0,
840
                        to: 1,
841
                    }],
842
                }],
843
            ))
844
            .unwrap();
845
        let payload = st.checkpoint_payload();
846
        let back = State::from_checkpoint_payload(&payload).unwrap();
847
        assert_eq!(back.len(), 2);
848
        assert_eq!(back.heads(), vec![h2]);
849
        assert_eq!(back.get(&h1).unwrap().message, "m");
850
        assert_eq!(back.checkpoint_payload(), payload, "deterministic");
851
        // Replaying a further change onto the restored state equals a full build.
852
        let c3 = change(
853
            vec![h2],
854
            vec![],
855
            vec![Edit::Insert {
856
                node: f,
857
                after: AtomRef::Atom {
858
                    change: h1,
859
                    index: 1,
860
                },
861
                before: AtomRef::End,
862
                lines: vec![b"3\n".to_vec()],
863
            }],
864
        );
865
        let mut full = st.clone();
866
        full.add(c3.clone()).unwrap();
867
        let mut restored = back;
868
        restored.add(c3).unwrap();
869
        let (a, b) = (full.materialize(), restored.materialize());
870
        assert_eq!(
871
            a.files.keys().collect::<Vec<_>>(),
872
            b.files.keys().collect::<Vec<_>>()
873
        );
874
        assert_eq!(a.files["a"].content, b.files["a"].content);
875
        assert!(
876
            matches!(&b.files["a"].content, FileContent::Text { bytes, .. } if bytes == b"2\n3\n")
877
        );
878
        assert!(State::from_checkpoint_payload(b"nonsense").is_err());
879
    }
880
881
    #[test]
882
    fn maximal_elements() {
883
        let mut st = State::new();
884
        let h1 = st.add(change(vec![], vec![], vec![])).unwrap();
885
        let h2 = st.add(change(vec![h1], vec![], vec![])).unwrap();
886
        let h3 = st.add(change(vec![], vec![], vec![])).unwrap();
887
        let mut m = st.maximal(&[h1, h2, h3]);
888
        m.sort();
889
        let mut want = vec![h2, h3];
890
        want.sort();
891
        assert_eq!(m, want);
892
        assert_eq!(st.heads(), want);
893
    }
894
895
    /// The one-walk `maximal` agrees with the definition (not in the
896
    /// closure of another element) on random graphs, with repeated and
897
    /// unknown hashes among the input.
898
    #[test]
899
    fn maximal_matches_its_definition() {
900
        use rand::{Rng, SeedableRng};
901
        let mut rng = rand::rngs::StdRng::seed_from_u64(7);
902
        for _ in 0..20 {
903
            let mut st = State::new();
904
            let mut all: Vec<ChangeHash> = Vec::new();
905
            for _ in 0..60 {
906
                let deps: Vec<ChangeHash> =
907
                    all.iter().copied().filter(|_| rng.gen_bool(0.05)).collect();
908
                all.push(st.add(change(deps, vec![], vec![])).unwrap());
909
            }
910
            let mut pick: Vec<ChangeHash> =
911
                all.iter().copied().filter(|_| rng.gen_bool(0.4)).collect();
912
            pick.extend(pick.clone().into_iter().take(3));
913
            pick.push(ObjectRef([0xee; 32]));
914
            let set: BTreeSet<ChangeHash> = pick.iter().copied().collect();
915
            let want: Vec<ChangeHash> = set
916
                .iter()
917
                .filter(|h| !set.iter().any(|o| o != *h && st.closure(o).contains(h)))
918
                .copied()
919
                .collect();
920
            assert_eq!(st.maximal(&pick), want);
921
            assert_eq!(st.maximal(&all), st.heads());
922
        }
923
    }
924
}