OXYGENHUB
# fb5b6181618a oxygen / crates / oxy-core / src / graph.rs
50.0 KiB · Rust · last changed Tree views updated in place, and a faster render of edited files 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
0bdc3f91 Andy McCall ↶ 7
use crate::fasthash::{FastMap as HashMap, FastSet as HashSet};
ff0af1ee unknown 8
use crate::hash::ChangeHash;
01a47cd7 Andy McCall ↶ 9
use crate::lines::{LineRef, Lines};
ff0af1ee unknown 10
use crate::objects::AtomRange;
0bdc3f91 Andy McCall ↶ 11
use std::collections::{BTreeMap, BTreeSet};
ff0af1ee unknown 12
13
/// `(introducing change, index within that change for this node)`.
14
pub type AtomKey = (ChangeHash, u32);
15
16
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
17
pub enum Vertex {
18
    Start,
19
    Atom(AtomKey),
20
    End,
21
}
22
23
/// Transitive dependency test between changes; `depends_on(a, a)` is true.
24
pub trait DepOracle {
25
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool;
26
}
27
28
/// No change depends on any other: every pair commutes.
29
pub struct NoDeps;
30
impl DepOracle for NoDeps {
31
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
32
        a == b
33
    }
34
}
35
36
#[derive(Default, Clone, Debug)]
37
struct AtomInfo {
01a47cd7 Andy McCall ↶ 38
    /// `None` until the introducing change has been applied; a line of the
39
    /// insert's shared buffer.
40
    content: Option<LineRef>,
ff0af1ee unknown 41
    deleted_by: BTreeSet<ChangeHash>,
42
}
43
44
#[derive(Clone, Debug)]
45
pub struct FileGraph {
46
    atoms: HashMap<AtomKey, AtomInfo>,
0bdc3f91 Andy McCall ↶ 47
    /// Every vertex, with its direct successors.
48
    succ: HashMap<Vertex, Edges>,
49
}
50
51
/// A vertex's direct successors, sorted and without repeats. Nearly every
52
/// vertex has one, which is kept inline without an allocation.
53
#[derive(Clone, Debug, Default, PartialEq, Eq)]
54
enum Edges {
55
    #[default]
56
    None,
57
    One(Vertex),
58
    Many(Vec<Vertex>),
ff0af1ee unknown 59
}
60
0bdc3f91 Andy McCall ↶ 61
impl Edges {
62
    fn insert(&mut self, v: Vertex) {
63
        match self {
64
            Edges::None => *self = Edges::One(v),
65
            Edges::One(a) if *a == v => {}
66
            Edges::One(a) => {
67
                let a = *a;
68
                *self = Edges::Many(if a < v { vec![a, v] } else { vec![v, a] });
69
            }
70
            Edges::Many(list) => {
71
                if let Err(i) = list.binary_search(&v) {
72
                    list.insert(i, v);
73
                }
74
            }
75
        }
76
    }
77
78
    fn as_slice(&self) -> &[Vertex] {
79
        match self {
80
            Edges::None => &[],
81
            Edges::One(v) => std::slice::from_ref(v),
82
            Edges::Many(list) => list,
83
        }
84
    }
85
86
    fn iter(&self) -> std::slice::Iter<'_, Vertex> {
87
        self.as_slice().iter()
88
    }
