OXYGENHUB
# d02e545dc265 oxygen / crates / oxy-core / src / tree.rs
16.0 KiB · Rust · last changed Import the Oxygen specification and the first Rust implementation (root change) 1 month ago source blame history raw
1
//! The tree graph: nodes, placements, and modes as atoms (spec §7.1, §7.2),
2
//! with tree conflict detection and path assignment (spec §9.2).
3
4
use crate::hash::ChangeHash;
5
use crate::ids::NodeId;
6
use crate::objects::{Mode, NodeKind, OpRef, TreeOp};
7
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
8
9
/// `(change, index into that change's tree_ops)`.
10
pub type OpKey = (ChangeHash, u32);
11
12
#[derive(Clone, Debug, PartialEq, Eq)]
13
pub struct Placement {
14
    pub node: NodeId,
15
    pub parent: NodeId,
16
    pub name: String,
17
}
18
19
#[derive(Clone, Debug, Default)]
20
pub struct TreeGraph {
21
    kinds: HashMap<NodeId, NodeKind>,
22
    introduced: HashMap<NodeId, ChangeHash>,
23
    placements: HashMap<OpKey, Placement>,
24
    modes: HashMap<OpKey, (NodeId, Mode)>,
25
    /// Placement atoms retired by `RemoveNode`/`Move`, and mode atoms retired
26
    /// by `SetMode`. Kept apart because `AddNode` introduces one of each
27
    /// under the same key.
28
    deleted_placements: HashMap<OpKey, BTreeSet<ChangeHash>>,
29
    deleted_modes: HashMap<OpKey, BTreeSet<ChangeHash>>,
30
    placements_of: HashMap<NodeId, BTreeSet<OpKey>>,
31
    modes_of: HashMap<NodeId, BTreeSet<OpKey>>,
32
}
33
34
#[derive(Clone, Debug, PartialEq, Eq)]
35
pub enum TreeConflict {
36
    /// Two live nodes share `(parent, name)`.
37
    Name { parent: NodeId, name: String, nodes: Vec<NodeId> },
38
    /// One node has several live placements.
39
    MultiPlacement { node: NodeId, placements: Vec<OpKey> },
40
    /// A live node's parent is not live.
41
    Orphan { node: NodeId, parent: NodeId },
42
    /// A directory is placed inside its own descendant.
43
    Cycle { node: NodeId },
44
    /// Several live mode atoms.
45
    Mode { node: NodeId, modes: Vec<Mode> },
46
}
47
48
/// A node as it appears in the working copy.
49
#[derive(Clone, Debug, PartialEq, Eq)]
50
pub struct TreeEntry {
51
    pub path: String,
52
    pub node: NodeId,
53
    pub kind: NodeKind,
54
    pub mode: Mode,
55
    /// The placement this entry materializes (for building `RemoveNode`/`Move`).
56
    pub placement: OpKey,
57
    /// The live mode atom (for building `SetMode`).
58
    pub mode_atom: Option<OpKey>,
59
}
60
61
#[derive(Clone, Debug, Default, PartialEq, Eq)]
62
pub struct TreeView {
63
    /// Path → entry, paths use `/` and have no leading slash; conflicted
64
    /// entries carry a `.oxy-conflict-<n>` suffix.
65
    pub entries: BTreeMap<String, TreeEntry>,
66
    pub conflicts: Vec<TreeConflict>,
67
}
68
69
impl TreeView {
70
    pub fn is_conflicted(&self) -> bool {
71
        !self.conflicts.is_empty()
72
    }
73
    pub fn by_node(&self) -> HashMap<NodeId, &TreeEntry> {
74
        self.entries.values().map(|e| (e.node, e)).collect()
75
    }
76
}
77
78
impl TreeGraph {
79
    pub fn new() -> Self {
80
        Self::default()
81
    }
82
83
    /// Apply tree op `index` of `change`; `OpRef` self references must already be resolved.
84
    pub fn apply(&mut self, change: ChangeHash, index: u32, op: &TreeOp) {
85
        let key = (change, index);
86
        match op {
87
            TreeOp::AddNode { node, kind, mode, parent, name } => {
88
                self.kinds.insert(*node, *kind);
89
                self.introduced.insert(*node, change);
90
                self.placements.insert(key, Placement { node: *node, parent: *parent, name: name.clone() });
91
                self.placements_of.entry(*node).or_default().insert(key);
92
                if *kind != NodeKind::Dir {
93
                    self.modes.insert(key, (*node, *mode));
94
                    self.modes_of.entry(*node).or_default().insert(key);
95
                }
96
            }
97
            TreeOp::RemoveNode { placement, .. } => {
98
                self.deleted_placements.entry(op_key(*placement)).or_default().insert(change);
99
            }
100
            TreeOp::Move { node, old, parent, name } => {
101
                self.deleted_placements.entry(op_key(*old)).or_default().insert(change);
102
                self.placements.insert(key, Placement { node: *node, parent: *parent, name: name.clone() });
103
                self.placements_of.entry(*node).or_default().insert(key);
104
            }
105
            TreeOp::SetMode { node, old, mode } => {
106
                self.deleted_modes.entry(op_key(*old)).or_default().insert(change);
107
                self.modes.insert(key, (*node, *mode));
108
                self.modes_of.entry(*node).or_default().insert(key);
109
            }
110
        }
111
    }
112
113
    fn placement_live(&self, k: &OpKey) -> bool {
114
        self.deleted_placements.get(k).is_none_or(|d| d.is_empty())
115
    }
116
    fn mode_live(&self, k: &OpKey) -> bool {
117
        self.deleted_modes.get(k).is_none_or(|d| d.is_empty())
118
    }
119
120
    pub fn kind(&self, node: &NodeId) -> Option<NodeKind> {
121
        if node.is_root() { Some(NodeKind::Dir) } else { self.kinds.get(node).copied() }
122
    }
123
124
    /// The change whose `AddNode` introduced `node`.
125
    pub fn introducer(&self, node: &NodeId) -> Option<ChangeHash> {
126
        self.introduced.get(node).copied()
127
    }
128
129
    pub fn placement(&self, k: &OpKey) -> Option<&Placement> {
130
        self.placements.get(k)
131
    }
132
133
    pub fn live_placements(&self, node: &NodeId) -> Vec<(OpKey, &Placement)> {
134
        self.placements_of
135
            .get(node)
136
            .into_iter()
137
            .flat_map(|ks| ks.iter())
138
            .filter(|k| self.placement_live(k))
139
            .filter_map(|k| self.placements.get(k).map(|p| (*k, p)))
140
            .collect()
141
    }
142
143
    pub fn live_modes(&self, node: &NodeId) -> Vec<(OpKey, Mode)> {
144
        self.modes_of
145
            .get(node)
146
            .into_iter()
147
            .flat_map(|ks| ks.iter())
148
            .filter(|k| self.mode_live(k))
149
            .filter_map(|k| self.modes.get(k).map(|(_, m)| (*k, *m)))
150
            .collect()
151
    }
152
153
    /// Every node that has at least one live placement.
154
    pub fn placed_nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
155
        self.placements_of.keys().copied().filter(|n| !self.live_placements(n).is_empty())
156
    }
