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