89
}
90
ff0af1ee unknown 91
impl Default for FileGraph {
92
    fn default() -> Self {
93
        Self::new()
94
    }
95
}
96
97
/// One piece of a materialized file.
98
#[derive(Clone, Debug, PartialEq, Eq)]
99
pub enum Segment {
100
    Lines(Vec<AtomKey>),
101
    /// Order conflict: alternatives that cannot be ordered against each other.
102
    Conflict(Vec<Vec<AtomKey>>),
103
    /// Delete/insert conflict: `inserted` was anchored to `deleted` by a change
104
    /// that commutes with the deletion.
75167672 Andy McCall 105
    Zombie {
106
        deleted: Vec<AtomKey>,
107
        inserted: Vec<AtomKey>,
108
    },
ff0af1ee unknown 109
}
110
111
#[derive(Clone, Debug, PartialEq, Eq, Default)]
112
pub struct Materialized {
113
    pub segments: Vec<Segment>,
114
}
115
116
impl Materialized {
117
    pub fn is_conflicted(&self) -> bool {
75167672 Andy McCall 118
        self.segments
119
            .iter()
120
            .any(|s| !matches!(s, Segment::Lines(_)))
ff0af1ee unknown 121
    }
122
    pub fn conflict_count(&self) -> usize {
75167672 Andy McCall 123
        self.segments
124
            .iter()
125
            .filter(|s| !matches!(s, Segment::Lines(_)))
126
            .count()
ff0af1ee unknown 127
    }
128
    /// Every live atom in materialized order, conflict sides concatenated.
129
    pub fn live_atoms(&self) -> Vec<AtomKey> {
130
        let mut out = Vec::new();
131
        for s in &self.segments {
132
            match s {
133
                Segment::Lines(a) => out.extend(a),
134
                Segment::Conflict(sides) => sides.iter().for_each(|x| out.extend(x)),
135
                Segment::Zombie { inserted, .. } => out.extend(inserted),
136
            }
137
        }
138
        out
139
    }
140
}
141
142
impl FileGraph {
143
    pub fn new() -> Self {
75167672 Andy McCall 144
        let mut g = FileGraph {
0bdc3f91 Andy McCall ↶ 145
            atoms: HashMap::default(),
146
            succ: HashMap::default(),
75167672 Andy McCall 147
        };
ff0af1ee unknown 148
        g.edge(Vertex::Start, Vertex::End);
149
        g
150
    }
151
152
    fn edge(&mut self, a: Vertex, b: Vertex) {
153
        self.succ.entry(a).or_default().insert(b);
154
        self.succ.entry(b).or_default();
155
    }
156
157
    /// Apply an `Insert` by `change` whose first new atom has index `first_index`.
75167672 Andy McCall 158
    pub fn insert(
159
        &mut self,
160
        change: ChangeHash,
161
        first_index: u32,
162
        after: Vertex,
163
        before: Vertex,
01a47cd7 Andy McCall ↶ 164
        lines: &Lines,
0bdc3f91 Andy McCall ↶ 165
    ) {
01a47cd7 Andy McCall ↶ 166
        self.insert_owned(change, first_index, after, before, lines.clone());
0bdc3f91 Andy McCall ↶ 167
    }
168
169
    /// [`FileGraph::insert`] taking the lines, so loading a state moves
170
    /// them in rather than copying. Each new atom's entry is made once, as
171
    /// the source of the next edge, and the chain's ends are ensured last.
172
    pub fn insert_owned(
173
        &mut self,
174
        change: ChangeHash,
175
        first_index: u32,
176
        after: Vertex,
177
        before: Vertex,
01a47cd7 Andy McCall ↶ 178
        lines: Lines,
75167672 Andy McCall 179
    ) {
0bdc3f91 Andy McCall ↶ 180
        self.atoms.reserve(lines.len());
181
        self.succ.reserve(lines.len() + 1);
ff0af1ee unknown 182
        let mut prev = after;
01a47cd7 Andy McCall ↶ 183
        for i in 0..lines.len() {
ff0af1ee unknown 184
            let key = (change, first_index + i as u32);
01a47cd7 Andy McCall ↶ 185
            self.atoms.entry(key).or_default().content =
186
                Some(LineRef::new(lines.clone(), i as u32));
ff0af1ee unknown 187
            let v = Vertex::Atom(key);
0bdc3f91 Andy McCall ↶ 188
            self.succ.entry(prev).or_default().insert(v);
ff0af1ee unknown 189
            prev = v;
190
        }
0bdc3f91 Andy McCall ↶ 191
        self.succ.entry(prev).or_default().insert(before);
192
        self.succ.entry(before).or_default();
ff0af1ee unknown 193
    }
194
195
    /// Apply a `Delete` of `range` by change `by`.
196
    pub fn delete(&mut self, by: ChangeHash, range: &AtomRange) {
197
        for i in range.from..range.to {
75167672 Andy McCall 198
            self.atoms
199
                .entry((range.change, i))
200
                .or_default()
201
                .deleted_by
202
                .insert(by);
ff0af1ee unknown 203
        }
204
    }
205
206
    pub fn is_live(&self, k: &AtomKey) -> bool {
75167672 Andy McCall 207
        self.atoms
208
            .get(k)
209
            .is_some_and(|a| a.content.is_some() && a.deleted_by.is_empty())
ff0af1ee unknown 210
    }
211
212
    pub fn content(&self, k: &AtomKey) -> Option<&[u8]> {
01a47cd7 Andy McCall ↶ 213
        self.atoms
214
            .get(k)
215
            .and_then(|a| a.content.as_ref().map(LineRef::bytes))
ff0af1ee unknown 216
    }
217
218
    pub fn deleters(&self, k: &AtomKey) -> impl Iterator<Item = &ChangeHash> {
75167672 Andy McCall 219
        self.atoms
220
            .get(k)
221
            .into_iter()
222
            .flat_map(|a| a.deleted_by.iter())
ff0af1ee unknown 223
    }
224
225
    /// Number of atoms introduced by `change` in this graph (for index bookkeeping).
226
    pub fn atom_count(&self) -> usize {
227
        self.atoms.len()
228
    }
229
62794ecc Andy McCall ↶ 230
    /// The changes that inserted a live atom directly after `k`. A change
231
    /// that deletes `k` must depend on them (spec §8.1), or `is_ghost`
232
    /// reads the deletion as concurrent with those insertions.
83d61e80 Andy McCall ↶ 233
    /// Every atom on some path from `first` to `last`, both included, live
234
    /// or deleted (spec §8): the atoms a range of lines is made of, and
235
    /// those that were deleted from between them. Empty when `last` does
236
    /// not follow `first`.
237
    pub fn between(&self, first: &AtomKey, last: &AtomKey) -> Vec<AtomKey> {
238
        let (from, to) = (Vertex::Atom(*first), Vertex::Atom(*last));
239
        let mut ahead: HashSet<Vertex> = HashSet::default();
240
        let mut stack = vec![from];
241
        while let Some(v) = stack.pop() {
242
            if ahead.insert(v) {
243
                stack.extend(
244
                    self.succ
245
                        .get(&v)
246
                        .map(|e| e.as_slice())
247
                        .unwrap_or(&[])
248
                        .iter()
249
                        .copied(),
250
                );
251
            }
252
        }
253
        if !ahead.contains(&to) {
254
            return vec![];
255
        }
256
        let mut pred: HashMap<Vertex, Vec<Vertex>> = HashMap::default();
257
        for (u, es) in &self.succ {
258
            if ahead.contains(u) {
259
                for w in es.iter() {
260
                    pred.entry(*w).or_default().push(*u);
261
                }
262
            }
263
        }
264
        let mut behind: HashSet<Vertex> = HashSet::default();
265
        let mut stack = vec![to];
266
        while let Some(v) = stack.pop() {
267
            if behind.insert(v) {
268
                stack.extend(pred.get(&v).into_iter().flatten().copied());
269
            }
270
        }
271
        let mut out: Vec<AtomKey> = ahead
272
            .intersection(&behind)
273
            .filter_map(|v| match v {
274
                Vertex::Atom(k) if self.atoms.contains_key(k) => Some(*k),
275
                _ => None,
276
            })
277
            .collect();
278
        out.sort();
279
        out
280
    }
281
282
    /// The changes that inserted or deleted any of `atoms`.
283
    pub fn touching(&self, atoms: &[AtomKey]) -> BTreeSet<ChangeHash> {
284
        let mut out = BTreeSet::new();
285
        for k in atoms {
286
            out.insert(k.0);
287
            if let Some(info) = self.atoms.get(k) {
288
                out.extend(info.deleted_by.iter().copied());
289
            }
290
        }
291
        out
292
    }
293
62794ecc Andy McCall ↶ 294
    pub fn live_successor_changes(&self, k: &AtomKey) -> Vec<ChangeHash> {
295
        self.succ
296
            .get(&Vertex::Atom(*k))
297
            .map(|ss| {
298
                ss.iter()
299
                    .filter_map(|s| match s {
300
                        Vertex::Atom(y) if self.is_live(y) => Some(y.0),
301
                        _ => None,
302
                    })
303
                    .collect()
304
            })
305
            .unwrap_or_default()
306
    }
307
ff0af1ee unknown 308
    fn is_ghost(&self, k: &AtomKey, info: &AtomInfo, deps: &dyn DepOracle) -> bool {
23b217d7 Andy McCall ↶ 309
        // A deleted atom is a ghost when a live direct successor was inserted
310
        // by a change that commutes with every deleter: neither depends on
311
        // the other (spec §9.1, delete/insert). If the inserter came after the
312
        // deletion, or the deleter saw the insertion, there is no conflict.
ff0af1ee unknown 313
        self.succ.get(&Vertex::Atom(*k)).is_some_and(|ss| {
314
            ss.iter().any(|s| match s {
75167672 Andy McCall 315
                Vertex::Atom(y) if self.is_live(y) => !info
316
                    .deleted_by
317
                    .iter()
318
                    .any(|d| deps.depends_on(d, &y.0) || deps.depends_on(&y.0, d)),
ff0af1ee unknown 319
                _ => false,
320
            })
321
        })
322
    }
323
324
    /// Successors of `v` in the graph with non-kept vertices contracted away.
75167672 Andy McCall 325
    fn reduced_succ(
326
        &self,
327
        v: Vertex,
328
        kept: &HashSet<Vertex>,
329
        memo: &mut HashMap<Vertex, BTreeSet<Vertex>>,
330
    ) -> BTreeSet<Vertex> {
598009a7 Andy McCall ↶ 331
        let direct = self.succ.get(&v).map(Edges::as_slice).unwrap_or_default();
332
        // The common case, a successor that is itself kept: nothing to
333
        // contract.
334
        if direct.iter().all(|s| kept.contains(s)) {
335
            return direct.iter().copied().collect();
336
        }
ff0af1ee unknown 337
        let mut out = BTreeSet::new();
598009a7 Andy McCall ↶ 338
        let mut stack: Vec<Vertex> = direct.to_vec();
0bdc3f91 Andy McCall ↶ 339
        let mut seen = HashSet::default();
ff0af1ee unknown 340
        while let Some(s) = stack.pop() {
341
            if kept.contains(&s) {
342
                out.insert(s);
343
            } else if seen.insert(s) {
344
                if let Some(m) = memo.get(&s) {
345
                    out.extend(m.iter().copied());
346
                } else if let Some(next) = self.succ.get(&s) {
347
                    stack.extend(next.iter().copied());
348
                }
349
            }
350
        }
351
        out
598009a7 Andy McCall ↶ 352
    }
353
354
    /// The atoms in order when the graph is one chain from Start to End with
355
    /// nothing deleted: a file only ever inserted into in sequence, as every
356
    /// file of a fresh commit or a clone is. Such a graph has no conflict.
357
    /// An edge to End besides the chain's next atom (Start's own, or an
358
    /// append's anchor) orders nothing, since End is last anyway.
359
    fn as_chain(&self) -> Option<Vec<AtomKey>> {
360
        if self
361
            .atoms
362
            .values()
363
            .any(|a| a.content.is_none() || !a.deleted_by.is_empty())
364
        {
365
            return None;
366
        }
367
        let mut order = Vec::with_capacity(self.atoms.len());
368
        let mut v = Vertex::Start;
369
        loop {
370
            let succ = self.succ.get(&v)?.as_slice();
371
            let mut atoms = succ.iter().filter(|s| **s != Vertex::End);
372
            match (atoms.next(), atoms.next()) {
373
                (None, _) if succ.contains(&Vertex::End) => break,
374
                (Some(Vertex::Atom(k)), None) if order.len() < self.atoms.len() => {
375
                    order.push(*k);
376
                    v = Vertex::Atom(*k);
377
                }
378
                _ => return None,
379
            }
380
        }
381
        (order.len() == self.atoms.len()).then_some(order)
ff0af1ee unknown 382
    }
383
384
    /// Order the live atoms and locate conflicts.
385
    pub fn materialize(&self, deps: &dyn DepOracle) -> Materialized {
e83354cd Andy McCall ↶ 386
        if let Some(order) = self.as_chain().or_else(|| self.as_order(deps)) {
598009a7 Andy McCall ↶ 387
            return Materialized {
388
                segments: if order.is_empty() {
389
                    vec![]
390
                } else {
391
                    vec![Segment::Lines(order)]
392
                },
393
            };
394
        }
395
        self.materialize_general(deps)
396
    }
397
e83354cd Andy McCall ↶ 398
    /// The live atoms in order when, with the dead ones contracted away,
399
    /// the graph orders them one way only: no deleted atom is a ghost, and
400
    /// at every step of the topological sort one vertex is next. A file
401
    /// edited without concurrent edits is such a graph, however long its
402
    /// history. `None` otherwise, for [`FileGraph::materialize_general`]
403
    /// to find the conflicts; where this answers, it gives the same order.
404
    fn as_order(&self, deps: &dyn DepOracle) -> Option<Vec<AtomKey>> {
405
        // Kept vertices by dense index: Start, End, then the live atoms.
406
        let mut index: HashMap<Vertex, u32> =
407
            HashMap::with_capacity_and_hasher(self.atoms.len() + 2, Default::default());
408
        let mut verts: Vec<Vertex> = Vec::with_capacity(self.atoms.len() + 2);
409
        verts.extend([Vertex::Start, Vertex::End]);
410
        index.insert(Vertex::Start, 0);
411
        index.insert(Vertex::End, 1);
412
        for (k, info) in &self.atoms {
413
            if info.content.is_none() {
414
                continue;
415
            }
416
            if info.deleted_by.is_empty() {
417
                index.insert(Vertex::Atom(*k), verts.len() as u32);
418
                verts.push(Vertex::Atom(*k));
419
            } else if self.is_ghost(k, info, deps) {
420
                return None;
421
            }
422
        }
423
        // Each kept vertex's successors, contracting the others: what a
424
        // contracted vertex reaches is worked out once (post-order).
425
        let mut reach: HashMap<Vertex, Vec<u32>> = HashMap::default();
426
        let contracted = |v: Vertex, reach: &mut HashMap<Vertex, Vec<u32>>| {
427
            let mut stack: Vec<(Vertex, bool)> = vec![(v, false)];
428
            while let Some((x, done)) = stack.pop() {
429
                if reach.contains_key(&x) {
430
                    continue;
431
                }
432
                let next = self.succ.get(&x).map(Edges::as_slice).unwrap_or_default();
433
                if !done {
434
                    stack.push((x, true));
435
                    for s in next {
436
                        if !index.contains_key(s) && !reach.contains_key(s) {
437
                            stack.push((*s, false));
438
                        }
439
                    }
440
                    continue;
441
                }
442
                let mut out: Vec<u32> = Vec::new();
443
                for s in next {
444
                    match index.get(s) {
445
                        Some(i) => out.push(*i),
446
                        None => out.extend(reach.get(s).into_iter().flatten()),
447
                    }
448
                }
449
                out.sort_unstable();
450
                out.dedup();
451
                reach.insert(x, out);
452
            }
453
        };
454
        // Successors of vertex `i`: `succ[starts[i]..starts[i + 1]]`.
455
        let mut succ: Vec<u32> = Vec::with_capacity(verts.len() * 2);
456
        let mut starts: Vec<usize> = Vec::with_capacity(verts.len() + 1);
457
        let mut indeg: Vec<u32> = vec![0; verts.len()];
458
        for v in &verts {
459
            let from = succ.len();
460
            starts.push(from);
461
            for s in self.succ.get(v).map(Edges::as_slice).unwrap_or_default() {
462
                match index.get(s) {
463
                    Some(i) => succ.push(*i),
464
                    None => {
465
                        contracted(*s, &mut reach);
466
                        succ.extend(reach.get(s).into_iter().flatten());
467
                    }
468
                }
469
            }
470
            if succ.len() - from > 1 {
471
                succ[from..].sort_unstable();
472
                let mut kept = from;
473
                for i in from..succ.len() {
474
                    if i == from || succ[i] != succ[kept - 1] {
475
                        succ[kept] = succ[i];
476
                        kept += 1;
477
                    }
478
                }
479
                succ.truncate(kept);
480
            }
481
            for &w in &succ[from..] {
482
                indeg[w as usize] += 1;
483
            }
484
        }
485
        starts.push(succ.len());
486
        // Kahn's algorithm, giving up where two vertices are ready at once.
487
        let mut ready: Vec<u32> = (0..verts.len() as u32)
488
            .filter(|&i| indeg[i as usize] == 0)
489
            .collect();
490
        let mut order: Vec<AtomKey> = Vec::with_capacity(verts.len());
491
        let mut seen = 0;
492
        while let Some(v) = ready.pop() {
493
            if !ready.is_empty() {
494
                return None;
495
            }
496
            seen += 1;
497
            if let Vertex::Atom(k) = verts[v as usize] {
498
                order.push(k);
499
            }
500
            for &w in &succ[starts[v as usize]..starts[v as usize + 1]] {
501
                indeg[w as usize] -= 1;
502
                if indeg[w as usize] == 0 {
503
                    ready.push(w);
504
                }
505
            }
506
            if ready.len() > 1 {
507
                return None;
508
            }
509
        }
510
        (seen == verts.len()).then_some(order)
511
    }
512
598009a7 Andy McCall ↶ 513
    /// [`FileGraph::materialize`] for any graph: contraction, then a
514
    /// topological order with conflict regions.
515
    fn materialize_general(&self, deps: &dyn DepOracle) -> Materialized {
ff0af1ee unknown 516
        // 1. Vertices that survive contraction: live atoms, ghosts, Start, End.
0bdc3f91 Andy McCall ↶ 517
        let mut kept: HashSet<Vertex> = HashSet::default();
518
        let mut ghosts: HashSet<AtomKey> = HashSet::default();
ff0af1ee unknown 519
        kept.insert(Vertex::Start);
520
        kept.insert(Vertex::End);
521
        for (k, info) in &self.atoms {
522
            if info.content.is_none() {
523
                continue;
524
            }
525
            if info.deleted_by.is_empty() {
526
                kept.insert(Vertex::Atom(*k));
527
            } else if self.is_ghost(k, info, deps) {
528
                kept.insert(Vertex::Atom(*k));
529
                ghosts.insert(*k);
530
            }
531
        }
532
533
        // 2. Reduced graph over kept vertices.
0bdc3f91 Andy McCall ↶ 534
        let mut memo = HashMap::default();
535
        let mut rsucc: HashMap<Vertex, BTreeSet<Vertex>> = HashMap::default();
536
        let mut indeg: HashMap<Vertex, usize> = HashMap::default();
ff0af1ee unknown 537
        for &u in &kept {
538
            indeg.entry(u).or_insert(0);
539
            let rs = self.reduced_succ(u, &kept, &mut memo);
540
            for &w in &rs {
541
                *indeg.entry(w).or_insert(0) += 1;
542
            }
543
            rsucc.insert(u, rs);
544
        }
545
546
        // 3. Kahn's algorithm with side labelling for conflict regions.
75167672 Andy McCall 547
        let mut ready: BTreeSet<Vertex> = indeg
548
            .iter()
549
            .filter(|(_, &d)| d == 0)
550
            .map(|(v, _)| *v)
551
            .collect();
0bdc3f91 Andy McCall ↶ 552
        let mut side: HashMap<Vertex, u32> = HashMap::default();
553
        let mut joins: HashSet<Vertex> = HashSet::default();
ff0af1ee unknown 554
        let mut next_side = 0u32;
555
        let mut plain: Vec<Vertex> = Vec::new();
556
        let mut conflict: Option<BTreeMap<u32, Vec<Vertex>>> = None;
557
        let mut segments: Vec<Segment> = Vec::new();
558
559
        let flush_plain = |plain: &mut Vec<Vertex>, segments: &mut Vec<Segment>| {
75167672 Andy McCall 560
            let atoms: Vec<AtomKey> = plain
561
                .iter()
562
                .filter_map(|v| {
563
                    if let Vertex::Atom(k) = v {
564
                        Some(*k)
565
                    } else {
566
                        None
567
                    }
568
                })
569
                .collect();
ff0af1ee unknown 570
            plain.clear();
571
            if !atoms.is_empty() {
572
                segments.extend(split_zombies(atoms, &ghosts));
573
            }
574
        };
575
        let finish_conflict = |c: BTreeMap<u32, Vec<Vertex>>, segments: &mut Vec<Segment>| {
576
            let sides: Vec<Vec<AtomKey>> = c
577
                .into_values()
578
                .map(|vs| {
579
                    vs.into_iter()
580
                        .filter_map(|v| match v {
581
                            Vertex::Atom(k) if !ghosts.contains(&k) => Some(k),
582
                            _ => None,
583
                        })
584
                        .collect::<Vec<_>>()
585
                })
586
                .filter(|s| !s.is_empty())
587
                .collect();
588
            match sides.len() {
589
                0 => {}
590
                1 => segments.push(Segment::Lines(sides.into_iter().next().unwrap())),
591
                _ => segments.push(Segment::Conflict(sides)),
592
            }
593
        };
594
595
        while !ready.is_empty() {
596
            if ready.len() >= 2 {
597
                if conflict.is_none() {
598
                    flush_plain(&mut plain, &mut segments);
599
                    conflict = Some(BTreeMap::new());
600
                }
601
                for r in &ready {
602
                    if !side.contains_key(r) {
603
                        side.insert(*r, next_side);
604
                        next_side += 1;
605
                    }
606
                }
607
            }
608
            let v = ready.pop_first().unwrap();
609
            let my_side = side.get(&v).copied();
610
            match (&mut conflict, my_side) {
611
                (Some(c), Some(s)) => c.entry(s).or_default().push(v),
612
                (Some(_), None) => {
613
                    let c = conflict.take().unwrap();
614
                    finish_conflict(c, &mut segments);
615
                    plain.push(v);
616
                }
617
                (None, _) => plain.push(v),
618
            }
619
            for &w in &rsucc[&v] {
620
                let d = indeg.get_mut(&w).unwrap();
621
                *d -= 1;
622
                if conflict.is_some() && !joins.contains(&w) {
623
                    match (my_side, side.get(&w).copied()) {
624
                        (Some(s), None) => {
625
                            side.insert(w, s);
626
                        }
627
                        (Some(s), Some(t)) if s != t => {
628
                            side.remove(&w);
629
                            joins.insert(w);
630
                        }
631
                        (None, Some(_)) => {
632
                            side.remove(&w);
633
                            joins.insert(w);
634
                        }
635
                        _ => {}
636
                    }
637
                }
638
                if *d == 0 {
639
                    ready.insert(w);
640
                }
641
            }
642
        }
643
        if let Some(c) = conflict.take() {
644
            finish_conflict(c, &mut segments);
645
        }
646
        flush_plain(&mut plain, &mut segments);
647
        Materialized { segments }
648
    }
649
650
    /// Render a materialization to bytes with conflict markers (spec §9.5).
651
    /// `label` gives the short description of a change for marker headers.
652
    pub fn render(&self, m: &Materialized, label: &dyn Fn(&ChangeHash) -> String) -> Vec<u8> {
653
        let total = m.conflict_count();
654
        let mut n = 0;
655
        let mut out: Vec<u8> = Vec::new();
656
        let push_atoms = |out: &mut Vec<u8>, atoms: &[AtomKey]| {
657
            for a in atoms {
658
                if let Some(c) = self.content(a) {
659
                    out.extend_from_slice(c);
660
                }
661
            }
662
        };
663
        let newline_if_needed = |out: &mut Vec<u8>| {
664
            if !out.is_empty() && *out.last().unwrap() != b'\n' {
665
                out.push(b'\n');
666
            }
667
        };
668
        for seg in &m.segments {
669
            match seg {
670
                Segment::Lines(a) => push_atoms(&mut out, a),
671
                Segment::Conflict(sides) => {
672
                    n += 1;
673
                    let k = sides.len();
674
                    for (i, s) in sides.iter().enumerate() {
675
                        newline_if_needed(&mut out);
676
                        let who = s.first().map(|a| label(&a.0)).unwrap_or_default();
677
                        let marker = if i == 0 { "<<<<<<<" } else { "=======" };
678
                        out.extend_from_slice(
679
                            format!("{marker} oxy conflict {n} of {total}, side {} of {k}: change {who}\n", i + 1).as_bytes(),
680
                        );
681
                        push_atoms(&mut out, s);
682
                    }
683
                    newline_if_needed(&mut out);
75167672 Andy McCall 684
                    out.extend_from_slice(
685
                        format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes(),
686
                    );
ff0af1ee unknown 687
                }
688
                Segment::Zombie { deleted, inserted } => {
689
                    n += 1;
690
                    newline_if_needed(&mut out);
691
                    let deleter = deleted
692
                        .first()
693
                        .and_then(|a| self.deleters(a).next().copied())
694
                        .map(|d| label(&d))
695
                        .unwrap_or_default();
696
                    let inserter = inserted.first().map(|a| label(&a.0)).unwrap_or_default();
697
                    out.extend_from_slice(
698
                        format!("<<<<<<< oxy conflict {n} of {total}, side 1 of 2: deleted by change {deleter}\n-------\n").as_bytes(),
699
                    );
700
                    push_atoms(&mut out, deleted);
701
                    newline_if_needed(&mut out);
75167672 Andy McCall 702
                    out.extend_from_slice(
703
                        format!(
704
                            "======= oxy conflict {n} of {total}, side 2 of 2: change {inserter}\n"
705
                        )
706
                        .as_bytes(),
707
                    );
ff0af1ee unknown 708
                    push_atoms(&mut out, inserted);
709
                    newline_if_needed(&mut out);
75167672 Andy McCall 710
                    out.extend_from_slice(
711
                        format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes(),
712
                    );
ff0af1ee unknown 713
                }
714
            }
715
        }
716
        out
717
    }
718
}
719
252e8cb2 Andy McCall ↶ 720
fn vertex_value(v: &Vertex) -> crate::cbor::Value {
721
    use crate::cbor::Value;
722
    match v {
723
        Vertex::Start => Value::Uint(0),
724
        Vertex::End => Value::Uint(1),
75167672 Andy McCall 725
        Vertex::Atom((c, i)) => {
726
            Value::Array(vec![Value::Bytes(c.0.to_vec()), Value::Uint(*i as u64)])
727
        }
252e8cb2 Andy McCall ↶ 728
    }
729
}
730
731
fn vertex_from(v: &crate::cbor::Value) -> Result<Vertex, crate::objects::ObjectError> {
732
    use crate::cbor::Value;
733
    use crate::objects::ObjectError;
734
    match v {
735
        Value::Uint(0) => Ok(Vertex::Start),
736
        Value::Uint(1) => Ok(Vertex::End),
75167672 Andy McCall 737
        Value::Array(a) if a.len() == 2 => Ok(Vertex::Atom((
738
            crate::objects::oref(&a[0], "vertex")?,
739
            crate::objects::u32_(&a[1], "vertex")?,
740
        ))),
741
        _ => Err(ObjectError::Schema(
742
            "vertex: expected 0, 1, or [hash, index]".into(),
743
        )),
252e8cb2 Andy McCall ↶ 744
    }
745
}
746
747
impl FileGraph {
748
    /// Serialize for a checkpoint: `{0: atoms [[hash, index, content|null, [deleters]]], 1: edges [[from, to]]}`.
749
    pub fn to_value(&self) -> crate::cbor::Value {
750
        use crate::cbor::Value;
751
        let mut atoms: Vec<(&AtomKey, &AtomInfo)> = self.atoms.iter().collect();
752
        atoms.sort_by_key(|(k, _)| **k);
75167672 Andy McCall 753
        let mut edges: Vec<(Vertex, Vertex)> = self
754
            .succ
755
            .iter()
756
            .flat_map(|(a, bs)| bs.iter().map(move |b| (*a, *b)))
757
            .collect();
252e8cb2 Andy McCall ↶ 758
        edges.sort();
759
        Value::map([
760
            (
761
                0,
762
                Value::Array(
763
                    atoms
764
                        .into_iter()
765
                        .map(|((c, i), info)| {
766
                            Value::Array(vec![
767
                                Value::Bytes(c.0.to_vec()),
768
                                Value::Uint(*i as u64),
75167672 Andy McCall 769
                                info.content
770
                                    .as_ref()
01a47cd7 Andy McCall ↶ 771
                                    .map(|b| Value::Bytes(b.bytes().to_vec()))
75167672 Andy McCall 772
                                    .unwrap_or(Value::Null),
773
                                Value::Array(
774
                                    info.deleted_by
775
                                        .iter()
776
                                        .map(|d| Value::Bytes(d.0.to_vec()))
777
                                        .collect(),
778
                                ),
252e8cb2 Andy McCall ↶ 779
                            ])
780
                        })
781
                        .collect(),
782
                ),
783
            ),
75167672 Andy McCall 784
            (
785
                1,
786
                Value::Array(
787
                    edges
788
                        .iter()
789
                        .map(|(a, b)| Value::Array(vec![vertex_value(a), vertex_value(b)]))
790
                        .collect(),
791
                ),
792
            ),
252e8cb2 Andy McCall ↶ 793
        ])
794
    }
795
796
    pub fn from_value(v: &crate::cbor::Value) -> Result<FileGraph, crate::objects::ObjectError> {
797
        use crate::objects::{array, bytes, field, map, oref, u32_, ObjectError};
798
        let m = map(v, "file graph")?;
75167672 Andy McCall 799
        let mut g = FileGraph {
0bdc3f91 Andy McCall ↶ 800
            atoms: HashMap::default(),
801
            succ: HashMap::default(),
75167672 Andy McCall 802
        };
01a47cd7 Andy McCall ↶ 803
        let atoms = array(field(m, 0, "atoms")?, "atoms")?;
804
        // Every atom's line in one buffer.
805
        let mut texts: Vec<&[u8]> = Vec::new();
806
        for a in atoms {
807
            let e = array(a, "atom")?;
808
            if e.len() == 4 && !e[2].is_null() {
809
                texts.push(bytes(&e[2], "atom content")?);
810
            }
811
        }
812
        let all: Lines = texts.into_iter().collect();
813
        let mut next = 0u32;
814
        for a in atoms {
252e8cb2 Andy McCall ↶ 815
            let e = array(a, "atom")?;
816
            if e.len() != 4 {
817
                return Err(ObjectError::Schema("atom: expected 4 fields".into()));
818
            }
819
            let key = (oref(&e[0], "atom change")?, u32_(&e[1], "atom index")?);
75167672 Andy McCall 820
            let content = if e[2].is_null() {
821
                None
822
            } else {
01a47cd7 Andy McCall ↶ 823
                next += 1;
824
                Some(LineRef::new(all.clone(), next - 1))
75167672 Andy McCall 825
            };
826
            let deleted_by = array(&e[3], "deleters")?
827
                .iter()
828
                .map(|d| oref(d, "deleter"))
829
                .collect::<Result<BTreeSet<_>, _>>()?;
830
            g.atoms.insert(
831
                key,
832
                AtomInfo {
833
                    content,
834
                    deleted_by,
835
                },
836
            );
252e8cb2 Andy McCall ↶ 837
        }
838
        for e in array(field(m, 1, "edges")?, "edges")? {
839
            let pair = array(e, "edge")?;
840
            if pair.len() != 2 {
841
                return Err(ObjectError::Schema("edge: expected [from, to]".into()));
842
            }
843
            g.edge(vertex_from(&pair[0])?, vertex_from(&pair[1])?);
844
        }
845
        Ok(g)
846
    }
847
}
848
ff0af1ee unknown 849
/// Split a plain run into `Lines` and `Zombie` segments around ghost atoms.
850
fn split_zombies(atoms: Vec<AtomKey>, ghosts: &HashSet<AtomKey>) -> Vec<Segment> {
851
    let mut out = Vec::new();
852
    let mut lines: Vec<AtomKey> = Vec::new();
853
    let mut i = 0;
854
    while i < atoms.len() {
855
        if !ghosts.contains(&atoms[i]) {
856
            lines.push(atoms[i]);
857
            i += 1;
858
            continue;
859
        }
860
        if !lines.is_empty() {
861
            out.push(Segment::Lines(std::mem::take(&mut lines)));
862
        }
863
        let mut deleted = Vec::new();
864
        while i < atoms.len() && ghosts.contains(&atoms[i]) {
865
            deleted.push(atoms[i]);
866
            i += 1;
867
        }
868
        let mut inserted = Vec::new();
869
        if i < atoms.len() {
870
            let ch = atoms[i].0;
871
            while i < atoms.len() && atoms[i].0 == ch && !ghosts.contains(&atoms[i]) {
872
                inserted.push(atoms[i]);
873
                i += 1;
874
            }
875
        }
876
        out.push(Segment::Zombie { deleted, inserted });
877
    }
878
    if !lines.is_empty() {
879
        out.push(Segment::Lines(lines));
880
    }
881
    out
882
}
883
884
#[cfg(test)]
885
mod tests {
886
    use super::*;
887
    use crate::hash::ObjectRef;
888
889
    fn h(n: u8) -> ChangeHash {
890
        ObjectRef([n; 32])
891
    }
01a47cd7 Andy McCall ↶ 892
    fn lines(v: &[&str]) -> Lines {
893
        v.iter().map(|s| s.as_bytes()).collect()
ff0af1ee unknown 894
    }
895
    fn text(g: &FileGraph, m: &Materialized) -> String {
896
        String::from_utf8(g.render(m, &|c| c.short(4))).unwrap()
897
    }
898
899
    struct Deps(HashMap<ChangeHash, Vec<ChangeHash>>);
900
    impl DepOracle for Deps {
901
        fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
902
            if a == b {
903
                return true;
904
            }
905
            let mut stack = vec![*a];
0bdc3f91 Andy McCall ↶ 906
            let mut seen = HashSet::default();
ff0af1ee unknown 907
            while let Some(x) = stack.pop() {
908
                if let Some(ds) = self.0.get(&x) {
909
                    for d in ds {
910
                        if d == b {
911
                            return true;
912
                        }
913
                        if seen.insert(*d) {
914
                            stack.push(*d);
915
                        }
916
                    }
917
                }
918
            }
919
            false
e83354cd Andy McCall ↶ 920
        }
921
    }
