OXYGENHUB
# 569802ebae0e oxygen / crates / oxy-core / src / rewrite.rs
27.0 KiB · Rust · last changed Keep an insert's lines in one shared buffer 1 month ago source blame history raw
1
//! Rewrite propagation (spec §8.4): when change `old` is replaced by `new`,
2
//! every change that depends on `old` is rewritten with its references
3
//! translated through a correspondence from `old`'s atoms and tree operations
4
//! to `new`'s. Rewritten changes keep their ChangeIds.
5
//!
6
//! Where a reference has no counterpart it is reified rather than refused:
7
//! an insert anchor moves to the nearest surviving atom of the same insert
8
//! chain in `old`, else to that chain's own anchors (an order conflict then
9
//! arises naturally if `new` placed other content there); deletions of
10
//! vanished atoms are dropped; operations and edits on nodes that vanished
11
//! are dropped. Every such adjustment is reported as a note.
12
13
use crate::hash::ChangeHash;
14
use crate::objects::{AtomRange, AtomRef, Change, Edit, OpRef, TreeOp};
15
use crate::state::State;
16
use std::collections::{BTreeSet, HashMap};
17
18
/// How `old`'s atoms and tree operations map onto `new`'s.
19
#[derive(Clone, Debug, Default, PartialEq, Eq)]
20
pub struct Correspondence {
21
    /// `(node, old atom index)` → new atom index. Absent means the atom no
22
    /// longer exists.
23
    pub atoms: HashMap<(crate::ids::NodeId, u32), u32>,
24
    /// Old tree-op index → new tree-op index.
25
    pub ops: HashMap<u32, u32>,
26
}
27
28
impl Correspondence {
29
    /// Every atom and operation keeps its index: `new` has the same edits as `old`.
30
    pub fn identity(change: &Change) -> Correspondence {
31
        let mut atoms = HashMap::new();
32
        let mut per_node: HashMap<crate::ids::NodeId, u32> = HashMap::new();
33
        for e in &change.edits {
34
            if let Edit::Insert { node, lines, .. } = e {
35
                let idx = per_node.entry(*node).or_default();
36
                for _ in lines.iter() {
37
                    atoms.insert((*node, *idx), *idx);
38
                    *idx += 1;
39
                }
40
            }
41
        }
42
        let ops = (0..change.tree_ops.len() as u32).map(|i| (i, i)).collect();
43
        Correspondence { atoms, ops }
44
    }
45
46
    /// Match `old`'s atoms to `new`'s by content, per node, in order (the
47
    /// correspondence of a re-derived change, spec §8.5); operations match
48
    /// by node and variant.
49
    pub fn by_content(old: &Change, new: &Change) -> Correspondence {
50
        let lines_of = |c: &Change| -> HashMap<crate::ids::NodeId, Vec<Vec<u8>>> {
51
            let mut m: HashMap<crate::ids::NodeId, Vec<Vec<u8>>> = HashMap::new();
52
            for e in &c.edits {
53
                if let Edit::Insert { node, lines, .. } = e {
54
                    m.entry(*node)
55
                        .or_default()
56
                        .extend(lines.iter().map(<[u8]>::to_vec));
57
                }
58
            }
59
            m
60
        };
61
        let (a, b) = (lines_of(old), lines_of(new));
62
        let mut atoms = HashMap::new();
63
        for (node, olds) in &a {
64
            let Some(news) = b.get(node) else { continue };
65
            let mut j = 0usize;
66
            for (i, line) in olds.iter().enumerate() {
67
                while j < news.len() && news[j] != *line {
68
                    j += 1;
69
                }
70
                if j < news.len() {
71
                    atoms.insert((*node, i as u32), j as u32);
72
                    j += 1;
73
                }
74
            }
75
        }
76
        let mut ops = HashMap::new();
77
        for (i, op) in old.tree_ops.iter().enumerate() {
78
            if let Some(j) = new.tree_ops.iter().position(|n| {
79
                n.node() == op.node() && std::mem::discriminant(n) == std::mem::discriminant(op)
80
            }) {
81
                ops.insert(i as u32, j as u32);
82
            }
83
        }
84
        Correspondence { atoms, ops }
85
    }
86
}
87
88
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
89
pub enum RewriteError {
90
    #[error("change {0} is not in the state")]
91
    Missing(ChangeHash),
92
}
93
94
/// The result of propagating a rewrite.
95
#[derive(Clone, Debug, Default)]
96
pub struct Propagation {
97
    /// `(old hash, rewritten change)` in dependency order.
98
    pub changes: Vec<(ChangeHash, Change)>,
99
    /// Human-readable descriptions of reified orphaned references.
100
    pub notes: Vec<String>,
101
}
102
103
/// Where each atom of a change was inserted: the chain it belongs to.
104
struct Chain {
105
    node: crate::ids::NodeId,
106
    first: u32,
107
    last: u32,
108
    after: AtomRef,
109
    before: AtomRef,
110
}
111
112
fn chains_of(c: &Change) -> Vec<Chain> {
113
    let mut per_node: HashMap<crate::ids::NodeId, u32> = HashMap::new();
114
    let mut out = Vec::new();
115
    for e in &c.edits {
116
        if let Edit::Insert {
117
            node,
118
            after,
119
            before,
120
            lines,
121
        } = e
122
        {
123
            let idx = per_node.entry(*node).or_default();
124
            out.push(Chain {
125
                node: *node,
126
                first: *idx,
127
                last: *idx + lines.len() as u32 - 1,
128
                after: *after,
129
                before: *before,
130
            });
131
            *idx += lines.len() as u32;
132
        }
133
    }
134
    out
135
}
136
137
/// What a rewritten change was replaced by.
138
#[derive(Clone, Debug)]
139
enum Replacement {
140
    Change(ChangeHash),
141
    /// Removed: dependents inherit these dependencies instead.
142
    Removed(Vec<ChangeHash>),
143
}
144
145
/// Rewrite every change in `state` that depends on `old`, given `new` (or
146
/// `None` when `old` is removed) and the correspondence between them.
147
/// The result lists `(old hash, rewritten change)` pairs in dependency
148
/// order, not including `old` → `new` itself, plus notes about reified
149
/// orphaned references.
150
///
151
/// `load` fetches a change's full object (a state keeps only metadata).
152
pub fn propagate(
153
    state: &State,
154
    old: ChangeHash,
155
    new: Option<&Change>,
156
    corr: &Correspondence,
157
    load: &dyn Fn(&ChangeHash) -> Option<Change>,
158
) -> Result<Propagation, RewriteError> {
159
    let old_meta = state.get(&old).ok_or(RewriteError::Missing(old))?;
160
    let old_full = load(&old);
161
    let mut mapping: HashMap<ChangeHash, Replacement> = HashMap::new();
162
    mapping.insert(
163
        old,
164
        match new {
165
            Some(n) => Replacement::Change(n.hash()),
166
            None => Replacement::Removed(old_meta.deps.clone()),
167
        },
168
    );
169
    let mut corrs: HashMap<ChangeHash, Correspondence> = HashMap::new();
170
    corrs.insert(old, corr.clone());
171
    // Chains of the rewritten change, for re-anchoring; vanished nodes.
172
    let chains: HashMap<ChangeHash, Vec<Chain>> = old_full
173
        .as_ref()
174
        .map(|c| (old, chains_of(c)))
175
        .into_iter()
176
        .collect();
177
    let mut vanished_nodes: BTreeSet<crate::ids::NodeId> = BTreeSet::new();
178
    if let Some(c) = &old_full {
179
        for (i, op) in c.tree_ops.iter().enumerate() {
180
            if let TreeOp::AddNode { node, .. } = op {
181
                let survives = new.is_some() && corr.ops.contains_key(&(i as u32));
182
                if !survives {
183
                    vanished_nodes.insert(*node);
184
                }
185
            }
186
        }
187
    }
188
    let mut notes = Vec::new();
189
190
    // Dependents in topological order: every change whose closure has `old`,
191
    // sorted so that dependencies come first.
192
    let mut dependents: Vec<ChangeHash> = state
193
        .changes()
194
        .filter(|(h, _)| **h != old && state.closure(h).contains(&old))
195
        .map(|(h, _)| *h)
196
        .collect();
197
    dependents.sort_by_key(|h| (state.closure(h).len(), *h));
198
199
    let mut out = Vec::new();
200
    for h in dependents {
201
        let c = load(&h).ok_or(RewriteError::Missing(h))?;
202
        let rewritten = rewrite_one(
203
            &h,
204
            &c,
205
            &mapping,
206
            &corrs,
207
            &chains,
208
            &vanished_nodes,
209
            &mut notes,
210
        );
211
        let rh = rewritten.hash();
212
        corrs.insert(h, Correspondence::identity(&rewritten));
213
        mapping.insert(h, Replacement::Change(rh));
214
        out.push((h, rewritten));
215
    }
216
    Ok(Propagation {
217
        changes: out,
218
        notes,
219
    })
220
}
221
222
/// Re-anchor a reference to a vanished atom of `change`: the nearest
223
/// surviving atom of its chain (before it for `after` anchors, after it for
224
/// `before` anchors), else the chain's own anchor.
225
fn reanchor(
226
    change: &ChangeHash,
227
    index: u32,
228
    want_after: bool,
229
    mapping: &HashMap<ChangeHash, Replacement>,
230
    corrs: &HashMap<ChangeHash, Correspondence>,
231
    chains: &HashMap<ChangeHash, Vec<Chain>>,
232
) -> AtomRef {
233
    let corr = corrs.get(change);
234
    let chain = chains
235
        .get(change)
236
        .and_then(|cs| cs.iter().find(|ch| ch.first <= index && index <= ch.last));
237
    if let (Some(Replacement::Change(nh)), Some(corr), Some(ch)) =
238
        (mapping.get(change), corr, chain)
239
    {
240
        let candidates: Box<dyn Iterator<Item = u32>> = if want_after {
241
            Box::new((ch.first..index).rev())
242
        } else {
243
            Box::new(index + 1..=ch.last)
244
        };
245
        for j in candidates {
246
            if let Some(nj) = corr.atoms.get(&(ch.node, j)) {
247
                return AtomRef::Atom {
248
                    change: *nh,
249
                    index: *nj,
250
                };
251
            }
252
        }
253
    }
254
    match chain {
255
        Some(ch) => {
256
            if want_after {
257
                ch.after
258
            } else {
259
                ch.before
260
            }
261
        }
262
        None => {
263
            if want_after {
264
                AtomRef::Start
265
            } else {
266
                AtomRef::End
267
            }
268
        }
269
    }
270
}
271
272
fn rewrite_one(
273
    hash: &ChangeHash,
274
    c: &Change,
275
    mapping: &HashMap<ChangeHash, Replacement>,
276
    corrs: &HashMap<ChangeHash, Correspondence>,
277
    chains: &HashMap<ChangeHash, Vec<Chain>>,
278
    vanished_nodes: &BTreeSet<crate::ids::NodeId>,
279
    notes: &mut Vec<String>,
280
) -> Change {
281
    let short = hash.short(12);
282
    // An anchor: translate, or re-anchor when its counterpart is gone.
283
    let map_anchor = |r: AtomRef,
284
                      node: &crate::ids::NodeId,
285
                      want_after: bool,
286
                      notes: &mut Vec<String>|
287
     -> AtomRef {
288
        match r {
289
            AtomRef::Atom { change, index } => match mapping.get(&change) {
290
                Some(Replacement::Change(nh)) => match corrs[&change].atoms.get(&(*node, index)) {
291
                    Some(idx) => AtomRef::Atom {
292
                        change: *nh,
293
                        index: *idx,
294
                    },
295
                    None => {
296
                        notes.push(format!("change {short}: an anchor line of the rewritten change vanished; re-anchored to its nearest neighbour"));
297
                        reanchor(&change, index, want_after, mapping, corrs, chains)
298
                    }
299
                },
300
                Some(Replacement::Removed(_)) => {
301
                    notes.push(format!("change {short}: an anchor line of the removed change vanished; re-anchored to its neighbours"));
302
                    reanchor(&change, index, want_after, mapping, corrs, chains)
303
                }
304
                None => r,
305
            },
306
            other => other,
307
        }
308
    };
309
    // A tree-operation reference: translate, or `None` when gone.
310
    let map_op = |r: OpRef| -> Option<OpRef> {
311
        match mapping.get(&r.change) {
312
            Some(Replacement::Change(nh)) => corrs[&r.change].ops.get(&r.index).map(|idx| OpRef {
313
                change: *nh,
314
                index: *idx,
315
            }),
316
            Some(Replacement::Removed(_)) => None,
317
            None => Some(r),
318
        }
319
    };
320
    let mut out = c.clone();
321
    let mut deps: BTreeSet<ChangeHash> = BTreeSet::new();
322
    for d in &c.deps {
323
        match mapping.get(d) {
324
            Some(Replacement::Change(nh)) => {
325
                deps.insert(*nh);
326
            }
327
            Some(Replacement::Removed(inherited)) => deps.extend(inherited.iter().copied()),
328
            None => {
329
                deps.insert(*d);
330
            }
331
        }
332
    }
333
    out.deps = deps.into_iter().collect();
334
    let mut ops = Vec::new();
335
    for op in &out.tree_ops {
336
        let node = op.node();
337
        if vanished_nodes.contains(&node)
338
            || matches!(op, TreeOp::AddNode { parent, .. } if vanished_nodes.contains(parent))
339
        {
340
            notes.push(format!("change {short}: an operation on a file that vanished with the rewritten change was dropped"));
341
            continue;
342
        }
343
        match op {
344
            TreeOp::RemoveNode { node, placement } => match map_op(*placement) {
345
                Some(p) => ops.push(TreeOp::RemoveNode {
346
                    node: *node,
347
                    placement: p,
348
                }),
349
                None => notes.push(format!(
350
                    "change {short}: removal of a placement that vanished was dropped"
351
                )),
352
            },
353
            TreeOp::Move {
354
                node,
355
                old,
356
                parent,
357
                name,
358
            } => match map_op(*old) {
359
                Some(o) => ops.push(TreeOp::Move {
360
                    node: *node,
361
                    old: o,
362
                    parent: *parent,
363
                    name: name.clone(),
364
                }),
365
                None => notes.push(format!(
366
                    "change {short}: a move of {name} lost its origin and was dropped"
367
                )),
368
            },
369
            TreeOp::SetMode { node, old, mode } => match map_op(*old) {
370
                Some(o) => ops.push(TreeOp::SetMode {
371
                    node: *node,
372
                    old: o,
373
                    mode: *mode,
374
                }),
375
                None => notes.push(format!(
376
                    "change {short}: a mode change lost its origin and was dropped"
377
                )),
378
            },
379
            add @ TreeOp::AddNode { .. } => ops.push(add.clone()),
380
        }
381
    }
382
    out.tree_ops = ops;
383
    let mut edits = Vec::new();
384
    for e in &out.edits {
385
        if vanished_nodes.contains(&e.node()) {
386
            notes.push(format!("change {short}: an edit of a file that vanished with the rewritten change was dropped"));
387
            continue;
388
        }
389
        match e {
390
            Edit::Insert {
391
                node,
392
                after,
393
                before,
394
                lines,
395
            } => edits.push(Edit::Insert {
396
                node: *node,
397
                after: map_anchor(*after, node, true, notes),
398
                before: map_anchor(*before, node, false, notes),
399
                lines: lines.clone(),
400
            }),
401
            Edit::Delete { node, atoms } => {
402
                let mut mapped: Vec<(ChangeHash, u32)> = Vec::new();
403
                let mut dropped = 0;
404
                for r in atoms.iter() {
405
                    for i in r.from..r.to {
406
                        match mapping.get(&r.change) {
407
                            Some(Replacement::Change(nh)) => {
408
                                match corrs[&r.change].atoms.get(&(*node, i)) {
409
                                    Some(idx) => mapped.push((*nh, *idx)),
410
                                    None => dropped += 1,
411
                                }
412
                            }
413
                            Some(Replacement::Removed(_)) => dropped += 1,
414
                            None => mapped.push((r.change, i)),
415
                        }
416
                    }
417
                }
418
                if dropped > 0 {
419
                    notes.push(format!(
420
                        "change {short}: {dropped} deletion(s) of lines that vanished were dropped"
421
                    ));
422
                }
423
                let mut ranges: Vec<AtomRange> = Vec::new();
424
                for (ch, i) in mapped {
425
                    match ranges.last_mut() {
426
                        Some(r) if r.change == ch && r.to == i => r.to = i + 1,
427
                        _ => ranges.push(AtomRange {
428
                            change: ch,
429
                            from: i,
430
                            to: i + 1,
431
                        }),
432
                    }
433
                }
434
                if !ranges.is_empty() {
435
                    edits.push(Edit::Delete {
436
                        node: *node,
437
                        atoms: ranges,
438
                    });
439
                }
440
            }
441
            other => edits.push(other.clone()),
442
        }
443
    }
444
    out.edits = edits;
445
    out.normalize();
446
    out
447
}
448
449
#[cfg(test)]
450
mod tests {
451
    use super::*;
452
    use crate::hash::ObjectRef;
453
    use crate::ids::{ChangeId, NodeId};
454
    use crate::objects::{ChangeKind, Hlc, Mode, NodeKind};
455
    use crate::state::FileContent;
456
457
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>, msg: &str) -> Change {
458
        let mut c = Change {
459
            version: crate::objects::CHANGE_VERSION,
460
            change_id: ChangeId::random(),
461
            deps,
462
            author: ObjectRef::ZERO,
463
            hlc: Hlc {
464
                wall_ms: 1,
465
                logical: 0,
466
            },
467
            message: msg.into(),
468
            tree_ops,
469
            edits,
470
            kind: ChangeKind::Normal,
471
        };
472
        c.normalize();
473
        c
474
    }
