OXYGENHUB
# e4b8bcf04a86 oxygen / crates / oxy-core / src / graph.rs
41.5 KiB · Rust · last changed Render a file that is one chain of lines without contracting it 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
    /// Every atom on some path from `first` to `last`, both included, live
229
    /// or deleted (spec §8): the atoms a range of lines is made of, and
230
    /// those that were deleted from between them. Empty when `last` does
231
    /// not follow `first`.
232
    pub fn between(&self, first: &AtomKey, last: &AtomKey) -> Vec<AtomKey> {
233
        let (from, to) = (Vertex::Atom(*first), Vertex::Atom(*last));
234
        let mut ahead: HashSet<Vertex> = HashSet::default();
235
        let mut stack = vec![from];
236
        while let Some(v) = stack.pop() {
237
            if ahead.insert(v) {
238
                stack.extend(
239
                    self.succ
240
                        .get(&v)
241
                        .map(|e| e.as_slice())
242
                        .unwrap_or(&[])
243
                        .iter()
244
                        .copied(),
245
                );
246
            }
247
        }
248
        if !ahead.contains(&to) {
249
            return vec![];
250
        }
251
        let mut pred: HashMap<Vertex, Vec<Vertex>> = HashMap::default();
252
        for (u, es) in &self.succ {
253
            if ahead.contains(u) {
254
                for w in es.iter() {
255
                    pred.entry(*w).or_default().push(*u);
256
                }
257
            }
258
        }
259
        let mut behind: HashSet<Vertex> = HashSet::default();
260
        let mut stack = vec![to];
261
        while let Some(v) = stack.pop() {
262
            if behind.insert(v) {
263
                stack.extend(pred.get(&v).into_iter().flatten().copied());
264
            }
265
        }
266
        let mut out: Vec<AtomKey> = ahead
267
            .intersection(&behind)
268
            .filter_map(|v| match v {
269
                Vertex::Atom(k) if self.atoms.contains_key(k) => Some(*k),
270
                _ => None,
271
            })
272
            .collect();
273
        out.sort();
274
        out
275
    }
276
277
    /// The changes that inserted or deleted any of `atoms`.
278
    pub fn touching(&self, atoms: &[AtomKey]) -> BTreeSet<ChangeHash> {
279
        let mut out = BTreeSet::new();
280
        for k in atoms {
281
            out.insert(k.0);
282
            if let Some(info) = self.atoms.get(k) {
283
                out.extend(info.deleted_by.iter().copied());
284
            }
285
        }
286
        out
287
    }
288
289
    pub fn live_successor_changes(&self, k: &AtomKey) -> Vec<ChangeHash> {
290
        self.succ
291
            .get(&Vertex::Atom(*k))
292
            .map(|ss| {
293
                ss.iter()
294
                    .filter_map(|s| match s {
295
                        Vertex::Atom(y) if self.is_live(y) => Some(y.0),
296
                        _ => None,
297
                    })
298
                    .collect()
299
            })
300
            .unwrap_or_default()
301
    }
302
303
    fn is_ghost(&self, k: &AtomKey, info: &AtomInfo, deps: &dyn DepOracle) -> bool {
304
        // A deleted atom is a ghost when a live direct successor was inserted
305
        // by a change that commutes with every deleter: neither depends on
306
        // the other (spec §9.1, delete/insert). If the inserter came after the
307
        // deletion, or the deleter saw the insertion, there is no conflict.
308
        self.succ.get(&Vertex::Atom(*k)).is_some_and(|ss| {
309
            ss.iter().any(|s| match s {
310
                Vertex::Atom(y) if self.is_live(y) => !info
311
                    .deleted_by
312
                    .iter()
313
                    .any(|d| deps.depends_on(d, &y.0) || deps.depends_on(&y.0, d)),
314
                _ => false,
315
            })
316
        })
317
    }
318
319
    /// Successors of `v` in the graph with non-kept vertices contracted away.
320
    fn reduced_succ(
321
        &self,
322
        v: Vertex,
323
        kept: &HashSet<Vertex>,
324
        memo: &mut HashMap<Vertex, BTreeSet<Vertex>>,
325
    ) -> BTreeSet<Vertex> {
326
        let direct = self.succ.get(&v).map(Edges::as_slice).unwrap_or_default();
327
        // The common case, a successor that is itself kept: nothing to
328
        // contract.
329
        if direct.iter().all(|s| kept.contains(s)) {
330
            return direct.iter().copied().collect();
331
        }
332
        let mut out = BTreeSet::new();
333
        let mut stack: Vec<Vertex> = direct.to_vec();
334
        let mut seen = HashSet::default();
335
        while let Some(s) = stack.pop() {
336
            if kept.contains(&s) {
337
                out.insert(s);
338
            } else if seen.insert(s) {
339
                if let Some(m) = memo.get(&s) {
340
                    out.extend(m.iter().copied());
341
                } else if let Some(next) = self.succ.get(&s) {
342
                    stack.extend(next.iter().copied());
343
                }
344
            }
345
        }
346
        out
347
    }
