OXYGENHUB
# 89a1d0243544 oxygen / crates / oxy-core / src / graph.rs
28.4 KiB · Rust · last changed History editing: oxy amend --into folds the working copy into an older change with dependents rewritten; oxy split; graph: delete/insert conflicts only between commuting changes (spec 0.6l) 1 month ago source blame history raw
1
//! The per-file atom graph (spec §7.3), its materialization, and text conflict
2
//! detection (spec §9.1, §9.5).
3
//!
4
//! The graph is a pure function of the set of changes applied to it: inserts
5
//! add vertices and edges, deletes set a flag, and nothing is ever removed.
6
7
use crate::hash::ChangeHash;
8
use crate::objects::AtomRange;
9
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
10
11
/// `(introducing change, index within that change for this node)`.
12
pub type AtomKey = (ChangeHash, u32);
13
14
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
15
pub enum Vertex {
16
    Start,
17
    Atom(AtomKey),
18
    End,
19
}
20
21
/// Transitive dependency test between changes; `depends_on(a, a)` is true.
22
pub trait DepOracle {
23
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool;
24
}
25
26
/// No change depends on any other: every pair commutes.
27
pub struct NoDeps;
28
impl DepOracle for NoDeps {
29
    fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
30
        a == b
31
    }
32
}
33
34
#[derive(Default, Clone, Debug)]
35
struct AtomInfo {
36
    /// `None` until the introducing change has been applied.
37
    content: Option<Vec<u8>>,
38
    deleted_by: BTreeSet<ChangeHash>,
39
}
40
41
#[derive(Clone, Debug)]
42
pub struct FileGraph {
43
    atoms: HashMap<AtomKey, AtomInfo>,
44
    succ: HashMap<Vertex, BTreeSet<Vertex>>,
45
    pred: HashMap<Vertex, BTreeSet<Vertex>>,
46
}
47
48
impl Default for FileGraph {
49
    fn default() -> Self {
50
        Self::new()
51
    }
52
}
53
54
/// One piece of a materialized file.
55
#[derive(Clone, Debug, PartialEq, Eq)]
56
pub enum Segment {
57
    Lines(Vec<AtomKey>),
58
    /// Order conflict: alternatives that cannot be ordered against each other.
59
    Conflict(Vec<Vec<AtomKey>>),
60
    /// Delete/insert conflict: `inserted` was anchored to `deleted` by a change
61
    /// that commutes with the deletion.
62
    Zombie { deleted: Vec<AtomKey>, inserted: Vec<AtomKey> },
63
}
64
65
#[derive(Clone, Debug, PartialEq, Eq, Default)]
66
pub struct Materialized {
67
    pub segments: Vec<Segment>,
68
}
69
70
impl Materialized {
71
    pub fn is_conflicted(&self) -> bool {
72
        self.segments.iter().any(|s| !matches!(s, Segment::Lines(_)))
73
    }
74
    pub fn conflict_count(&self) -> usize {
75
        self.segments.iter().filter(|s| !matches!(s, Segment::Lines(_))).count()
76
    }
77
    /// Every live atom in materialized order, conflict sides concatenated.
78
    pub fn live_atoms(&self) -> Vec<AtomKey> {
79
        let mut out = Vec::new();
80
        for s in &self.segments {
81
            match s {
82
                Segment::Lines(a) => out.extend(a),
83
                Segment::Conflict(sides) => sides.iter().for_each(|x| out.extend(x)),
84
                Segment::Zombie { inserted, .. } => out.extend(inserted),
85
            }
86
        }
87
        out
88
    }
89
}
90
91
impl FileGraph {
92
    pub fn new() -> Self {
93
        let mut g = FileGraph { atoms: HashMap::new(), succ: HashMap::new(), pred: HashMap::new() };
94
        g.edge(Vertex::Start, Vertex::End);
95
        g
96
    }
97
98
    fn edge(&mut self, a: Vertex, b: Vertex) {
99
        self.succ.entry(a).or_default().insert(b);
100
        self.pred.entry(b).or_default().insert(a);
101
        self.succ.entry(b).or_default();
102
        self.pred.entry(a).or_default();
103
    }
104
105
    /// Apply an `Insert` by `change` whose first new atom has index `first_index`.
106
    pub fn insert(&mut self, change: ChangeHash, first_index: u32, after: Vertex, before: Vertex, lines: &[Vec<u8>]) {
107
        let mut prev = after;
108
        for (i, line) in lines.iter().enumerate() {
109
            let key = (change, first_index + i as u32);
110
            self.atoms.entry(key).or_default().content = Some(line.clone());
111
            let v = Vertex::Atom(key);
112
            self.edge(prev, v);
113
            prev = v;
114
        }
115
        self.edge(prev, before);
116
    }
117
118
    /// Apply a `Delete` of `range` by change `by`.
119
    pub fn delete(&mut self, by: ChangeHash, range: &AtomRange) {
120
        for i in range.from..range.to {
121
            self.atoms.entry((range.change, i)).or_default().deleted_by.insert(by);
122
        }
123
    }
124
125
    pub fn is_live(&self, k: &AtomKey) -> bool {
126
        self.atoms.get(k).is_some_and(|a| a.content.is_some() && a.deleted_by.is_empty())
127
    }
128
129
    pub fn content(&self, k: &AtomKey) -> Option<&[u8]> {
130
        self.atoms.get(k).and_then(|a| a.content.as_deref())
131
    }
132
133
    pub fn deleters(&self, k: &AtomKey) -> impl Iterator<Item = &ChangeHash> {
134
        self.atoms.get(k).into_iter().flat_map(|a| a.deleted_by.iter())
135
    }
136
137
    /// Number of atoms introduced by `change` in this graph (for index bookkeeping).
138
    pub fn atom_count(&self) -> usize {
139
        self.atoms.len()
140
    }
141
142
    fn is_ghost(&self, k: &AtomKey, info: &AtomInfo, deps: &dyn DepOracle) -> bool {
143
        // A deleted atom is a ghost when a live direct successor was inserted
144
        // by a change that commutes with every deleter: neither depends on
145
        // the other (spec §9.1, delete/insert). If the inserter came after the
146
        // deletion, or the deleter saw the insertion, there is no conflict.
147
        self.succ.get(&Vertex::Atom(*k)).is_some_and(|ss| {
148
            ss.iter().any(|s| match s {
149
                Vertex::Atom(y) if self.is_live(y) => !info.deleted_by.iter().any(|d| deps.depends_on(d, &y.0) || deps.depends_on(&y.0, d)),
150
                _ => false,
151
            })
152
        })
153
    }
154
155
    /// Successors of `v` in the graph with non-kept vertices contracted away.
156
    fn reduced_succ(&self, v: Vertex, kept: &HashSet<Vertex>, memo: &mut HashMap<Vertex, BTreeSet<Vertex>>) -> BTreeSet<Vertex> {
157
        let mut out = BTreeSet::new();
158
        let mut stack: Vec<Vertex> = self.succ.get(&v).map(|s| s.iter().copied().collect()).unwrap_or_default();
159
        let mut seen = HashSet::new();
160
        while let Some(s) = stack.pop() {
161
            if kept.contains(&s) {
162
                out.insert(s);
163
            } else if seen.insert(s) {
164
                if let Some(m) = memo.get(&s) {
165
                    out.extend(m.iter().copied());
166
                } else if let Some(next) = self.succ.get(&s) {
167
                    stack.extend(next.iter().copied());
168
                }
169
            }
170
        }
171
        out
172
    }
173
174
    /// Order the live atoms and locate conflicts.
175
    pub fn materialize(&self, deps: &dyn DepOracle) -> Materialized {
176
        // 1. Vertices that survive contraction: live atoms, ghosts, Start, End.
177
        let mut kept: HashSet<Vertex> = HashSet::new();
178
        let mut ghosts: HashSet<AtomKey> = HashSet::new();
179
        kept.insert(Vertex::Start);
180
        kept.insert(Vertex::End);
181
        for (k, info) in &self.atoms {
182
            if info.content.is_none() {
183
                continue;
184
            }
185
            if info.deleted_by.is_empty() {
186
                kept.insert(Vertex::Atom(*k));
187
            } else if self.is_ghost(k, info, deps) {
188
                kept.insert(Vertex::Atom(*k));
189
                ghosts.insert(*k);
190
            }
191
        }
192
193
        // 2. Reduced graph over kept vertices.
194
        let mut memo = HashMap::new();
195
        let mut rsucc: HashMap<Vertex, BTreeSet<Vertex>> = HashMap::new();
196
        let mut indeg: HashMap<Vertex, usize> = HashMap::new();
197
        for &u in &kept {
198
            indeg.entry(u).or_insert(0);
199
            let rs = self.reduced_succ(u, &kept, &mut memo);
200
            for &w in &rs {
201
                *indeg.entry(w).or_insert(0) += 1;
202
            }
203
            rsucc.insert(u, rs);
204
        }
205
206
        // 3. Kahn's algorithm with side labelling for conflict regions.
207
        let mut ready: BTreeSet<Vertex> = indeg.iter().filter(|(_, &d)| d == 0).map(|(v, _)| *v).collect();
208
        let mut side: HashMap<Vertex, u32> = HashMap::new();
209
        let mut joins: HashSet<Vertex> = HashSet::new();
210
        let mut next_side = 0u32;
211
        let mut plain: Vec<Vertex> = Vec::new();
212
        let mut conflict: Option<BTreeMap<u32, Vec<Vertex>>> = None;
213
        let mut segments: Vec<Segment> = Vec::new();
214
215
        let flush_plain = |plain: &mut Vec<Vertex>, segments: &mut Vec<Segment>| {
216
            let atoms: Vec<AtomKey> = plain.iter().filter_map(|v| if let Vertex::Atom(k) = v { Some(*k) } else { None }).collect();
217
            plain.clear();
218
            if !atoms.is_empty() {
219
                segments.extend(split_zombies(atoms, &ghosts));
220
            }
221
        };
222
        let finish_conflict = |c: BTreeMap<u32, Vec<Vertex>>, segments: &mut Vec<Segment>| {
223
            let sides: Vec<Vec<AtomKey>> = c
224
                .into_values()
225
                .map(|vs| {
226
                    vs.into_iter()
227
                        .filter_map(|v| match v {
228
                            Vertex::Atom(k) if !ghosts.contains(&k) => Some(k),
229
                            _ => None,
230
                        })
231
                        .collect::<Vec<_>>()
232
                })
233
                .filter(|s| !s.is_empty())
234
                .collect();
235
            match sides.len() {
236
                0 => {}
237
                1 => segments.push(Segment::Lines(sides.into_iter().next().unwrap())),
238
                _ => segments.push(Segment::Conflict(sides)),
239
            }
240
        };
241
242
        while !ready.is_empty() {
243
            if ready.len() >= 2 {
244
                if conflict.is_none() {
245
                    flush_plain(&mut plain, &mut segments);
246
                    conflict = Some(BTreeMap::new());
247
                }
248
                for r in &ready {
249
                    if !side.contains_key(r) {
250
                        side.insert(*r, next_side);
251
                        next_side += 1;
252
                    }
253
                }
254
            }
255
            let v = ready.pop_first().unwrap();
256
            let my_side = side.get(&v).copied();
257
            match (&mut conflict, my_side) {
258
                (Some(c), Some(s)) => c.entry(s).or_default().push(v),
259
                (Some(_), None) => {
260
                    let c = conflict.take().unwrap();
261
                    finish_conflict(c, &mut segments);
262
                    plain.push(v);
263
                }
264
                (None, _) => plain.push(v),
265
            }
266
            for &w in &rsucc[&v] {
267
                let d = indeg.get_mut(&w).unwrap();
268
                *d -= 1;
269
                if conflict.is_some() && !joins.contains(&w) {
270
                    match (my_side, side.get(&w).copied()) {
271
                        (Some(s), None) => {
272
                            side.insert(w, s);
273
                        }
274
                        (Some(s), Some(t)) if s != t => {
275
                            side.remove(&w);
276
                            joins.insert(w);
277
                        }
278
                        (None, Some(_)) => {
279
                            side.remove(&w);
280
                            joins.insert(w);
281
                        }
282
                        _ => {}
283
                    }
284
                }
285
                if *d == 0 {
286
                    ready.insert(w);
287
                }
288
            }
289
        }
290
        if let Some(c) = conflict.take() {
291
            finish_conflict(c, &mut segments);
292
        }
293
        flush_plain(&mut plain, &mut segments);
294
        Materialized { segments }
295
    }
296
297
    /// Render a materialization to bytes with conflict markers (spec §9.5).
298
    /// `label` gives the short description of a change for marker headers.
299
    pub fn render(&self, m: &Materialized, label: &dyn Fn(&ChangeHash) -> String) -> Vec<u8> {
300
        let total = m.conflict_count();
301
        let mut n = 0;
302
        let mut out: Vec<u8> = Vec::new();
303
        let push_atoms = |out: &mut Vec<u8>, atoms: &[AtomKey]| {
304
            for a in atoms {
305
                if let Some(c) = self.content(a) {
306
                    out.extend_from_slice(c);
307
                }
308
            }
309
        };
310
        let newline_if_needed = |out: &mut Vec<u8>| {
311
            if !out.is_empty() && *out.last().unwrap() != b'\n' {
312
                out.push(b'\n');
313
            }
314
        };
315
        for seg in &m.segments {
316
            match seg {
317
                Segment::Lines(a) => push_atoms(&mut out, a),
318
                Segment::Conflict(sides) => {
319
                    n += 1;
320
                    let k = sides.len();
321
                    for (i, s) in sides.iter().enumerate() {
322
                        newline_if_needed(&mut out);
323
                        let who = s.first().map(|a| label(&a.0)).unwrap_or_default();
324
                        let marker = if i == 0 { "<<<<<<<" } else { "=======" };
325
                        out.extend_from_slice(
326
                            format!("{marker} oxy conflict {n} of {total}, side {} of {k}: change {who}\n", i + 1).as_bytes(),
327
                        );
328
                        push_atoms(&mut out, s);
329
                    }
330
                    newline_if_needed(&mut out);
331
                    out.extend_from_slice(format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes());
332
                }
333
                Segment::Zombie { deleted, inserted } => {
334
                    n += 1;
335
                    newline_if_needed(&mut out);
336
                    let deleter = deleted
337
                        .first()
338
                        .and_then(|a| self.deleters(a).next().copied())
339
                        .map(|d| label(&d))
340
                        .unwrap_or_default();
341
                    let inserter = inserted.first().map(|a| label(&a.0)).unwrap_or_default();
342
                    out.extend_from_slice(
343
                        format!("<<<<<<< oxy conflict {n} of {total}, side 1 of 2: deleted by change {deleter}\n-------\n").as_bytes(),
344
                    );
345
                    push_atoms(&mut out, deleted);
346
                    newline_if_needed(&mut out);
347
                    out.extend_from_slice(format!("======= oxy conflict {n} of {total}, side 2 of 2: change {inserter}\n").as_bytes());
348
                    push_atoms(&mut out, inserted);
349
                    newline_if_needed(&mut out);
350
                    out.extend_from_slice(format!(">>>>>>> oxy conflict {n} of {total} end\n").as_bytes());
351
                }
352
            }
353
        }
354
        out
355
    }
356
}
357
358
fn vertex_value(v: &Vertex) -> crate::cbor::Value {
359
    use crate::cbor::Value;
360
    match v {
361
        Vertex::Start => Value::Uint(0),
362
        Vertex::End => Value::Uint(1),
363
        Vertex::Atom((c, i)) => Value::Array(vec![Value::Bytes(c.0.to_vec()), Value::Uint(*i as u64)]),
364
    }
365
}
366
367
fn vertex_from(v: &crate::cbor::Value) -> Result<Vertex, crate::objects::ObjectError> {
368
    use crate::cbor::Value;
369
    use crate::objects::ObjectError;
370
    match v {
371
        Value::Uint(0) => Ok(Vertex::Start),
372
        Value::Uint(1) => Ok(Vertex::End),
373
        Value::Array(a) if a.len() == 2 => Ok(Vertex::Atom((crate::objects::oref(&a[0], "vertex")?, crate::objects::u32_(&a[1], "vertex")?))),
374
        _ => Err(ObjectError::Schema("vertex: expected 0, 1, or [hash, index]".into())),
375
    }
376
}
377
378
impl FileGraph {
379
    /// Serialize for a checkpoint: `{0: atoms [[hash, index, content|null, [deleters]]], 1: edges [[from, to]]}`.
380
    pub fn to_value(&self) -> crate::cbor::Value {
381
        use crate::cbor::Value;
382
        let mut atoms: Vec<(&AtomKey, &AtomInfo)> = self.atoms.iter().collect();
383
        atoms.sort_by_key(|(k, _)| **k);
384
        let mut edges: Vec<(Vertex, Vertex)> = self.succ.iter().flat_map(|(a, bs)| bs.iter().map(move |b| (*a, *b))).collect();
385
        edges.sort();
386
        Value::map([
387
            (
388
                0,
389
                Value::Array(
390
                    atoms
391
                        .into_iter()
392
                        .map(|((c, i), info)| {
393
                            Value::Array(vec![
394
                                Value::Bytes(c.0.to_vec()),
395
                                Value::Uint(*i as u64),
396
                                info.content.as_ref().map(|b| Value::Bytes(b.clone())).unwrap_or(Value::Null),
397
                                Value::Array(info.deleted_by.iter().map(|d| Value::Bytes(d.0.to_vec())).collect()),
398
                            ])
399
                        })
400
                        .collect(),
401
                ),
402
            ),
403
            (1, Value::Array(edges.iter().map(|(a, b)| Value::Array(vec![vertex_value(a), vertex_value(b)])).collect())),
404
        ])
405
    }
406
407
    pub fn from_value(v: &crate::cbor::Value) -> Result<FileGraph, crate::objects::ObjectError> {
408
        use crate::objects::{array, bytes, field, map, oref, u32_, ObjectError};
409
        let m = map(v, "file graph")?;
410
        let mut g = FileGraph { atoms: HashMap::new(), succ: HashMap::new(), pred: HashMap::new() };
411
        for a in array(field(m, 0, "atoms")?, "atoms")? {
412
            let e = array(a, "atom")?;
413
            if e.len() != 4 {
414
                return Err(ObjectError::Schema("atom: expected 4 fields".into()));
415
            }
416
            let key = (oref(&e[0], "atom change")?, u32_(&e[1], "atom index")?);
417
            let content = if e[2].is_null() { None } else { Some(bytes(&e[2], "atom content")?.to_vec()) };
418
            let deleted_by = array(&e[3], "deleters")?.iter().map(|d| oref(d, "deleter")).collect::<Result<BTreeSet<_>, _>>()?;
419
            g.atoms.insert(key, AtomInfo { content, deleted_by });
420
        }
421
        for e in array(field(m, 1, "edges")?, "edges")? {
422
            let pair = array(e, "edge")?;
423
            if pair.len() != 2 {
424
                return Err(ObjectError::Schema("edge: expected [from, to]".into()));
425
            }
426
            g.edge(vertex_from(&pair[0])?, vertex_from(&pair[1])?);
427
        }
428
        Ok(g)
429
    }
430
}
431
432
/// Split a plain run into `Lines` and `Zombie` segments around ghost atoms.
433
fn split_zombies(atoms: Vec<AtomKey>, ghosts: &HashSet<AtomKey>) -> Vec<Segment> {
434
    let mut out = Vec::new();
435
    let mut lines: Vec<AtomKey> = Vec::new();
436
    let mut i = 0;
437
    while i < atoms.len() {
438
        if !ghosts.contains(&atoms[i]) {
439
            lines.push(atoms[i]);
440
            i += 1;
441
            continue;
442
        }
443
        if !lines.is_empty() {
444
            out.push(Segment::Lines(std::mem::take(&mut lines)));
445
        }
446
        let mut deleted = Vec::new();
447
        while i < atoms.len() && ghosts.contains(&atoms[i]) {
448
            deleted.push(atoms[i]);
449
            i += 1;
450
        }
451
        let mut inserted = Vec::new();
452
        if i < atoms.len() {
453
            let ch = atoms[i].0;
454
            while i < atoms.len() && atoms[i].0 == ch && !ghosts.contains(&atoms[i]) {
455
                inserted.push(atoms[i]);
456
                i += 1;
457
            }
458
        }
459
        out.push(Segment::Zombie { deleted, inserted });
460
    }
461
    if !lines.is_empty() {
462
        out.push(Segment::Lines(lines));
463
    }
464
    out
465
}
466
467
#[cfg(test)]
468
mod tests {
469
    use super::*;
470
    use crate::hash::ObjectRef;
471
472
    fn h(n: u8) -> ChangeHash {
473
        ObjectRef([n; 32])
474
    }
475
    fn lines(v: &[&str]) -> Vec<Vec<u8>> {
476
        v.iter().map(|s| s.as_bytes().to_vec()).collect()
477
    }
478
    fn text(g: &FileGraph, m: &Materialized) -> String {
479
        String::from_utf8(g.render(m, &|c| c.short(4))).unwrap()
480
    }
481
482
    struct Deps(HashMap<ChangeHash, Vec<ChangeHash>>);
483
    impl DepOracle for Deps {
484
        fn depends_on(&self, a: &ChangeHash, b: &ChangeHash) -> bool {
485
            if a == b {
486
                return true;
487
            }
488
            let mut stack = vec![*a];
489
            let mut seen = HashSet::new();
490
            while let Some(x) = stack.pop() {
491
                if let Some(ds) = self.0.get(&x) {
492
                    for d in ds {
493
                        if d == b {
494
                            return true;
495
                        }
496
                        if seen.insert(*d) {
497
                            stack.push(*d);
498
                        }
499
                    }
500
                }
501
            }
502
            false
503
        }
504
    }
505
506
    #[test]
507
    fn sequential_inserts_and_deletes() {
508
        let mut g = FileGraph::new();
509
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n", "c\n"]));
510
        let m = g.materialize(&NoDeps);
511
        assert!(!m.is_conflicted());
512
        assert_eq!(text(&g, &m), "a\nb\nc\n");
513
514
        // Change 2 depends on 1, inserts between b and c and deletes a.
515
        g.insert(h(2), 0, Vertex::Atom((h(1), 1)), Vertex::Atom((h(1), 2)), &lines(&["x\n"]));
516
        g.delete(h(2), &AtomRange { change: h(1), from: 0, to: 1 });
517
        let deps = Deps([(h(2), vec![h(1)])].into_iter().collect());
518
        let m = g.materialize(&deps);
519
        assert!(!m.is_conflicted());
520
        assert_eq!(text(&g, &m), "b\nx\nc\n");
521
        assert_eq!(m.live_atoms(), vec![(h(1), 1), (h(2), 0), (h(1), 2)]);
522
    }
523
524
    #[test]
525
    fn concurrent_inserts_at_same_anchor_conflict() {
526
        let mut g = FileGraph::new();
527
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
528
        g.insert(h(2), 0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), &lines(&["from-2\n"]));
529
        g.insert(h(3), 0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), &lines(&["from-3\n", "more-3\n"]));