475
476
    #[test]
477
    fn describe_propagates_through_dependents() {
478
        let mut st = State::new();
479
        let f = NodeId::random();
480
        let c1 = change(
481
            vec![],
482
            vec![TreeOp::AddNode {
483
                node: f,
484
                kind: NodeKind::File,
485
                mode: Mode::TEXT,
486
                parent: NodeId::ROOT,
487
                name: "f".into(),
488
            }],
489
            vec![Edit::Insert {
490
                node: f,
491
                after: AtomRef::Start,
492
                before: AtomRef::End,
493
                lines: vec![b"a\n".to_vec(), b"b\n".to_vec()].into(),
494
            }],
495
            "one",
496
        );
497
        let h1 = st.add(c1.clone()).unwrap();
498
        let c2 = change(
499
            vec![h1],
500
            vec![],
501
            vec![
502
                Edit::Delete {
503
                    node: f,
504
                    atoms: vec![AtomRange {
505
                        change: h1,
506
                        from: 0,
507
                        to: 1,
508
                    }],
509
                },
510
                Edit::Insert {
511
                    node: f,
512
                    after: AtomRef::Atom {
513
                        change: h1,
514
                        index: 1,
515
                    },
516
                    before: AtomRef::End,
517
                    lines: vec![b"c\n".to_vec()].into(),
518
                },
519
            ],
520
            "two",
521
        );
522
        let h2 = st.add(c2.clone()).unwrap();
523
        let c3 = change(
524
            vec![h2],
525
            vec![TreeOp::SetMode {
526
                node: f,
527
                old: OpRef {
528
                    change: h1,
529
                    index: 0,
530
                },
531
                mode: Mode {
532
                    exec: true,
533
                    opaque: false,
534
                },
535
            }],
536
            vec![Edit::Insert {
537
                node: f,
538
                after: AtomRef::Atom {
539
                    change: h2,
540
                    index: 0,
541
                },
542
                before: AtomRef::End,
543
                lines: vec![b"d\n".to_vec()].into(),
544
            }],
545
            "three",
546
        );
547
        let h3 = st.add(c3.clone()).unwrap();
548
        let before = st.materialize();
549
550
        // Rewrite c1's message; everything downstream follows.
551
        let mut n1 = c1.clone();
552
        n1.message = "one, described".into();
553
        let all: HashMap<ChangeHash, Change> =
554
            [(h1, c1.clone()), (h2, c2.clone()), (h3, c3.clone())]
555
                .into_iter()
556
                .collect();
557
        let load = |h: &ChangeHash| all.get(h).cloned();
558
        let rewritten = propagate(&st, h1, Some(&n1), &Correspondence::identity(&c1), &load)
559
            .unwrap()
560
            .changes;
561
        assert_eq!(
562
            rewritten.iter().map(|(h, _)| *h).collect::<Vec<_>>(),
563
            vec![h2, h3]
564
        );
565
        let mut st2 = State::new();
566
        st2.add(n1.clone()).unwrap();
567
        for (_, c) in &rewritten {
568
            st2.add(c.clone()).unwrap();
569
        }
570
        assert_eq!(st2.len(), 3);
571
        let after = st2.materialize();
572
        assert_eq!(after.files["f"].mode, before.files["f"].mode);
573
        match (&after.files["f"].content, &before.files["f"].content) {
574
            (FileContent::Text { bytes: a, .. }, FileContent::Text { bytes: b, .. }) => {
575
                assert_eq!(a, b)
576
            }
577
            _ => panic!(),
578
        }
579
        assert!(
580
            matches!(&after.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"b\nc\nd\n")
581
        );
582
        // ChangeIds survive; hashes change; deps point at the new hashes.
583
        assert_eq!(rewritten[0].1.change_id, c2.change_id);
584
        assert_eq!(rewritten[0].1.deps, vec![n1.hash()]);
585
        assert_ne!(rewritten[0].1.hash(), h2);
586
        assert_eq!(rewritten[1].1.deps, vec![rewritten[0].1.hash()]);
587
    }