922
923
    /// Linear history: each change depends on every earlier one.
924
    struct Linear;
925
    impl DepOracle for Linear {
926
        fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
927
            a.0[..2] >= b.0[..2]
928
        }
929
    }
930
931
    /// A file of `n` lines edited `revs` times, `per` lines replaced each
932
    /// time, as a long-lived file is: most atoms dead, one live chain.
933
    fn edited_file(n: usize, revs: usize, per: usize) -> FileGraph {
934
        let hn = |i: usize| {
935
            let mut b = [0u8; 32];
936
            b[..2].copy_from_slice(&(i as u16).to_be_bytes());
937
            ObjectRef(b)
938
        };
939
        let mut g = FileGraph::new();
940
        let first: Vec<String> = (0..n).map(|i| format!("line {i}\n")).collect();
941
        let first: Vec<&str> = first.iter().map(String::as_str).collect();
942
        g.insert(hn(1), 0, Vertex::Start, Vertex::End, &lines(&first));
943
        for r in 2..revs + 2 {
944
            let order = g.materialize(&Linear).live_atoms();
945
            let step = order.len() / per;
946
            for j in 0..per {
947
                let at = j * step + (r % step.max(1));
948
                let k = order[at];
949
                g.delete(
950
                    hn(r),
951
                    &AtomRange {
952
                        change: k.0,
953
                        from: k.1,
954
                        to: k.1 + 1,
955
                    },
956
                );
957
                let after = if at == 0 {
958
                    Vertex::Start
959
                } else {
960
                    Vertex::Atom(order[at - 1])
961
                };
962
                let before = order
963
                    .get(at + 1)
964
                    .map(|k| Vertex::Atom(*k))
965
                    .unwrap_or(Vertex::End);
966
                g.insert(hn(r), j as u32, after, before, &lines(&["new\n"]));
967
            }
ff0af1ee unknown 968
        }
e83354cd Andy McCall ↶ 969
        g
970
    }