530
        let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect());
531
        let m = g.materialize(&deps);
532
        assert!(m.is_conflicted());
533
        assert_eq!(m.conflict_count(), 1);
534
        assert_eq!(
535
            m.segments,
536
            vec![
537
                Segment::Lines(vec![(h(1), 0)]),
538
                Segment::Conflict(vec![vec![(h(2), 0)], vec![(h(3), 0), (h(3), 1)]]),
539
                Segment::Lines(vec![(h(1), 1)]),
540
            ]
541
        );
542
        let t = text(&g, &m);
543
        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}");
544
    }
545
546
    #[test]
547
    fn concurrent_inserts_at_different_anchors_do_not_conflict() {
548
        let mut g = FileGraph::new();
549
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n", "c\n"]));
550
        g.insert(h(2), 0, Vertex::Start, Vertex::Atom((h(1), 0)), &lines(&["top\n"]));
551
        g.insert(h(3), 0, Vertex::Atom((h(1), 2)), Vertex::End, &lines(&["bottom\n"]));
552
        let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect());
553
        let m = g.materialize(&deps);
554
        assert!(!m.is_conflicted());
555
        assert_eq!(text(&g, &m), "top\na\nb\nc\nbottom\n");
556
    }
557
558
    #[test]
559
    fn concurrent_deletes_of_the_same_line_agree() {
560
        let mut g = FileGraph::new();
561
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
562
        g.delete(h(2), &AtomRange { change: h(1), from: 0, to: 1 });
563
        g.delete(h(3), &AtomRange { change: h(1), from: 0, to: 1 });
564
        let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect());
