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