OXYGENHUB
# 62794ecc7f74 oxygen / crates / oxy-core / src / graph.rs
33.8 KiB · Rust · last changed Git import verified commit by commit; two dependency bugs fixed 1 month ago source blame history raw
ff0af1ee unknown 1
//! The per-file atom graph (spec §7.3), its materialization, and text conflict
2
//! detection (spec §9.1, §9.5).
3
//!
4
//! The graph is a pure function of the set of changes applied to it: inserts
5
//! add vertices and edges, deletes set a flag, and nothing is ever removed.
6
7
use crate::hash::ChangeHash;
8
use crate::objects::AtomRange;
9
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
10
11
/// `(introducing change, index within that change for this node)`.
12
pub type AtomKey = (ChangeHash, u32);
13
14
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
15
pub enum Vertex {
16
    Start,
17
    Atom(AtomKey),
18
    End,
19
}
20
21
/// Transitive dependency test between changes; `depends_on(a, a)` is true.
22
pub trait DepOracle {
23
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool;
24
}
25
26
/// No change depends on any other: every pair commutes.
27
pub struct NoDeps;
28
impl DepOracle for NoDeps {
29
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
30
        a == b
31
    }
32
}
33
34
#[derive(Default, Clone, Debug)]
35
struct AtomInfo {
36
    /// `None` until the introducing change has been applied.
37
    content: Option<Vec<u8>>,
38
    deleted_by: BTreeSet<ChangeHash>,
39
}
40
41
#[derive(Clone, Debug)]
42
pub struct FileGraph {
43
    atoms: HashMap<AtomKey, AtomInfo>,
44
    succ: HashMap<Vertex, BTreeSet<Vertex>>,
45
    pred: HashMap<Vertex, BTreeSet<Vertex>>,
46
}
47
48
impl Default for FileGraph {
49
    fn default() -> Self {
50
        Self::new()
51
    }
52
}
53
54
/// One piece of a materialized file.
55
#[derive(Clone, Debug, PartialEq, Eq)]
56
pub enum Segment {
57
    Lines(Vec<AtomKey>),
58
    /// Order conflict: alternatives that cannot be ordered against each other.
59
    Conflict(Vec<Vec<AtomKey>>),
60
    /// Delete/insert conflict: `inserted` was anchored to `deleted` by a change
61
    /// that commutes with the deletion.
75167672 Andy McCall 62
    Zombie {
63
        deleted: Vec<AtomKey>,
64
        inserted: Vec<AtomKey>,
65
    },
ff0af1ee unknown 66
}
67
68
#[derive(Clone, Debug, PartialEq, Eq, Default)]
69
pub struct Materialized {
70
    pub segments: Vec<Segment>,
71
}
72
73
impl Materialized {
74
    pub fn is_conflicted(&self) -> bool {
75167672 Andy McCall 75
        self.segments
76
            .iter()
77
            .any(|s| !matches!(s, Segment::Lines(_)))
ff0af1ee unknown 78
    }
79
    pub fn conflict_count(&self) -> usize {
75167672 Andy McCall 80
        self.segments
81
            .iter()
82
            .filter(|s| !matches!(s, Segment::Lines(_)))
83
            .count()
ff0af1ee unknown 84
    }
85
    /// Every live atom in materialized order, conflict sides concatenated.
86
    pub fn live_atoms(&self) -> Vec<AtomKey> {
87
        let mut out = Vec::new();
88
        for s in &self.segments {
89
            match s {
90
                Segment::Lines(a) => out.extend(a),
91
                Segment::Conflict(sides) => sides.iter().for_each(|x| out.extend(x)),
92
                Segment::Zombie { inserted, .. } => out.extend(inserted),
93
            }
94
        }
95
        out
96
    }
97
}
98
99
impl FileGraph {
100
    pub fn new() -> Self {
75167672 Andy McCall 101
        let mut g = FileGraph {
102
            atoms: HashMap::new(),
103
            succ: HashMap::new(),
104
            pred: HashMap::new(),
105
        };
ff0af1ee unknown 106
        g.edge(Vertex::Start, Vertex::End);
107
        g
108
    }
109
110
    fn edge(&mut self, a: Vertex, b: Vertex) {
111
        self.succ.entry(a).or_default().insert(b);
112
        self.pred.entry(b).or_default().insert(a);
113
        self.succ.entry(b).or_default();
114
        self.pred.entry(a).or_default();
115
    }
116
117
    /// Apply an `Insert` by `change` whose first new atom has index `first_index`.
75167672 Andy McCall 118
    pub fn insert(
119
        &mut self,
120
        change: ChangeHash,
121
        first_index: u32,
122
        after: Vertex,
123
        before: Vertex,
124
        lines: &[Vec<u8>],
125
    ) {
ff0af1ee unknown 126
        let mut prev = after;
127
        for (i, line) in lines.iter().enumerate() {
128
            let key = (change, first_index + i as u32);
129
            self.atoms.entry(key).or_default().content = Some(line.clone());
130
            let v = Vertex::Atom(key);
131
            self.edge(prev, v);
132
            prev = v;
133
        }
134
        self.edge(prev, before);
135
    }
136
137
    /// Apply a `Delete` of `range` by change `by`.
138
    pub fn delete(&mut self, by: ChangeHash, range: &AtomRange) {
139
        for i in range.from..range.to {
75167672 Andy McCall 140
            self.atoms
141
                .entry((range.change, i))
142
                .or_default()
143
                .deleted_by
144
                .insert(by);
ff0af1ee unknown 145
        }
146
    }
147
148
    pub fn is_live(&self, k: &AtomKey) -> bool {
75167672 Andy McCall 149
        self.atoms
150
            .get(k)
151
            .is_some_and(|a| a.content.is_some() && a.deleted_by.is_empty())
ff0af1ee unknown 152
    }
153
154
    pub fn content(&self, k: &AtomKey) -> Option<&[u8]> {
155
        self.atoms.get(k).and_then(|a| a.content.as_deref())
156
    }
157
158
    pub fn deleters(&self, k: &AtomKey) -> impl Iterator<Item = &ChangeHash> {
75167672 Andy McCall 159
        self.atoms
160
            .get(k)
161
            .into_iter()
162
            .flat_map(|a| a.deleted_by.iter())
ff0af1ee unknown 163
    }
164
165
    /// Number of atoms introduced by `change` in this graph (for index bookkeeping).
166
    pub fn atom_count(&self) -> usize {
167
        self.atoms.len()
168
    }
169
62794ecc Andy McCall ↶ 170
    /// The changes that inserted a live atom directly after `k`. A change
171
    /// that deletes `k` must depend on them (spec §8.1), or `is_ghost`
172
    /// reads the deletion as concurrent with those insertions.
173
    pub fn live_successor_changes(&self, k: &AtomKey) -> Vec<ChangeHash> {
174
        self.succ
175
            .get(&Vertex::Atom(*k))
176
            .map(|ss| {
177
                ss.iter()
178
                    .filter_map(|s| match s {
179
                        Vertex::Atom(y) if self.is_live(y) => Some(y.0),
180
                        _ => None,
181
                    })
182
                    .collect()
183
            })
184
            .unwrap_or_default()
185
    }
186
ff0af1ee unknown 187
    fn is_ghost(&self, k: &AtomKey, info: &AtomInfo, deps: &dyn DepOracle) -> bool {
23b217d7 Andy McCall ↶ 188
        // A deleted atom is a ghost when a live direct successor was inserted
189
        // by a change that commutes with every deleter: neither depends on
190
        // the other (spec §9.1, delete/insert). If the inserter came after the
191
        // deletion, or the deleter saw the insertion, there is no conflict.
ff0af1ee unknown 192
        self.succ.get(&Vertex::Atom(*k)).is_some_and(|ss| {
193
            ss.iter().any(|s| match s {
75167672 Andy McCall 194
                Vertex::Atom(y) if self.is_live(y) => !info
195
                    .deleted_by
196
                    .iter()
197
                    .any(|d| deps.depends_on(d, &y.0) || deps.depends_on(&y.0, d)),
ff0af1ee unknown 198
                _ => false,
199
            })
200
        })
201
    }
202
203
    /// Successors of `v` in the graph with non-kept vertices contracted away.
75167672 Andy McCall 204
    fn reduced_succ(
205
        &self,
206
        v: Vertex,
207
        kept: &HashSet<Vertex>,
208
        memo: &mut HashMap<Vertex, BTreeSet<Vertex>>,
209
    ) -> BTreeSet<Vertex> {
ff0af1ee unknown 210
        let mut out = BTreeSet::new();
75167672 Andy McCall 211
        let mut stack: Vec<Vertex> = self
212
            .succ
213
            .get(&v)
214
            .map(|s| s.iter().copied().collect())
215
            .unwrap_or_default();
ff0af1ee unknown 216
        let mut seen = HashSet::new();
217
        while let Some(s) = stack.pop() {
218
            if kept.contains(&s) {
219
                out.insert(s);
220
            } else if seen.insert(s) {
221
                if let Some(m) = memo.get(&s) {
222
                    out.extend(m.iter().copied());
223
                } else if let Some(next) = self.succ.get(&s) {
224
                    stack.extend(next.iter().copied());
225
                }
226
            }
227
        }
228
        out
229
    }
230
231
    /// Order the live atoms and locate conflicts.
232
    pub fn materialize(&self, deps: &dyn DepOracle) -> Materialized {
233
        // 1. Vertices that survive contraction: live atoms, ghosts, Start, End.
234
        let mut kept: HashSet<Vertex> = HashSet::new();
235
        let mut ghosts: HashSet<AtomKey> = HashSet::new();
236
        kept.insert(Vertex::Start);
237
        kept.insert(Vertex::End);
238
        for (k, info) in &self.atoms {
239
            if info.content.is_none() {
240
                continue;
241
            }
242
            if info.deleted_by.is_empty() {
243
                kept.insert(Vertex::Atom(*k));
244
            } else if self.is_ghost(k, info, deps) {
245
                kept.insert(Vertex::Atom(*k));
246
                ghosts.insert(*k);
247
            }
248
        }
249
250
        // 2. Reduced graph over kept vertices.
251
        let mut memo = HashMap::new();
252
        let mut rsucc: HashMap<Vertex, BTreeSet<Vertex>> = HashMap::new();
253
        let mut indeg: HashMap<Vertex, usize> = HashMap::new();
254
        for &u in &kept {
255
            indeg.entry(u).or_insert(0);
256
            let rs = self.reduced_succ(u, &kept, &mut memo);
257
            for &w in &rs {
258
                *indeg.entry(w).or_insert(0) += 1;
259
            }
260
            rsucc.insert(u, rs);
261
        }
262
263
        // 3. Kahn's algorithm with side labelling for conflict regions.
75167672 Andy McCall 264
        let mut ready: BTreeSet<Vertex> = indeg
265
            .iter()
266
            .filter(|(_, &d)| d == 0)
267
            .map(|(v, _)| *v)
268
            .collect();
ff0af1ee unknown 269
        let mut side: HashMap<Vertex, u32> = HashMap::new();
270
        let mut joins: HashSet<Vertex> = HashSet::new();
271
        let mut next_side = 0u32;
272
        let mut plain: Vec<Vertex> = Vec::new();
273
        let mut conflict: Option<BTreeMap<u32, Vec<Vertex>>> = None;
274
        let mut segments: Vec<Segment> = Vec::new();
275
276
        let flush_plain = |plain: &mut Vec<Vertex>, segments: &mut Vec<Segment>| {
75167672 Andy McCall 277
            let atoms: Vec<AtomKey> = plain
278
                .iter()
279
                .filter_map(|v| {
280
                    if let Vertex::Atom(k) = v {
281
                        Some(*k)
282
                    } else {
283
                        None
284
                    }
285
                })
286
                .collect();
ff0af1ee unknown 287
            plain.clear();
288
            if !atoms.is_empty() {
289
                segments.extend(split_zombies(atoms, &ghosts));
290
            }
291
        };
292
        let finish_conflict = |c: BTreeMap<u32, Vec<Vertex>>, segments: &mut Vec<Segment>| {
293
            let sides: Vec<Vec<AtomKey>> = c
294
                .into_values()
295
                .map(|vs| {
296
                    vs.into_iter()
297
                        .filter_map(|v| match v {
298
                            Vertex::Atom(k) if !ghosts.contains(&k) => Some(k),
299
                            _ => None,
300
                        })
301
                        .collect::<Vec<_>>()
302
                })
303
                .filter(|s| !s.is_empty())
304
                .collect();
305
            match sides.len() {
306
                0 => {}
307
                1 => segments.push(Segment::Lines(sides.into_iter().next().unwrap())),
308
                _ => segments.push(Segment::Conflict(sides)),
309
            }
310
        };
311
312
        while !ready.is_empty() {
313
            if ready.len() >= 2 {
314
                if conflict.is_none() {
315
                    flush_plain(&mut plain, &mut segments);
316
                    conflict = Some(BTreeMap::new());
317
                }
318
                for r in &ready {
319
                    if !side.contains_key(r) {
320
                        side.insert(*r, next_side);
321
                        next_side += 1;
322
                    }
323
                }
324
            }
325
            let v = ready.pop_first().unwrap();
326
            let my_side = side.get(&v).copied();
327
            match (&mut conflict, my_side) {
328
                (Some(c), Some(s)) => c.entry(s).or_default().push(v),
329
                (Some(_), None) => {
330
                    let c = conflict.take().unwrap();
331
                    finish_conflict(c, &mut segments);
332
                    plain.push(v);
333
                }
334
                (None, _) => plain.push(v),
335
            }
336
            for &w in &rsucc[&v] {
337
                let d = indeg.get_mut(&w).unwrap();
338
                *d -= 1;
339
                if conflict.is_some() && !joins.contains(&w) {
340
                    match (my_side, side.get(&w).copied()) {
341
                        (Some(s), None) => {
342
                            side.insert(w, s);
343
                        }
344
                        (Some(s), Some(t)) if s != t => {
345
                            side.remove(&w);
346
                            joins.insert(w);
347
                        }
348
                        (None, Some(_)) => {
349
                            side.remove(&w);
350
                            joins.insert(w);
351
                        }
352
                        _ => {}
353
                    }
354
                }
355
                if *d == 0 {
356
                    ready.insert(w);
357
                }
358
            }
359
        }
360
        if let Some(c) = conflict.take() {
361
            finish_conflict(c, &mut segments);
362
        }
363
        flush_plain(&mut plain, &mut segments);
364
        Materialized { segments }
365
    }
366
367
    /// Render a materialization to bytes with conflict markers (spec §9.5).
368
    /// `label` gives the short description of a change for marker headers.
369
    pub fn render(&self, m: &Materialized, label: &dyn Fn(&ChangeHash) -> String) -> Vec<u8> {
370
        let total = m.conflict_count();
371
        let mut n = 0;
372
        let mut out: Vec<u8> = Vec::new();
373
        let push_atoms = |out: &mut Vec<u8>, atoms: &[AtomKey]| {
374
            for a in atoms {
375
                if let Some(c) = self.content(a) {
376
                    out.extend_from_slice(c);
377
                }
378
            }
379
        };
380
        let newline_if_needed = |out: &mut Vec<u8>| {
381
            if !out.is_empty() && *out.last().unwrap() != b'\n' {
382
                out.push(b'\n');
383
            }
384
        };
385
        for seg in &m.segments {
386
            match seg {
387
                Segment::Lines(a) => push_atoms(&mut out, a),
388
                Segment::Conflict(sides) => {
389
                    n += 1;
390
                    let k = sides.len();
391
                    for (i, s) in sides.iter().enumerate() {
392
                        newline_if_needed(&mut out);
393
                        let who = s.first().map(|a| label(&a.0)).unwrap_or_default();
394
                        let marker = if i == 0 { "<<<<<<<" } else { "=======" };
395
                        out.extend_from_slice(
396
                            format!("{marker} oxy conflict {n} of {total}, side {} of {k}: change {who}\n", i + 1).as_bytes(),
397
                        );
398
                        push_atoms(&mut out, s);
399
                    }
400
                    newline_if_needed(&mut out);
75167672 Andy McCall 401
                    out.extend_from_slice(
402
                        format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes(),
403
                    );
ff0af1ee unknown 404
                }
405
                Segment::Zombie { deleted, inserted } => {
406
                    n += 1;
407
                    newline_if_needed(&mut out);
408
                    let deleter = deleted
409
                        .first()
410
                        .and_then(|a| self.deleters(a).next().copied())
411
                        .map(|d| label(&d))
412
                        .unwrap_or_default();
413
                    let inserter = inserted.first().map(|a| label(&a.0)).unwrap_or_default();
414
                    out.extend_from_slice(
415
                        format!("<<<<<<< oxy conflict {n} of {total}, side 1 of 2: deleted by change {deleter}\n-------\n").as_bytes(),
416
                    );
417
                    push_atoms(&mut out, deleted);
418
                    newline_if_needed(&mut out);
75167672 Andy McCall 419
                    out.extend_from_slice(
420
                        format!(
421
                            "======= oxy conflict {n} of {total}, side 2 of 2: change {inserter}\n"
422
                        )
423
                        .as_bytes(),
424
                    );
ff0af1ee unknown 425
                    push_atoms(&mut out, inserted);
426
                    newline_if_needed(&mut out);
75167672 Andy McCall 427
                    out.extend_from_slice(
428
                        format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes(),
429
                    );
ff0af1ee unknown 430
                }
431
            }
432
        }
433
        out
434
    }
435
}
436
252e8cb2 Andy McCall ↶ 437
fn vertex_value(v: &Vertex) -> crate::cbor::Value {
438
    use crate::cbor::Value;
439
    match v {
440
        Vertex::Start => Value::Uint(0),
441
        Vertex::End => Value::Uint(1),
75167672 Andy McCall 442
        Vertex::Atom((c, i)) => {
443
            Value::Array(vec![Value::Bytes(c.0.to_vec()), Value::Uint(*i as u64)])
444
        }
252e8cb2 Andy McCall ↶ 445
    }
446
}
447
448
fn vertex_from(v: &crate::cbor::Value) -> Result<Vertex, crate::objects::ObjectError> {
449
    use crate::cbor::Value;
450
    use crate::objects::ObjectError;
451
    match v {
452
        Value::Uint(0) => Ok(Vertex::Start),
453
        Value::Uint(1) => Ok(Vertex::End),
75167672 Andy McCall 454
        Value::Array(a) if a.len() == 2 => Ok(Vertex::Atom((
455
            crate::objects::oref(&a[0], "vertex")?,
456
            crate::objects::u32_(&a[1], "vertex")?,
457
        ))),
458
        _ => Err(ObjectError::Schema(
459
            "vertex: expected 0, 1, or [hash, index]".into(),
460
        )),
252e8cb2 Andy McCall ↶ 461
    }
462
}
463
464
impl FileGraph {
465
    /// Serialize for a checkpoint: `{0: atoms [[hash, index, content|null, [deleters]]], 1: edges [[from, to]]}`.
466
    pub fn to_value(&self) -> crate::cbor::Value {
467
        use crate::cbor::Value;
468
        let mut atoms: Vec<(&AtomKey, &AtomInfo)> = self.atoms.iter().collect();
469
        atoms.sort_by_key(|(k, _)| **k);
75167672 Andy McCall 470
        let mut edges: Vec<(Vertex, Vertex)> = self
471
            .succ
472
            .iter()
473
            .flat_map(|(a, bs)| bs.iter().map(move |b| (*a, *b)))
474
            .collect();
252e8cb2 Andy McCall ↶ 475
        edges.sort();
476
        Value::map([
477
            (
478
                0,
479
                Value::Array(
480
                    atoms
481
                        .into_iter()
482
                        .map(|((c, i), info)| {
483
                            Value::Array(vec![
484
                                Value::Bytes(c.0.to_vec()),
485
                                Value::Uint(*i as u64),
75167672 Andy McCall 486
                                info.content
487
                                    .as_ref()
488
                                    .map(|b| Value::Bytes(b.clone()))
489
                                    .unwrap_or(Value::Null),
490
                                Value::Array(
491
                                    info.deleted_by
492
                                        .iter()
493
                                        .map(|d| Value::Bytes(d.0.to_vec()))
494
                                        .collect(),
495
                                ),
252e8cb2 Andy McCall ↶ 496
                            ])
497
                        })
498
                        .collect(),
499
                ),
500
            ),
75167672 Andy McCall 501
            (
502
                1,
503
                Value::Array(
504
                    edges
505
                        .iter()
506
                        .map(|(a, b)| Value::Array(vec![vertex_value(a), vertex_value(b)]))
507
                        .collect(),
508
                ),
509
            ),
252e8cb2 Andy McCall ↶ 510
        ])
511
    }
512
513
    pub fn from_value(v: &crate::cbor::Value) -> Result<FileGraph, crate::objects::ObjectError> {
514
        use crate::objects::{array, bytes, field, map, oref, u32_, ObjectError};
515
        let m = map(v, "file graph")?;
75167672 Andy McCall 516
        let mut g = FileGraph {
517
            atoms: HashMap::new(),
518
            succ: HashMap::new(),
519
            pred: HashMap::new(),
520
        };
252e8cb2 Andy McCall ↶ 521
        for a in array(field(m, 0, "atoms")?, "atoms")? {
522
            let e = array(a, "atom")?;
523
            if e.len() != 4 {
524
                return Err(ObjectError::Schema("atom: expected 4 fields".into()));
525
            }
526
            let key = (oref(&e[0], "atom change")?, u32_(&e[1], "atom index")?);
75167672 Andy McCall 527
            let content = if e[2].is_null() {
528
                None
529
            } else {
530
                Some(bytes(&e[2], "atom content")?.to_vec())
531
            };
532
            let deleted_by = array(&e[3], "deleters")?
533
                .iter()
534
                .map(|d| oref(d, "deleter"))
535
                .collect::<Result<BTreeSet<_>, _>>()?;
536
            g.atoms.insert(
537
                key,
538
                AtomInfo {
539
                    content,
540
                    deleted_by,
541
                },
542
            );
252e8cb2 Andy McCall ↶ 543
        }
544
        for e in array(field(m, 1, "edges")?, "edges")? {
545
            let pair = array(e, "edge")?;
546
            if pair.len() != 2 {
547
                return Err(ObjectError::Schema("edge: expected [from, to]".into()));
548
            }
549
            g.edge(vertex_from(&pair[0])?, vertex_from(&pair[1])?);
550
        }
551
        Ok(g)
552
    }
553
}
554
ff0af1ee unknown 555
/// Split a plain run into `Lines` and `Zombie` segments around ghost atoms.
556
fn split_zombies(atoms: Vec<AtomKey>, ghosts: &HashSet<AtomKey>) -> Vec<Segment> {
557
    let mut out = Vec::new();
558
    let mut lines: Vec<AtomKey> = Vec::new();
559
    let mut i = 0;
560
    while i < atoms.len() {
561
        if !ghosts.contains(&atoms[i]) {
562
            lines.push(atoms[i]);
563
            i += 1;
564
            continue;
565
        }
566
        if !lines.is_empty() {
567
            out.push(Segment::Lines(std::mem::take(&mut lines)));
568
        }
569
        let mut deleted = Vec::new();
570
        while i < atoms.len() && ghosts.contains(&atoms[i]) {
571
            deleted.push(atoms[i]);
572
            i += 1;
573
        }
574
        let mut inserted = Vec::new();
575
        if i < atoms.len() {
576
            let ch = atoms[i].0;
577
            while i < atoms.len() && atoms[i].0 == ch && !ghosts.contains(&atoms[i]) {
578
                inserted.push(atoms[i]);
579
                i += 1;
580
            }
581
        }
582
        out.push(Segment::Zombie { deleted, inserted });
583
    }
584
    if !lines.is_empty() {
585
        out.push(Segment::Lines(lines));
586
    }
587
    out
588
}
589
590
#[cfg(test)]
591
mod tests {
592
    use super::*;
593
    use crate::hash::ObjectRef;
594
595
    fn h(n: u8) -> ChangeHash {
596
        ObjectRef([n; 32])
597
    }
598
    fn lines(v: &[&str]) -> Vec<Vec<u8>> {
599
        v.iter().map(|s| s.as_bytes().to_vec()).collect()
600
    }
601
    fn text(g: &FileGraph, m: &Materialized) -> String {
602
        String::from_utf8(g.render(m, &|c| c.short(4))).unwrap()
603
    }
604
605
    struct Deps(HashMap<ChangeHash, Vec<ChangeHash>>);
606
    impl DepOracle for Deps {
607
        fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
608
            if a == b {
609
                return true;
610
            }
611
            let mut stack = vec![*a];
612
            let mut seen = HashSet::new();
613
            while let Some(x) = stack.pop() {
614
                if let Some(ds) = self.0.get(&x) {
615
                    for d in ds {
616
                        if d == b {
617
                            return true;
618
                        }
619
                        if seen.insert(*d) {
620
                            stack.push(*d);
621
                        }
622
                    }
623
                }
624
            }
625
            false
626
        }
627
    }