588
589
    #[test]
590
    fn missing_counterpart_is_reanchored() {
591
        let mut st = State::new();
592
        let f = NodeId::random();
593
        let c1 = change(
594
            vec![],
595
            vec![TreeOp::AddNode {
596
                node: f,
597
                kind: NodeKind::File,
598
                mode: Mode::TEXT,
599
                parent: NodeId::ROOT,
600
                name: "f".into(),
601
            }],
602
            vec![Edit::Insert {
603
                node: f,
604
                after: AtomRef::Start,
605
                before: AtomRef::End,
606
                lines: vec![b"a\n".to_vec(), b"b\n".to_vec()].into(),
607
            }],
608
            "one",
609
        );
610
        let h1 = st.add(c1.clone()).unwrap();
611
        let c2 = change(
612
            vec![h1],
613
            vec![],
614
            vec![Edit::Insert {
615
                node: f,
616
                after: AtomRef::Atom {
617
                    change: h1,
618
                    index: 1,
619
                },
620
                before: AtomRef::End,
621
                lines: vec![b"c\n".to_vec()].into(),
622
            }],
623
            "two",
624
        );
625
        let h2 = st.add(c2.clone()).unwrap();
626
        let all: HashMap<ChangeHash, Change> = [(h1, c1.clone()), (h2, c2)].into_iter().collect();
627
        let load = |h: &ChangeHash| all.get(h).cloned();
628
        // The rewritten c1 drops line "b": atom 1 has no counterpart, so c2's
629
        // insert after "b" is re-anchored after "a", the nearest survivor.
630
        let mut n1 = c1.clone();
631
        n1.edits = vec![Edit::Insert {
632
            node: f,
633
            after: AtomRef::Start,
634
            before: AtomRef::End,
635
            lines: vec![b"a\n".to_vec()].into(),
636
        }];
637
        let corr = Correspondence {
638
            atoms: [((f, 0), 0)].into_iter().collect(),
639
            ops: [(0, 0)].into_iter().collect(),
640
        };
641
        let p = propagate(&st, h1, Some(&n1), &corr, &load).unwrap();
642
        assert_eq!(p.notes.len(), 1, "{:?}", p.notes);
643
        let n1h = n1.hash();
644
        assert!(
645
            matches!(p.changes[0].1.edits[0], Edit::Insert { after: AtomRef::Atom { change, index: 0 }, before: AtomRef::End, .. } if change == n1h)
646
        );
647
        let mut st2 = State::new();
648
        st2.add(n1.clone()).unwrap();
649
        st2.add(p.changes[0].1.clone()).unwrap();
650
        let m = st2.materialize();
651
        assert!(!m.is_conflicted());
652
        assert!(
653
            matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"a\nc\n")
654
        );
