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