628
629
    #[test]
630
    fn sequential_inserts_and_deletes() {
631
        let mut g = FileGraph::new();
75167672 Andy McCall 632
        g.insert(
633
            h(1),
634
            0,
635
            Vertex::Start,
636
            Vertex::End,
637
            &lines(&["a\n", "b\n", "c\n"]),
638
        );
ff0af1ee unknown 639
        let m = g.materialize(&NoDeps);
640
        assert!(!m.is_conflicted());
641
        assert_eq!(text(&g, &m), "a\nb\nc\n");
642
643
        // Change 2 depends on 1, inserts between b and c and deletes a.
75167672 Andy McCall 644
        g.insert(
645
            h(2),
646
            0,
647
            Vertex::Atom((h(1), 1)),
648
            Vertex::Atom((h(1), 2)),
649
            &lines(&["x\n"]),
650
        );
651
        g.delete(
652
            h(2),
653
            &AtomRange {
654
                change: h(1),
655
                from: 0,
656
                to: 1,
657
            },
658
        );
ff0af1ee unknown 659
        let deps = Deps([(h(2), vec![h(1)])].into_iter().collect());
660
        let m = g.materialize(&deps);
661
        assert!(!m.is_conflicted());
662
        assert_eq!(text(&g, &m), "b\nx\nc\n");
663
        assert_eq!(m.live_atoms(), vec![(h(1), 1), (h(2), 0), (h(1), 2)]);
664
    }