655
        assert_eq!(
656
            propagate(&st, ObjectRef([9; 32]), Some(&n1), &corr, &load).unwrap_err(),
657
            RewriteError::Missing(ObjectRef([9; 32]))
658
        );
659
660
        // Removing c1 altogether: f vanishes with it, so c2's edit is dropped.
661
        let p = propagate(&st, h1, None, &Correspondence::default(), &load).unwrap();
662
        assert_eq!(p.notes.len(), 1, "{:?}", p.notes);
663
        assert!(p.changes[0].1.edits.is_empty() && p.changes[0].1.deps.is_empty());
664
665
        // Rewriting c1 to keep the file but none of its lines: c2's line goes
666
        // to c1's chain anchors (START, END).
667
        let mut n2 = c1.clone();
668
        n2.edits = vec![];
669
        let corr = Correspondence {
670
            atoms: HashMap::new(),
671
            ops: [(0, 0)].into_iter().collect(),
672
        };
673
        let p = propagate(&st, h1, Some(&n2), &corr, &load).unwrap();
674
        assert!(matches!(
675
            p.changes[0].1.edits[0],
676
            Edit::Insert {
677
                after: AtomRef::Start,
678
                before: AtomRef::End,
679
                ..
680
            }
681
        ));
682
        let mut st3 = State::new();
