OXYGENHUB
# 4d6c9c1b358c oxygen / crates / oxy-core / src / state.rs
69.0 KiB · Rust · last changed Log, blame and fsck at the scale of the Linux tree 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
/// Checkpoint payload version 2 (spec §11): file graphs by reference.
17
pub const CHECKPOINT_INDEX_VERSION: u64 = 2;
18
19
/// A file graph encoded in fewer bytes stays inline in a version 2 index.
20
pub const INLINE_GRAPH_LIMIT: usize = 2048;
21
22
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
23
pub enum StateError {
24
    #[error("change {1} depends on {0}, which is not in the state")]
25
    MissingDep(ChangeHash, ChangeHash),
26
}
27
28
/// One `Replace` edit: `(change, old blob, new blob)`.
29
type ReplaceRecord = (ChangeHash, Option<ObjectRef>, Option<ObjectRef>);
30
31
/// What a state remembers about a change: everything but its operations,
32
/// which live in the graphs once applied (and in the store).
33
#[derive(Clone, Debug, PartialEq, Eq)]
34
pub struct ChangeMeta {
35
    pub change_id: crate::ids::ChangeId,
36
    pub deps: Vec<ChangeHash>,
37
    pub author: KeyId,
38
    pub hlc: Hlc,
39
    pub message: String,
40
    pub kind: ChangeKind,
41
}
42
43
impl ChangeMeta {
44
    pub fn of(c: &Change) -> ChangeMeta {
45
        ChangeMeta {
46
            change_id: c.change_id,
47
            deps: c.deps.clone(),
48
            author: c.author,
49
            hlc: c.hlc,
50
            message: c.message.clone(),
51
            kind: c.kind,
52
        }
53
    }
54
}
55
56
#[derive(Default)]
57
pub struct State {
58
    changes: HashMap<ChangeHash, ChangeMeta>,
59
    tree: TreeGraph,
60
    /// Shared between clones: a clone copies a graph only when an edit
61
    /// changes it (`Arc::make_mut`), so keeping several states that differ
62
    /// by a few changes costs little more than one.
63
    files: HashMap<NodeId, Arc<FileGraph>>,
64
    replaces: HashMap<NodeId, Vec<ReplaceRecord>>,
65
    /// Memoised transitive dependencies. A mutex, not a `RefCell`, so a
66
    /// loaded state can be shared across threads by a host that caches it.
67
    closure: Mutex<HashMap<ChangeHash, Arc<HashSet<ChangeHash>>>>,
68
    /// `None` for a full state. For a history-only state (loaded for its
69
    /// changes and tree), the file graphs not built yet, by node: `files`
70
    /// holds only those loaded with [`State::load_graphs`].
71
    deferred: Option<HashMap<NodeId, Deferred>>,
72
}
73
74
use crate::objects::TextRef;
75
76
/// A file graph a history-only state has not built: its base from a
77
/// version 2 checkpoint, if any, and the edits of later changes in the
78
/// order they were applied (spec §11). Building it later gives the same
79
/// graph, since graph construction does not depend on order (§8).
80
#[derive(Clone, Default)]
81
struct Deferred {
82
    base: Option<DeferredBase>,
83
    edits: Vec<(ChangeHash, Vec<Edit>)>,
84
    /// The text blob and line count of each insert in `edits` left without
85
    /// its lines, in order: read when the graph is built.
86
    texts: Vec<TextRef>,
87
}
88
89
#[derive(Clone)]
90
enum DeferredBase {
91
    Inline(Value),
92
    Blob(ObjectRef),
93
}
94
95
impl Clone for State {
96
    fn clone(&self) -> State {
97
        State {
98
            changes: self.changes.clone(),
99
            tree: self.tree.clone(),
100
            files: self.files.clone(),
101
            replaces: self.replaces.clone(),
102
            closure: Mutex::new(self.closure.lock().unwrap().clone()),
103
            deferred: self.deferred.clone(),
104
        }
105
    }
106
}
107
108
#[derive(Clone, Debug, PartialEq, Eq)]
109
pub enum FileContent {
110
    Text {
111
        bytes: Vec<u8>,
112
        materialized: Materialized,
113
    },
114
    /// Live blobs; more than one is a replace conflict (spec §9.3).
115
    Opaque {
116
        blobs: Vec<ObjectRef>,
117
    },
118
    Dir,
119
}
120
121
#[derive(Clone, Debug, PartialEq, Eq)]
122
pub struct FileState {
123
    pub node: NodeId,
124
    pub kind: NodeKind,
125
    pub mode: Mode,
126
    pub content: FileContent,
127
}
128
129
impl FileState {
130
    pub fn is_conflicted(&self) -> bool {
131
        match &self.content {
132
            FileContent::Text { materialized, .. } => materialized.is_conflicted(),
133
            FileContent::Opaque { blobs } => blobs.len() > 1,
134
            FileContent::Dir => false,
135
        }
136
    }
137
}
138
139
/// See `State::conflict_regions`.
140
#[derive(Clone, Debug, Default, PartialEq, Eq)]
141
pub struct ConflictRegions {
142
    pub plain: Vec<(AtomKey, Vec<u8>)>,
143
    pub region_atoms: Vec<AtomKey>,
144
    pub contributors: Vec<ChangeHash>,
145
}
146
147
/// The whole tree of a state, materialized.
148
#[derive(Clone, Debug, Default)]
149
pub struct Snapshot {
150
    pub tree: TreeView,
151
    pub files: BTreeMap<String, FileState>,
152
    /// Text files left unrendered by [`State::materialize_except`]: their
153
    /// `bytes` are empty and they are known not to be conflicted.
154
    pub unrendered: BTreeSet<String>,
155
}
156
157
impl Snapshot {
158
    pub fn is_conflicted(&self) -> bool {
159
        self.tree.is_conflicted() || self.files.values().any(|f| f.is_conflicted())
160
    }
161
}
162
163
fn vertex(r: AtomRef) -> Vertex {
164
    match r {
165
        AtomRef::Start => Vertex::Start,
166
        AtomRef::End => Vertex::End,
167
        AtomRef::Atom { change, index } => Vertex::Atom((change, index)),
168
    }
169
}
170
171
/// Replace self references in a tree op with the concrete change hash.
172
pub fn resolve_tree_op(op: &TreeOp, me: ChangeHash) -> TreeOp {
173
    match op {
174
        TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode {
175
            node: *node,
176
            placement: placement.resolve(me),
177
        },
178
        TreeOp::Move {
179
            node,
180
            old,
181
            parent,
182
            name,
183
        } => TreeOp::Move {
184
            node: *node,
185
            old: old.resolve(me),
186
            parent: *parent,
187
            name: name.clone(),
188
        },
189
        TreeOp::SetMode { node, old, mode } => TreeOp::SetMode {
190
            node: *node,
191
            old: old.resolve(me),
192
            mode: *mode,
193
        },
194
        add @ TreeOp::AddNode { .. } => add.clone(),
195
    }
196
}
197
198
/// Apply one change's edits of one file (inserts and deletes; replaces
199
/// live beside the graphs) to that file's graph.
200
fn apply_file_edits(g: &mut FileGraph, h: ChangeHash, edits: Vec<Edit>) {
201
    let mut next = 0u32;
202
    for e in edits {
203
        match e {
204
            Edit::Insert {
205
                after,
206
                before,
207
                lines,
208
                ..
209
            } => {
210
                let first = next;
211
                next += lines.len() as u32;
212
                g.insert_owned(
213
                    h,
214
                    first,
215
                    vertex(after.resolve(h)),
216
                    vertex(before.resolve(h)),
217
                    lines,
218
                );
219
            }
220
            Edit::Delete { atoms, .. } => {
221
                for r in atoms {
222
                    g.delete(h, &r.resolve(h));
223
                }
224
            }
225
            Edit::Replace { .. } => {}
226
        }
227
    }
228
}
229
230
impl State {
231
    pub fn new() -> Self {
232
        Self::default()
233
    }
234
235
    /// An empty state for history only: changes and tree are kept, file
236
    /// edits are recorded but no file graph is built until
237
    /// [`State::load_graphs`] asks for it. For log and other commands that
238
    /// need no file contents, or only a few files.
239
    pub fn new_history_only() -> Self {
240
        State {
241
            deferred: Some(HashMap::new()),
242
            ..Self::default()
243
        }
244
    }
245
246
    /// Whether this state builds file graphs only when asked
247
    /// ([`State::new_history_only`]), even if it has none waiting.
248
    pub fn is_history_only(&self) -> bool {
249
        self.deferred.is_some()
250
    }
251
252
    /// Whether this state holds every file graph.
253
    pub fn is_full(&self) -> bool {
254
        self.deferred.as_ref().is_none_or(|d| d.is_empty())
255
    }
256
257
    /// The graph of `node`, which must be loaded: asking a history-only
258
    /// state for a graph it has not built is a bug, never an empty file.
259
    fn graph(&self, node: &NodeId) -> Option<&FileGraph> {
260
        if let Some(d) = &self.deferred {
261
            assert!(
262
                !d.contains_key(node),
263
                "file graph {} of a history-only state was not loaded (State::load_graphs)",
264
                node.short(12)
265
            );
266
        }
267
        self.files.get(node).map(|g| g.as_ref())
268
    }
269
270
    /// Operations over every file need every graph.
271
    fn require_full(&self, what: &str) {
272
        assert!(
273
            self.is_full(),
274
            "{what} needs every file graph; this state was loaded for history only"
275
        );
276
    }
277
278
    /// Build the graphs of `nodes` in a history-only state: from their
279
    /// checkpoint base (blobs read in one batch through `load`) and their
280
    /// deferred edits. Nodes already built, or with no graph, are skipped.
281
    pub fn load_graphs(
282
        &mut self,
283
        nodes: &[NodeId],
284
        load: impl FnOnce(&[ObjectRef]) -> Result<Vec<Vec<u8>>, ObjectError>,
285
        read_change: impl Fn(&ChangeHash) -> Result<Vec<u8>, ObjectError>,
286
    ) -> Result<(), ObjectError> {
287
        let Some(deferred) = self.deferred.as_mut() else {
288
            return Ok(());
289
        };
290
        let mut taken: Vec<(NodeId, Deferred)> = nodes
291
            .iter()
292
            .filter_map(|n| deferred.remove(n).map(|d| (*n, d)))
293
            .collect();
294
        let mut refs: Vec<ObjectRef> = taken
295
            .iter()
296
            .filter_map(|(_, d)| match &d.base {
297
                Some(DeferredBase::Blob(r)) => Some(*r),
298
                _ => None,
299
            })
300
            .collect();
301
        // Text blobs of inserts read without their lines, after the bases.
302
        let bases = refs.len();
303
        refs.extend(taken.iter().flat_map(|(_, d)| {
304
            d.texts.iter().filter_map(|t| match t {
305
                TextRef::Blob(r, _) => Some(*r),
306
                TextRef::Inline { .. } => None,
307
            })
308
        }));
309
        // And the encodings of changes whose inline inserts are needed,
310
        // each read once.
311
        let mut changes: HashMap<ChangeHash, Vec<u8>> = HashMap::new();
312
        for t in taken.iter().flat_map(|(_, d)| d.texts.iter()) {
313
            if let TextRef::Inline { change, .. } = t {
314
                if !changes.contains_key(change) {
315
                    changes.insert(*change, read_change(change)?);
316
                }
317
            }
318
        }
319
        let mut blobs = if refs.is_empty() {
320
            Vec::new()
321
        } else {
322
            load(&refs)?
323
        };
324
        if blobs.len() != refs.len() {
325
            return Err(ObjectError::Schema("graph blobs: wrong count".into()));
326
        }
327
        let mut texts = blobs.split_off(bases).into_iter();
328
        let mut blobs = blobs.into_iter();
329
        // Pair each graph with its blob, then build them all in parallel:
330
        // graphs are independent.
331
        let mut work: Vec<(NodeId, Deferred, Option<Vec<u8>>)> = Vec::with_capacity(taken.len());
332
        for (node, mut d) in taken.drain(..) {
333
            // Fill in the lines of inserts read without them, in order.
334
            let mut want = std::mem::take(&mut d.texts).into_iter();
335
            for (_, edits) in d.edits.iter_mut() {
336
                for e in edits.iter_mut() {
337
                    if let Edit::Insert { lines, .. } = e {
338
                        if lines.is_empty() {
339
                            *lines = match want.next() {
340
                                Some(TextRef::Blob(_, count)) => {
341
                                    let content = texts.next().ok_or_else(|| {
342
                                        ObjectError::Schema("text blobs: wrong count".into())
343
                                    })?;
344
                                    crate::objects::text_lines(&content, count)?
345
                                }
346
                                Some(TextRef::Inline { change, start, end }) => {
347
                                    let bytes = &changes[&change];
348
                                    let range = bytes
349
                                        .get(start as usize..end as usize)
350
                                        .ok_or_else(|| {
351
                                            ObjectError::Schema(
352
                                                "an inline insert lies outside its change".into(),
353
                                            )
354
                                        })?;
355
                                    crate::objects::inline_lines(range)?
356
                                }
357
                                None => {
358
                                    return Err(ObjectError::Schema(
359
                                        "an insert's text is missing".into(),
360
                                    ))
361
                                }
362
                            };
363
                        }
364
                    }
365
                }
366
            }
367
            let bytes = match &d.base {
368
                Some(DeferredBase::Blob(_)) => Some(
369
                    blobs
370
                        .next()
371
                        .ok_or_else(|| ObjectError::Schema("graph blobs: wrong count".into()))?,
372
                ),
373
                _ => None,
374
            };
375
            work.push((node, d, bytes));
376
        }
377
        use rayon::prelude::*;
378
        let built: Vec<(NodeId, FileGraph)> = work
379
            .into_par_iter()
380
            .map(|(node, mut d, bytes)| {
381
                let mut graph = match (d.base.take(), bytes) {
382
                    (Some(DeferredBase::Inline(v)), _) => FileGraph::from_value(&v)?,
383
                    (Some(DeferredBase::Blob(_)), Some(bytes)) => {
384
                        FileGraph::from_value(&cbor::decode(&bytes)?)?
385
                    }
386
                    _ => FileGraph::default(),
387
                };
388
                for (h, edits) in std::mem::take(&mut d.edits) {
389
                    apply_file_edits(&mut graph, h, edits);
390
                }
391
                Ok((node, graph))
392
            })
393
            .collect::<Result<_, ObjectError>>()?;
394
        self.files
395
            .extend(built.into_iter().map(|(n, g)| (n, Arc::new(g))));
396
        Ok(())
397
    }
398
399
    /// Every file graph a history-only state has not built, deleted files'
400
    /// included.
401
    pub fn unbuilt_nodes(&self) -> Vec<NodeId> {
402
        self.deferred
403
            .as_ref()
404
            .map(|d| d.keys().copied().collect())
405
            .unwrap_or_default()
406
    }
407
408
    /// Drop the built graphs of `nodes`, for a caller rendering a large
409
    /// tree a batch at a time. The state can no longer render those files,
410
    /// so it must not be used for them again.
411
    pub fn drop_graphs(&mut self, nodes: &[NodeId]) {
412
        for n in nodes {
413
            self.files.remove(n);
414
        }
415
    }
416
417
    /// Build every graph a history-only state has not built, making it full.
418
    pub fn load_all_graphs(
419
        &mut self,
420
        load: impl FnOnce(&[ObjectRef]) -> Result<Vec<Vec<u8>>, ObjectError>,
421
        read_change: impl Fn(&ChangeHash) -> Result<Vec<u8>, ObjectError>,
422
    ) -> Result<(), ObjectError> {
423
        let nodes: Vec<NodeId> = match &self.deferred {
424
            Some(d) => d.keys().copied().collect(),
425
            None => return Ok(()),
426
        };
427
        self.load_graphs(&nodes, load, read_change)?;
428
        self.deferred = None;
429
        Ok(())
430
    }
431
432
    /// A key for the graph of `node` in a history-only state that has not
433
    /// built it: equal keys mean equal graphs, so equal renderings (a
434
    /// rendering depends only on the graph and on the changes' own
435
    /// immutable metadata). It names the node, its checkpoint base (a blob,
436
    /// an inline value, or none) and the set of later changes that edited
437
    /// the file.
438
    /// `None` for a full state or a graph already built.
439
    pub fn graph_key(&self, node: &NodeId) -> Option<[u8; 32]> {
440
        let deferred = self.deferred.as_ref()?;
441
        if self.files.contains_key(node) {
442
            return None;
443
        }
444
        let mut h = blake3::Hasher::new();
445
        h.update(b"oxy graph key 1\0");
446
        // A change's edits are applied per node: the node is part of it.
447
        h.update(&node.0);
448
        let Some(d) = deferred.get(node) else {
449
            // Never edited: the empty graph.
450
            h.update(&[0]);
451
            return Some(*h.finalize().as_bytes());
452
        };
453
        match &d.base {
454
            None => h.update(&[0]),
455
            Some(DeferredBase::Blob(r)) => h.update(&[1]).update(&r.0),
456
            Some(DeferredBase::Inline(v)) => h.update(&[2]).update(&cbor::encode(v)),
457
        };
458
        let mut changes: Vec<&ChangeHash> = d.edits.iter().map(|(c, _)| c).collect();
459
        changes.sort();
460
        changes.dedup();
461
        h.update(&(changes.len() as u64).to_le_bytes());
462
        for c in changes {
463
            h.update(&c.0);
464
        }
465
        Some(*h.finalize().as_bytes())
466
    }
467
468
    pub fn len(&self) -> usize {
469
        self.changes.len()
470
    }
471
    pub fn is_empty(&self) -> bool {
472
        self.changes.is_empty()
473
    }
474
    pub fn contains(&self, h: &ChangeHash) -> bool {
475
        self.changes.contains_key(h)
476
    }
477
    pub fn get(&self, h: &ChangeHash) -> Option<&ChangeMeta> {
478
        self.changes.get(h)
479
    }
480
    pub fn changes(&self) -> impl Iterator<Item = (&ChangeHash, &ChangeMeta)> {
481
        self.changes.iter()
482
    }
483
    pub fn tree(&self) -> &TreeGraph {
484
        &self.tree
485
    }
486
    pub fn file(&self, node: &NodeId) -> Option<&FileGraph> {
487
        self.graph(node)
488
    }
489
490
    /// Add a change whose dependencies are already present.
491
    pub fn add(&mut self, change: Change) -> Result<ChangeHash, StateError> {
492
        let h = change.hash();
493
        if self.changes.contains_key(&h) {
494
            return Ok(h);
495
        }
496
        for d in &change.deps {
497
            if !self.changes.contains_key(d) {
498
                return Err(StateError::MissingDep(*d, h));
499
            }
500
        }
501
        Ok(self.add_unchecked(change))
502
    }
503
504
    /// Add a change in any order; call `validate` once all are in.
505
    pub fn add_unchecked(&mut self, change: Change) -> ChangeHash {
506
        let h = change.hash();
507
        self.add_known(h, change, Vec::new());
508
        h
509
    }
510
511
    /// Add a change read from the store under hash `h`, without hashing it
512
    /// again (for a version 2 change that means chunking all its text).
513
    /// `texts` names, in order, the inserts left without their lines (see
514
    /// [`crate::store::Store::get_change_lazy`]); only a history-only state
515
    /// takes them, and only for graphs it has not built.
516
    pub fn add_known(&mut self, h: ChangeHash, change: Change, texts: Vec<TextRef>) {
517
        if self.changes.contains_key(&h) {
518
            return;
519
        }
520
        let meta = ChangeMeta::of(&change);
521
        self.apply_with(h, change, texts);
522
        self.changes.insert(h, meta);
523
        self.closure.lock().unwrap().clear();
524
    }
525
526
    // ------------------------------------------------------------ checkpoints (spec §11)
527
528
    /// Serialize the whole state in payload version 1: change metadata,
529
    /// tree, file graphs, replaces, all in one value (spec §11).
530
    pub fn checkpoint_payload(&self) -> Vec<u8> {
531
        self.require_full("a checkpoint");
532
        let mut nodes: Vec<&NodeId> = self.files.keys().collect();
533
        nodes.sort();
534
        let files = Value::Array(
535
            nodes
536
                .into_iter()
537
                .map(|n| Value::Array(vec![Value::Bytes(n.0.to_vec()), self.files[n].to_value()]))
538
                .collect(),
539
        );
540
        cbor::encode(&self.payload_value(o::FORMAT_VERSION, files))
541
    }
542
543
    /// Each file graph encoded on its own, by node, for a version 2 payload
544
    /// (spec §11): the graphs of large files become blobs of their own.
545
    pub fn checkpoint_file_graphs(&self) -> Vec<(NodeId, Vec<u8>)> {
546
        use rayon::prelude::*;
547
        self.require_full("a checkpoint");
548
        let mut nodes: Vec<(&NodeId, &FileGraph)> =
549
            self.files.iter().map(|(n, g)| (n, g.as_ref())).collect();
550
        nodes.sort_by_key(|(n, _)| **n);
551
        nodes
552
            .into_par_iter()
553
            .map(|(n, g)| (*n, cbor::encode(&g.to_value())))
554
            .collect()
555
    }
556
557
    /// The payload version 2 index for the graphs of
558
    /// [`State::checkpoint_file_graphs`]: a graph encoded in fewer than
559
    /// [`INLINE_GRAPH_LIMIT`] bytes is kept inline, a larger one is named by
560
    /// `blob_of` (the ObjectRef of the blob holding its encoding).
561
    pub fn checkpoint_index(
562
        &self,
563
        graphs: &[(NodeId, Vec<u8>)],
564
        mut blob_of: impl FnMut(&[u8]) -> ObjectRef,
565
    ) -> Vec<u8> {
566
        let files = Value::Array(
567
            graphs
568
                .iter()
569
                .map(|(n, enc)| {
570
                    let entry = if enc.len() < INLINE_GRAPH_LIMIT {
571
                        cbor::decode(enc).expect("an encoded graph decodes")
572
                    } else {
573
                        Value::Bytes(blob_of(enc).0.to_vec())
574
                    };
575
                    Value::Array(vec![Value::Bytes(n.0.to_vec()), entry])
576
                })
577
                .collect(),
578
        );
579
        cbor::encode(&self.payload_value(CHECKPOINT_INDEX_VERSION, files))
580
    }
581
582
    /// The payload map around `files`: changes, tree and replaces as in version 1.
583
    fn payload_value(&self, version: u64, files: Value) -> Value {
584
        let mut hashes: Vec<&ChangeHash> = self.changes.keys().collect();
585
        hashes.sort();
586
        let changes = Value::Array(
587
            hashes
588
                .into_iter()
589
                .map(|h| {
590
                    let m = &self.changes[h];
591
                    Value::Array(vec![
592
                        Value::Bytes(h.0.to_vec()),
593
                        Value::Bytes(m.change_id.0.to_vec()),
594
                        Value::Array(m.deps.iter().map(|d| Value::Bytes(d.0.to_vec())).collect()),
595
                        Value::Text(m.message.clone()),
596
                        Value::Uint(m.hlc.wall_ms),
597
                        Value::Uint(m.hlc.logical as u64),
598
                        Value::Uint(m.kind.tag()),
599
                        Value::Bytes(m.author.0.to_vec()),
600
                    ])
601
                })
602
                .collect(),
603
        );
604
        let mut rnodes: Vec<&NodeId> = self.replaces.keys().collect();
605
        rnodes.sort();
606
        let opt =
607
            |o: &Option<ObjectRef>| o.map(|r| Value::Bytes(r.0.to_vec())).unwrap_or(Value::Null);
608
        let replaces = Value::Array(
609
            rnodes
610
                .into_iter()
611
                .map(|n| {
612
                    Value::Array(vec![
613
                        Value::Bytes(n.0.to_vec()),
614
                        Value::Array(
615
                            self.replaces[n]
616
                                .iter()
617
                                .map(|(c, o, nw)| {
618
                                    Value::Array(vec![Value::Bytes(c.0.to_vec()), opt(o), opt(nw)])
619
                                })
620
                                .collect(),
621
                        ),
622
                    ])
623
                })
624
                .collect(),
625
        );
626
        Value::map([
627
            (0, Value::Uint(version)),
628
            (1, changes),
629
            (2, self.tree.to_value()),
630
            (3, files),
631
            (4, replaces),
632
        ])
633
    }
634
635
    /// Decode a version 1 payload (the whole state in one value). A
636
    /// version 2 payload needs its graph blobs: use [`State::from_checkpoint`].
637
    pub fn from_checkpoint_payload(bytes: &[u8]) -> Result<State, ObjectError> {
638
        State::from_checkpoint(bytes, |_| {
639
            Err(ObjectError::Schema(
640
                "a version 2 checkpoint needs its graph blobs".into(),
641
            ))
642
        })
643
    }
644
645
    /// Decode a checkpoint payload of either version (spec §11). For
646
    /// version 2, `load` returns the contents of the named graph blobs, in
647
    /// order; the graphs are then decoded in parallel.
648
    pub fn from_checkpoint(
649
        bytes: &[u8],
650
        load: impl FnOnce(&[ObjectRef]) -> Result<Vec<Vec<u8>>, ObjectError>,
651
    ) -> Result<State, ObjectError> {
652
        use rayon::prelude::*;
653
        let v = cbor::decode(bytes)?;
654
        let m = o::map(&v, "checkpoint payload")?;
655
        let version = o::uint(o::field(m, 0, "version")?, "version")?;
656
        if version != o::FORMAT_VERSION && version != CHECKPOINT_INDEX_VERSION {
657
            return Err(ObjectError::Schema(format!(
658
                "checkpoint payload version {version} is not supported"
659
            )));
660
        }
661
        let mut st = State::payload_common(m)?;
662
        let entries = o::array(o::field(m, 3, "files")?, "files")?;
663
        let mut parsed: Vec<(NodeId, &Value)> = Vec::with_capacity(entries.len());
664
        for f in entries {
665
            let e = o::array(f, "file")?;
666
            if e.len() != 2 {
667
                return Err(ObjectError::Schema("file: expected [node, graph]".into()));
668
            }
669
            let node = NodeId::from_slice(o::bytes(&e[0], "node")?)
670
                .ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?;
671
            parsed.push((node, &e[1]));
672
        }
673
        // Version 2 names large graphs by blob: fetch those in one batch.
674
        let mut fetched: HashMap<NodeId, Vec<u8>> = HashMap::new();
675
        if version == CHECKPOINT_INDEX_VERSION {
676
            let wanted: Vec<(NodeId, ObjectRef)> = parsed
677
                .iter()
678
                .filter(|(_, e)| matches!(e, Value::Bytes(_)))
679
                .map(|(n, e)| Ok((*n, o::oref(e, "graph blob")?)))
680
                .collect::<Result<_, ObjectError>>()?;
681
            if !wanted.is_empty() {
682
                let refs: Vec<ObjectRef> = wanted.iter().map(|(_, r)| *r).collect();
683
                let contents = {
684
                    let _t = crate::timing::phase("load:   graph blobs read");
685
                    load(&refs)?
686
                };
687
                if contents.len() != refs.len() {
688
                    return Err(ObjectError::Schema("graph blobs: wrong count".into()));
689
                }
690
                fetched.extend(wanted.iter().map(|(n, _)| *n).zip(contents));
691
            }
692
        } else if parsed.iter().any(|(_, e)| matches!(e, Value::Bytes(_))) {
693
            return Err(ObjectError::Schema(
694
                "a version 1 payload holds graphs inline".into(),
695
            ));
696
        }
697
        // File graphs are independent; decode and build them in parallel.
698
        let graphs = parsed
699
            .par_iter()
700
            .map(|(node, entry)| {
701
                let graph = match fetched.get(node) {
702
                    Some(bytes) => FileGraph::from_value(&cbor::decode(bytes)?)?,
703
                    None => FileGraph::from_value(entry)?,
704
                };
705
                Ok((*node, graph))
706
            })
707
            .collect::<Result<Vec<_>, ObjectError>>()?;
708
        st.files
709
            .extend(graphs.into_iter().map(|(n, g)| (n, Arc::new(g))));
710
        Ok(st)
711
    }
712
713
    /// The graph blobs a payload names: empty for version 1.
714
    pub fn checkpoint_graph_refs(bytes: &[u8]) -> Result<Vec<ObjectRef>, ObjectError> {
715
        let v = cbor::decode(bytes)?;
716
        let m = o::map(&v, "checkpoint payload")?;
717
        let mut out = Vec::new();
718
        for f in o::array(o::field(m, 3, "files")?, "files")? {
719
            if let Some(entry @ Value::Bytes(_)) = o::array(f, "file")?.get(1) {
720
                out.push(o::oref(entry, "graph blob")?);
721
            }
722
        }
723
        Ok(out)
724
    }
725
726
    /// Decode a checkpoint payload for history only: a version 2 payload's
727
    /// graphs are noted, not read or built, and come later through
728
    /// [`State::load_graphs`]. A version 1 payload holds them inline and is
729
    /// decoded in full.
730
    pub fn from_checkpoint_history(bytes: &[u8]) -> Result<State, ObjectError> {
731
        let v = cbor::decode(bytes)?;
732
        let m = o::map(&v, "checkpoint payload")?;
733
        let version = o::uint(o::field(m, 0, "version")?, "version")?;
734
        if version != CHECKPOINT_INDEX_VERSION {
735
            return State::from_checkpoint_payload(bytes);
736
        }
737
        let mut st = State::payload_common(m)?;
738
        let mut deferred = HashMap::new();
739
        for f in o::array(o::field(m, 3, "files")?, "files")? {
740
            let e = o::array(f, "file")?;
741
            if e.len() != 2 {
742
                return Err(ObjectError::Schema("file: expected [node, graph]".into()));
743
            }
744
            let node = NodeId::from_slice(o::bytes(&e[0], "node")?)
745
                .ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?;
746
            let base = match &e[1] {
747
                Value::Bytes(_) => DeferredBase::Blob(o::oref(&e[1], "graph blob")?),
748
                other => DeferredBase::Inline(other.clone()),
749
            };
750
            deferred.insert(
751
                node,
752
                Deferred {
753
                    base: Some(base),
754
                    edits: Vec::new(),
755
                    texts: Vec::new(),
756
                },
757
            );
758
        }
759
        st.deferred = Some(deferred);
760
        Ok(st)
761
    }
762
763
    /// Changes, tree and replaces of a payload of either version.
764
    fn payload_common(m: &BTreeMap<u64, Value>) -> Result<State, ObjectError> {
765
        let mut st = State::new();
766
        for c in o::array(o::field(m, 1, "changes")?, "changes")? {
767
            let e = o::array(c, "change meta")?;
768
            if e.len() != 8 {
769
                return Err(ObjectError::Schema("change meta: expected 8 fields".into()));
770
            }
771
            let hash = o::oref(&e[0], "hash")?;
772
            let meta = ChangeMeta {
773
                change_id: crate::ids::ChangeId::from_slice(o::bytes(&e[1], "change id")?)
774
                    .ok_or_else(|| ObjectError::Schema("change id: expected 16 bytes".into()))?,
775
                deps: o::array(&e[2], "deps")?
776
                    .iter()
777
                    .map(|d| o::oref(d, "dep"))
778
                    .collect::<Result<Vec<_>, _>>()?,
779
                message: o::text(&e[3], "message")?.to_owned(),
780
                hlc: Hlc {
781
                    wall_ms: o::uint(&e[4], "wall")?,
782
                    logical: o::u32_(&e[5], "logical")?,
783
                },
784
                kind: ChangeKind::from_tag(o::uint(&e[6], "kind")?)?,
785
                author: o::oref(&e[7], "author")?,
786
            };
787
            st.changes.insert(hash, meta);
788
        }
789
        st.tree = TreeGraph::from_value(o::field(m, 2, "tree")?)?;
790
        let opt = |v: &Value| -> Result<Option<ObjectRef>, ObjectError> {
791
            if v.is_null() {
792
                Ok(None)
793
            } else {
794
                o::oref(v, "blob").map(Some)
795
            }
796
        };
797
        for r in o::array(o::field(m, 4, "replaces")?, "replaces")? {
798
            let e = o::array(r, "replace")?;
799
            if e.len() != 2 {
800
                return Err(ObjectError::Schema(
801
                    "replace: expected [node, records]".into(),
802
                ));
803
            }
804
            let node = NodeId::from_slice(o::bytes(&e[0], "node")?)
805
                .ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?;
806
            let mut recs = Vec::new();
807
            for rec in o::array(&e[1], "records")? {
808
                let x = o::array(rec, "record")?;
809
                if x.len() != 3 {
810
                    return Err(ObjectError::Schema("record: expected 3 fields".into()));
811
                }
812
                recs.push((o::oref(&x[0], "change")?, opt(&x[1])?, opt(&x[2])?));
813
            }
814
            st.replaces.insert(node, recs);
815
        }
816
        Ok(st)
817
    }
818
819
    pub fn validate(&self) -> Result<(), StateError> {
820
        for (h, c) in &self.changes {
821
            for d in &c.deps {
822
                if !self.changes.contains_key(d) {
823
                    return Err(StateError::MissingDep(*d, *h));
824
                }
825
            }
826
        }
827
        Ok(())
828
    }
829
830
    fn apply_with(&mut self, h: ChangeHash, c: Change, texts: Vec<TextRef>) {
831
        for (i, op) in c.tree_ops.iter().enumerate() {
832
            self.tree.apply(h, i as u32, &resolve_tree_op(op, h));
833
        }
834
        if let Some(deferred) = self.deferred.as_mut() {
835
            // History only: edits of files not built wait, grouped per file,
836
            // with the texts of inserts read without their lines.
837
            let mut texts = texts.into_iter();
838
            let mut per_node: HashMap<NodeId, (Vec<Edit>, Vec<TextRef>)> = HashMap::new();
839
            for e in c.edits {
840
                match e {
841
                    Edit::Replace { node, old, new } => {
842
                        self.replaces.entry(node).or_default().push((h, old, new))
843
                    }
844
                    e => {
845
                        let slot = per_node.entry(e.node()).or_default();
846
                        if matches!(&e, Edit::Insert { lines, .. } if lines.is_empty()) {
847
                            slot.1.extend(texts.next());
848
                        }
849
                        slot.0.push(e);
850
                    }
851
                }
852
            }
853
            for (node, (edits, node_texts)) in per_node {
854
                match self.files.get_mut(&node) {
855
                    Some(g) => {
856
                        assert!(
857
                            node_texts.is_empty(),
858
                            "a change read without its text applied to a built graph"
859
                        );
860
                        apply_file_edits(Arc::make_mut(g), h, edits)
861
                    }
862
                    None => {
863
                        let d = deferred.entry(node).or_default();
864
                        d.edits.push((h, edits));
865
                        d.texts.extend(node_texts);
866
                    }
867
                }
868
            }
869
            return;
870
        }
871
        assert!(
872
            texts.is_empty(),
873
            "a change read without its text applied to a full state"
874
        );
875
        let mut counters: HashMap<NodeId, u32> = HashMap::new();
876
        for e in c.edits {
877
            match e {
878
                Edit::Insert {
879
                    node,
880
                    after,
881
                    before,
882
                    lines,
883
                } => {
884
                    let idx = counters.entry(node).or_default();
885
                    let first = *idx;
886
                    *idx += lines.len() as u32;
887
                    Arc::make_mut(self.files.entry(node).or_default()).insert_owned(
888
                        h,
889
                        first,
890
                        vertex(after.resolve(h)),
891
                        vertex(before.resolve(h)),
892
                        lines,
893
                    );
894
                }
895
                Edit::Delete { node, atoms } => {
896
                    let g = Arc::make_mut(self.files.entry(node).or_default());
897
                    for r in atoms {
898
                        g.delete(h, &r.resolve(h));
899
                    }
900
                }
901
                Edit::Replace { node, old, new } => {
902
                    self.replaces.entry(node).or_default().push((h, old, new));
903
                }
904
            }
905
        }
906
    }
907
908
    /// Transitive dependencies of `h`, excluding `h` itself.
909
    pub fn closure(&self, h: &ChangeHash) -> Arc<HashSet<ChangeHash>> {
910
        if let Some(c) = self.closure.lock().unwrap().get(h) {
911
            return c.clone();
912
        }
913
        let mut out = HashSet::new();
914
        let mut stack: Vec<ChangeHash> = self
915
            .changes
916
            .get(h)
917
            .map(|c| c.deps.clone())
918
            .unwrap_or_default();
919
        while let Some(d) = stack.pop() {
920
            if out.insert(d) {
921
                if let Some(c) = self.changes.get(&d) {
922
                    stack.extend(c.deps.iter().copied());
923
                }
924
            }
925
        }
926
        let rc = Arc::new(out);
927
        self.closure.lock().unwrap().insert(*h, rc.clone());
928
        rc
929
    }
930
931
    /// Changes that no other change depends on directly, sorted.
932
    pub fn heads(&self) -> Vec<ChangeHash> {
933
        let depended: HashSet<&ChangeHash> =
934
            self.changes.values().flat_map(|c| c.deps.iter()).collect();
935
        let mut heads: Vec<ChangeHash> = self
936
            .changes
937
            .keys()
938
            .filter(|h| !depended.contains(h))
939
            .copied()
940
            .collect();
941
        heads.sort();
942
        heads
943
    }
944
945
    /// Maximal elements among `hashes` (those not in the closure of another),
946
    /// sorted. One walk down from all of them at once: whatever the walk
947
    /// reaches is below some other element. Linear in the changes reached,
948
    /// where comparing every pair's closures was quadratic in the number
949
    /// of heads, which a bookmark of independent changes has many of.
950
    pub fn maximal(&self, hashes: &[ChangeHash]) -> Vec<ChangeHash> {
951
        let set: BTreeSet<ChangeHash> = hashes.iter().copied().collect();
952
        let mut below: HashSet<ChangeHash> = HashSet::new();
953
        let mut stack: Vec<ChangeHash> = set
954
            .iter()
955
            .filter_map(|h| self.changes.get(h))
956
            .flat_map(|c| c.deps.iter().copied())
957
            .collect();
958
        while let Some(d) = stack.pop() {
959
            if below.insert(d) {
960
                if let Some(c) = self.changes.get(&d) {
961
                    stack.extend(c.deps.iter().copied());
962
                }
963
            }
964
        }
965
        set.into_iter().filter(|h| !below.contains(h)).collect()
966
    }
967
968
    pub fn materialize_file(&self, node: &NodeId) -> Materialized {
969
        self.graph(node)
970
            .map(|g| g.materialize(self))
971
            .unwrap_or_default()
972
    }
973
974
    /// Live lines of a text node in materialized order.
975
    pub fn live_lines(&self, node: &NodeId) -> Vec<(AtomKey, Vec<u8>)> {
976
        let Some(g) = self.graph(node) else {
977
            return vec![];
978
        };
979
        let m = g.materialize(self);
980
        m.live_atoms()
981
            .into_iter()
982
            .map(|a| (a, g.content(&a).unwrap_or_default().to_vec()))
983
            .collect()
984
    }
985
986
    /// Live blobs of an opaque node: every `new` that no later `Replace` retired.
987
    pub fn live_blobs(&self, node: &NodeId) -> Vec<ObjectRef> {
988
        let mut live: Vec<ObjectRef> = self
989
            .live_replaces(node)
990
            .into_iter()
991
            .map(|(_, n)| n)
992
            .collect();
993
        live.sort();
994
        live.dedup();
995
        live
996
    }
997
998
    /// The replace records whose blob is live, with the change that set it
999
    /// (spec §9.3). A record's blob is superseded by a later replacement of
1000
    /// that blob: one whose change depends on the record's change, as every
1001
    /// replacement depends on the change that made its old blob current
1002
    /// (§8.1). Two replacements that do not know each other both stay live
1003
    /// (a replace conflict), and a file reverted to an earlier blob keeps it.
1004
    /// A replacement without that dependency, from before the rule, is read
1005
    /// as before: it supersedes every record that did not come after it.
1006
    pub fn live_replaces(&self, node: &NodeId) -> Vec<(ChangeHash, ObjectRef)> {
1007
        let Some(rs) = self.replaces.get(node) else {
1008
            return vec![];
1009
        };
1010
        let knows_its_old = |s: &ChangeHash, old: &ObjectRef| {
1011
            rs.iter()
1012
                .any(|(q, _, n)| n.as_ref() == Some(old) && q != s && self.closure(s).contains(q))
1013
        };
1014
        rs.iter()
1015
            .filter_map(|(c, _, n)| n.map(|n| (*c, n)))
1016
            .filter(|(c, n)| {
1017
                !rs.iter().any(|(s, o, _)| {
1018
                    s != c
1019
                        && *o == Some(*n)
1020
                        && (self.closure(s).contains(c)
1021
                            || (!knows_its_old(s, n) && !self.closure(c).contains(s)))
1022
                })
1023
            })
1024
            .collect()
1025
    }
1026
1027
    /// Short label for conflict markers: `changeid-prefix "message"`.
1028
    pub fn label(&self, h: &ChangeHash) -> String {
1029
        match self.changes.get(h) {
1030
            Some(c) => format!(
1031
                "{} \"{}\"",
1032
                c.change_id.short(8),
1033
                c.message.lines().next().unwrap_or("")
1034
            ),
1035
            None => format!("#{}", h.short(12)),
1036
        }
1037
    }
1038
1039
    /// For resolving a conflicted text node (spec §9.6): the live atoms that
1040
    /// are not part of any conflict region in order, every live atom that is
1041
    /// part of a region, and every change that contributed to a region.
1042
    /// The changes that introduced the live blobs of an opaque node, for a
1043
    /// replace conflict (spec §9.3).
1044
    pub fn blob_contributors(&self, node: &NodeId, blobs: &[ObjectRef]) -> Vec<ChangeHash> {
1045
        let mut out: Vec<ChangeHash> = Vec::new();
1046
        for (h, _, new) in self.replaces.get(node).map(|v| v.as_slice()).unwrap_or(&[]) {
1047
            if new.is_some_and(|b| blobs.contains(&b)) && !out.contains(h) {
1048
                out.push(*h);
1049
            }
1050
        }
1051
        out.sort();
1052
        out
1053
    }
1054
1055
    pub fn conflict_regions(&self, node: &NodeId) -> ConflictRegions {
1056
        let mut out = ConflictRegions::default();
1057
        let Some(g) = self.graph(node) else {
1058
            return out;
1059
        };
1060
        let m = g.materialize(self);
1061
        let mut contributors = BTreeSet::new();
1062
        for seg in &m.segments {
1063
            match seg {
1064
                crate::graph::Segment::Lines(atoms) => {
1065
                    for a in atoms {
1066
                        out.plain
1067
                            .push((*a, g.content(a).unwrap_or_default().to_vec()));
1068
                    }
1069
                }
1070
                crate::graph::Segment::Conflict(sides) => {
1071
                    for side in sides {
1072
                        for a in side {
1073
                            out.region_atoms.push(*a);
1074
                            contributors.insert(a.0);
1075
                        }
1076
                    }
1077
                }
1078
                crate::graph::Segment::Zombie { deleted, inserted } => {
1079
                    for a in deleted {
1080
                        contributors.insert(a.0);
1081
                        contributors.extend(g.deleters(a).copied());
1082
                    }
1083
                    for a in inserted {
1084
                        out.region_atoms.push(*a);
1085
                        contributors.insert(a.0);
1086
                    }
1087
                }
1088
            }
1089
        }
1090
        out.contributors = contributors.into_iter().collect();
1091
        out
1092
    }
1093
1094
    pub fn render_file(&self, node: &NodeId) -> (Vec<u8>, Materialized) {
1095
        let Some(g) = self.graph(node) else {
1096
            return (Vec::new(), Materialized::default());
1097
        };
1098
        let m = g.materialize(self);
1099
        let bytes = g.render(&m, &|h| self.label(h));
1100
        (bytes, m)
1101
    }
1102
1103
    /// Every file rendered, in parallel: files are independent, and
1104
    /// rendering them is most of a snapshot's cost in a large tree.
1105
    pub fn materialize(&self) -> Snapshot {
1106
        use rayon::prelude::*;
1107
        self.require_full("materializing a state");
1108
        let tree = self.tree.view();
1109
        let entries: Vec<(&String, &crate::tree::TreeEntry)> = tree.entries.iter().collect();
1110
        let rendered: Vec<(String, FileState)> = entries
1111
            .par_iter()
1112
            .map(|(path, e)| (path.to_string(), self.file_state(e)))
1113
            .collect();
1114
        let files: BTreeMap<String, FileState> = rendered.into_iter().collect();
1115
        Snapshot {
1116
            tree,
1117
            files,
1118
            unrendered: BTreeSet::new(),
1119
        }
1120
    }
1121
1122
    /// Like [`State::materialize`], leaving the text files of nodes in
1123
    /// `skip` unrendered (listed in [`Snapshot::unrendered`]); every other
1124
    /// file's graph must be loaded. For a caller that already knows those
1125
    /// files are unchanged and not conflicted.
1126
    pub fn materialize_except(&self, skip: &HashSet<NodeId>) -> Snapshot {
1127
        use rayon::prelude::*;
1128
        let tree = self.tree.view();
1129
        let entries: Vec<(&String, &crate::tree::TreeEntry)> = tree.entries.iter().collect();
1130
        let rendered: Vec<(String, FileState, bool)> = entries
1131
            .par_iter()
1132
            .map(|(path, e)| {
1133
                if e.kind == NodeKind::File && !e.mode.opaque && skip.contains(&e.node) {
1134
                    let f = FileState {
1135
                        node: e.node,
1136
                        kind: e.kind,
1137
                        mode: e.mode,
1138
                        content: FileContent::Text {
1139
                            bytes: Vec::new(),
1140
                            materialized: Materialized::default(),
1141
                        },
1142
                    };
1143
                    (path.to_string(), f, true)
1144
                } else {
1145
                    (path.to_string(), self.file_state(e), false)
1146
                }
1147
            })
1148
            .collect();
1149
        let mut files = BTreeMap::new();
1150
        let mut unrendered = BTreeSet::new();
1151
        for (p, f, skipped) in rendered {
1152
            if skipped {
1153
                unrendered.insert(p.clone());
1154
            }
1155
            files.insert(p, f);
1156
        }
1157
        Snapshot {
1158
            tree,
1159
            files,
1160
            unrendered,
1161
        }
1162
    }
1163
1164
    fn file_state(&self, e: &crate::tree::TreeEntry) -> FileState {
1165
        {
1166
            let content = match e.kind {
1167
                NodeKind::Dir => FileContent::Dir,
1168
                NodeKind::Symlink | NodeKind::Subrepo => FileContent::Opaque {
1169
                    blobs: self.live_blobs(&e.node),
1170
                },
1171
                NodeKind::File if e.mode.opaque => FileContent::Opaque {
1172
                    blobs: self.live_blobs(&e.node),
1173
                },
1174
                NodeKind::File => {
1175
                    let (bytes, materialized) = self.render_file(&e.node);
1176
                    FileContent::Text {
1177
                        bytes,
1178
                        materialized,
1179
                    }
1180
                }
1181
            };
1182
            FileState {
1183
                node: e.node,
1184
                kind: e.kind,
1185
                mode: e.mode,
1186
                content,
1187
            }
1188
        }
1189
    }
1190
}
1191
1192
impl DepOracle for State {
1193
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
1194
        a == b || self.closure(a).contains(b)
1195
    }
