OXYGENHUB
# 23ff1288a5dd oxygen / crates / oxy-core / src / tree.rs
31.1 KiB · Rust · last changed A Git import commit costs what it changes 1 month ago source blame history raw
ff0af1ee unknown 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>>,
2306a4d2 Andy McCall ↶ 32
    /// The live subsets of `placements_of` and `modes_of`, kept as changes
33
    /// apply, so the view walks what is live rather than all history.
34
    live_placements_of: HashMap<NodeId, BTreeSet<OpKey>>,
35
    live_modes_of: HashMap<NodeId, BTreeSet<OpKey>>,
206c76ca Andy McCall ↶ 36
    /// The view, once computed; cleared by every change to the graph.
37
    view_cache: std::sync::OnceLock<TreeView>,
ff0af1ee unknown 38
}
39
40
#[derive(Clone, Debug, PartialEq, Eq)]
41
pub enum TreeConflict {
42
    /// Two live nodes share `(parent, name)`.
75167672 Andy McCall 43
    Name {
44
        parent: NodeId,
45
        name: String,
46
        nodes: Vec<NodeId>,
47
    },
ff0af1ee unknown 48
    /// One node has several live placements.
75167672 Andy McCall 49
    MultiPlacement {
50
        node: NodeId,
51
        placements: Vec<OpKey>,
52
    },
ff0af1ee unknown 53
    /// A live node's parent is not live.
54
    Orphan { node: NodeId, parent: NodeId },
55
    /// A directory is placed inside its own descendant.
56
    Cycle { node: NodeId },
57
    /// Several live mode atoms.
58
    Mode { node: NodeId, modes: Vec<Mode> },
59
}
60
61
/// A node as it appears in the working copy.
62
#[derive(Clone, Debug, PartialEq, Eq)]
63
pub struct TreeEntry {
64
    pub path: String,
65
    pub node: NodeId,
66
    pub kind: NodeKind,
67
    pub mode: Mode,
68
    /// The placement this entry materializes (for building `RemoveNode`/`Move`).
69
    pub placement: OpKey,
70
    /// The live mode atom (for building `SetMode`).
71
    pub mode_atom: Option<OpKey>,
72
}
73
74
#[derive(Clone, Debug, Default, PartialEq, Eq)]
75
pub struct TreeView {
76
    /// Path → entry, paths use `/` and have no leading slash; conflicted
77
    /// entries carry a `.oxy-conflict-<n>` suffix.
78
    pub entries: BTreeMap<String, TreeEntry>,
79
    pub conflicts: Vec<TreeConflict>,
80
}
81
82
impl TreeView {
83
    pub fn is_conflicted(&self) -> bool {
84
        !self.conflicts.is_empty()
85
    }
86
    pub fn by_node(&self) -> HashMap<NodeId, &TreeEntry> {
87
        self.entries.values().map(|e| (e.node, e)).collect()
88
    }
89
}
90
91
impl TreeGraph {
92
    pub fn new() -> Self {
93
        Self::default()
94
    }
95
96
    /// Apply tree op `index` of `change`; `OpRef` self references must already be resolved.
97
    pub fn apply(&mut self, change: ChangeHash, index: u32, op: &TreeOp) {
206c76ca Andy McCall ↶ 98
        self.view_cache = std::sync::OnceLock::new();
ff0af1ee unknown 99
        let key = (change, index);
100
        match op {
75167672 Andy McCall 101
            TreeOp::AddNode {
102
                node,
103
                kind,
104
                mode,
105
                parent,
106
                name,
107
            } => {
ff0af1ee unknown 108
                self.kinds.insert(*node, *kind);
109
                self.introduced.insert(*node, change);
75167672 Andy McCall 110
                self.placements.insert(
111
                    key,
112
                    Placement {
113
                        node: *node,
114
                        parent: *parent,
115
                        name: name.clone(),
116
                    },
117
                );
ff0af1ee unknown 118
                self.placements_of.entry(*node).or_default().insert(key);
2306a4d2 Andy McCall ↶ 119
                self.index_placement(key);
ff0af1ee unknown 120
                if *kind != NodeKind::Dir {
121
                    self.modes.insert(key, (*node, *mode));
122
                    self.modes_of.entry(*node).or_default().insert(key);
2306a4d2 Andy McCall ↶ 123
                    self.index_mode(key);
ff0af1ee unknown 124
                }
125
            }
126
            TreeOp::RemoveNode { placement, .. } => {
75167672 Andy McCall 127
                self.deleted_placements
128
                    .entry(op_key(*placement))
129
                    .or_default()
130
                    .insert(change);
2306a4d2 Andy McCall ↶ 131
                self.index_placement(op_key(*placement));
ff0af1ee unknown 132
            }
75167672 Andy McCall 133
            TreeOp::Move {
134
                node,
135
                old,
136
                parent,
137
                name,
138
            } => {
139
                self.deleted_placements
140
                    .entry(op_key(*old))
141
                    .or_default()
142
                    .insert(change);
2306a4d2 Andy McCall ↶ 143
                self.index_placement(op_key(*old));
75167672 Andy McCall 144
                self.placements.insert(
145
                    key,
146
                    Placement {
147
                        node: *node,
148
                        parent: *parent,
149
                        name: name.clone(),
150
                    },
151
                );
ff0af1ee unknown 152
                self.placements_of.entry(*node).or_default().insert(key);
2306a4d2 Andy McCall ↶ 153
                self.index_placement(key);
ff0af1ee unknown 154
            }
155
            TreeOp::SetMode { node, old, mode } => {
75167672 Andy McCall 156
                self.deleted_modes
157
                    .entry(op_key(*old))
158
                    .or_default()
159
                    .insert(change);
2306a4d2 Andy McCall ↶ 160
                self.index_mode(op_key(*old));
ff0af1ee unknown 161
                self.modes.insert(key, (*node, *mode));
162
                self.modes_of.entry(*node).or_default().insert(key);
2306a4d2 Andy McCall ↶ 163
                self.index_mode(key);
164
            }
165
        }
166
    }
167
168
    /// Bring placement `k` in the live index up to date.
169
    fn index_placement(&mut self, k: OpKey) {
170
        let Some(node) = self.placements.get(&k).map(|p| p.node) else {
171
            return;
172
        };
173
        if self.placement_live(&k) {
174
            self.live_placements_of.entry(node).or_default().insert(k);
175
        } else if let Some(set) = self.live_placements_of.get_mut(&node) {
176
            set.remove(&k);
177
            if set.is_empty() {
178
                self.live_placements_of.remove(&node);
ff0af1ee unknown 179
            }
180
        }
181
    }
182
2306a4d2 Andy McCall ↶ 183
    /// Bring mode atom `k` in the live index up to date.
184
    fn index_mode(&mut self, k: OpKey) {
185
        let Some(node) = self.modes.get(&k).map(|(n, _)| *n) else {
186
            return;
187
        };
188
        if self.mode_live(&k) {
189
            self.live_modes_of.entry(node).or_default().insert(k);
190
        } else if let Some(set) = self.live_modes_of.get_mut(&node) {
191
            set.remove(&k);
192
            if set.is_empty() {
193
                self.live_modes_of.remove(&node);
194
            }
195
        }
196
    }
197
a87c7b9d Andy McCall ↶ 198
    /// Directories no longer placed anywhere, by the (parent, name) they
199
    /// were last placed at, with that placement: what an import can bring
200
    /// back rather than make a second node for one Git path.
201
    pub fn dead_dirs(&self) -> HashMap<(NodeId, String), (NodeId, OpKey)> {
202
        let mut out: HashMap<(NodeId, String), (NodeId, OpKey)> = HashMap::new();
203
        for (k, p) in &self.placements {
204
            if self.kind(&p.node) != Some(NodeKind::Dir)
205
                || self.placement_live(k)
206
                || !self.live_placements(&p.node).is_empty()
207
            {
208
                continue;
209
            }
210
            let slot = out
211
                .entry((p.parent, p.name.clone()))
212
                .or_insert((p.node, *k));
213
            // The newest placement, by key: the same answer in any order.
214
            if *k > slot.1 {
215
                *slot = (p.node, *k);
216
            }
217
        }
218
        out
219
    }
220
221
    /// Every placement `node` ever had, live or not, with the changes that
222
    /// retired it (none while live).
223
    pub fn placement_history(&self, node: &NodeId) -> Vec<(OpKey, &Placement, Vec<ChangeHash>)> {
224
        self.placements_of
225
            .get(node)
226
            .into_iter()
227
            .flatten()
228
            .filter_map(|k| {
229
                let p = self.placements.get(k)?;
230
                let by = self
231
                    .deleted_placements
232
                    .get(k)
233
                    .map(|d| d.iter().copied().collect())
234
                    .unwrap_or_default();
235
                Some((*k, p, by))
236
            })
237
            .collect()
238
    }
239
ff0af1ee unknown 240
    fn placement_live(&self, k: &OpKey) -> bool {
241
        self.deleted_placements.get(k).is_none_or(|d| d.is_empty())
242
    }
243
    fn mode_live(&self, k: &OpKey) -> bool {
244
        self.deleted_modes.get(k).is_none_or(|d| d.is_empty())
245
    }
246
247
    pub fn kind(&self, node: &NodeId) -> Option<NodeKind> {
75167672 Andy McCall 248
        if node.is_root() {
249
            Some(NodeKind::Dir)
250
        } else {
251
            self.kinds.get(node).copied()
252
        }
ff0af1ee unknown 253
    }
254
255
    /// The change whose `AddNode` introduced `node`.
256
    pub fn introducer(&self, node: &NodeId) -> Option<ChangeHash> {
257
        self.introduced.get(node).copied()
258
    }
259
260
    pub fn placement(&self, k: &OpKey) -> Option<&Placement> {
261
        self.placements.get(k)
262
    }
263
264
    pub fn live_placements(&self, node: &NodeId) -> Vec<(OpKey, &Placement)> {
2306a4d2 Andy McCall ↶ 265
        self.live_placements_of
ff0af1ee unknown 266
            .get(node)
267
            .into_iter()
268
            .flat_map(|ks| ks.iter())
269
            .filter_map(|k| self.placements.get(k).map(|p| (*k, p)))
270
            .collect()
271
    }
272
273
    pub fn live_modes(&self, node: &NodeId) -> Vec<(OpKey, Mode)> {
2306a4d2 Andy McCall ↶ 274
        self.live_modes_of
ff0af1ee unknown 275
            .get(node)
276
            .into_iter()
277
            .flat_map(|ks| ks.iter())
278
            .filter_map(|k| self.modes.get(k).map(|(_, m)| (*k, *m)))
279
            .collect()
280
    }
281
282
    /// Every node that has at least one live placement.
283
    pub fn placed_nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
2306a4d2 Andy McCall ↶ 284
        self.live_placements_of.keys().copied()
ff0af1ee unknown 285
    }