971
972
    /// The single-order shortcut gives what the general algorithm gives.
973
    #[test]
974
    fn the_order_shortcut_agrees_with_the_general_order() {
975
        let g = edited_file(500, 10, 20);
976
        assert!(g.as_chain().is_none());
977
        let fast = g.as_order(&Linear).expect("a single order");
978
        assert_eq!(
979
            Materialized {
980
                segments: vec![Segment::Lines(fast)]
981
            },
982
            g.materialize_general(&Linear)
983
        );
984
        // Two changes inserting at one place concurrently: a conflict,
985
        // which the shortcut leaves to the general algorithm.
986
        let mut g = FileGraph::new();
987
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n"]));
988
        let a = Vertex::Atom((h(1), 0));
989
        g.insert(h(2), 0, a, Vertex::End, &lines(&["b\n"]));
990
        g.insert(h(3), 0, a, Vertex::End, &lines(&["c\n"]));
991
        assert!(g.as_order(&NoDeps).is_none());
992
        assert!(g.materialize(&NoDeps).is_conflicted());
993
    }
994
995
    /// How long rendering a long-lived file takes (run by hand, in release).
996
    #[test]
997
    #[ignore]
998
    fn bench_materialize_edited_file() {
999
        let g = edited_file(20_000, 50, 100);
1000
        let t = std::time::Instant::now();
1001
        let mut n = 0;
1002
        for _ in 0..20 {
1003
            n += g.materialize(&Linear).live_atoms().len();
1004
        }
1005
        eprintln!(
1006
            "materialize: {:.2} ms each ({n} atoms)",
1007
            t.elapsed().as_secs_f64() * 1000.0 / 20.0
1008
        );
598009a7 Andy McCall ↶ 1009
    }