1196
}
1197
1198
#[cfg(test)]
1199
mod tests {
1200
    use super::*;
1201
    use crate::ids::ChangeId;
1202
    use crate::objects::{AtomRange, ChangeKind, Hlc, OpRef};
1203
1204
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>) -> Change {
1205
        let mut c = Change {
1206
            version: crate::objects::CHANGE_VERSION,
1207
            change_id: ChangeId::random(),
1208
            deps,
1209
            author: ObjectRef::ZERO,
1210
            hlc: Hlc {
1211
                wall_ms: 1,
1212
                logical: 0,
1213
            },
1214
            message: "m".into(),
1215
            tree_ops,
1216
            edits,
1217
            kind: ChangeKind::Normal,
1218
        };
1219
        c.normalize();
1220
        c
1221
    }
1222
1223
    #[test]
1224
    fn build_edit_and_materialize() {
1225
        let mut st = State::new();
1226
        let f = NodeId::random();
1227
        let c1 = change(
1228
            vec![],
1229
            vec![TreeOp::AddNode {
1230
                node: f,
1231
                kind: NodeKind::File,
1232
                mode: Mode::TEXT,
1233
                parent: NodeId::ROOT,
1234
                name: "a.txt".into(),
1235
            }],
1236
            vec![Edit::Insert {
1237
                node: f,
1238
                after: AtomRef::Start,
1239
                before: AtomRef::End,
1240
                lines: vec![b"one\n".to_vec(), b"two\n".to_vec()],
1241
            }],
1242
        );
1243
        let h1 = st.add(c1).unwrap();
1244
        // Depends on h1: append a line and delete "one".
1245
        let c2 = change(
1246
            vec![h1],
1247
            vec![],
1248
            vec![
1249
                Edit::Delete {
1250
                    node: f,
1251
                    atoms: vec![AtomRange {
1252
                        change: h1,
1253
                        from: 0,
1254
                        to: 1,
1255
                    }],
1256
                },
1257
                Edit::Insert {
1258
                    node: f,
1259
                    after: AtomRef::Atom {
1260
                        change: h1,
1261
                        index: 1,
1262
                    },
1263
                    before: AtomRef::End,
1264
                    lines: vec![b"three\n".to_vec()],
1265
                },
1266
            ],
1267
        );
1268
        let h2 = st.add(c2).unwrap();
1269
        assert_eq!(st.heads(), vec![h2]);
1270
        assert!(st.depends_on(&h2, &h1));
1271
        assert!(!st.depends_on(&h1, &h2));
1272
        let snap = st.materialize();
1273
        assert!(!snap.is_conflicted());
1274
        let a = &snap.files["a.txt"];
1275
        assert!(matches!(&a.content, FileContent::Text { bytes, .. } if bytes == b"two\nthree\n"));
1276
        assert_eq!(st.live_lines(&f).len(), 2);
1277
    }