665
666
    #[test]
667
    fn concurrent_inserts_at_same_anchor_conflict() {
668
        let mut g = FileGraph::new();
669
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
75167672 Andy McCall 670
        g.insert(
671
            h(2),
672
            0,
673
            Vertex::Atom((h(1), 0)),
674
            Vertex::Atom((h(1), 1)),
675
            &lines(&["from-2\n"]),
676
        );
677
        g.insert(
678
            h(3),
679
            0,
680
            Vertex::Atom((h(1), 0)),
681
            Vertex::Atom((h(1), 1)),
682
            &lines(&["from-3\n", "more-3\n"]),
683
        );
684
        let deps = Deps(
685
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
686
                .into_iter()
687
                .collect(),
688
        );
ff0af1ee unknown 689
        let m = g.materialize(&deps);
690
        assert!(m.is_conflicted());
691
        assert_eq!(m.conflict_count(), 1);
692
        assert_eq!(
693
            m.segments,
694
            vec![
695
                Segment::Lines(vec![(h(1), 0)]),
696
                Segment::Conflict(vec![vec![(h(2), 0)], vec![(h(3), 0), (h(3), 1)]]),
697
                Segment::Lines(vec![(h(1), 1)]),
698
            ]
699
        );
700
        let t = text(&g, &m);
701
        assert!(t.starts_with("a\n<<<<<<< oxy conflict 1 of 1, side 1 of 2: change 0202\nfrom-2\n======= oxy conflict 1 of 1, side 2 of 2: change 0303\nfrom-3\nmore-3\n>>>>>>> oxy conflict 1 of 1 end\nb\n"), "{t}");
702
    }