348
349
    /// The atoms in order when the graph is one chain from Start to End with
350
    /// nothing deleted: a file only ever inserted into in sequence, as every
351
    /// file of a fresh commit or a clone is. Such a graph has no conflict.
352
    /// An edge to End besides the chain's next atom (Start's own, or an
353
    /// append's anchor) orders nothing, since End is last anyway.
354
    fn as_chain(&self) -> Option<Vec<AtomKey>> {
355
        if self
356
            .atoms
357
            .values()
358
            .any(|a| a.content.is_none() || !a.deleted_by.is_empty())
359
        {
360
            return None;
361
        }
362
        let mut order = Vec::with_capacity(self.atoms.len());
363
        let mut v = Vertex::Start;
364
        loop {
365
            let succ = self.succ.get(&v)?.as_slice();
366
            let mut atoms = succ.iter().filter(|s| **s != Vertex::End);
367
            match (atoms.next(), atoms.next()) {
368
                (None, _) if succ.contains(&Vertex::End) => break,
369
                (Some(Vertex::Atom(k)), None) if order.len() < self.atoms.len() => {
370
                    order.push(*k);
371
                    v = Vertex::Atom(*k);
372
                }
373
                _ => return None,
374
            }
375
        }
376
        (order.len() == self.atoms.len()).then_some(order)
377
    }
378
379
    /// Order the live atoms and locate conflicts.
380
    pub fn materialize(&self, deps: &dyn DepOracle) -> Materialized {
381
        if let Some(order) = self.as_chain() {
382
            return Materialized {
383
                segments: if order.is_empty() {
384
                    vec![]
385
                } else {
386
                    vec![Segment::Lines(order)]
387
                },
388
            };
389
        }
390
        self.materialize_general(deps)
391
    }
392
393
    /// [`FileGraph::materialize`] for any graph: contraction, then a
394
    /// topological order with conflict regions.
395
    fn materialize_general(&self, deps: &dyn DepOracle) -> Materialized {
396
        // 1. Vertices that survive contraction: live atoms, ghosts, Start, End.
397
        let mut kept: HashSet<Vertex> = HashSet::default();
398
        let mut ghosts: HashSet<AtomKey> = HashSet::default();
399
        kept.insert(Vertex::Start);
400
        kept.insert(Vertex::End);
401
        for (k, info) in &self.atoms {
402
            if info.content.is_none() {
403
                continue;
404
            }
405
            if info.deleted_by.is_empty() {
406
                kept.insert(Vertex::Atom(*k));
407
            } else if self.is_ghost(k, info, deps) {
408
                kept.insert(Vertex::Atom(*k));
409
                ghosts.insert(*k);
410
            }
411
        }
412
413
        // 2. Reduced graph over kept vertices.
414
        let mut memo = HashMap::default();
415
        let mut rsucc: HashMap<Vertex, BTreeSet<Vertex>> = HashMap::default();
416
        let mut indeg: HashMap<Vertex, usize> = HashMap::default();
417
        for &u in &kept {
418
            indeg.entry(u).or_insert(0);
419
            let rs = self.reduced_succ(u, &kept, &mut memo);
420
            for &w in &rs {
421
                *indeg.entry(w).or_insert(0) += 1;
422
            }
423
            rsucc.insert(u, rs);
424
        }
425
426
        // 3. Kahn's algorithm with side labelling for conflict regions.
427
        let mut ready: BTreeSet<Vertex> = indeg
428
            .iter()
429
            .filter(|(_, &d)| d == 0)
430
            .map(|(v, _)| *v)
431
            .collect();
432
        let mut side: HashMap<Vertex, u32> = HashMap::default();
433
        let mut joins: HashSet<Vertex> = HashSet::default();
434
        let mut next_side = 0u32;
435
        let mut plain: Vec<Vertex> = Vec::new();
436
        let mut conflict: Option<BTreeMap<u32, Vec<Vertex>>> = None;
437
        let mut segments: Vec<Segment> = Vec::new();
438
439
        let flush_plain = |plain: &mut Vec<Vertex>, segments: &mut Vec<Segment>| {
440
            let atoms: Vec<AtomKey> = plain
441
                .iter()
442
                .filter_map(|v| {
443
                    if let Vertex::Atom(k) = v {
444
                        Some(*k)
445
                    } else {
446
                        None
447
                    }
448
                })
449
                .collect();
450
            plain.clear();
451
            if !atoms.is_empty() {
452
                segments.extend(split_zombies(atoms, &ghosts));
453
            }
454
        };
455
        let finish_conflict = |c: BTreeMap<u32, Vec<Vertex>>, segments: &mut Vec<Segment>| {
456
            let sides: Vec<Vec<AtomKey>> = c
457
                .into_values()
458
                .map(|vs| {
459
                    vs.into_iter()
460
                        .filter_map(|v| match v {
461
                            Vertex::Atom(k) if !ghosts.contains(&k) => Some(k),
462
                            _ => None,
463
                        })
464
                        .collect::<Vec<_>>()
465
                })
466
                .filter(|s| !s.is_empty())
467
                .collect();
468
            match sides.len() {
469
                0 => {}
470
                1 => segments.push(Segment::Lines(sides.into_iter().next().unwrap())),
471
                _ => segments.push(Segment::Conflict(sides)),
472
            }
473
        };
474
475
        while !ready.is_empty() {
476
            if ready.len() >= 2 {
477
                if conflict.is_none() {
478
                    flush_plain(&mut plain, &mut segments);
479
                    conflict = Some(BTreeMap::new());
480
                }
481
                for r in &ready {
482
                    if !side.contains_key(r) {
483
                        side.insert(*r, next_side);
484
                        next_side += 1;
485
                    }
486
                }
487
            }
488
            let v = ready.pop_first().unwrap();
489
            let my_side = side.get(&v).copied();
490
            match (&mut conflict, my_side) {
491
                (Some(c), Some(s)) => c.entry(s).or_default().push(v),
492
                (Some(_), None) => {
493
                    let c = conflict.take().unwrap();
494
                    finish_conflict(c, &mut segments);
495
                    plain.push(v);
496
                }
497
                (None, _) => plain.push(v),
498
            }
499
            for &w in &rsucc[&v] {
500
                let d = indeg.get_mut(&w).unwrap();
501
                *d -= 1;
502
                if conflict.is_some() && !joins.contains(&w) {
503
                    match (my_side, side.get(&w).copied()) {
504
                        (Some(s), None) => {
505
                            side.insert(w, s);
506
                        }
507
                        (Some(s), Some(t)) if s != t => {
508
                            side.remove(&w);
509
                            joins.insert(w);
510
                        }
511
                        (None, Some(_)) => {
512
                            side.remove(&w);
513
                            joins.insert(w);
514
                        }
515
                        _ => {}
516
                    }
517
                }
518
                if *d == 0 {
519
                    ready.insert(w);
520
                }
521
            }
522
        }
523
        if let Some(c) = conflict.take() {
524
            finish_conflict(c, &mut segments);
525
        }
526
        flush_plain(&mut plain, &mut segments);
527
        Materialized { segments }
528
    }