1278
1279
    #[test]
1280
    fn missing_dependency_is_rejected() {
1281
        let mut st = State::new();
1282
        let c = change(vec![ObjectRef([1; 32])], vec![], vec![]);
1283
        assert!(matches!(st.add(c), Err(StateError::MissingDep(..))));
1284
    }
1285
1286
    #[test]
1287
    fn self_references_and_multi_insert_indexing() {
1288
        let mut st = State::new();
1289
        let f = NodeId::random();
1290
        let me = OpRef {
1291
            change: ObjectRef::ZERO,
1292
            index: 0,
1293
        };
1294
        let c1 = change(
1295
            vec![],
1296
            vec![
1297
                TreeOp::AddNode {
1298
                    node: f,
1299
                    kind: NodeKind::File,
1300
                    mode: Mode::TEXT,
1301
                    parent: NodeId::ROOT,
1302
                    name: "x".into(),
1303
                },
1304
                TreeOp::SetMode {
1305
                    node: f,
1306
                    old: me,
1307
                    mode: Mode {
1308
                        exec: true,
1309
                        opaque: false,
1310
                    },
1311
                },
1312
            ],
1313
            vec![
1314
                Edit::Insert {
1315
                    node: f,
1316
                    after: AtomRef::Start,
1317
                    before: AtomRef::End,
1318
                    lines: vec![b"a\n".to_vec(), b"c\n".to_vec()],
1319
                },
1320
                // Insert between the two lines just inserted, using self references.
1321
                Edit::Insert {
1322
                    node: f,
1323
                    after: AtomRef::Atom {
1324
                        change: ObjectRef::ZERO,
1325
                        index: 0,
1326
                    },
1327
                    before: AtomRef::Atom {
1328
                        change: ObjectRef::ZERO,
1329
                        index: 1,
1330
                    },
1331
                    lines: vec![b"b\n".to_vec()],
1332
                },
1333
            ],
1334
        );
1335
        st.add(c1).unwrap();
1336
        let snap = st.materialize();
1337
        let x = &snap.files["x"];
1338
        assert_eq!(
1339
            x.mode,
1340
            Mode {
1341
                exec: true,
1342
                opaque: false
1343
            }
1344
        );
1345
        assert!(matches!(&x.content, FileContent::Text { bytes, .. } if bytes == b"a\nb\nc\n"));
1346
    }