703
704
    #[test]
705
    fn concurrent_inserts_at_different_anchors_do_not_conflict() {
706
        let mut g = FileGraph::new();
75167672 Andy McCall 707
        g.insert(
708
            h(1),
709
            0,
710
            Vertex::Start,
711
            Vertex::End,
712
            &lines(&["a\n", "b\n", "c\n"]),
713
        );
714
        g.insert(
715
            h(2),
716
            0,
717
            Vertex::Start,
718
            Vertex::Atom((h(1), 0)),
719
            &lines(&["top\n"]),
720
        );
721
        g.insert(
722
            h(3),
723
            0,
724
            Vertex::Atom((h(1), 2)),
725
            Vertex::End,
726
            &lines(&["bottom\n"]),
727
        );
728
        let deps = Deps(
729
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
730
                .into_iter()
731
                .collect(),
732
        );
ff0af1ee unknown 733
        let m = g.materialize(&deps);
734
        assert!(!m.is_conflicted());
735
        assert_eq!(text(&g, &m), "top\na\nb\nc\nbottom\n");
736
    }
737
738
    #[test]
739
    fn concurrent_deletes_of_the_same_line_agree() {
740
        let mut g = FileGraph::new();
741
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
75167672 Andy McCall 742
        g.delete(
743
            h(2),
744
            &AtomRange {
745
                change: h(1),
746
                from: 0,
747
                to: 1,
748
            },
749
        );
750
        g.delete(
751
            h(3),
752
            &AtomRange {
753
                change: h(1),
754
                from: 0,
755
                to: 1,
756
            },
757
        );
758
        let deps = Deps(
759
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
760
                .into_iter()
761
                .collect(),
762
        );
ff0af1ee unknown 763
        let m = g.materialize(&deps);
764
        assert!(!m.is_conflicted());
765
        assert_eq!(text(&g, &m), "b\n");
766
    }