529
530
    /// Render a materialization to bytes with conflict markers (spec §9.5).
531
    /// `label` gives the short description of a change for marker headers.
532
    pub fn render(&self, m: &Materialized, label: &dyn Fn(&ChangeHash) -> String) -> Vec<u8> {
533
        let total = m.conflict_count();
534
        let mut n = 0;
535
        let mut out: Vec<u8> = Vec::new();
536
        let push_atoms = |out: &mut Vec<u8>, atoms: &[AtomKey]| {
537
            for a in atoms {
538
                if let Some(c) = self.content(a) {
539
                    out.extend_from_slice(c);
540
                }
541
            }
542
        };
543
        let newline_if_needed = |out: &mut Vec<u8>| {
544
            if !out.is_empty() && *out.last().unwrap() != b'\n' {
545
                out.push(b'\n');
546
            }
547
        };
548
        for seg in &m.segments {
549
            match seg {
550
                Segment::Lines(a) => push_atoms(&mut out, a),
551
                Segment::Conflict(sides) => {
552
                    n += 1;
553
                    let k = sides.len();
554
                    for (i, s) in sides.iter().enumerate() {
555
                        newline_if_needed(&mut out);
556
                        let who = s.first().map(|a| label(&a.0)).unwrap_or_default();
557
                        let marker = if i == 0 { "<<<<<<<" } else { "=======" };
558
                        out.extend_from_slice(
559
                            format!("{marker} oxy conflict {n} of {total}, side {} of {k}: change {who}\n", i + 1).as_bytes(),
560
                        );
561
                        push_atoms(&mut out, s);
562
                    }
563
                    newline_if_needed(&mut out);
564
                    out.extend_from_slice(
565
                        format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes(),
566
                    );
567
                }
568
                Segment::Zombie { deleted, inserted } => {
569
                    n += 1;
570
                    newline_if_needed(&mut out);
571
                    let deleter = deleted
572
                        .first()
573
                        .and_then(|a| self.deleters(a).next().copied())
574
                        .map(|d| label(&d))
575
                        .unwrap_or_default();
576
                    let inserter = inserted.first().map(|a| label(&a.0)).unwrap_or_default();
577
                    out.extend_from_slice(
578
                        format!("<<<<<<< oxy conflict {n} of {total}, side 1 of 2: deleted by change {deleter}\n-------\n").as_bytes(),
579
                    );
580
                    push_atoms(&mut out, deleted);
581
                    newline_if_needed(&mut out);
582
                    out.extend_from_slice(
583
                        format!(
584
                            "======= oxy conflict {n} of {total}, side 2 of 2: change {inserter}\n"
585
                        )
586
                        .as_bytes(),
587
                    );
588
                    push_atoms(&mut out, inserted);
589
                    newline_if_needed(&mut out);
590
                    out.extend_from_slice(
591
                        format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes(),
592
                    );
593
                }
594
            }
595
        }
596
        out
597
    }