1347
1348
    #[test]
1349
    fn opaque_replace_and_conflict() {
1350
        let mut st = State::new();
1351
        let f = NodeId::random();
1352
        let b1 = ObjectRef([1; 32]);
1353
        let b2 = ObjectRef([2; 32]);
1354
        let b3 = ObjectRef([3; 32]);
1355
        let h1 = st
1356
            .add(change(
1357
                vec![],
1358
                vec![TreeOp::AddNode {
1359
                    node: f,
1360
                    kind: NodeKind::File,
1361
                    mode: Mode::OPAQUE,
1362
                    parent: NodeId::ROOT,
1363
                    name: "img".into(),
1364
                }],
1365
                vec![Edit::Replace {
1366
                    node: f,
1367
                    old: None,
1368
                    new: Some(b1),
1369
                }],
1370
            ))
1371
            .unwrap();
1372
        assert_eq!(st.live_blobs(&f), vec![b1]);
1373
        st.add(change(
1374
            vec![h1],
1375
            vec![],
1376
            vec![Edit::Replace {
1377
                node: f,
1378
                old: Some(b1),
1379
                new: Some(b2),
1380
            }],
1381
        ))
1382
        .unwrap();
1383
        st.add(change(
1384
            vec![h1],
1385
            vec![],
1386
            vec![Edit::Replace {
1387
                node: f,
1388
                old: Some(b1),
1389
                new: Some(b3),
1390
            }],
1391
        ))
1392
        .unwrap();
1393
        let snap = st.materialize();
1394
        assert!(snap.is_conflicted());
1395
        assert!(
1396
            matches!(&snap.files["img"].content, FileContent::Opaque { blobs } if blobs == &vec![b2, b3])
1397
        );
1398
    }