286
287
    /// Resolve the tree: assign paths, find conflicts.
206c76ca Andy McCall ↶ 288
    /// The materialized tree (spec §7.1): computed once per graph and
289
    /// cloned for each caller.
ff0af1ee unknown 290
    pub fn view(&self) -> TreeView {
206c76ca Andy McCall ↶ 291
        self.view_cache.get_or_init(|| self.compute_view()).clone()
292
    }
293
294
    fn compute_view(&self) -> TreeView {
ff0af1ee unknown 295
        let mut view = TreeView::default();
296
297
        // Live placements grouped by parent.
298
        let mut children: HashMap<NodeId, Vec<(OpKey, &Placement)>> = HashMap::new();
2306a4d2 Andy McCall ↶ 299
        for ks in self.live_placements_of.values() {
300
            for k in ks {
301
                if let Some(p) = self.placements.get(k) {
302
                    children.entry(p.parent).or_default().push((*k, p));
303
                }
ff0af1ee unknown 304
            }
305
        }
306
        for c in children.values_mut() {
307
            c.sort_by(|a, b| a.1.name.cmp(&b.1.name).then(a.0.cmp(&b.0)));
308
        }
309
310
        // Multi-placement and mode conflicts.
2306a4d2 Andy McCall ↶ 311
        // Only placed nodes: one with no live placement is not in the tree.
312
        let mut nodes: Vec<NodeId> = self.live_placements_of.keys().copied().collect();
ff0af1ee unknown 313
        nodes.sort();
314
        let mut multi: HashSet<NodeId> = HashSet::new();
315
        for n in &nodes {
316
            let lp = self.live_placements(n);
75167672 Andy McCall 317
            let distinct_places: BTreeSet<(&NodeId, &str)> = lp
318
                .iter()
319
                .map(|(_, p)| (&p.parent, p.name.as_str()))
320
                .collect();
ff0af1ee unknown 321
            if distinct_places.len() > 1 {
322
                multi.insert(*n);
75167672 Andy McCall 323
                view.conflicts.push(TreeConflict::MultiPlacement {
324
                    node: *n,
325
                    placements: lp.iter().map(|(k, _)| *k).collect(),
326
                });
ff0af1ee unknown 327
            }
328
            let lm = self.live_modes(n);
329
            let distinct_modes: BTreeSet<Mode> = lm.iter().map(|(_, m)| *m).collect();
330
            if distinct_modes.len() > 1 {
75167672 Andy McCall 331
                view.conflicts.push(TreeConflict::Mode {
332
                    node: *n,
333
                    modes: distinct_modes.into_iter().collect(),
334
                });
ff0af1ee unknown 335
            }
336
        }
337
338
        // BFS from the root, assigning paths.
339
        let mut reached: HashSet<NodeId> = HashSet::new();
340
        reached.insert(NodeId::ROOT);
341
        let mut queue: Vec<(NodeId, String)> = vec![(NodeId::ROOT, String::new())];
342
        let mut on_path: HashSet<NodeId> = HashSet::new();
343
        while let Some((dir, dir_path)) = queue.pop() {
75167672 Andy McCall 344
            let Some(kids) = children.get(&dir) else {
345
                continue;
346
            };
ff0af1ee unknown 347
            // Name conflicts within this directory.
348
            let mut by_name: BTreeMap<&str, Vec<(OpKey, &Placement)>> = BTreeMap::new();
349
            for (k, p) in kids {
350
                by_name.entry(p.name.as_str()).or_default().push((*k, p));
351
            }
352
            for (name, group) in by_name {
353
                let distinct: BTreeSet<NodeId> = group.iter().map(|(_, p)| p.node).collect();
354
                if distinct.len() > 1 {
75167672 Andy McCall 355
                    view.conflicts.push(TreeConflict::Name {
356
                        parent: dir,
357
                        name: name.to_string(),
358
                        nodes: distinct.iter().copied().collect(),
359
                    });
ff0af1ee unknown 360
                }
361
                let suffixed = distinct.len() > 1;
362
                for (i, (k, p)) in group.iter().enumerate() {
363
                    let node = p.node;
364
                    if node == dir || on_path.contains(&node) {
365
                        view.conflicts.push(TreeConflict::Cycle { node });
366
                        continue;
367
                    }
368
                    let needs_suffix = suffixed || multi.contains(&node);
75167672 Andy McCall 369
                    let leaf = if needs_suffix {
370
                        format!("{name}.oxy-conflict-{}", i + 1)
371
                    } else {
372
                        name.to_string()
373
                    };
374
                    let path = if dir_path.is_empty() {
375
                        leaf
376
                    } else {
377
                        format!("{dir_path}/{leaf}")
378
                    };
ff0af1ee unknown 379
                    let kind = self.kind(&node).unwrap_or(NodeKind::File);
380
                    let modes = self.live_modes(&node);
381
                    let mode_atom = modes.iter().map(|(k, _)| *k).max();
75167672 Andy McCall 382
                    let mode = mode_atom
383
                        .and_then(|k| self.modes.get(&k).map(|(_, m)| *m))
384
                        .unwrap_or_default();
385
                    view.entries.insert(
386
                        path.clone(),
387
                        TreeEntry {
388
                            path: path.clone(),
389
                            node,
390
                            kind,
391
                            mode,
392
                            placement: *k,
393
                            mode_atom,
394
                        },
395
                    );
ff0af1ee unknown 396
                    if kind == NodeKind::Dir && reached.insert(node) {
397
                        on_path.insert(dir);
398
                        queue.push((node, path));
399
                    } else {
400
                        reached.insert(node);
401
                    }
402
                }
403
            }
404
        }
405
406
        // Nodes with live placements that were never reached: orphans or cycles.
407
        for n in &nodes {
408
            if reached.contains(n) {
409
                continue;
410
            }
411
            let lp = self.live_placements(n);
412
            if lp.is_empty() {
413
                continue;
414
            }
415
            let mut seen = HashSet::new();
416
            let mut cur = *n;
417
            let conflict = loop {
418
                if !seen.insert(cur) {
419
                    break TreeConflict::Cycle { node: *n };
420
                }
421
                let lp = self.live_placements(&cur);
422
                let Some((_, p)) = lp.first() else {
75167672 Andy McCall 423
                    break TreeConflict::Orphan {
424
                        node: *n,
425
                        parent: cur,
426
                    };
ff0af1ee unknown 427
                };
428
                if reached.contains(&p.parent) {
429
                    // Parent reached but this node was not: the parent is not a
430
                    // directory, so this is a name/kind problem surfaced as orphan.
75167672 Andy McCall 431
                    break TreeConflict::Orphan {
432
                        node: *n,
433
                        parent: p.parent,
434
                    };
ff0af1ee unknown 435
                }
436
                cur = p.parent;
437
            };
438
            if !view.conflicts.contains(&conflict) {
439
                view.conflicts.push(conflict);
440
            }
441
        }
442
        view
443
    }