767
768
    #[test]
769
    fn delete_insert_conflict_is_a_zombie() {
770
        let mut g = FileGraph::new();
771
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
772
        // 2 deletes a; 3 (concurrent) inserts after a.
75167672 Andy McCall 773
        g.delete(
774
            h(2),
775
            &AtomRange {
776
                change: h(1),
777
                from: 0,
778
                to: 1,
779
            },
780
        );
781
        g.insert(
782
            h(3),
783
            0,
784
            Vertex::Atom((h(1), 0)),
785
            Vertex::Atom((h(1), 1)),
786
            &lines(&["after-a\n"]),
787
        );
788
        let deps = Deps(
789
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
790
                .into_iter()
791
                .collect(),
792
        );
ff0af1ee unknown 793
        let m = g.materialize(&deps);
794
        assert!(m.is_conflicted());
795
        assert_eq!(
796
            m.segments,
797
            vec![
75167672 Andy McCall 798
                Segment::Zombie {
799
                    deleted: vec![(h(1), 0)],
800
                    inserted: vec![(h(3), 0)]
801
                },
ff0af1ee unknown 802
                Segment::Lines(vec![(h(1), 1)]),
803
            ]
804
        );
805
        let t = text(&g, &m);
806
        assert!(t.contains("-------\na\n======="), "{t}");
807
        assert!(t.contains("after-a\n>>>>>>>"), "{t}");
808
809
        // If the deleter saw the insert (depends on 3), there is no conflict.
75167672 Andy McCall 810
        let deps = Deps(
811
            [(h(2), vec![h(1), h(3)]), (h(3), vec![h(1)])]
812
                .into_iter()
813
                .collect(),
814
        );
ff0af1ee unknown 815
        let m = g.materialize(&deps);
816
        assert!(!m.is_conflicted());
817
        assert_eq!(text(&g, &m), "after-a\nb\n");
818
    }