1010
1011
    /// The chain shortcut agrees with the general algorithm wherever it
1012
    /// applies, and stands aside wherever it does not.
1013
    #[test]
1014
    fn the_chain_shortcut_agrees_with_the_general_order() {
1015
        let mut g = FileGraph::new();
1016
        g.insert(
1017
            h(1),
1018
            0,
1019
            Vertex::Start,
1020
            Vertex::End,
1021
            &lines(&["a\n", "b\n", "c\n"]),
1022
        );
1023
        assert!(g.as_chain().is_some());
1024
        assert_eq!(g.materialize(&NoDeps), g.materialize_general(&NoDeps));
1025
        // Appended after the last line: still a chain.
1026
        g.insert(
1027
            h(2),
1028
            0,
1029
            Vertex::Atom((h(1), 2)),
1030
            Vertex::End,
1031
            &lines(&["d\n"]),
1032
        );
1033
        assert!(g.as_chain().is_some());
1034
        assert_eq!(g.materialize(&NoDeps), g.materialize_general(&NoDeps));
1035
        // Inserted between two lines: not a chain (a has two successors).
1036
        g.insert(
1037
            h(4),
1038
            0,
1039
            Vertex::Atom((h(1), 0)),
1040
            Vertex::Atom((h(1), 1)),
1041
            &lines(&["x\n"]),
1042
        );
1043
        assert!(g.as_chain().is_none());
1044
        assert_eq!(text(&g, &g.materialize(&NoDeps)), "a\nx\nb\nc\nd\n");
1045
        // Two inserts at one anchor: a conflict, never a chain.
1046
        let mut c = FileGraph::new();
1047
        c.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n"]));
1048
        c.insert(h(2), 0, Vertex::Start, Vertex::End, &lines(&["b\n"]));
1049
        assert!(c.as_chain().is_none());
1050
        assert!(c.materialize(&NoDeps).is_conflicted());
1051
        // A deletion: not a chain.
1052
        let mut d = FileGraph::new();
1053
        d.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
1054
        d.delete(
1055
            h(3),
1056
            &AtomRange {
1057
                change: h(1),
1058
                from: 0,
1059
                to: 1,
1060
            },
1061
        );
1062
        assert!(d.as_chain().is_none());
1063
        assert_eq!(d.materialize(&NoDeps), d.materialize_general(&NoDeps));
1064
        // Empty.
1065
        let e = FileGraph::new();
1066
        assert_eq!(e.materialize(&NoDeps), e.materialize_general(&NoDeps));
ff0af1ee unknown 1067
    }