157
158
    /// Resolve the tree: assign paths, find conflicts.
159
    pub fn view(&self) -> TreeView {
160
        let mut view = TreeView::default();
161
162
        // Live placements grouped by parent.
163
        let mut children: HashMap<NodeId, Vec<(OpKey, &Placement)>> = HashMap::new();
164
        for (k, p) in &self.placements {
165
            if self.placement_live(k) {
166
                children.entry(p.parent).or_default().push((*k, p));
167
            }
168
        }
169
        for c in children.values_mut() {
170
            c.sort_by(|a, b| a.1.name.cmp(&b.1.name).then(a.0.cmp(&b.0)));
171
        }
172
173
        // Multi-placement and mode conflicts.
174
        let mut nodes: Vec<NodeId> = self.placements_of.keys().copied().collect();
175
        nodes.sort();
176
        let mut multi: HashSet<NodeId> = HashSet::new();
177
        for n in &nodes {
178
            let lp = self.live_placements(n);
179
            let distinct_places: BTreeSet<(&NodeId, &str)> = lp.iter().map(|(_, p)| (&p.parent, p.name.as_str())).collect();
180
            if distinct_places.len() > 1 {
181
                multi.insert(*n);
182
                view.conflicts.push(TreeConflict::MultiPlacement { node: *n, placements: lp.iter().map(|(k, _)| *k).collect() });
183
            }
184
            let lm = self.live_modes(n);
185
            let distinct_modes: BTreeSet<Mode> = lm.iter().map(|(_, m)| *m).collect();
186
            if distinct_modes.len() > 1 {
187
                view.conflicts.push(TreeConflict::Mode { node: *n, modes: distinct_modes.into_iter().collect() });
188
            }
189
        }
190
191
        // BFS from the root, assigning paths.
192
        let mut reached: HashSet<NodeId> = HashSet::new();
193
        reached.insert(NodeId::ROOT);
194
        let mut queue: Vec<(NodeId, String)> = vec![(NodeId::ROOT, String::new())];
195
        let mut on_path: HashSet<NodeId> = HashSet::new();
196
        while let Some((dir, dir_path)) = queue.pop() {
197
            let Some(kids) = children.get(&dir) else { continue };
198
            // Name conflicts within this directory.
199
            let mut by_name: BTreeMap<&str, Vec<(OpKey, &Placement)>> = BTreeMap::new();
200
            for (k, p) in kids {
201
                by_name.entry(p.name.as_str()).or_default().push((*k, p));
202
            }
203
            for (name, group) in by_name {
204
                let distinct: BTreeSet<NodeId> = group.iter().map(|(_, p)| p.node).collect();
205
                if distinct.len() > 1 {
206
                    view.conflicts.push(TreeConflict::Name { parent: dir, name: name.to_string(), nodes: distinct.iter().copied().collect() });
207
                }
208
                let suffixed = distinct.len() > 1;
209
                for (i, (k, p)) in group.iter().enumerate() {
210
                    let node = p.node;
211
                    if node == dir || on_path.contains(&node) {
212
                        view.conflicts.push(TreeConflict::Cycle { node });
213
                        continue;
214
                    }
215
                    let needs_suffix = suffixed || multi.contains(&node);
216
                    let leaf = if needs_suffix { format!("{name}.oxy-conflict-{}", i + 1) } else { name.to_string() };
217
                    let path = if dir_path.is_empty() { leaf } else { format!("{dir_path}/{leaf}") };
218
                    let kind = self.kind(&node).unwrap_or(NodeKind::File);
219
                    let modes = self.live_modes(&node);
220
                    let mode_atom = modes.iter().map(|(k, _)| *k).max();
221
                    let mode = mode_atom.and_then(|k| self.modes.get(&k).map(|(_, m)| *m)).unwrap_or_default();
222
                    view.entries.insert(path.clone(), TreeEntry { path: path.clone(), node, kind, mode, placement: *k, mode_atom });
223
                    if kind == NodeKind::Dir && reached.insert(node) {
224
                        on_path.insert(dir);
225
                        queue.push((node, path));
226
                    } else {
227
                        reached.insert(node);
228
                    }
229
                }
230
            }
231
        }
232
233
        // Nodes with live placements that were never reached: orphans or cycles.
234
        for n in &nodes {
235
            if reached.contains(n) {
236
                continue;
237
            }
238
            let lp = self.live_placements(n);
239
            if lp.is_empty() {
240
                continue;
241
            }
242
            let mut seen = HashSet::new();
243
            let mut cur = *n;
244
            let conflict = loop {
245
                if !seen.insert(cur) {
246
                    break TreeConflict::Cycle { node: *n };
247
                }
248
                let lp = self.live_placements(&cur);
249
                let Some((_, p)) = lp.first() else {
250
                    break TreeConflict::Orphan { node: *n, parent: cur };
251
                };
252
                if reached.contains(&p.parent) {
253
                    // Parent reached but this node was not: the parent is not a
254
                    // directory, so this is a name/kind problem surfaced as orphan.
255
                    break TreeConflict::Orphan { node: *n, parent: p.parent };
256
                }
257
                cur = p.parent;
258
            };
259
            if !view.conflicts.contains(&conflict) {
260
                view.conflicts.push(conflict);
261
            }
262
        }
263
        view
264
    }
