OXYGENHUB
# 2cd6bf89adb4 oxygen / crates / oxy-core / src / state.rs
15.7 KiB · Rust · last changed Test: a state index's long changes block reads back whole and in order 19 days 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
4
use crate::graph::{AtomKey, DepOracle, FileGraph, Materialized, Vertex};
5
use crate::hash::{ChangeHash, ObjectRef};
6
use crate::ids::NodeId;
7
use crate::objects::{AtomRef, Change, Edit, Mode, NodeKind, TreeOp};
8
use crate::tree::{TreeGraph, TreeView};
9
use std::cell::RefCell;
10
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
11
use std::rc::Rc;
12
13
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14
pub enum StateError {
15
    #[error("change {1} depends on {0}, which is not in the state")]
16
    MissingDep(ChangeHash, ChangeHash),
17
}
18
19
#[derive(Default)]
20
pub struct State {
21
    changes: HashMap<ChangeHash, Change>,
22
    tree: TreeGraph,
23
    files: HashMap<NodeId, FileGraph>,
24
    replaces: HashMap<NodeId, Vec<(ChangeHash, Option<ObjectRef>, Option<ObjectRef>)>>,
25
    closure: RefCell<HashMap<ChangeHash, Rc<HashSet<ChangeHash>>>>,
26
}
27
28
#[derive(Clone, Debug, PartialEq, Eq)]
29
pub enum FileContent {
30
    Text { bytes: Vec<u8>, materialized: Materialized },
31
    /// Live blobs; more than one is a replace conflict (spec §9.3).
32
    Opaque { blobs: Vec<ObjectRef> },
33
    Dir,
34
}
35
36
#[derive(Clone, Debug, PartialEq, Eq)]
37
pub struct FileState {
38
    pub node: NodeId,
39
    pub kind: NodeKind,
40
    pub mode: Mode,
41
    pub content: FileContent,
42
}
43
44
impl FileState {
45
    pub fn is_conflicted(&self) -> bool {
46
        match &self.content {
47
            FileContent::Text { materialized, .. } => materialized.is_conflicted(),
48
            FileContent::Opaque { blobs } => blobs.len() > 1,
49
            FileContent::Dir => false,
50
        }
51
    }
52
}
53
54
/// The whole tree of a state, materialized.
55
#[derive(Clone, Debug, Default)]
56
pub struct Snapshot {
57
    pub tree: TreeView,
58
    pub files: BTreeMap<String, FileState>,
59
}
60
61
impl Snapshot {
62
    pub fn is_conflicted(&self) -> bool {
63
        self.tree.is_conflicted() || self.files.values().any(|f| f.is_conflicted())
64
    }
65
}
66
67
fn vertex(r: AtomRef) -> Vertex {
68
    match r {
69
        AtomRef::Start => Vertex::Start,
70
        AtomRef::End => Vertex::End,
71
        AtomRef::Atom { change, index } => Vertex::Atom((change, index)),
72
    }
73
}
74
75
/// Replace self references in a tree op with the concrete change hash.
76
pub fn resolve_tree_op(op: &TreeOp, me: ChangeHash) -> TreeOp {
77
    match op {
78
        TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode { node: *node, placement: placement.resolve(me) },
79
        TreeOp::Move { node, old, parent, name } => {
80
            TreeOp::Move { node: *node, old: old.resolve(me), parent: *parent, name: name.clone() }
81
        }
82
        TreeOp::SetMode { node, old, mode } => TreeOp::SetMode { node: *node, old: old.resolve(me), mode: *mode },
83
        add @ TreeOp::AddNode { .. } => add.clone(),
84
    }
85
}
86
87
impl State {
88
    pub fn new() -> Self {
89
        Self::default()
90
    }
91
92
    pub fn len(&self) -> usize {
93
        self.changes.len()
94
    }
95
    pub fn is_empty(&self) -> bool {
96
        self.changes.is_empty()
97
    }
98
    pub fn contains(&self, h: &ChangeHash) -> bool {
99
        self.changes.contains_key(h)
100
    }
101
    pub fn get(&self, h: &ChangeHash) -> Option<&Change> {
102
        self.changes.get(h)
103
    }
104
    pub fn changes(&self) -> impl Iterator<Item = (&ChangeHash, &Change)> {
105
        self.changes.iter()
106
    }
107
    pub fn tree(&self) -> &TreeGraph {
108
        &self.tree
109
    }
110
    pub fn file(&self, node: &NodeId) -> Option<&FileGraph> {
111
        self.files.get(node)
112
    }
113
114
    /// Add a change whose dependencies are already present.
115
    pub fn add(&mut self, change: Change) -> Result<ChangeHash, StateError> {
116
        let h = change.hash();
117
        if self.changes.contains_key(&h) {
118
            return Ok(h);
119
        }
120
        for d in &change.deps {
121
            if !self.changes.contains_key(d) {
122
                return Err(StateError::MissingDep(*d, h));
123
            }
124
        }
125
        Ok(self.add_unchecked(change))
126
    }
127
128
    /// Add a change in any order; call `validate` once all are in.
129
    pub fn add_unchecked(&mut self, change: Change) -> ChangeHash {
130
        let h = change.hash();
131
        if self.changes.contains_key(&h) {
132
            return h;
133
        }
134
        self.apply(h, &change);
135
        self.changes.insert(h, change);
136
        self.closure.borrow_mut().clear();
137
        h
138
    }
139
140
    pub fn validate(&self) -> Result<(), StateError> {
141
        for (h, c) in &self.changes {
142
            for d in &c.deps {
143
                if !self.changes.contains_key(d) {
144
                    return Err(StateError::MissingDep(*d, *h));
145
                }
146
            }
147
        }
148
        Ok(())
149
    }
150
151
    fn apply(&mut self, h: ChangeHash, c: &Change) {
152
        for (i, op) in c.tree_ops.iter().enumerate() {
153
            self.tree.apply(h, i as u32, &resolve_tree_op(op, h));
154
        }
155
        let mut counters: HashMap<NodeId, u32> = HashMap::new();
156
        for e in &c.edits {
157
            match e {
158
                Edit::Insert { node, after, before, lines } => {
159
                    let idx = counters.entry(*node).or_default();
160
                    let first = *idx;
161
                    *idx += lines.len() as u32;
162
                    self.files.entry(*node).or_default().insert(h, first, vertex(after.resolve(h)), vertex(before.resolve(h)), lines);
163
                }
164
                Edit::Delete { node, atoms } => {
165
                    let g = self.files.entry(*node).or_default();
166
                    for r in atoms {
167
                        g.delete(h, &r.resolve(h));
168
                    }
169
                }
170
                Edit::Replace { node, old, new } => {
171
                    self.replaces.entry(*node).or_default().push((h, *old, *new));
172
                }
173
            }
174
        }
175
    }
176
177
    /// Transitive dependencies of `h`, excluding `h` itself.
178
    pub fn closure(&self, h: &ChangeHash) -> Rc<HashSet<ChangeHash>> {
179
        if let Some(c) = self.closure.borrow().get(h) {
180
            return c.clone();
181
        }
182
        let mut out = HashSet::new();
183
        let mut stack: Vec<ChangeHash> = self.changes.get(h).map(|c| c.deps.clone()).unwrap_or_default();
184
        while let Some(d) = stack.pop() {
185
            if out.insert(d) {
186
                if let Some(c) = self.changes.get(&d) {
187
                    stack.extend(c.deps.iter().copied());
188
                }
189
            }
190
        }
191
        let rc = Rc::new(out);
192
        self.closure.borrow_mut().insert(*h, rc.clone());
193
        rc
194
    }
195
196
    /// Changes that no other change depends on directly, sorted.
197
    pub fn heads(&self) -> Vec<ChangeHash> {
198
        let depended: HashSet<&ChangeHash> = self.changes.values().flat_map(|c| c.deps.iter()).collect();
199
        let mut heads: Vec<ChangeHash> = self.changes.keys().filter(|h| !depended.contains(h)).copied().collect();
200
        heads.sort();
201
        heads
202
    }
203
204
    /// Maximal elements among `hashes` (those not in the closure of another).
205
    pub fn maximal(&self, hashes: &[ChangeHash]) -> Vec<ChangeHash> {
206
        let set: BTreeSet<ChangeHash> = hashes.iter().copied().collect();
207
        set.iter().filter(|h| !set.iter().any(|o| o != *h && self.closure(o).contains(h))).copied().collect()
208
    }
209
210
    pub fn materialize_file(&self, node: &NodeId) -> Materialized {
211
        self.files.get(node).map(|g| g.materialize(self)).unwrap_or_default()
212
    }
213
214
    /// Live lines of a text node in materialized order.
215
    pub fn live_lines(&self, node: &NodeId) -> Vec<(AtomKey, Vec<u8>)> {
216
        let Some(g) = self.files.get(node) else { return vec![] };
217
        let m = g.materialize(self);
218
        m.live_atoms().into_iter().map(|a| (a, g.content(&a).unwrap_or_default().to_vec())).collect()
219
    }
220
221
    /// Live blobs of an opaque node: every `new` that no later `Replace` retired.
222
    pub fn live_blobs(&self, node: &NodeId) -> Vec<ObjectRef> {
223
        let Some(rs) = self.replaces.get(node) else { return vec![] };
224
        let olds: HashSet<ObjectRef> = rs.iter().filter_map(|(_, o, _)| *o).collect();
225
        let mut live: Vec<ObjectRef> = rs.iter().filter_map(|(_, _, n)| *n).filter(|n| !olds.contains(n)).collect();
226
        live.sort();
227
        live.dedup();
228
        live
229
    }
230
231
    /// Short label for conflict markers: `changeid-prefix "message"`.
232
    pub fn label(&self, h: &ChangeHash) -> String {
233
        match self.changes.get(h) {
234
            Some(c) => format!("{} \"{}\"", c.change_id.short(8), c.message.lines().next().unwrap_or("")),
235
            None => format!("#{}", h.short(12)),
236
        }
237
    }
238
239
    pub fn render_file(&self, node: &NodeId) -> (Vec<u8>, Materialized) {
240
        let Some(g) = self.files.get(node) else { return (Vec::new(), Materialized::default()) };
241
        let m = g.materialize(self);
242
        let bytes = g.render(&m, &|h| self.label(h));
243
        (bytes, m)
244
    }
245
246
    pub fn materialize(&self) -> Snapshot {
247
        let tree = self.tree.view();
248
        let mut files = BTreeMap::new();
249
        for (path, e) in &tree.entries {
250
            let content = match e.kind {
251
                NodeKind::Dir => FileContent::Dir,
252
                NodeKind::Symlink => FileContent::Opaque { blobs: self.live_blobs(&e.node) },
253
                NodeKind::File if e.mode.opaque => FileContent::Opaque { blobs: self.live_blobs(&e.node) },
254
                NodeKind::File => {
255
                    let (bytes, materialized) = self.render_file(&e.node);
256
                    FileContent::Text { bytes, materialized }
257
                }
258
            };
259
            files.insert(path.clone(), FileState { node: e.node, kind: e.kind, mode: e.mode, content });
260
        }
261
        Snapshot { tree, files }
262
    }
263
}
264
265
impl DepOracle for State {
266
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
267
        a == b || self.closure(a).contains(b)
268
    }