565
        let m = g.materialize(&deps);
566
        assert!(!m.is_conflicted());
567
        assert_eq!(text(&g, &m), "b\n");
568
    }
569
570
    #[test]
571
    fn delete_insert_conflict_is_a_zombie() {
572
        let mut g = FileGraph::new();
573
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
574
        // 2 deletes a; 3 (concurrent) inserts after a.
575
        g.delete(h(2), &AtomRange { change: h(1), from: 0, to: 1 });
576
        g.insert(h(3), 0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), &lines(&["after-a\n"]));
577
        let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect());
578
        let m = g.materialize(&deps);
579
        assert!(m.is_conflicted());
580
        assert_eq!(
581
            m.segments,
582
            vec![
583
                Segment::Zombie { deleted: vec![(h(1), 0)], inserted: vec![(h(3), 0)] },
584
                Segment::Lines(vec![(h(1), 1)]),
585
            ]
586
        );
587
        let t = text(&g, &m);
588
        assert!(t.contains("-------\na\n======="), "{t}");
589
        assert!(t.contains("after-a\n>>>>>>>"), "{t}");
590
591
        // If the deleter saw the insert (depends on 3), there is no conflict.
592
        let deps = Deps([(h(2), vec![h(1), h(3)]), (h(3), vec![h(1)])].into_iter().collect());