1068
1069
    #[test]
1070
    fn sequential_inserts_and_deletes() {
1071
        let mut g = FileGraph::new();
75167672 Andy McCall 1072
        g.insert(
1073
            h(1),
1074
            0,
1075
            Vertex::Start,
1076
            Vertex::End,
1077
            &lines(&["a\n", "b\n", "c\n"]),
1078
        );
ff0af1ee unknown 1079
        let m = g.materialize(&NoDeps);
1080
        assert!(!m.is_conflicted());
1081
        assert_eq!(text(&g, &m), "a\nb\nc\n");
1082
1083
        // Change 2 depends on 1, inserts between b and c and deletes a.
75167672 Andy McCall 1084
        g.insert(
1085
            h(2),
1086
            0,
1087
            Vertex::Atom((h(1), 1)),
1088
            Vertex::Atom((h(1), 2)),
1089
            &lines(&["x\n"]),
1090
        );
1091
        g.delete(
1092
            h(2),
1093
            &AtomRange {
1094
                change: h(1),
1095
                from: 0,
1096
                to: 1,
1097
            },
1098
        );
ff0af1ee unknown 1099
        let deps = Deps([(h(2), vec![h(1)])].into_iter().collect());
1100
        let m = g.materialize(&deps);
1101
        assert!(!m.is_conflicted());
1102
        assert_eq!(text(&g, &m), "b\nx\nc\n");
1103
        assert_eq!(m.live_atoms(), vec![(h(1), 1), (h(2), 0), (h(1), 2)]);
1104
    }