269
}
270
271
#[cfg(test)]
272
mod tests {
273
    use super::*;
2cd6bf89 Andy McCall ↶ 274
275
    /// A changes block of thousands comes back whole and in order, and a
276
    /// truncated one is refused (oxygen#94).
277
    #[test]
278
    fn a_long_changes_block_decodes_in_order() {
279
        use crate::flat::Writer;
280
        let metas: Vec<(ChangeHash, ChangeMeta)> = (0..5000u32)
281
            .map(|i| {
282
                let mut h = [0u8; 32];
283
                h[..4].copy_from_slice(&i.to_le_bytes());
284
                let meta = ChangeMeta {
285
                    change_id: crate::ids::ChangeId([i as u8; 16]),
286
                    deps: (0..i % 3).map(|d| ObjectRef([d as u8; 32])).collect(),
287
                    author: ObjectRef([7; 32]),
288
                    hlc: Hlc {
289
                        wall_ms: 1_000_000 - i as u64,
290
                        logical: i,
291
                    },
292
                    message: "x".repeat((i % 50) as usize),
293
                    kind: ChangeKind::Normal,
294
                };
295
                (ObjectRef(h), meta)
296
            })
297
            .collect();
298
        let mut w = Writer::default();
299
        w.fixed(State::INDEX_MAGIC);
300
        w.count(metas.len());
301
        for (h, m) in &metas {
302
            w.object(h);
303
            m.write_flat(&mut w);
304
        }
305
        assert_eq!(State::read_index_changes_listed(&w.0).unwrap(), metas);
306
        let mut cut = w.0.clone();
307
        cut.pop();
308
        assert!(
309
            State::read_index_changes_listed(&cut).is_err(),
310
            "a truncated block is refused"
311
        );
312
    }
ff0af1ee unknown 313
    use crate::ids::ChangeId;
314
    use crate::objects::{AtomRange, ChangeKind, Hlc, OpRef};
315
316
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>) -> Change {
317
        let mut c = Change {
318
            change_id: ChangeId::random(),
319
            deps,
320
            author: ObjectRef::ZERO,
321
            hlc: Hlc { wall_ms: 1, logical: 0 },
322
            message: "m".into(),
323
            tree_ops,
324
            edits,
325
            kind: ChangeKind::Normal,
326
        };
327
        c.normalize();
328
        c
329
    }