598
}
599
600
fn vertex_value(v: &Vertex) -> crate::cbor::Value {
601
    use crate::cbor::Value;
602
    match v {
603
        Vertex::Start => Value::Uint(0),
604
        Vertex::End => Value::Uint(1),
605
        Vertex::Atom((c, i)) => {
606
            Value::Array(vec![Value::Bytes(c.0.to_vec()), Value::Uint(*i as u64)])
607
        }
608
    }
609
}
610
611
fn vertex_from(v: &crate::cbor::Value) -> Result<Vertex, crate::objects::ObjectError> {
612
    use crate::cbor::Value;
613
    use crate::objects::ObjectError;
614
    match v {
615
        Value::Uint(0) => Ok(Vertex::Start),
616
        Value::Uint(1) => Ok(Vertex::End),
617
        Value::Array(a) if a.len() == 2 => Ok(Vertex::Atom((
618
            crate::objects::oref(&a[0], "vertex")?,
619
            crate::objects::u32_(&a[1], "vertex")?,
620
        ))),
621
        _ => Err(ObjectError::Schema(
622
            "vertex: expected 0, 1, or [hash, index]".into(),
623
        )),
624
    }
625
}
626
627
impl FileGraph {
628
    /// Serialize for a checkpoint: `{0: atoms [[hash, index, content|null, [deleters]]], 1: edges [[from, to]]}`.
629
    pub fn to_value(&self) -> crate::cbor::Value {
630
        use crate::cbor::Value;
631
        let mut atoms: Vec<(&AtomKey, &AtomInfo)> = self.atoms.iter().collect();
632
        atoms.sort_by_key(|(k, _)| **k);
633
        let mut edges: Vec<(Vertex, Vertex)> = self
634
            .succ
635
            .iter()
636
            .flat_map(|(a, bs)| bs.iter().map(move |b| (*a, *b)))
637
            .collect();
638
        edges.sort();
639
        Value::map([
640
            (
641
                0,
642
                Value::Array(
643
                    atoms
644
                        .into_iter()
645
                        .map(|((c, i), info)| {
646
                            Value::Array(vec![
647
                                Value::Bytes(c.0.to_vec()),
648
                                Value::Uint(*i as u64),
649
                                info.content
650
                                    .as_ref()
651
                                    .map(|b| Value::Bytes(b.clone()))
652
                                    .unwrap_or(Value::Null),
653
                                Value::Array(
654
                                    info.deleted_by
655
                                        .iter()
656
                                        .map(|d| Value::Bytes(d.0.to_vec()))
657
                                        .collect(),
658
                                ),
659
                            ])
660
                        })
661
                        .collect(),
662
                ),
663
            ),
664
            (
665
                1,
666
                Value::Array(
667
                    edges
668
                        .iter()
669
                        .map(|(a, b)| Value::Array(vec![vertex_value(a), vertex_value(b)]))
670
                        .collect(),
671
                ),
672
            ),
673
        ])
674
    }
675
676
    pub fn from_value(v: &crate::cbor::Value) -> Result<FileGraph, crate::objects::ObjectError> {
677
        use crate::objects::{array, bytes, field, map, oref, u32_, ObjectError};
678
        let m = map(v, "file graph")?;
679
        let mut g = FileGraph {
680
            atoms: HashMap::default(),
681
            succ: HashMap::default(),
682
        };
683
        for a in array(field(m, 0, "atoms")?, "atoms")? {
684
            let e = array(a, "atom")?;
685
            if e.len() != 4 {
686
                return Err(ObjectError::Schema("atom: expected 4 fields".into()));
687
            }
688
            let key = (oref(&e[0], "atom change")?, u32_(&e[1], "atom index")?);
689
            let content = if e[2].is_null() {
690
                None
691
            } else {
692
                Some(bytes(&e[2], "atom content")?.to_vec())
693
            };
694
            let deleted_by = array(&e[3], "deleters")?
695
                .iter()
696
                .map(|d| oref(d, "deleter"))
697
                .collect::<Result<BTreeSet<_>, _>>()?;
698
            g.atoms.insert(
699
                key,
700
                AtomInfo {
701
                    content,
702
                    deleted_by,
703
                },
704
            );
705
        }
706
        for e in array(field(m, 1, "edges")?, "edges")? {
707
            let pair = array(e, "edge")?;
708
            if pair.len() != 2 {
709
                return Err(ObjectError::Schema("edge: expected [from, to]".into()));
710
            }
711
            g.edge(vertex_from(&pair[0])?, vertex_from(&pair[1])?);
712
        }
713
        Ok(g)
714
    }
715
}
716
717
/// Split a plain run into `Lines` and `Zombie` segments around ghost atoms.
718
fn split_zombies(atoms: Vec<AtomKey>, ghosts: &HashSet<AtomKey>) -> Vec<Segment> {
719
    let mut out = Vec::new();
720
    let mut lines: Vec<AtomKey> = Vec::new();
721
    let mut i = 0;
722
    while i < atoms.len() {
723
        if !ghosts.contains(&atoms[i]) {
724
            lines.push(atoms[i]);
725
            i += 1;
726
            continue;
727
        }
728
        if !lines.is_empty() {
729
            out.push(Segment::Lines(std::mem::take(&mut lines)));
730
        }
731
        let mut deleted = Vec::new();
732
        while i < atoms.len() && ghosts.contains(&atoms[i]) {
733
            deleted.push(atoms[i]);
734
            i += 1;
735
        }
736
        let mut inserted = Vec::new();
737
        if i < atoms.len() {
738
            let ch = atoms[i].0;
739
            while i < atoms.len() && atoms[i].0 == ch && !ghosts.contains(&atoms[i]) {
740
                inserted.push(atoms[i]);
741
                i += 1;
742
            }
743
        }
744
        out.push(Segment::Zombie { deleted, inserted });
745
    }
746
    if !lines.is_empty() {
747
        out.push(Segment::Lines(lines));
748
    }
749
    out
750
}
751
752
#[cfg(test)]
753
mod tests {
754
    use super::*;
755
    use crate::hash::ObjectRef;
756
757
    fn h(n: u8) -> ChangeHash {
758
        ObjectRef([n; 32])
759
    }
760
    fn lines(v: &[&str]) -> Vec<Vec<u8>> {
761
        v.iter().map(|s| s.as_bytes().to_vec()).collect()
762
    }
763
    fn text(g: &FileGraph, m: &Materialized) -> String {
764
        String::from_utf8(g.render(m, &|c| c.short(4))).unwrap()
765
    }
766
767
    struct Deps(HashMap<ChangeHash, Vec<ChangeHash>>);
768
    impl DepOracle for Deps {
769
        fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
770
            if a == b {
771
                return true;
772
            }
773
            let mut stack = vec![*a];
774
            let mut seen = HashSet::default();
775
            while let Some(x) = stack.pop() {
776
                if let Some(ds) = self.0.get(&x) {
777
                    for d in ds {
778
                        if d == b {
779
                            return true;
780
                        }
781
                        if seen.insert(*d) {
782
                            stack.push(*d);
783
                        }
784
                    }
785
                }
786
            }
787
            false
788
        }
789
    }