593
        let m = g.materialize(&deps);
594
        assert!(!m.is_conflicted());
595
        assert_eq!(text(&g, &m), "after-a\nb\n");
596
    }
597
598
    #[test]
599
    fn insert_after_a_line_deleted_by_a_dependency_is_not_a_conflict() {
600
        // 1 inserts a,b; 2 deletes b (depends on 1); 3 depends on 2 yet anchors
601
        // after the dead b (a rewritten change may do this): no conflict.
602
        let mut g = FileGraph::new();
603
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
604
        g.delete(h(2), &AtomRange { change: h(1), from: 1, to: 2 });
605
        g.insert(h(3), 0, Vertex::Atom((h(1), 1)), Vertex::End, &lines(&["c\n"]));
606
        let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(2)])].into_iter().collect());
607
        let m = g.materialize(&deps);
608
        assert!(!m.is_conflicted(), "{:?}", m.segments);
609
        assert_eq!(text(&g, &m), "a\nc\n");
610
        // A change that deletes a line it introduced itself, with a dependent
611
        // anchoring after it, is likewise fine.
612
        let mut g = FileGraph::new();
613
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["x\n"]));
614
        g.delete(h(1), &AtomRange { change: h(1), from: 0, to: 1 });
615
        g.insert(h(2), 0, Vertex::Atom((h(1), 0)), Vertex::End, &lines(&["y\n"]));
