OXYGENHUB
# 648724d8e0de oxygen / crates / oxy-core / src / state.rs
14.3 KiB · Rust · last changed Import the Oxygen specification and the first Rust implementation (root change) 1 month ago source blame history raw
1
//! A state: a dependency-closed set of changes, applied to a tree graph and
2
//! per-node file graphs (spec §8.3), with materialization (spec §9.5, §11).
3
4
use crate::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::*;
274
    use crate::ids::ChangeId;
275
    use crate::objects::{AtomRange, ChangeKind, Hlc, OpRef};
276
277
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>) -> Change {
278
        let mut c = Change {
279
            change_id: ChangeId::random(),
280
            deps,
281
            author: ObjectRef::ZERO,
282
            hlc: Hlc { wall_ms: 1, logical: 0 },
283
            message: "m".into(),
284
            tree_ops,
285
            edits,
286
            kind: ChangeKind::Normal,
287
        };
288
        c.normalize();
289
        c
290
    }
291
292
    #[test]
293
    fn build_edit_and_materialize() {
294
        let mut st = State::new();
295
        let f = NodeId::random();
296
        let c1 = change(
297
            vec![],
298
            vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "a.txt".into() }],
299
            vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"one\n".to_vec(), b"two\n".to_vec()] }],
300
        );
301
        let h1 = st.add(c1).unwrap();
302
        // Depends on h1: append a line and delete "one".
303
        let c2 = change(
304
            vec![h1],
305
            vec![],
306
            vec![
307
                Edit::Delete { node: f, atoms: vec![AtomRange { change: h1, from: 0, to: 1 }] },
308
                Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"three\n".to_vec()] },
309
            ],
310
        );
311
        let h2 = st.add(c2).unwrap();
312
        assert_eq!(st.heads(), vec![h2]);
313
        assert!(st.depends_on(&h2, &h1));
314
        assert!(!st.depends_on(&h1, &h2));
315
        let snap = st.materialize();
316
        assert!(!snap.is_conflicted());
317
        let a = &snap.files["a.txt"];
318
        assert!(matches!(&a.content, FileContent::Text { bytes, .. } if bytes == b"two\nthree\n"));
319
        assert_eq!(st.live_lines(&f).len(), 2);
320
    }
321
322
    #[test]
323
    fn missing_dependency_is_rejected() {
324
        let mut st = State::new();
325
        let c = change(vec![ObjectRef([1; 32])], vec![], vec![]);
326
        assert!(matches!(st.add(c), Err(StateError::MissingDep(..))));
327
    }
328
329
    #[test]
330
    fn self_references_and_multi_insert_indexing() {
331
        let mut st = State::new();
332
        let f = NodeId::random();
333
        let me = OpRef { change: ObjectRef::ZERO, index: 0 };
334
        let c1 = change(
335
            vec![],
336
            vec![
337
                TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "x".into() },
338
                TreeOp::SetMode { node: f, old: me, mode: Mode { exec: true, opaque: false } },
339
            ],
340
            vec![
341
                Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec(), b"c\n".to_vec()] },
342
                // Insert between the two lines just inserted, using self references.
343
                Edit::Insert {
344
                    node: f,
345
                    after: AtomRef::Atom { change: ObjectRef::ZERO, index: 0 },
346
                    before: AtomRef::Atom { change: ObjectRef::ZERO, index: 1 },
347
                    lines: vec![b"b\n".to_vec()],
348
                },
349
            ],
350
        );
351
        st.add(c1).unwrap();
352
        let snap = st.materialize();
353
        let x = &snap.files["x"];
354
        assert_eq!(x.mode, Mode { exec: true, opaque: false });
355
        assert!(matches!(&x.content, FileContent::Text { bytes, .. } if bytes == b"a\nb\nc\n"));
356
    }
357
358
    #[test]
359
    fn opaque_replace_and_conflict() {
360
        let mut st = State::new();
361
        let f = NodeId::random();
362
        let b1 = ObjectRef([1; 32]);
363
        let b2 = ObjectRef([2; 32]);
364
        let b3 = ObjectRef([3; 32]);
365
        let h1 = st.add(change(
366
            vec![],
367
            vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::OPAQUE, parent: NodeId::ROOT, name: "img".into() }],
368
            vec![Edit::Replace { node: f, old: None, new: Some(b1) }],
369
        ))
370
        .unwrap();
371
        assert_eq!(st.live_blobs(&f), vec![b1]);
372
        st.add(change(vec![h1], vec![], vec![Edit::Replace { node: f, old: Some(b1), new: Some(b2) }])).unwrap();
373
        st.add(change(vec![h1], vec![], vec![Edit::Replace { node: f, old: Some(b1), new: Some(b3) }])).unwrap();
374
        let snap = st.materialize();
375
        assert!(snap.is_conflicted());
376
        assert!(matches!(&snap.files["img"].content, FileContent::Opaque { blobs } if blobs == &vec![b2, b3]));
377
    }
378
379
    #[test]
380
    fn maximal_elements() {
381
        let mut st = State::new();
382
        let h1 = st.add(change(vec![], vec![], vec![])).unwrap();
383
        let h2 = st.add(change(vec![h1], vec![], vec![])).unwrap();
384
        let h3 = st.add(change(vec![], vec![], vec![])).unwrap();
385
        let mut m = st.maximal(&[h1, h2, h3]);
386
        m.sort();
387
        let mut want = vec![h2, h3];
388
        want.sort();
389
        assert_eq!(m, want);
390
        assert_eq!(st.heads(), want);
391
    }
392
}