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