1399
1400
    #[test]
1401
    fn checkpoint_payload_round_trip_and_replay() {
1402
        let mut st = State::new();
1403
        let f = NodeId::random();
1404
        let h1 = st
1405
            .add(change(
1406
                vec![],
1407
                vec![TreeOp::AddNode {
1408
                    node: f,
1409
                    kind: NodeKind::File,
1410
                    mode: Mode::TEXT,
1411
                    parent: NodeId::ROOT,
1412
                    name: "a".into(),
1413
                }],
1414
                vec![Edit::Insert {
1415
                    node: f,
1416
                    after: AtomRef::Start,
1417
                    before: AtomRef::End,
1418
                    lines: vec![b"1\n".to_vec(), b"2\n".to_vec()],
1419
                }],
1420
            ))
1421
            .unwrap();
1422
        let h2 = st
1423
            .add(change(
1424
                vec![h1],
1425
                vec![],
1426
                vec![Edit::Delete {
1427
                    node: f,
1428
                    atoms: vec![AtomRange {
1429
                        change: h1,
1430
                        from: 0,
1431
                        to: 1,
1432
                    }],
1433
                }],
1434
            ))
1435
            .unwrap();
1436
        let payload = st.checkpoint_payload();
1437
        let back = State::from_checkpoint_payload(&payload).unwrap();
1438
        assert_eq!(back.len(), 2);
1439
        assert_eq!(back.heads(), vec![h2]);
1440
        assert_eq!(back.get(&h1).unwrap().message, "m");
1441
        assert_eq!(back.checkpoint_payload(), payload, "deterministic");
1442
        // Replaying a further change onto the restored state equals a full build.
1443
        let c3 = change(
1444
            vec![h2],
1445
            vec![],
1446
            vec![Edit::Insert {
1447
                node: f,
1448
                after: AtomRef::Atom {
1449
                    change: h1,
1450
                    index: 1,
1451
                },
1452
                before: AtomRef::End,
1453
                lines: vec![b"3\n".to_vec()],
1454
            }],
1455
        );
1456
        let mut full = st.clone();
1457
        full.add(c3.clone()).unwrap();
1458
        let mut restored = back;
1459
        restored.add(c3).unwrap();
1460
        let (a, b) = (full.materialize(), restored.materialize());
1461
        assert_eq!(
1462
            a.files.keys().collect::<Vec<_>>(),
1463
            b.files.keys().collect::<Vec<_>>()
1464
        );
1465
        assert_eq!(a.files["a"].content, b.files["a"].content);
1466
        assert!(
1467
            matches!(&b.files["a"].content, FileContent::Text { bytes, .. } if bytes == b"2\n3\n")
1468
        );
1469
        assert!(State::from_checkpoint_payload(b"nonsense").is_err());
1470
    }
