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