819
820
    #[test]
23b217d7 Andy McCall ↶ 821
    fn insert_after_a_line_deleted_by_a_dependency_is_not_a_conflict() {
822
        // 1 inserts a,b; 2 deletes b (depends on 1); 3 depends on 2 yet anchors
823
        // after the dead b (a rewritten change may do this): no conflict.
824
        let mut g = FileGraph::new();
825
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
75167672 Andy McCall 826
        g.delete(
827
            h(2),
828
            &AtomRange {
829
                change: h(1),
830
                from: 1,
831
                to: 2,
832
            },
833
        );
834
        g.insert(
835
            h(3),
836
            0,
837
            Vertex::Atom((h(1), 1)),
838
            Vertex::End,
839
            &lines(&["c\n"]),
840
        );
841
        let deps = Deps(
842
            [(h(2), vec![h(1)]), (h(3), vec![h(2)])]
843
                .into_iter()
844
                .collect(),
845
        );
23b217d7 Andy McCall ↶ 846
        let m = g.materialize(&deps);
847
        assert!(!m.is_conflicted(), "{:?}", m.segments);
848
        assert_eq!(text(&g, &m), "a\nc\n");
849
        // A change that deletes a line it introduced itself, with a dependent
850
        // anchoring after it, is likewise fine.
851
        let mut g = FileGraph::new();
852
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["x\n"]));
75167672 Andy McCall 853
        g.delete(
854
            h(1),
855
            &AtomRange {
856
                change: h(1),
857
                from: 0,
858
                to: 1,
859
            },
860
        );