1471
1472
    /// History-only states (O10f): the same state as a full load once its
1473
    /// graphs are built, by replay and from a version 2 checkpoint with
1474
    /// later changes; unloaded graphs are never read as empty.
1475
    #[test]
1476
    fn graph_keys_change_exactly_when_a_graph_does() {
1477
        let (f, g) = (NodeId::random(), NodeId::random());
1478
        let add = |node: NodeId, name: &str| TreeOp::AddNode {
1479
            node,
1480
            kind: NodeKind::File,
1481
            mode: Mode::TEXT,
1482
            parent: NodeId::ROOT,
1483
            name: name.into(),
1484
        };
1485
        let ins = |node: NodeId, line: &[u8]| Edit::Insert {
1486
            node,
1487
            after: AtomRef::Start,
1488
            before: AtomRef::End,
1489
            lines: vec![line.to_vec()],
1490
        };
1491
        let c1 = change(
1492
            vec![],
1493
            vec![add(f, "f"), add(g, "g")],
1494
            vec![ins(f, b"f\n"), ins(g, b"g\n")],
1495
        );
1496
        let h1 = c1.hash();
1497
        let c2 = change(vec![h1], vec![], vec![ins(g, b"g2\n")]);
1498
        let h2 = c2.hash();
1499
        let c3 = change(vec![h2], vec![], vec![ins(f, b"f2\n")]);
1500
        let load = |cs: &[&Change]| {
1501
            let mut st = State::new_history_only();
1502
            for c in cs {
1503
                st.add((*c).clone()).unwrap();
1504
            }
1505
            st
1506
        };
1507
        let s1 = load(&[&c1]);
1508
        let s2 = load(&[&c1, &c2]);
1509
        let s3 = load(&[&c1, &c2, &c3]);
1510
        let key = |s: &State, n: &NodeId| s.graph_key(n).unwrap();
1511
        assert_eq!(key(&s1, &f), key(&load(&[&c1]), &f), "same state, same key");
1512
        assert_eq!(key(&s1, &f), key(&s2, &f), "an edit elsewhere keeps it");
1513
        assert_ne!(key(&s1, &g), key(&s2, &g), "an edit of the file changes it");
1514
        assert_ne!(key(&s2, &f), key(&s3, &f));
1515
        assert_ne!(key(&s1, &f), key(&s1, &g), "different files differ");
1516
        let mut built = load(&[&c1]);
1517
        built
1518
            .load_graphs(&[f], |_| unreachable!(), |_| unreachable!())
1519
            .unwrap();
1520
        assert!(built.graph_key(&f).is_none(), "a built graph has no key");
1521
        let mut full = State::new();
1522
        full.add(c1.clone()).unwrap();
1523
        assert!(full.graph_key(&f).is_none(), "nor has a full state");
1524
1525
        // Through a version 2 checkpoint of c1, keys name its graph blobs
1526
        // or inline values: stable across loads, changed by later edits.
1527
        let graphs = full.checkpoint_file_graphs();
1528
        let index = full.checkpoint_index(&graphs, |enc| {
1529
            crate::hash::hash_object(crate::hash::ObjectType::Blob, enc)
1530
        });
1531
        let cp = |extra: &[&Change]| {
1532
            let mut st = State::from_checkpoint_history(&index).unwrap();
1533
            for c in extra {
1534
                st.add((*c).clone()).unwrap();
1535
            }
1536
            st
1537
        };
1538
        assert_eq!(key(&cp(&[]), &f), key(&cp(&[]), &f));
1539
        assert_eq!(key(&cp(&[]), &f), key(&cp(&[&c2]), &f));
1540
        assert_ne!(key(&cp(&[]), &g), key(&cp(&[&c2]), &g));
1541
        assert_ne!(key(&cp(&[&c2]), &f), key(&cp(&[&c2, &c3]), &f));
1542
1543
        // Partial rendering: skipped files are listed and empty, the others
1544
        // render as in a full state.
1545
        let mut part = load(&[&c1, &c2]);
1546
        part.load_graphs(&[g], |_| unreachable!(), |_| unreachable!())
1547
            .unwrap();
1548
        let snap = part.materialize_except(&[f].into_iter().collect());
1549
        assert_eq!(snap.unrendered, ["f".to_string()].into_iter().collect());
1550
        let mut whole = State::new();
1551
        whole.add(c1.clone()).unwrap();
1552
        whole.add(c2.clone()).unwrap();
1553
        assert_eq!(snap.files["g"], whole.materialize().files["g"]);
1554
        assert!(
1555
            matches!(&snap.files["f"].content, FileContent::Text { bytes, .. } if bytes.is_empty())
1556
        );
1557
    }