616
        let deps = Deps([(h(2), vec![h(1)])].into_iter().collect());
617
        let m = g.materialize(&deps);
618
        assert!(!m.is_conflicted());
619
        assert_eq!(text(&g, &m), "y\n");
620
    }
621
622
    #[test]
623
    fn resolution_collapses_a_conflict() {
624
        let mut g = FileGraph::new();
625
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n"]));
626
        g.insert(h(2), 0, Vertex::Start, Vertex::Atom((h(1), 0)), &lines(&["x\n"]));
627
        g.insert(h(3), 0, Vertex::Start, Vertex::Atom((h(1), 0)), &lines(&["y\n"]));
628
        let mut deps: HashMap<_, _> = [(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect();
629
        assert!(g.materialize(&Deps(deps.clone())).is_conflicted());
630
        // Resolution keeps y, drops x.
631
        g.delete(h(4), &AtomRange { change: h(2), from: 0, to: 1 });
632
        deps.insert(h(4), vec![h(2), h(3)]);
633
        let m = g.materialize(&Deps(deps));
634
        assert!(!m.is_conflicted());
635
        assert_eq!(text(&g, &m), "y\na\n");
636
    }
637
638
    #[test]
639
    fn empty_and_unterminated_files() {
640
        let g = FileGraph::new();
641
        let m = g.materialize(&NoDeps);
642
        assert_eq!(m.segments, vec![]);
643
        assert_eq!(text(&g, &m), "");
644
        let mut g = FileGraph::new();
645
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["no newline"]));
646
        assert_eq!(text(&g, &g.materialize(&NoDeps)), "no newline");
647
    }