444
}
445
252e8cb2 Andy McCall ↶ 446
impl TreeGraph {
447
    /// Serialize for a checkpoint.
448
    pub fn to_value(&self) -> crate::cbor::Value {
449
        use crate::cbor::Value;
450
        let opkey = |k: &OpKey| vec![Value::Bytes(k.0 .0.to_vec()), Value::Uint(k.1 as u64)];
451
        let deleters = |m: &HashMap<OpKey, BTreeSet<ChangeHash>>| {
452
            let mut ks: Vec<&OpKey> = m.keys().collect();
453
            ks.sort();
454
            Value::Array(
455
                ks.into_iter()
456
                    .map(|k| {
457
                        let mut v = opkey(k);
75167672 Andy McCall 458
                        v.push(Value::Array(
459
                            m[k].iter().map(|d| Value::Bytes(d.0.to_vec())).collect(),
460
                        ));
252e8cb2 Andy McCall ↶ 461
                        Value::Array(v)
462
                    })
463
                    .collect(),
464
            )
465
        };
466
        let mut nodes: Vec<&NodeId> = self.kinds.keys().collect();
467
        nodes.sort();
468
        let mut pks: Vec<&OpKey> = self.placements.keys().collect();
469
        pks.sort();
470
        let mut mks: Vec<&OpKey> = self.modes.keys().collect();
471
        mks.sort();
472
        Value::map([
473
            (
474
                0,
475
                Value::Array(
476
                    nodes
477
                        .into_iter()
478
                        .map(|n| {
479
                            Value::Array(vec![
480
                                Value::Bytes(n.0.to_vec()),
481
                                Value::Uint(self.kinds[n].tag()),
75167672 Andy McCall 482
                                self.introduced
483
                                    .get(n)
484
                                    .map(|h| Value::Bytes(h.0.to_vec()))
485
                                    .unwrap_or(Value::Null),
252e8cb2 Andy McCall ↶ 486
                            ])
487
                        })
488
                        .collect(),
489
                ),
490
            ),
491
            (
492
                1,
493
                Value::Array(
494
                    pks.into_iter()
495
                        .map(|k| {
496
                            let p = &self.placements[k];
497
                            let mut v = opkey(k);
75167672 Andy McCall 498
                            v.extend([
499
                                Value::Bytes(p.node.0.to_vec()),
500
                                Value::Bytes(p.parent.0.to_vec()),
501
                                Value::Text(p.name.clone()),
502
                            ]);
252e8cb2 Andy McCall ↶ 503
                            Value::Array(v)
504
                        })
505
                        .collect(),
506
                ),
507
            ),
508
            (2, deleters(&self.deleted_placements)),
509
            (
510
                3,
511
                Value::Array(
512
                    mks.into_iter()
513
                        .map(|k| {
514
                            let (n, m) = &self.modes[k];
515
                            let mut v = opkey(k);
516
                            v.extend([Value::Bytes(n.0.to_vec()), Value::Uint(m.bits())]);
517
                            Value::Array(v)
518
                        })
519
                        .collect(),
520
                ),
521
            ),
522
            (4, deleters(&self.deleted_modes)),
523
        ])
524
    }
525
526
    pub fn from_value(v: &crate::cbor::Value) -> Result<TreeGraph, crate::objects::ObjectError> {
527
        use crate::objects::{array, bytes, field, map, oref, text, u32_, uint, ObjectError};
528
        let m = map(v, "tree graph")?;
75167672 Andy McCall 529
        let node = |v: &crate::cbor::Value| {
530
            NodeId::from_slice(bytes(v, "node")?)
531
                .ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))
532
        };