1558
1559
    #[test]
1560
    fn history_only_states_build_the_same_graphs_on_demand() {
1561
        let small = NodeId::random();
1562
        let big = NodeId::random();
1563
        let add = |node: NodeId, name: &str| TreeOp::AddNode {
1564
            node,
1565
            kind: NodeKind::File,
1566
            mode: Mode::TEXT,
1567
            parent: NodeId::ROOT,
1568
            name: name.into(),
1569
        };
1570
        let lines: Vec<Vec<u8>> = (0..300)
1571
            .map(|i| format!("line {i}\n").into_bytes())
1572
            .collect();
1573
        let c1 = change(
1574
            vec![],
1575
            vec![add(small, "small"), add(big, "big")],
1576
            vec![
1577
                Edit::Insert {
1578
                    node: small,
1579
                    after: AtomRef::Start,
1580
                    before: AtomRef::End,
1581
                    lines: vec![b"x\n".to_vec()],
1582
                },
1583
                Edit::Insert {
1584
                    node: big,
1585
                    after: AtomRef::Start,
1586
                    before: AtomRef::End,
1587
                    lines,
1588
                },
1589
            ],
1590
        );
1591
        let h1 = c1.hash();
1592
        let c2 = change(
1593
            vec![h1],
1594
            vec![],
1595
            vec![
1596
                Edit::Delete {
1597
                    node: big,
1598
                    atoms: vec![AtomRange {
1599
                        change: h1,
1600
                        from: 2,
1601
                        to: 9,
1602
                    }],
1603
                },
1604
                Edit::Insert {
1605
                    node: big,
1606
                    after: AtomRef::Atom {
1607
                        change: h1,
1608
                        index: 20,
1609
                    },
1610
                    before: AtomRef::Atom {
1611
                        change: h1,
1612
                        index: 21,
1613
                    },
1614
                    lines: vec![b"new a\n".to_vec(), b"new b\n".to_vec()],
1615
                },
1616
                Edit::Insert {
1617
                    node: small,
1618
                    after: AtomRef::Atom {
1619
                        change: h1,
1620
                        index: 0,
1621
                    },
1622
                    before: AtomRef::End,
1623
                    lines: vec![b"y\n".to_vec()],
1624
                },
1625
            ],
1626
        );
1627
        let h2 = c2.hash();
1628
        let c3 = change(
1629
            vec![h2],
1630
            vec![],
1631
            vec![Edit::Delete {
1632
                node: small,
1633
                atoms: vec![AtomRange {
1634
                    change: h1,
1635
                    from: 0,
1636
                    to: 1,
1637
                }],
1638
            }],
1639
        );
1640
1641
        let mut full = State::new();
1642
        for c in [&c1, &c2, &c3] {
1643
            full.add(c.clone()).unwrap();
1644
        }
1645
        // By replay: nothing built until asked, then exactly the full graphs.
1646
        let mut hist = State::new_history_only();
1647
        for c in [&c1, &c2, &c3] {
1648
            hist.add(c.clone()).unwrap();
1649
        }
1650
        assert!(!hist.is_full() && hist.files.is_empty(), "no graph built");
1651
        assert_eq!(hist.len(), 3);
1652
        hist.load_graphs(
1653
            &[big],
1654
            |_| unreachable!("no checkpoint"),
1655
            |_| unreachable!(),
1656
        )
1657
        .unwrap();
1658
        assert_eq!(
1659
            hist.render_file(&big).0,
1660
            full.render_file(&big).0,
1661
            "one file on its own"
1662
        );
1663
        assert!(!hist.is_full());
1664
        hist.load_graphs(&[small, big], |_| unreachable!(), |_| unreachable!())
1665
            .unwrap();
1666
        assert!(hist.is_full());
1667
        assert_eq!(hist.checkpoint_payload(), full.checkpoint_payload());
1668
1669
        // From a version 2 checkpoint of c1+c2, then c3 applied later.
1670
        let mut base = State::new();
1671
        base.add(c1.clone()).unwrap();
1672
        base.add(c2.clone()).unwrap();
1673
        let graphs = base.checkpoint_file_graphs();
1674
        let mut blobs: HashMap<ObjectRef, Vec<u8>> = HashMap::new();
1675
        let index = base.checkpoint_index(&graphs, |enc| {
1676
            let r = crate::hash::hash_object(crate::hash::ObjectType::Blob, enc);
1677
            blobs.insert(r, enc.to_vec());
1678
            r
1679
        });
1680
        let mut from_cp = State::from_checkpoint_history(&index).unwrap();
1681
        from_cp.add(c3.clone()).unwrap();
1682
        assert!(!from_cp.is_full());
1683
        let mut fetched = 0;
1684
        from_cp
1685
            .load_graphs(
1686
                &[small, big],
1687
                |refs| {
1688
                    fetched += refs.len();
1689
                    refs.iter()
1690
                        .map(|r| {
1691
                            blobs
1692
                                .get(r)
1693
                                .cloned()
1694
                                .ok_or_else(|| ObjectError::Schema("missing".into()))
1695
                        })
1696
                        .collect()
1697
                },
1698
                |_| unreachable!(),
1699
            )
1700
            .unwrap();
1701
        assert_eq!(
1702
            fetched, 1,
1703
            "the large graph's blob, in one batch; the small one was inline"
1704
        );
1705
        assert_eq!(from_cp.checkpoint_payload(), full.checkpoint_payload());
1706
    }