1105
1106
    #[test]
1107
    fn concurrent_inserts_at_same_anchor_conflict() {
1108
        let mut g = FileGraph::new();
1109
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
75167672 Andy McCall 1110
        g.insert(
1111
            h(2),
1112
            0,
1113
            Vertex::Atom((h(1), 0)),
1114
            Vertex::Atom((h(1), 1)),
1115
            &lines(&["from-2\n"]),
1116
        );
1117
        g.insert(
1118
            h(3),
1119
            0,
1120
            Vertex::Atom((h(1), 0)),
1121
            Vertex::Atom((h(1), 1)),
1122
            &lines(&["from-3\n", "more-3\n"]),
1123
        );
1124
        let deps = Deps(
1125
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1126
                .into_iter()
1127
                .collect(),
1128
        );
ff0af1ee unknown 1129
        let m = g.materialize(&deps);
1130
        assert!(m.is_conflicted());
1131
        assert_eq!(m.conflict_count(), 1);
1132
        assert_eq!(
1133
            m.segments,
1134
            vec![
1135
                Segment::Lines(vec![(h(1), 0)]),
1136
                Segment::Conflict(vec![vec![(h(2), 0)], vec![(h(3), 0), (h(3), 1)]]),
1137
                Segment::Lines(vec![(h(1), 1)]),
1138
            ]
1139
        );
1140
        let t = text(&g, &m);
1141
        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}");
1142
    }
1143
1144
    #[test]
1145
    fn concurrent_inserts_at_different_anchors_do_not_conflict() {
1146
        let mut g = FileGraph::new();
75167672 Andy McCall 1147
        g.insert(
1148
            h(1),
1149
            0,
1150
            Vertex::Start,
1151
            Vertex::End,
1152
            &lines(&["a\n", "b\n", "c\n"]),
1153
        );
1154
        g.insert(
1155
            h(2),
1156
            0,
1157
            Vertex::Start,
1158
            Vertex::Atom((h(1), 0)),
1159
            &lines(&["top\n"]),
1160
        );
1161
        g.insert(
1162
            h(3),
1163
            0,
1164
            Vertex::Atom((h(1), 2)),
1165
            Vertex::End,
1166
            &lines(&["bottom\n"]),
1167
        );
1168
        let deps = Deps(
1169
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1170
                .into_iter()
1171
                .collect(),
1172
        );
ff0af1ee unknown 1173
        let m = g.materialize(&deps);
1174
        assert!(!m.is_conflicted());
1175
        assert_eq!(text(&g, &m), "top\na\nb\nc\nbottom\n");
1176
    }
1177
1178
    #[test]
1179
    fn concurrent_deletes_of_the_same_line_agree() {
1180
        let mut g = FileGraph::new();
1181
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
75167672 Andy McCall 1182
        g.delete(
1183
            h(2),
1184
            &AtomRange {
1185
                change: h(1),
1186
                from: 0,
1187
                to: 1,
1188
            },
1189
        );
1190
        g.delete(
1191
            h(3),
1192
            &AtomRange {
1193
                change: h(1),
1194
                from: 0,
1195
                to: 1,
1196
            },
1197
        );
1198
        let deps = Deps(
1199
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1200
                .into_iter()
1201
                .collect(),
1202
        );
ff0af1ee unknown 1203
        let m = g.materialize(&deps);
1204
        assert!(!m.is_conflicted());
1205
        assert_eq!(text(&g, &m), "b\n");
1206
    }
1207
1208
    #[test]
1209
    fn delete_insert_conflict_is_a_zombie() {
1210
        let mut g = FileGraph::new();
1211
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
1212
        // 2 deletes a; 3 (concurrent) inserts after a.
75167672 Andy McCall 1213
        g.delete(
1214
            h(2),
1215
            &AtomRange {
1216
                change: h(1),
1217
                from: 0,
1218
                to: 1,
1219
            },
1220
        );
1221
        g.insert(
1222
            h(3),
1223
            0,
1224
            Vertex::Atom((h(1), 0)),
1225
            Vertex::Atom((h(1), 1)),
1226
            &lines(&["after-a\n"]),
1227
        );
1228
        let deps = Deps(
1229
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1230
                .into_iter()
1231
                .collect(),
1232
        );
ff0af1ee unknown 1233
        let m = g.materialize(&deps);
1234
        assert!(m.is_conflicted());
1235
        assert_eq!(
1236
            m.segments,
1237
            vec![
75167672 Andy McCall 1238
                Segment::Zombie {
1239
                    deleted: vec![(h(1), 0)],
1240
                    inserted: vec![(h(3), 0)]
1241
                },
ff0af1ee unknown 1242
                Segment::Lines(vec![(h(1), 1)]),
1243
            ]
1244
        );
1245
        let t = text(&g, &m);
1246
        assert!(t.contains("-------\na\n======="), "{t}");
1247
        assert!(t.contains("after-a\n>>>>>>>"), "{t}");
1248
1249
        // If the deleter saw the insert (depends on 3), there is no conflict.
75167672 Andy McCall 1250
        let deps = Deps(
1251
            [(h(2), vec![h(1), h(3)]), (h(3), vec![h(1)])]
1252
                .into_iter()
1253
                .collect(),
1254
        );
ff0af1ee unknown 1255
        let m = g.materialize(&deps);
1256
        assert!(!m.is_conflicted());
1257
        assert_eq!(text(&g, &m), "after-a\nb\n");
1258
    }
1259
1260
    #[test]
23b217d7 Andy McCall ↶ 1261
    fn insert_after_a_line_deleted_by_a_dependency_is_not_a_conflict() {
1262
        // 1 inserts a,b; 2 deletes b (depends on 1); 3 depends on 2 yet anchors
1263
        // after the dead b (a rewritten change may do this): no conflict.
1264
        let mut g = FileGraph::new();
1265
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
75167672 Andy McCall 1266
        g.delete(
1267
            h(2),
1268
            &AtomRange {
1269
                change: h(1),
1270
                from: 1,
1271
                to: 2,
1272
            },
1273
        );
1274
        g.insert(
1275
            h(3),
1276
            0,
1277
            Vertex::Atom((h(1), 1)),
1278
            Vertex::End,
1279
            &lines(&["c\n"]),
1280
        );
1281
        let deps = Deps(
1282
            [(h(2), vec![h(1)]), (h(3), vec![h(2)])]
1283
                .into_iter()
1284
                .collect(),
1285
        );
23b217d7 Andy McCall ↶ 1286
        let m = g.materialize(&deps);
1287
        assert!(!m.is_conflicted(), "{:?}", m.segments);
1288
        assert_eq!(text(&g, &m), "a\nc\n");
1289
        // A change that deletes a line it introduced itself, with a dependent
1290
        // anchoring after it, is likewise fine.
1291
        let mut g = FileGraph::new();
1292
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["x\n"]));
75167672 Andy McCall 1293
        g.delete(
1294
            h(1),
1295
            &AtomRange {
1296
                change: h(1),
1297
                from: 0,
1298
                to: 1,
1299
            },
1300
        );