533
        let opkey = |e: &[crate::cbor::Value]| -> Result<OpKey, ObjectError> {
534
            Ok((oref(&e[0], "op change")?, u32_(&e[1], "op index")?))
535
        };
252e8cb2 Andy McCall ↶ 536
        let mut g = TreeGraph::default();
537
        for n in array(field(m, 0, "nodes")?, "nodes")? {
538
            let e = array(n, "node entry")?;
539
            if e.len() != 3 {
540
                return Err(ObjectError::Schema("node entry: expected 3 fields".into()));
541
            }
542
            let id = node(&e[0])?;
75167672 Andy McCall 543
            g.kinds
544
                .insert(id, NodeKind::from_tag(uint(&e[1], "kind")?)?);
252e8cb2 Andy McCall ↶ 545
            if !e[2].is_null() {
546
                g.introduced.insert(id, oref(&e[2], "introducer")?);
547
            }
548
        }
549
        for p in array(field(m, 1, "placements")?, "placements")? {
550
            let e = array(p, "placement")?;
551
            if e.len() != 5 {
552
                return Err(ObjectError::Schema("placement: expected 5 fields".into()));
553
            }
554
            let k = opkey(e)?;
75167672 Andy McCall 555
            let pl = Placement {
556
                node: node(&e[2])?,
557
                parent: node(&e[3])?,
558
                name: text(&e[4], "name")?.to_owned(),
559
            };
252e8cb2 Andy McCall ↶ 560
            g.placements_of.entry(pl.node).or_default().insert(k);
561
            g.placements.insert(k, pl);
562
        }