1707
1708
    #[test]
1709
    #[should_panic(expected = "was not loaded")]
1710
    fn a_history_only_state_never_reads_an_unloaded_graph_as_empty() {
1711
        let f = NodeId::random();
1712
        let mut st = State::new_history_only();
1713
        st.add(change(
1714
            vec![],
1715
            vec![TreeOp::AddNode {
1716
                node: f,
1717
                kind: NodeKind::File,
1718
                mode: Mode::TEXT,
1719
                parent: NodeId::ROOT,
1720
                name: "a".into(),
1721
            }],
1722
            vec![Edit::Insert {
1723
                node: f,
1724
                after: AtomRef::Start,
1725
                before: AtomRef::End,
1726
                lines: vec![b"a\n".to_vec()],
1727
            }],
1728
        ))
1729
        .unwrap();
1730
        st.render_file(&f);
1731
    }
1732
1733
    #[test]
1734
    #[should_panic(expected = "needs every file graph")]
1735
    fn materializing_a_history_only_state_is_refused() {
1736
        let f = NodeId::random();
1737
        let mut st = State::new_history_only();
1738
        st.add(change(
1739
            vec![],
1740
            vec![TreeOp::AddNode {
1741
                node: f,
1742
                kind: NodeKind::File,
1743
                mode: Mode::TEXT,
1744
                parent: NodeId::ROOT,
1745
                name: "a".into(),
1746
            }],
1747
            vec![Edit::Insert {
1748
                node: f,
1749
                after: AtomRef::Start,
1750
                before: AtomRef::End,
1751
                lines: vec![b"a\n".to_vec()],
1752
            }],
1753
        ))
1754
        .unwrap();
1755
        st.materialize();
1756
    }
1757
1758
    /// Version 2 payloads (spec §11): small graphs inline, large ones by
1759
    /// blob; the state read back is the same as from version 1.
1760
    #[test]
1761
    fn checkpoint_index_round_trip_matches_version_1() {
1762
        let mut st = State::new();
1763
        let small = NodeId::random();
1764
        let big = NodeId::random();
1765
        let add = |node: NodeId, name: &str| TreeOp::AddNode {
1766
            node,
1767
            kind: NodeKind::File,
1768
            mode: Mode::TEXT,
1769
            parent: NodeId::ROOT,
1770
            name: name.into(),
1771
        };
1772
        let big_lines: Vec<Vec<u8>> = (0..400)
1773
            .map(|i| format!("line {i} of a long file\n").into_bytes())
1774
            .collect();
1775
        let h1 = st
1776
            .add(change(
1777
                vec![],
1778
                vec![add(small, "small"), add(big, "big")],
1779
                vec![
1780
                    Edit::Insert {
1781
                        node: small,
1782
                        after: AtomRef::Start,
1783
                        before: AtomRef::End,
1784
                        lines: vec![b"x\n".to_vec()],
1785
                    },
1786
                    Edit::Insert {
1787
                        node: big,
1788
                        after: AtomRef::Start,
1789
                        before: AtomRef::End,
1790
                        lines: big_lines,
1791
                    },
1792
                ],
1793
            ))
1794
            .unwrap();
1795
        st.add(change(
1796
            vec![h1],
1797
            vec![],
1798
            vec![Edit::Delete {
1799
                node: big,
1800
                atoms: vec![AtomRange {
1801
                    change: h1,
1802
                    from: 3,
1803
                    to: 7,
1804
                }],
1805
            }],
1806
        ))
1807
        .unwrap();
1808
1809
        let graphs = st.checkpoint_file_graphs();
1810
        let mut blobs: HashMap<ObjectRef, Vec<u8>> = HashMap::new();
1811
        let index = st.checkpoint_index(&graphs, |enc| {
1812
            let r = crate::hash::hash_object(crate::hash::ObjectType::Blob, enc);
1813
            blobs.insert(r, enc.to_vec());
1814
            r
1815
        });
1816
        assert_eq!(blobs.len(), 1, "only the large graph is a blob of its own");
1817
        assert_eq!(
1818
            State::checkpoint_graph_refs(&index).unwrap(),
1819
            blobs.keys().copied().collect::<Vec<_>>()
1820
        );
1821
        assert!(
1822
            State::checkpoint_graph_refs(&st.checkpoint_payload())
1823
                .unwrap()
1824
                .is_empty(),
1825
            "version 1 names none"
1826
        );
1827
        assert!(index.len() < st.checkpoint_payload().len());
1828
1829
        let back = State::from_checkpoint(&index, |refs| {
1830
            refs.iter()
1831
                .map(|r| {
1832
                    blobs
1833
                        .get(r)
1834
                        .cloned()
1835
                        .ok_or_else(|| ObjectError::Schema("missing".into()))
1836
                })
1837
                .collect()
1838
        })
1839
        .unwrap();
1840
        assert_eq!(
1841
            back.checkpoint_payload(),
1842
            st.checkpoint_payload(),
1843
            "the same state as version 1"
1844
        );
1845
        assert_eq!(
1846
            back.materialize().files["big"].content,
1847
            st.materialize().files["big"].content
1848
        );
1849
        // Deterministic: the same state gives the same index.
1850
        assert_eq!(
1851
            back.checkpoint_index(&back.checkpoint_file_graphs(), |enc| {
1852
                crate::hash::hash_object(crate::hash::ObjectType::Blob, enc)
1853
            }),
1854
            index
1855
        );
1856
        // A missing graph blob is an error, not an empty file.
1857
        assert!(
1858
            State::from_checkpoint(&index, |_| Err(ObjectError::Schema("gone".into()))).is_err()
1859
        );
1860
        assert!(
1861
            State::from_checkpoint_payload(&index).is_err(),
1862
            "version 2 needs its blobs"
1863
        );
1864
        // Version 1 payloads still read, and unknown versions do not.
1865
        assert!(State::from_checkpoint(&st.checkpoint_payload(), |_| unreachable!()).is_ok());
1866
        let mut v3 = cbor::decode(&index).unwrap();
1867
        if let Value::Map(m) = &mut v3 {
1868
            m.insert(0, Value::Uint(3));
1869
        }
1870
        assert!(State::from_checkpoint(&cbor::encode(&v3), |_| unreachable!()).is_err());
1871
    }
1872
1873
    #[test]
1874
    fn maximal_elements() {
1875
        let mut st = State::new();
1876
        let h1 = st.add(change(vec![], vec![], vec![])).unwrap();
1877
        let h2 = st.add(change(vec![h1], vec![], vec![])).unwrap();
1878
        let h3 = st.add(change(vec![], vec![], vec![])).unwrap();
1879
        let mut m = st.maximal(&[h1, h2, h3]);
1880
        m.sort();
1881
        let mut want = vec![h2, h3];
1882
        want.sort();
1883
        assert_eq!(m, want);
1884
        assert_eq!(st.heads(), want);
1885
    }
1886
1887
    /// The one-walk `maximal` agrees with the definition (not in the
1888
    /// closure of another element) on random graphs, with repeated and
1889
    /// unknown hashes among the input.
1890
    #[test]
1891
    fn maximal_matches_its_definition() {
1892
        use rand::{Rng, SeedableRng};
1893
        let mut rng = rand::rngs::StdRng::seed_from_u64(7);
1894
        for _ in 0..20 {
1895
            let mut st = State::new();
1896
            let mut all: Vec<ChangeHash> = Vec::new();
1897
            for _ in 0..60 {
1898
                let deps: Vec<ChangeHash> =
1899
                    all.iter().copied().filter(|_| rng.gen_bool(0.05)).collect();
1900
                all.push(st.add(change(deps, vec![], vec![])).unwrap());
1901
            }
1902
            let mut pick: Vec<ChangeHash> =
1903
                all.iter().copied().filter(|_| rng.gen_bool(0.4)).collect();
1904
            pick.extend(pick.clone().into_iter().take(3));
1905
            pick.push(ObjectRef([0xee; 32]));
1906
            let set: BTreeSet<ChangeHash> = pick.iter().copied().collect();
1907
            let want: Vec<ChangeHash> = set
1908
                .iter()
1909
                .filter(|h| !set.iter().any(|o| o != *h && st.closure(o).contains(h)))
1910
                .copied()
1911
                .collect();
1912
            assert_eq!(st.maximal(&pick), want);
1913
            assert_eq!(st.maximal(&all), st.heads());
1914
        }
1915
    }
1916
}