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