683
        st3.add(n2.clone()).unwrap();
684
        st3.add(p.changes[0].1.clone()).unwrap();
685
        let m = st3.materialize();
686
        assert!(!m.is_conflicted());
687
        assert!(
688
            matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"c\n")
689
        );
690
    }
691
692
    #[test]
693
    fn removal_rewires_dependencies_or_fails_on_references() {
694
        let mut st = State::new();
695
        let f = NodeId::random();
696
        let g = NodeId::random();
697
        let c1 = change(
698
            vec![],
699
            vec![TreeOp::AddNode {
700
                node: f,
701
                kind: NodeKind::File,
702
                mode: Mode::TEXT,
703
                parent: NodeId::ROOT,
704
                name: "f".into(),
705
            }],
706
            vec![],
707
            "one",
708
        );
709
        let h1 = st.add(c1.clone()).unwrap();
710
        // c2 merely depends on c1 explicitly (touches another file).
711
        let c2 = change(
712
            vec![h1],
713
            vec![TreeOp::AddNode {
714
                node: g,
715
                kind: NodeKind::File,
716
                mode: Mode::TEXT,
717
                parent: NodeId::ROOT,
718
                name: "g".into(),
719
            }],
720
            vec![],
721
            "two",
722
        );
723
        let h2 = st.add(c2.clone()).unwrap();
724
        let c3 = change(
725
            vec![h2],
726
            vec![],
727
            vec![Edit::Insert {
728
                node: g,
729
                after: AtomRef::Start,
730
                before: AtomRef::End,
731
                lines: vec![b"x\n".to_vec()].into(),
732
            }],
733
            "three",
734
        );
735
        let h3 = st.add(c3.clone()).unwrap();
736
        let mut all: HashMap<ChangeHash, Change> =
737
            [(h1, c1), (h2, c2), (h3, c3)].into_iter().collect();
738
        let load_all = all.clone();
739
        let load = move |h: &ChangeHash| load_all.get(h).cloned();
740
        // Removing c2 (an explicit-only dependency of nothing that references it) works: c3 inherits h1.
741
        let out = propagate(&st, h2, None, &Correspondence::default(), &load)
742
            .unwrap()
743
            .changes;
744
        assert_eq!(out.len(), 1);
745
        assert_eq!(out[0].0, h3);
746
        assert_eq!(out[0].1.deps, vec![h1]);
747
        // Removing c1 fails: c2 does not reference c1 (only an explicit dep), but
748
        // nothing else does either, so it succeeds and c2 inherits no deps.
749
        let out = propagate(&st, h1, None, &Correspondence::default(), &load)
750
            .unwrap()
751
            .changes;
752
        assert!(out.iter().any(|(h, c)| *h == h2 && c.deps.is_empty()));
753
        // Removing c2, which created g: c3's edit of g and c4's removal of g's
754
        // placement are dropped with notes, since the file no longer exists.
755
        let c4 = change(
756
            vec![h2],
757
            vec![TreeOp::RemoveNode {
758
                node: g,
759
                placement: OpRef {
760
                    change: h2,
761
                    index: 0,
762
                },
763
            }],
764
            vec![],
765
            "four",
766
        );
767
        let h4 = st.add(c4.clone()).unwrap();
768
        all.insert(h4, c4);
769
        let load2 = |h: &ChangeHash| all.get(h).cloned();
770
        let p = propagate(&st, h2, None, &Correspondence::default(), &load2).unwrap();
771
        assert_eq!(p.notes.len(), 2, "{:?}", p.notes);
772
        for (_, c) in &p.changes {
773
            assert!(c.edits.is_empty() && c.tree_ops.is_empty());
774
        }
775
    }
776
}