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