1301
        g.insert(
1302
            h(2),
1303
            0,
1304
            Vertex::Atom((h(1), 0)),
1305
            Vertex::End,
1306
            &lines(&["y\n"]),
1307
        );
23b217d7 Andy McCall ↶ 1308
        let deps = Deps([(h(2), vec![h(1)])].into_iter().collect());
1309
        let m = g.materialize(&deps);
1310
        assert!(!m.is_conflicted());
1311
        assert_eq!(text(&g, &m), "y\n");
1312
    }
1313
1314
    #[test]
ff0af1ee unknown 1315
    fn resolution_collapses_a_conflict() {
1316
        let mut g = FileGraph::new();
1317
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n"]));
75167672 Andy McCall 1318
        g.insert(
1319
            h(2),
1320
            0,
1321
            Vertex::Start,
1322
            Vertex::Atom((h(1), 0)),
1323
            &lines(&["x\n"]),
1324
        );
1325
        g.insert(
1326
            h(3),
1327
            0,
1328
            Vertex::Start,
1329
            Vertex::Atom((h(1), 0)),
1330
            &lines(&["y\n"]),
1331
        );
1332
        let mut deps: HashMap<_, _> = [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1333
            .into_iter()
1334
            .collect();
ff0af1ee unknown 1335
        assert!(g.materialize(&Deps(deps.clone())).is_conflicted());
1336
        // Resolution keeps y, drops x.
75167672 Andy McCall 1337
        g.delete(
1338
            h(4),
1339
            &AtomRange {
1340
                change: h(2),
1341
                from: 0,
1342
                to: 1,
1343
            },
1344
        );
ff0af1ee unknown 1345
        deps.insert(h(4), vec![h(2), h(3)]);
1346
        let m = g.materialize(&Deps(deps));
1347
        assert!(!m.is_conflicted());
1348
        assert_eq!(text(&g, &m), "y\na\n");
1349
    }
1350
1351
    #[test]
1352
    fn empty_and_unterminated_files() {
1353
        let g = FileGraph::new();
1354
        let m = g.materialize(&NoDeps);
1355
        assert_eq!(m.segments, vec![]);
1356
        assert_eq!(text(&g, &m), "");
1357
        let mut g = FileGraph::new();
1358
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["no newline"]));
1359
        assert_eq!(text(&g, &g.materialize(&NoDeps)), "no newline");
1360
    }
1361
252e8cb2 Andy McCall ↶ 1362
    #[test]
1363
    fn serialization_round_trip() {
1364
        let mut g = FileGraph::new();
1365
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
75167672 Andy McCall 1366
        g.insert(
1367
            h(2),
1368
            0,
1369
            Vertex::Atom((h(1), 0)),
1370
            Vertex::Atom((h(1), 1)),
1371
            &lines(&["x\n"]),
1372
        );
1373
        g.delete(
1374
            h(3),
1375
            &AtomRange {
1376
                change: h(1),
1377
                from: 1,
1378
                to: 2,
1379
            },
1380
        );
252e8cb2 Andy McCall ↶ 1381
        let v = g.to_value();
1382
        let bytes = crate::cbor::encode(&v);
1383
        let back = FileGraph::from_value(&crate::cbor::decode(&bytes).unwrap()).unwrap();
75167672 Andy McCall 1384
        let deps = Deps(
1385
            [(h(2), vec![h(1)]), (h(3), vec![h(1)])]
1386
                .into_iter()
1387
                .collect(),
1388
        );
252e8cb2 Andy McCall ↶ 1389
        assert_eq!(back.materialize(&deps), g.materialize(&deps));
1390
        assert_eq!(text(&back, &back.materialize(&deps)), "a\nx\n");
75167672 Andy McCall 1391
        assert_eq!(
1392
            crate::cbor::encode(&back.to_value()),
1393
            bytes,
1394
            "deterministic"
1395
        );
252e8cb2 Andy McCall ↶ 1396
    }
1397
ff0af1ee unknown 1398
    /// Spec §7.3: the graph is a function of the set of changes, not their order.
1399
    #[test]
1400
    fn application_order_is_irrelevant() {
1401
        use rand::seq::SliceRandom;
75167672 Andy McCall 1402
        type Op = (
1403
            ChangeHash,
01a47cd7 Andy McCall ↶ 1404
            Vec<(u32, Vertex, Vertex, Lines)>,
75167672 Andy McCall 1405
            Vec<AtomRange>,
1406
        );
ff0af1ee unknown 1407
        let ops: Vec<Op> = vec![
75167672 Andy McCall 1408
            (
1409
                h(1),
1410
                vec![(0, Vertex::Start, Vertex::End, lines(&["a\n", "b\n", "c\n"]))],
1411
                vec![],
1412
            ),
1413
            (
1414
                h(2),
1415
                vec![(
1416
                    0,
1417
                    Vertex::Atom((h(1), 0)),
1418
                    Vertex::Atom((h(1), 1)),
1419
                    lines(&["p\n"]),
1420
                )],
1421
                vec![],
1422
            ),
1423
            (
1424
                h(3),
1425
                vec![(
1426
                    0,
1427
                    Vertex::Atom((h(1), 0)),
1428
                    Vertex::Atom((h(1), 1)),
1429
                    lines(&["q\n", "r\n"]),
1430
                )],
1431
                vec![AtomRange {
1432
                    change: h(1),
1433
                    from: 2,
1434
                    to: 3,
1435
                }],
1436
            ),
1437
            (
1438
                h(4),
1439
                vec![(
1440
                    0,
1441
                    Vertex::Atom((h(3), 1)),
1442
                    Vertex::Atom((h(1), 1)),
1443
                    lines(&["s\n"]),
1444
                )],
1445
                vec![AtomRange {
1446
                    change: h(1),
1447
                    from: 1,
1448
                    to: 2,
1449
                }],
1450
            ),
1451
            (
1452
                h(5),
1453
                vec![(0, Vertex::Atom((h(1), 2)), Vertex::End, lines(&["z\n"]))],
1454
                vec![],
1455
            ),
ff0af1ee unknown 1456
        ];
1457
        let deps = Deps(
75167672 Andy McCall 1458
            [
1459
                (h(2), vec![h(1)]),
1460
                (h(3), vec![h(1)]),
1461
                (h(4), vec![h(3), h(1)]),
1462
                (h(5), vec![h(1)]),
1463
            ]
1464
            .into_iter()
1465
            .collect(),
ff0af1ee unknown 1466
        );
1467
        let build = |order: &[usize]| {
1468
            let mut g = FileGraph::new();
1469
            for &i in order {
1470
                let (c, ins, dels) = &ops[i];
1471
                for (idx, a, b, l) in ins {
1472
                    g.insert(*c, *idx, *a, *b, l);
1473
                }
1474
                for d in dels {
1475
                    g.delete(*c, d);
1476
                }
1477
            }
1478
            let m = g.materialize(&deps);
1479
            (m.clone(), text(&g, &m))
1480
        };
1481
        let reference = build(&[0, 1, 2, 3, 4]);
1482
        assert!(reference.0.is_conflicted());
1483
        let mut rng = rand::thread_rng();
1484
        let mut order: Vec<usize> = (0..ops.len()).collect();
1485
        for _ in 0..50 {
1486
            order.shuffle(&mut rng);
1487
            assert_eq!(build(&order), reference, "order {order:?}");
1488
        }
1489
    }
1490
}