265
}
266
267
pub fn op_key(r: OpRef) -> OpKey {
268
    (r.change, r.index)
269
}
270
271
#[cfg(test)]
272
mod tests {
273
    use super::*;
274
    use crate::hash::ObjectRef;
275
276
    fn h(n: u8) -> ChangeHash {
277
        ObjectRef([n; 32])
278
    }
279
    fn nid(n: u8) -> NodeId {
280
        NodeId([n; 16])
281
    }
282
    fn add(g: &mut TreeGraph, c: u8, i: u32, node: u8, kind: NodeKind, parent: NodeId, name: &str) {
283
        g.apply(h(c), i, &TreeOp::AddNode { node: nid(node), kind, mode: Mode::TEXT, parent, name: name.into() });
284
    }
285
    fn paths(v: &TreeView) -> Vec<String> {
286
        v.entries.keys().cloned().collect()
287
    }
288
289
    #[test]
290
    fn nested_paths_and_rename() {
291
        let mut g = TreeGraph::new();
292
        add(&mut g, 1, 0, 1, NodeKind::Dir, NodeId::ROOT, "src");
293
        add(&mut g, 1, 1, 2, NodeKind::File, nid(1), "main.rs");
294
        add(&mut g, 1, 2, 3, NodeKind::File, NodeId::ROOT, "README");
295
        let v = g.view();
296
        assert_eq!(paths(&v), ["README", "src", "src/main.rs"]);
297
        assert!(!v.is_conflicted());
298
299
        // Rename src/main.rs → src/lib.rs, move src → lib (subtree follows).
300
        g.apply(h(2), 0, &TreeOp::Move { node: nid(2), old: OpRef { change: h(1), index: 1 }, parent: nid(1), name: "lib.rs".into() });
301
        g.apply(h(2), 1, &TreeOp::Move { node: nid(1), old: OpRef { change: h(1), index: 0 }, parent: NodeId::ROOT, name: "lib".into() });
302
        let v = g.view();
303
        assert_eq!(paths(&v), ["README", "lib", "lib/lib.rs"]);
304
        assert_eq!(v.entries["lib/lib.rs"].placement, (h(2), 0));
305
306
        g.apply(h(3), 0, &TreeOp::RemoveNode { node: nid(3), placement: OpRef { change: h(1), index: 2 } });
307
        assert_eq!(paths(&g.view()), ["lib", "lib/lib.rs"]);
308
    }
309
310
    #[test]
311
    fn name_conflict_gets_suffixes() {
312
        let mut g = TreeGraph::new();
313
        add(&mut g, 1, 0, 1, NodeKind::File, NodeId::ROOT, "a.txt");
314
        add(&mut g, 2, 0, 2, NodeKind::File, NodeId::ROOT, "a.txt");
315
        let v = g.view();
316
        assert_eq!(paths(&v), ["a.txt.oxy-conflict-1", "a.txt.oxy-conflict-2"]);
317
        assert_eq!(v.conflicts, vec![TreeConflict::Name { parent: NodeId::ROOT, name: "a.txt".into(), nodes: vec![nid(1), nid(2)] }]);
318
    }
319
320
    #[test]
321
    fn concurrent_renames_are_multi_placement() {
322
        let mut g = TreeGraph::new();
323
        add(&mut g, 1, 0, 1, NodeKind::File, NodeId::ROOT, "a");
324
        g.apply(h(2), 0, &TreeOp::Move { node: nid(1), old: OpRef { change: h(1), index: 0 }, parent: NodeId::ROOT, name: "b".into() });
325
        g.apply(h(3), 0, &TreeOp::Move { node: nid(1), old: OpRef { change: h(1), index: 0 }, parent: NodeId::ROOT, name: "c".into() });
326
        let v = g.view();
327
        assert_eq!(paths(&v), ["b.oxy-conflict-1", "c.oxy-conflict-1"]);
328
        assert!(matches!(v.conflicts[0], TreeConflict::MultiPlacement { node, .. } if node == nid(1)));
329
    }
330
331
    #[test]
332
    fn orphan_and_cycle() {
333
        let mut g = TreeGraph::new();
334
        add(&mut g, 1, 0, 1, NodeKind::Dir, NodeId::ROOT, "d");
335
        add(&mut g, 1, 1, 2, NodeKind::File, nid(1), "f");
336
        // Concurrently: 2 removes d, 3 adds d/g.
337
        g.apply(h(2), 0, &TreeOp::RemoveNode { node: nid(1), placement: OpRef { change: h(1), index: 0 } });
338
        add(&mut g, 3, 0, 3, NodeKind::File, nid(1), "g");
339
        let v = g.view();
340
        assert_eq!(paths(&v), Vec::<String>::new());
341
        assert!(v.conflicts.contains(&TreeConflict::Orphan { node: nid(2), parent: nid(1) }));
342
        assert!(v.conflicts.contains(&TreeConflict::Orphan { node: nid(3), parent: nid(1) }));
343
344
        // Cycle: a/b, then concurrently move a under b and b under a's parent... simplest: move a into b.
345
        let mut g = TreeGraph::new();
346
        add(&mut g, 1, 0, 1, NodeKind::Dir, NodeId::ROOT, "a");
347
        add(&mut g, 1, 1, 2, NodeKind::Dir, nid(1), "b");
348
        g.apply(h(2), 0, &TreeOp::Move { node: nid(1), old: OpRef { change: h(1), index: 0 }, parent: nid(2), name: "a".into() });
349
        let v = g.view();
350
        assert!(v.conflicts.iter().any(|c| matches!(c, TreeConflict::Cycle { .. })), "{:?}", v.conflicts);
351
    }
352
353
    #[test]
354
    fn mode_conflict_and_resolution() {
355
        let mut g = TreeGraph::new();
356
        add(&mut g, 1, 0, 1, NodeKind::File, NodeId::ROOT, "run.sh");
357
        g.apply(h(2), 0, &TreeOp::SetMode { node: nid(1), old: OpRef { change: h(1), index: 0 }, mode: Mode { exec: true, opaque: false } });
358
        g.apply(h(3), 0, &TreeOp::SetMode { node: nid(1), old: OpRef { change: h(1), index: 0 }, mode: Mode { exec: false, opaque: true } });
359
        let v = g.view();
360
        assert!(matches!(v.conflicts[0], TreeConflict::Mode { .. }));
361
        // Materialized with the greatest mode atom.
362
        assert_eq!(v.entries["run.sh"].mode, Mode { exec: false, opaque: true });
363
        // The placement introduced by the same AddNode is unaffected.
364
        assert_eq!(v.entries["run.sh"].placement, (h(1), 0));
365
        // Resolution: retire both live mode atoms; identical live values do not conflict.
366
        g.apply(h(4), 0, &TreeOp::SetMode { node: nid(1), old: OpRef { change: h(2), index: 0 }, mode: Mode { exec: true, opaque: false } });
367
        g.apply(h(4), 1, &TreeOp::SetMode { node: nid(1), old: OpRef { change: h(3), index: 0 }, mode: Mode { exec: true, opaque: false } });
368
        let v = g.view();
369
        assert!(!v.is_conflicted(), "{:?}", v.conflicts);
370
        assert_eq!(v.entries["run.sh"].mode, Mode { exec: true, opaque: false });
371
    }
372
}