861
        g.insert(
862
            h(2),
863
            0,
864
            Vertex::Atom((h(1), 0)),
865
            Vertex::End,
866
            &lines(&["y\n"]),
867
        );
23b217d7 Andy McCall ↶ 868
        let deps = Deps([(h(2), vec![h(1)])].into_iter().collect());
869
        let m = g.materialize(&deps);
870
        assert!(!m.is_conflicted());
871
        assert_eq!(text(&g, &m), "y\n");
872
    }
873
874
    #[test]
ff0af1ee unknown 875
    fn resolution_collapses_a_conflict() {
876
        let mut g = FileGraph::new();
877
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n"]));
75167672 Andy McCall 878
        g.insert(
879
            h(2),
880
            0,
881
            Vertex::Start,
882
            Vertex::Atom((h(1), 0)),
883
            &lines(&["x\n"]),
884
        );
885
        g.insert(
886
            h(3),
887
            0,
888
            Vertex::Start,
889
            Vertex::Atom((h(1), 0)),
890
            &lines(&["y\n"]),
891
        );
892
        let mut deps: HashMap<_, _> = [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
893
            .into_iter()
894
            .collect();
ff0af1ee unknown 895
        assert!(g.materialize(&Deps(deps.clone())).is_conflicted());
896
        // Resolution keeps y, drops x.
75167672 Andy McCall 897
        g.delete(
898
            h(4),
899
            &AtomRange {
900
                change: h(2),
901
                from: 0,
902
                to: 1,
903
            },
904
        );
ff0af1ee unknown 905
        deps.insert(h(4), vec![h(2), h(3)]);
906
        let m = g.materialize(&Deps(deps));
907
        assert!(!m.is_conflicted());
908
        assert_eq!(text(&g, &m), "y\na\n");
909
    }
910
911
    #[test]
912
    fn empty_and_unterminated_files() {
913
        let g = FileGraph::new();
914
        let m = g.materialize(&NoDeps);
915
        assert_eq!(m.segments, vec![]);
916
        assert_eq!(text(&g, &m), "");
917
        let mut g = FileGraph::new();
918
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["no newline"]));
919
        assert_eq!(text(&g, &g.materialize(&NoDeps)), "no newline");