790
791
    /// The chain shortcut agrees with the general algorithm wherever it
792
    /// applies, and stands aside wherever it does not.
793
    #[test]
794
    fn the_chain_shortcut_agrees_with_the_general_order() {
795
        let mut g = FileGraph::new();
796
        g.insert(
797
            h(1),
798
            0,
799
            Vertex::Start,
800
            Vertex::End,
801
            &lines(&["a\n", "b\n", "c\n"]),
802
        );
803
        assert!(g.as_chain().is_some());
804
        assert_eq!(g.materialize(&NoDeps), g.materialize_general(&NoDeps));
805
        // Appended after the last line: still a chain.
806
        g.insert(
807
            h(2),
808
            0,
809
            Vertex::Atom((h(1), 2)),
810
            Vertex::End,
811
            &lines(&["d\n"]),
812
        );
813
        assert!(g.as_chain().is_some());
814
        assert_eq!(g.materialize(&NoDeps), g.materialize_general(&NoDeps));
815
        // Inserted between two lines: not a chain (a has two successors).
816
        g.insert(
817
            h(4),
818
            0,
819
            Vertex::Atom((h(1), 0)),
820
            Vertex::Atom((h(1), 1)),
821
            &lines(&["x\n"]),
822
        );
823
        assert!(g.as_chain().is_none());
824
        assert_eq!(text(&g, &g.materialize(&NoDeps)), "a\nx\nb\nc\nd\n");
825
        // Two inserts at one anchor: a conflict, never a chain.
826
        let mut c = FileGraph::new();
827
        c.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n"]));
828
        c.insert(h(2), 0, Vertex::Start, Vertex::End, &lines(&["b\n"]));
829
        assert!(c.as_chain().is_none());
830
        assert!(c.materialize(&NoDeps).is_conflicted());
831
        // A deletion: not a chain.
832
        let mut d = FileGraph::new();
833
        d.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
834
        d.delete(
835
            h(3),
836
            &AtomRange {
837
                change: h(1),
838
                from: 0,
839
                to: 1,
840
            },
841
        );
842
        assert!(d.as_chain().is_none());
843
        assert_eq!(d.materialize(&NoDeps), d.materialize_general(&NoDeps));
844
        // Empty.
845
        let e = FileGraph::new();
846
        assert_eq!(e.materialize(&NoDeps), e.materialize_general(&NoDeps));
847
    }