75167672 Andy McCall 563
        let deleters = |v: &crate::cbor::Value,
564
                        what: &str|
565
         -> Result<HashMap<OpKey, BTreeSet<ChangeHash>>, ObjectError> {
252e8cb2 Andy McCall ↶ 566
            let mut out = HashMap::new();
567
            for d in array(v, what)? {
568
                let e = array(d, what)?;
569
                if e.len() != 3 {
570
                    return Err(ObjectError::Schema(format!("{what}: expected 3 fields")));
571
                }
75167672 Andy McCall 572
                out.insert(
573
                    opkey(e)?,
574
                    array(&e[2], "deleters")?
575
                        .iter()
576
                        .map(|x| oref(x, "deleter"))
577
                        .collect::<Result<BTreeSet<_>, _>>()?,
578
                );
252e8cb2 Andy McCall ↶ 579
            }
580
            Ok(out)
581
        };
582
        g.deleted_placements = deleters(field(m, 2, "deleted placements")?, "deleted placement")?;
583
        for md in array(field(m, 3, "modes")?, "modes")? {
584
            let e = array(md, "mode")?;
585
            if e.len() != 4 {
586
                return Err(ObjectError::Schema("mode: expected 4 fields".into()));
587
            }
588
            let k = opkey(e)?;
589
            let n = node(&e[2])?;
75167672 Andy McCall 590
            g.modes
591
                .insert(k, (n, Mode::from_bits(uint(&e[3], "mode bits")?)?));
252e8cb2 Andy McCall ↶ 592
            g.modes_of.entry(n).or_default().insert(k);
593
        }