330
331
    #[test]
332
    fn build_edit_and_materialize() {
333
        let mut st = State::new();
334
        let f = NodeId::random();
335
        let c1 = change(
336
            vec![],
337
            vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "a.txt".into() }],
338
            vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"one\n".to_vec(), b"two\n".to_vec()] }],
339
        );
340
        let h1 = st.add(c1).unwrap();
341
        // Depends on h1: append a line and delete "one".
342
        let c2 = change(
343
            vec![h1],
344
            vec![],
345
            vec![
346
                Edit::Delete { node: f, atoms: vec![AtomRange { change: h1, from: 0, to: 1 }] },
347
                Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"three\n".to_vec()] },
348
            ],
349
        );
350
        let h2 = st.add(c2).unwrap();
351
        assert_eq!(st.heads(), vec![h2]);
352
        assert!(st.depends_on(&h2, &h1));
353
        assert!(!st.depends_on(&h1, &h2));
354
        let snap = st.materialize();
355
        assert!(!snap.is_conflicted());
356
        let a = &snap.files["a.txt"];
357
        assert!(matches!(&a.content, FileContent::Text { bytes, .. } if bytes == b"two\nthree\n"));
358
        assert_eq!(st.live_lines(&f).len(), 2);
359
    }
360
361
    #[test]