848
849
    #[test]
850
    fn sequential_inserts_and_deletes() {
851
        let mut g = FileGraph::new();
852
        g.insert(
853
            h(1),
854
            0,
855
            Vertex::Start,
856
            Vertex::End,
857
            &lines(&["a\n", "b\n", "c\n"]),
858
        );
859
        let m = g.materialize(&NoDeps);
860
        assert!(!m.is_conflicted());
861
        assert_eq!(text(&g, &m), "a\nb\nc\n");
862
863
        // Change 2 depends on 1, inserts between b and c and deletes a.
864
        g.insert(
865
            h(2),
866
            0,
867
            Vertex::Atom((h(1), 1)),
868
            Vertex::Atom((h(1), 2)),
869
            &lines(&["x\n"]),
870
        );
871
        g.delete(
872
            h(2),
873
            &AtomRange {
874
                change: h(1),
875
                from: 0,
876
                to: 1,
877
            },
878
        );
879
        let deps = Deps([(h(2), vec![h(1)])].into_iter().collect());
880
        let m = g.materialize(&deps);
881
        assert!(!m.is_conflicted());
882
        assert_eq!(text(&g, &m), "b\nx\nc\n");
883
        assert_eq!(m.live_atoms(), vec![(h(1), 1), (h(2), 0), (h(1), 2)]);
884
    }
885
886
    #[test]
887
    fn concurrent_inserts_at_same_anchor_conflict() {
888
        let mut g = FileGraph::new();
889
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
890
        g.insert(
891
            h(2),
892
            0,
893
            Vertex::Atom((h(1), 0)),
894
            Vertex::Atom((h(1), 1)),
895
            &lines(&["from-2\n"]),
896
        );
897
        g.insert(
898
            h(3),
899
            0,
900
            Vertex::Atom((h(1), 0)),
901
            Vertex::Atom((h(1), 1)),
902
            &lines(&["from-3\n", "more-3\n"]),
903
        );
904
        let deps = Deps(
905
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
906
                .into_iter()
907
                .collect(),
908
        );
909
        let m = g.materialize(&deps);
910
        assert!(m.is_conflicted());
911
        assert_eq!(m.conflict_count(), 1);
912
        assert_eq!(
913
            m.segments,
914
            vec![
915
                Segment::Lines(vec![(h(1), 0)]),
916
                Segment::Conflict(vec![vec![(h(2), 0)], vec![(h(3), 0), (h(3), 1)]]),
917
                Segment::Lines(vec![(h(1), 1)]),
918
            ]
919
        );
920
        let t = text(&g, &m);
921
        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}");
922
    }
923
924
    #[test]
925
    fn concurrent_inserts_at_different_anchors_do_not_conflict() {
926
        let mut g = FileGraph::new();
927
        g.insert(
928
            h(1),
929
            0,
930
            Vertex::Start,
931
            Vertex::End,
932
            &lines(&["a\n", "b\n", "c\n"]),
933
        );
934
        g.insert(
935
            h(2),
936
            0,
937
            Vertex::Start,
938
            Vertex::Atom((h(1), 0)),
939
            &lines(&["top\n"]),
940
        );
941
        g.insert(
942
            h(3),
943
            0,
944
            Vertex::Atom((h(1), 2)),
945
            Vertex::End,
946
            &lines(&["bottom\n"]),
947
        );
948
        let deps = Deps(
949
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
950
                .into_iter()
951
                .collect(),
952
        );
953
        let m = g.materialize(&deps);
954
        assert!(!m.is_conflicted());
955
        assert_eq!(text(&g, &m), "top\na\nb\nc\nbottom\n");
956
    }
957
958
    #[test]
959
    fn concurrent_deletes_of_the_same_line_agree() {
960
        let mut g = FileGraph::new();
961
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
962
        g.delete(
963
            h(2),
964
            &AtomRange {
965
                change: h(1),
966
                from: 0,
967
                to: 1,
968
            },
969
        );
970
        g.delete(
971
            h(3),
972
            &AtomRange {
973
                change: h(1),
974
                from: 0,
975
                to: 1,
976
            },
977
        );
978
        let deps = Deps(
979
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
980
                .into_iter()
981
                .collect(),
982
        );
983
        let m = g.materialize(&deps);
984
        assert!(!m.is_conflicted());
985
        assert_eq!(text(&g, &m), "b\n");
986
    }
987
988
    #[test]