594
        g.deleted_modes = deleters(field(m, 4, "deleted modes")?, "deleted mode")?;
2306a4d2 Andy McCall ↶ 595
        let keys: Vec<OpKey> = g.placements.keys().copied().collect();
596
        for k in keys {
597
            g.index_placement(k);
598
        }
599
        let keys: Vec<OpKey> = g.modes.keys().copied().collect();
600
        for k in keys {
601
            g.index_mode(k);
602
        }
252e8cb2 Andy McCall ↶ 603
        Ok(g)
604
    }
605
}
606
ff0af1ee unknown 607
pub fn op_key(r: OpRef) -> OpKey {
608
    (r.change, r.index)
609
}
610
611
#[cfg(test)]
612
mod tests {
613
    use super::*;
614
    use crate::hash::ObjectRef;
615
616
    fn h(n: u8) -> ChangeHash {
617
        ObjectRef([n; 32])
618
    }
619
    fn nid(n: u8) -> NodeId {
620
        NodeId([n; 16])
621
    }
622
    fn add(g: &mut TreeGraph, c: u8, i: u32, node: u8, kind: NodeKind, parent: NodeId, name: &str) {
75167672 Andy McCall 623
        g.apply(
624
            h(c),
625
            i,
626
            &TreeOp::AddNode {
627
                node: nid(node),
628
                kind,
629
                mode: Mode::TEXT,
630
                parent,
631
                name: name.into(),
632
            },
633
        );
ff0af1ee unknown 634
    }
635
    fn paths(v: &TreeView) -> Vec<String> {
636
        v.entries.keys().cloned().collect()
637
    }
638
639
    #[test]
640
    fn nested_paths_and_rename() {
641
        let mut g = TreeGraph::new();
642
        add(&mut g, 1, 0, 1, NodeKind::Dir, NodeId::ROOT, "src");
643
        add(&mut g, 1, 1, 2, NodeKind::File, nid(1), "main.rs");
644
        add(&mut g, 1, 2, 3, NodeKind::File, NodeId::ROOT, "README");
645
        let v = g.view();
646
        assert_eq!(paths(&v), ["README", "src", "src/main.rs"]);
647
        assert!(!v.is_conflicted());
648
649
        // Rename src/main.rs → src/lib.rs, move src → lib (subtree follows).
75167672 Andy McCall 650
        g.apply(
651
            h(2),
652
            0,
653
            &TreeOp::Move {
654
                node: nid(2),
655
                old: OpRef {
656
                    change: h(1),
657
                    index: 1,
658
                },
659
                parent: nid(1),
660
                name: "lib.rs".into(),
661
            },
662
        );
663
        g.apply(
664
            h(2),
665
            1,
666
            &TreeOp::Move {
667
                node: nid(1),
668
                old: OpRef {
669
                    change: h(1),
670
                    index: 0,
671
                },
672
                parent: NodeId::ROOT,
673
                name: "lib".into(),
674
            },
675
        );
ff0af1ee unknown 676
        let v = g.view();
677
        assert_eq!(paths(&v), ["README", "lib", "lib/lib.rs"]);
678
        assert_eq!(v.entries["lib/lib.rs"].placement, (h(2), 0));
679
75167672 Andy McCall 680
        g.apply(
681
            h(3),
682
            0,
683
            &TreeOp::RemoveNode {
684
                node: nid(3),
685
                placement: OpRef {
686
                    change: h(1),
687
                    index: 2,
688
                },
689
            },
690
        );
ff0af1ee unknown 691
        assert_eq!(paths(&g.view()), ["lib", "lib/lib.rs"]);
692
    }
693
694
    #[test]
695
    fn name_conflict_gets_suffixes() {
696
        let mut g = TreeGraph::new();
697
        add(&mut g, 1, 0, 1, NodeKind::File, NodeId::ROOT, "a.txt");
698
        add(&mut g, 2, 0, 2, NodeKind::File, NodeId::ROOT, "a.txt");
699
        let v = g.view();
700
        assert_eq!(paths(&v), ["a.txt.oxy-conflict-1", "a.txt.oxy-conflict-2"]);
75167672 Andy McCall 701
        assert_eq!(
702
            v.conflicts,
703
            vec![TreeConflict::Name {
704
                parent: NodeId::ROOT,
705
                name: "a.txt".into(),
706
                nodes: vec![nid(1), nid(2)]
707
            }]
708
        );
ff0af1ee unknown 709
    }
710
711
    #[test]
712
    fn concurrent_renames_are_multi_placement() {
713
        let mut g = TreeGraph::new();
714
        add(&mut g, 1, 0, 1, NodeKind::File, NodeId::ROOT, "a");
75167672 Andy McCall 715
        g.apply(
716
            h(2),
717
            0,
718
            &TreeOp::Move {
719
                node: nid(1),
720
                old: OpRef {
721
                    change: h(1),
722
                    index: 0,
723
                },
724
                parent: NodeId::ROOT,
725
                name: "b".into(),
726
            },
727
        );
728
        g.apply(
729
            h(3),
730
            0,
731
            &TreeOp::Move {
732
                node: nid(1),
733
                old: OpRef {
734
                    change: h(1),
735
                    index: 0,
736
                },
737
                parent: NodeId::ROOT,
738
                name: "c".into(),
739
            },
740
        );
ff0af1ee unknown 741
        let v = g.view();
742
        assert_eq!(paths(&v), ["b.oxy-conflict-1", "c.oxy-conflict-1"]);
75167672 Andy McCall 743
        assert!(
744
            matches!(v.conflicts[0], TreeConflict::MultiPlacement { node, .. } if node == nid(1))
745
        );
ff0af1ee unknown 746
    }
747
748
    #[test]
749
    fn orphan_and_cycle() {
750
        let mut g = TreeGraph::new();
751
        add(&mut g, 1, 0, 1, NodeKind::Dir, NodeId::ROOT, "d");
752
        add(&mut g, 1, 1, 2, NodeKind::File, nid(1), "f");
753
        // Concurrently: 2 removes d, 3 adds d/g.
75167672 Andy McCall 754
        g.apply(
755
            h(2),
756
            0,
757
            &TreeOp::RemoveNode {
758
                node: nid(1),
759
                placement: OpRef {
760
                    change: h(1),
761
                    index: 0,
762
                },
763
            },
764
        );
ff0af1ee unknown 765
        add(&mut g, 3, 0, 3, NodeKind::File, nid(1), "g");
766
        let v = g.view();
767
        assert_eq!(paths(&v), Vec::<String>::new());
75167672 Andy McCall 768
        assert!(v.conflicts.contains(&TreeConflict::Orphan {
769
            node: nid(2),
770
            parent: nid(1)
771
        }));
772
        assert!(v.conflicts.contains(&TreeConflict::Orphan {
773
            node: nid(3),
774
            parent: nid(1)
775
        }));
ff0af1ee unknown 776
777
        // Cycle: a/b, then concurrently move a under b and b under a's parent... simplest: move a into b.
778
        let mut g = TreeGraph::new();
779
        add(&mut g, 1, 0, 1, NodeKind::Dir, NodeId::ROOT, "a");
780
        add(&mut g, 1, 1, 2, NodeKind::Dir, nid(1), "b");
75167672 Andy McCall 781
        g.apply(
782
            h(2),
783
            0,
784
            &TreeOp::Move {
785
                node: nid(1),
786
                old: OpRef {
787
                    change: h(1),
788
                    index: 0,
789
                },
790
                parent: nid(2),
791
                name: "a".into(),
792
            },
793
        );
ff0af1ee unknown 794
        let v = g.view();
75167672 Andy McCall 795
        assert!(
796
            v.conflicts
797
                .iter()
798
                .any(|c| matches!(c, TreeConflict::Cycle { .. })),
799
            "{:?}",
800
            v.conflicts
801
        );
ff0af1ee unknown 802
    }
803
804
    #[test]
252e8cb2 Andy McCall ↶ 805
    fn serialization_round_trip() {
806
        let mut g = TreeGraph::new();
807
        add(&mut g, 1, 0, 1, NodeKind::Dir, NodeId::ROOT, "src");
808
        add(&mut g, 1, 1, 2, NodeKind::File, nid(1), "main.rs");
75167672 Andy McCall 809
        g.apply(
810
            h(2),
811
            0,
812
            &TreeOp::Move {
813
                node: nid(2),
814
                old: OpRef {
815
                    change: h(1),
816
                    index: 1,
817
                },
818
                parent: nid(1),
819
                name: "lib.rs".into(),
820
            },
821
        );
822
        g.apply(
823
            h(3),
824
            0,
825
            &TreeOp::SetMode {
826
                node: nid(2),
827
                old: OpRef {
828
                    change: h(1),
829
                    index: 1,
830
                },
831
                mode: Mode {
832
                    exec: true,
833
                    opaque: false,
834
                },
835
            },
836
        );
252e8cb2 Andy McCall ↶ 837
        let bytes = crate::cbor::encode(&g.to_value());
838
        let back = TreeGraph::from_value(&crate::cbor::decode(&bytes).unwrap()).unwrap();
839
        assert_eq!(back.view(), g.view());
840
        assert_eq!(back.introducer(&nid(2)), Some(h(1)));
841
        assert_eq!(crate::cbor::encode(&back.to_value()), bytes);
842
    }
843
844
    #[test]
ff0af1ee unknown 845
    fn mode_conflict_and_resolution() {
846
        let mut g = TreeGraph::new();
847
        add(&mut g, 1, 0, 1, NodeKind::File, NodeId::ROOT, "run.sh");
75167672 Andy McCall 848
        g.apply(
849
            h(2),
850
            0,
851
            &TreeOp::SetMode {
852
                node: nid(1),
853
                old: OpRef {
854
                    change: h(1),
855
                    index: 0,
856
                },
857
                mode: Mode {
858
                    exec: true,
859
                    opaque: false,
860
                },
861
            },
862
        );
863
        g.apply(
864
            h(3),
865
            0,
866
            &TreeOp::SetMode {
867
                node: nid(1),
868
                old: OpRef {
869
                    change: h(1),
870
                    index: 0,
871
                },
872
                mode: Mode {
873
                    exec: false,
874
                    opaque: true,
875
                },
876
            },
877
        );
ff0af1ee unknown 878
        let v = g.view();
879
        assert!(matches!(v.conflicts[0], TreeConflict::Mode { .. }));
880
        // Materialized with the greatest mode atom.
75167672 Andy McCall 881
        assert_eq!(
882
            v.entries["run.sh"].mode,
883
            Mode {
884
                exec: false,
885
                opaque: true
886
            }
887
        );
ff0af1ee unknown 888
        // The placement introduced by the same AddNode is unaffected.
889
        assert_eq!(v.entries["run.sh"].placement, (h(1), 0));
890
        // Resolution: retire both live mode atoms; identical live values do not conflict.
75167672 Andy McCall 891
        g.apply(
892
            h(4),
893
            0,
894
            &TreeOp::SetMode {
895
                node: nid(1),
896
                old: OpRef {
897
                    change: h(2),
898
                    index: 0,
899
                },
900
                mode: Mode {
901
                    exec: true,
902
                    opaque: false,
903
                },
904
            },
905
        );
906
        g.apply(
907
            h(4),
908
            1,
909
            &TreeOp::SetMode {
910
                node: nid(1),
911
                old: OpRef {
912
                    change: h(3),
913
                    index: 0,
914
                },
915
                mode: Mode {
916
                    exec: true,
917
                    opaque: false,
918
                },
919
            },
920
        );
ff0af1ee unknown 921
        let v = g.view();
922
        assert!(!v.is_conflicted(), "{:?}", v.conflicts);
75167672 Andy McCall 923
        assert_eq!(
924
            v.entries["run.sh"].mode,
925
            Mode {
926
                exec: true,
927
                opaque: false
928
            }
929
        );
ff0af1ee unknown 930
    }
931
}