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