989
    fn delete_insert_conflict_is_a_zombie() {
990
        let mut g = FileGraph::new();
991
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
992
        // 2 deletes a; 3 (concurrent) inserts after a.
993
        g.delete(
994
            h(2),
995
            &AtomRange {
996
                change: h(1),
997
                from: 0,
998
                to: 1,
999
            },
1000
        );
1001
        g.insert(
1002
            h(3),
1003
            0,
1004
            Vertex::Atom((h(1), 0)),
1005
            Vertex::Atom((h(1), 1)),
1006
            &lines(&["after-a\n"]),
1007
        );
1008
        let deps = Deps(
1009
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1010
                .into_iter()
1011
                .collect(),
1012
        );
1013
        let m = g.materialize(&deps);
1014
        assert!(m.is_conflicted());
1015
        assert_eq!(
1016
            m.segments,
1017
            vec![
1018
                Segment::Zombie {
1019
                    deleted: vec![(h(1), 0)],
1020
                    inserted: vec![(h(3), 0)]
1021
                },
1022
                Segment::Lines(vec![(h(1), 1)]),
1023
            ]
1024
        );
1025
        let t = text(&g, &m);
1026
        assert!(t.contains("-------\na\n======="), "{t}");
1027
        assert!(t.contains("after-a\n>>>>>>>"), "{t}");
1028
1029
        // If the deleter saw the insert (depends on 3), there is no conflict.
1030
        let deps = Deps(
1031
            [(h(2), vec![h(1), h(3)]), (h(3), vec![h(1)])]
1032
                .into_iter()
1033
                .collect(),
1034
        );
1035
        let m = g.materialize(&deps);
1036
        assert!(!m.is_conflicted());
1037
        assert_eq!(text(&g, &m), "after-a\nb\n");
1038
    }
1039
1040
    #[test]
1041
    fn insert_after_a_line_deleted_by_a_dependency_is_not_a_conflict() {
1042
        // 1 inserts a,b; 2 deletes b (depends on 1); 3 depends on 2 yet anchors
1043
        // after the dead b (a rewritten change may do this): no conflict.
1044
        let mut g = FileGraph::new();
1045
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
1046
        g.delete(
1047
            h(2),
1048
            &AtomRange {
1049
                change: h(1),
1050
                from: 1,
1051
                to: 2,
1052
            },
1053
        );
1054
        g.insert(
1055
            h(3),
1056
            0,
1057
            Vertex::Atom((h(1), 1)),
1058
            Vertex::End,
1059
            &lines(&["c\n"]),
1060
        );
1061
        let deps = Deps(
1062
            [(h(2), vec![h(1)]), (h(3), vec![h(2)])]
1063
                .into_iter()
1064
                .collect(),
1065
        );
1066
        let m = g.materialize(&deps);
1067
        assert!(!m.is_conflicted(), "{:?}", m.segments);
1068
        assert_eq!(text(&g, &m), "a\nc\n");
1069
        // A change that deletes a line it introduced itself, with a dependent
1070
        // anchoring after it, is likewise fine.
1071
        let mut g = FileGraph::new();
1072
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["x\n"]));
1073
        g.delete(
1074
            h(1),
1075
            &AtomRange {
1076
                change: h(1),
1077
                from: 0,
1078
                to: 1,
1079
            },
1080
        );
1081
        g.insert(
1082
            h(2),
1083
            0,
1084
            Vertex::Atom((h(1), 0)),
1085
            Vertex::End,
1086
            &lines(&["y\n"]),
1087
        );
1088
        let deps = Deps([(h(2), vec![h(1)])].into_iter().collect());
1089
        let m = g.materialize(&deps);
1090
        assert!(!m.is_conflicted());
1091
        assert_eq!(text(&g, &m), "y\n");
1092
    }
1093
1094
    #[test]
1095
    fn resolution_collapses_a_conflict() {
1096
        let mut g = FileGraph::new();
1097
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n"]));
1098
        g.insert(
1099
            h(2),
1100
            0,
1101
            Vertex::Start,
1102
            Vertex::Atom((h(1), 0)),
1103
            &lines(&["x\n"]),
1104
        );
1105
        g.insert(
1106
            h(3),
1107
            0,
1108
            Vertex::Start,
1109
            Vertex::Atom((h(1), 0)),
1110
            &lines(&["y\n"]),
1111
        );
1112
        let mut deps: HashMap<_, _> = [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1113
            .into_iter()
1114
            .collect();
1115
        assert!(g.materialize(&Deps(deps.clone())).is_conflicted());
1116
        // Resolution keeps y, drops x.
1117
        g.delete(
1118
            h(4),
1119
            &AtomRange {
1120
                change: h(2),
1121
                from: 0,
1122
                to: 1,
1123
            },
1124
        );
1125
        deps.insert(h(4), vec![h(2), h(3)]);
1126
        let m = g.materialize(&Deps(deps));
1127
        assert!(!m.is_conflicted());
1128
        assert_eq!(text(&g, &m), "y\na\n");
1129
    }
1130
1131
    #[test]
1132
    fn empty_and_unterminated_files() {
1133
        let g = FileGraph::new();
1134
        let m = g.materialize(&NoDeps);
1135
        assert_eq!(m.segments, vec![]);
1136
        assert_eq!(text(&g, &m), "");
1137
        let mut g = FileGraph::new();
1138
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["no newline"]));
1139
        assert_eq!(text(&g, &g.materialize(&NoDeps)), "no newline");