362
    fn missing_dependency_is_rejected() {
363
        let mut st = State::new();
364
        let c = change(vec![ObjectRef([1; 32])], vec![], vec![]);
365
        assert!(matches!(st.add(c), Err(StateError::MissingDep(..))));
366
    }
367
368
    #[test]
369
    fn self_references_and_multi_insert_indexing() {
370
        let mut st = State::new();
371
        let f = NodeId::random();
372
        let me = OpRef { change: ObjectRef::ZERO, index: 0 };
373
        let c1 = change(
374
            vec![],
375
            vec![
376
                TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "x".into() },
377
                TreeOp::SetMode { node: f, old: me, mode: Mode { exec: true, opaque: false } },
378
            ],
379
            vec![
380
                Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec(), b"c\n".to_vec()] },
381
                // Insert between the two lines just inserted, using self references.
382
                Edit::Insert {
383
                    node: f,
384
                    after: AtomRef::Atom { change: ObjectRef::ZERO, index: 0 },
385
                    before: AtomRef::Atom { change: ObjectRef::ZERO, index: 1 },
386
                    lines: vec![b"b\n".to_vec()],
387
                },
388
            ],
389
        );
390
        st.add(c1).unwrap();
391
        let snap = st.materialize();
392
        let x = &snap.files["x"];
393
        assert_eq!(x.mode, Mode { exec: true, opaque: false });
394
        assert!(matches!(&x.content, FileContent::Text { bytes, .. } if bytes == b"a\nb\nc\n"));
395
    }
396
397
    #[test]
398
    fn opaque_replace_and_conflict() {
399
        let mut st = State::new();
400
        let f = NodeId::random();
401
        let b1 = ObjectRef([1; 32]);
402
        let b2 = ObjectRef([2; 32]);
403
        let b3 = ObjectRef([3; 32]);
404
        let h1 = st.add(change(
405
            vec![],
406
            vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::OPAQUE, parent: NodeId::ROOT, name: "img".into() }],
407
            vec![Edit::Replace { node: f, old: None, new: Some(b1) }],
408
        ))
409
        .unwrap();
410
        assert_eq!(st.live_blobs(&f), vec![b1]);
411
        st.add(change(vec![h1], vec![], vec![Edit::Replace { node: f, old: Some(b1), new: Some(b2) }])).unwrap();
412
        st.add(change(vec![h1], vec![], vec![Edit::Replace { node: f, old: Some(b1), new: Some(b3) }])).unwrap();
413
        let snap = st.materialize();
414
        assert!(snap.is_conflicted());
415
        assert!(matches!(&snap.files["img"].content, FileContent::Opaque { blobs } if blobs == &vec![b2, b3]));
416
    }
417
418
    #[test]
419
    fn maximal_elements() {
420
        let mut st = State::new();
421
        let h1 = st.add(change(vec![], vec![], vec![])).unwrap();
422
        let h2 = st.add(change(vec![h1], vec![], vec![])).unwrap();
423
        let h3 = st.add(change(vec![], vec![], vec![])).unwrap();
424
        let mut m = st.maximal(&[h1, h2, h3]);
425
        m.sort();
426
        let mut want = vec![h2, h3];
427
        want.sort();
428
        assert_eq!(m, want);
429
        assert_eq!(st.heads(), want);
430
    }
431
}