648
649
    #[test]
650
    fn serialization_round_trip() {
651
        let mut g = FileGraph::new();
652
        g.insert(h(1), 0, Vertex::Start, Vertex::End, &lines(&["a\n", "b\n"]));
653
        g.insert(h(2), 0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), &lines(&["x\n"]));
654
        g.delete(h(3), &AtomRange { change: h(1), from: 1, to: 2 });
655
        let v = g.to_value();
656
        let bytes = crate::cbor::encode(&v);
657
        let back = FileGraph::from_value(&crate::cbor::decode(&bytes).unwrap()).unwrap();
658
        let deps = Deps([(h(2), vec![h(1)]), (h(3), vec![h(1)])].into_iter().collect());
659
        assert_eq!(back.materialize(&deps), g.materialize(&deps));
660
        assert_eq!(text(&back, &back.materialize(&deps)), "a\nx\n");
661
        assert_eq!(crate::cbor::encode(&back.to_value()), bytes, "deterministic");
662
    }
663
664
    /// Spec §7.3: the graph is a function of the set of changes, not their order.
665
    #[test]
666
    fn application_order_is_irrelevant() {
667
        use rand::seq::SliceRandom;
668
        type Op = (ChangeHash, Vec<(u32, Vertex, Vertex, Vec<Vec<u8>>)>, Vec<AtomRange>);
669
        let ops: Vec<Op> = vec![
670
            (h(1), vec![(0, Vertex::Start, Vertex::End, lines(&["a\n", "b\n", "c\n"]))], vec![]),
671
            (h(2), vec![(0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), lines(&["p\n"]))], vec![]),
672
            (h(3), vec![(0, Vertex::Atom((h(1), 0)), Vertex::Atom((h(1), 1)), lines(&["q\n", "r\n"]))], vec![AtomRange { change: h(1), from: 2, to: 3 }]),
673
            (h(4), vec![(0, Vertex::Atom((h(3), 1)), Vertex::Atom((h(1), 1)), lines(&["s\n"]))], vec![AtomRange { change: h(1), from: 1, to: 2 }]),
674
            (h(5), vec![(0, Vertex::Atom((h(1), 2)), Vertex::End, lines(&["z\n"]))], vec![]),
675
        ];
676
        let deps = Deps(
677
            [(h(2), vec![h(1)]), (h(3), vec![h(1)]), (h(4), vec![h(3), h(1)]), (h(5), vec![h(1)])].into_iter().collect(),
678
        );
679
        let build = |order: &[usize]| {
680
            let mut g = FileGraph::new();
681
            for &i in order {
682
                let (c, ins, dels) = &ops[i];
683
                for (idx, a, b, l) in ins {
684
                    g.insert(*c, *idx, *a, *b, l);
685
                }
686
                for d in dels {
687
                    g.delete(*c, d);
688
                }
689
            }
690
            let m = g.materialize(&deps);
691
            (m.clone(), text(&g, &m))
692
        };
693
        let reference = build(&[0, 1, 2, 3, 4]);
694
        assert!(reference.0.is_conflicted());
695
        let mut rng = rand::thread_rng();
696
        let mut order: Vec<usize> = (0..ops.len()).collect();
697
        for _ in 0..50 {
698
            order.shuffle(&mut rng);
699
            assert_eq!(build(&order), reference, "order {order:?}");
700
        }
701
    }
702
}