1140
    }
1141
1142
    #[test]
1143
    fn serialization_round_trip() {
1144
        let mut g = FileGraph::new();
1145
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
1146
        g.insert(
1147
            h(2),
1148
            0,
1149
            Vertex::Atom((h(1), 0)),
1150
            Vertex::Atom((h(1), 1)),
1151
            &lines(&["x\n"]),
1152
        );
1153
        g.delete(
1154
            h(3),
1155
            &AtomRange {
1156
                change: h(1),
1157
                from: 1,
1158
                to: 2,
1159
            },
1160
        );
1161
        let v = g.to_value();
1162
        let bytes = crate::cbor::encode(&v);
1163
        let back = FileGraph::from_value(&crate::cbor::decode(&bytes).unwrap()).unwrap();
1164
        let deps = Deps(
1165
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1166
                .into_iter()
1167
                .collect(),
1168
        );
1169
        assert_eq!(back.materialize(&deps), g.materialize(&deps));
1170
        assert_eq!(text(&back, &back.materialize(&deps)), "a\nx\n");
1171
        assert_eq!(
1172
            crate::cbor::encode(&back.to_value()),
1173
            bytes,
1174
            "deterministic"
1175
        );
1176
    }
1177
1178
    /// Spec §7.3: the graph is a function of the set of changes, not their order.
1179
    #[test]
1180
    fn application_order_is_irrelevant() {
1181
        use rand::seq::SliceRandom;
1182
        type Op = (
1183
            ChangeHash,
1184
            Vec<(u32, Vertex, Vertex, Vec<Vec<u8>>)>,
1185
            Vec<AtomRange>,
1186
        );
1187
        let ops: Vec<Op> = vec![
1188
            (
1189
                h(1),
1190
                vec![(0, Vertex::Start, Vertex::End, lines(&["a\n", "b\n", "c\n"]))],
1191
                vec![],
1192
            ),
1193
            (
1194
                h(2),
1195
                vec![(
1196
                    0,
1197
                    Vertex::Atom((h(1), 0)),
1198
                    Vertex::Atom((h(1), 1)),
1199
                    lines(&["p\n"]),
1200
                )],
1201
                vec![],
1202
            ),
1203
            (
1204
                h(3),
1205
                vec![(
1206
                    0,
1207
                    Vertex::Atom((h(1), 0)),
1208
                    Vertex::Atom((h(1), 1)),
1209
                    lines(&["q\n", "r\n"]),
1210
                )],
1211
                vec![AtomRange {
1212
                    change: h(1),
1213
                    from: 2,
1214
                    to: 3,
1215
                }],
1216
            ),
1217
            (
1218
                h(4),
1219
                vec![(
1220
                    0,
1221
                    Vertex::Atom((h(3), 1)),
1222
                    Vertex::Atom((h(1), 1)),
1223
                    lines(&["s\n"]),
1224
                )],
1225
                vec![AtomRange {
1226
                    change: h(1),
1227
                    from: 1,
1228
                    to: 2,
1229
                }],
1230
            ),
1231
            (
1232
                h(5),
1233
                vec![(0, Vertex::Atom((h(1), 2)), Vertex::End, lines(&["z\n"]))],
1234
                vec![],
1235
            ),
1236
        ];
1237
        let deps = Deps(
1238
            [
1239
                (h(2), vec![h(1)]),
1240
                (h(3), vec![h(1)]),
1241
                (h(4), vec![h(3), h(1)]),
1242
                (h(5), vec![h(1)]),
1243
            ]
1244
            .into_iter()
1245
            .collect(),
1246
        );
1247
        let build = |order: &[usize]| {
1248
            let mut g = FileGraph::new();
1249
            for &i in order {
1250
                let (c, ins, dels) = &ops[i];
1251
                for (idx, a, b, l) in ins {
1252
                    g.insert(*c, *idx, *a, *b, l);
1253
                }
1254
                for d in dels {
1255
                    g.delete(*c, d);
1256
                }
1257
            }
1258
            let m = g.materialize(&deps);
1259
            (m.clone(), text(&g, &m))
1260
        };
1261
        let reference = build(&[0, 1, 2, 3, 4]);
1262
        assert!(reference.0.is_conflicted());
1263
        let mut rng = rand::thread_rng();
1264
        let mut order: Vec<usize> = (0..ops.len()).collect();
1265
        for _ in 0..50 {
1266
            order.shuffle(&mut rng);
1267
            assert_eq!(build(&order), reference, "order {order:?}");
1268
        }
1269
    }
1270
}