920
    }
921
252e8cb2 Andy McCall ↶ 922
    #[test]
923
    fn serialization_round_trip() {
924
        let mut g = FileGraph::new();
925
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
75167672 Andy McCall 926
        g.insert(
927
            h(2),
928
            0,
929
            Vertex::Atom((h(1), 0)),
930
            Vertex::Atom((h(1), 1)),
931
            &lines(&["x\n"]),
932
        );
933
        g.delete(
934
            h(3),
935
            &AtomRange {
936
                change: h(1),
937
                from: 1,
938
                to: 2,
939
            },
940
        );
252e8cb2 Andy McCall ↶ 941
        let v = g.to_value();
942
        let bytes = crate::cbor::encode(&v);
943
        let back = FileGraph::from_value(&crate::cbor::decode(&bytes).unwrap()).unwrap();
75167672 Andy McCall 944
        let deps = Deps(
945
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
946
                .into_iter()
947
                .collect(),
948
        );
252e8cb2 Andy McCall ↶ 949
        assert_eq!(back.materialize(&deps), g.materialize(&deps));
950
        assert_eq!(text(&back, &back.materialize(&deps)), "a\nx\n");
75167672 Andy McCall 951
        assert_eq!(
952
            crate::cbor::encode(&back.to_value()),
953
            bytes,
954
            "deterministic"
955
        );
252e8cb2 Andy McCall ↶ 956
    }
957
ff0af1ee unknown 958
    /// Spec §7.3: the graph is a function of the set of changes, not their order.
959
    #[test]
960
    fn application_order_is_irrelevant() {
961
        use rand::seq::SliceRandom;
75167672 Andy McCall 962
        type Op = (
963
            ChangeHash,
964
            Vec<(u32, Vertex, Vertex, Vec<Vec<u8>>)>,
965
            Vec<AtomRange>,
966
        );
ff0af1ee unknown 967
        let ops: Vec<Op> = vec![
75167672 Andy McCall 968
            (
969
                h(1),
970
                vec![(0, Vertex::Start, Vertex::End, lines(&["a\n", "b\n", "c\n"]))],
971
                vec![],
972
            ),
973
            (
974
                h(2),
975
                vec![(
976
                    0,
977
                    Vertex::Atom((h(1), 0)),
978
                    Vertex::Atom((h(1), 1)),
979
                    lines(&["p\n"]),
980
                )],
981
                vec![],
982
            ),
983
            (
984
                h(3),
985
                vec![(
986
                    0,
987
                    Vertex::Atom((h(1), 0)),
988
                    Vertex::Atom((h(1), 1)),
989
                    lines(&["q\n", "r\n"]),
990
                )],
991
                vec![AtomRange {
992
                    change: h(1),
993
                    from: 2,
994
                    to: 3,
995
                }],
996
            ),
997
            (
998
                h(4),
999
                vec![(
1000
                    0,
1001
                    Vertex::Atom((h(3), 1)),
1002
                    Vertex::Atom((h(1), 1)),
1003
                    lines(&["s\n"]),
1004
                )],
1005
                vec![AtomRange {
1006
                    change: h(1),
1007
                    from: 1,
1008
                    to: 2,
1009
                }],
1010
            ),
1011
            (
1012
                h(5),
1013
                vec![(0, Vertex::Atom((h(1), 2)), Vertex::End, lines(&["z\n"]))],
1014
                vec![],
1015
            ),
ff0af1ee unknown 1016
        ];
1017
        let deps = Deps(
75167672 Andy McCall 1018
            [
1019
                (h(2), vec![h(1)]),
1020
                (h(3), vec![h(1)]),
1021
                (h(4), vec![h(3), h(1)]),
1022
                (h(5), vec![h(1)]),
1023
            ]
1024
            .into_iter()
1025
            .collect(),
ff0af1ee unknown 1026
        );
1027
        let build = |order: &[usize]| {
1028
            let mut g = FileGraph::new();
1029
            for &i in order {
1030
                let (c, ins, dels) = &ops[i];
1031
                for (idx, a, b, l) in ins {
1032
                    g.insert(*c, *idx, *a, *b, l);
1033
                }
1034
                for d in dels {
1035
                    g.delete(*c, d);
1036
                }
1037
            }
1038
            let m = g.materialize(&deps);
1039
            (m.clone(), text(&g, &m))
1040
        };
1041
        let reference = build(&[0, 1, 2, 3, 4]);
1042
        assert!(reference.0.is_conflicted());
1043
        let mut rng = rand::thread_rng();
1044
        let mut order: Vec<usize> = (0..ops.len()).collect();
1045
        for _ in 0..50 {
1046
            order.shuffle(&mut rng);
1047
            assert_eq!(build(&order), reference, "order {order:?}");
1048
        }
1049
    }
1050
}