OXYGENHUB
# 17f94b13ae54 oxygen / crates / oxy-core / src / rewrite.rs
26.9 KiB · Rust · last changed Store large inserts as text blobs (change format 2) 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
            version: crate::objects::CHANGE_VERSION,
458
            change_id: ChangeId::random(),
459
            deps,
460
            author: ObjectRef::ZERO,
461
            hlc: Hlc {
462
                wall_ms: 1,
463
                logical: 0,
464
            },
465
            message: msg.into(),
466
            tree_ops,
467
            edits,
468
            kind: ChangeKind::Normal,
469
        };
470
        c.normalize();
471
        c
472
    }
473
474
    #[test]
475
    fn describe_propagates_through_dependents() {
476
        let mut st = State::new();
477
        let f = NodeId::random();
478
        let c1 = change(
479
            vec![],
480
            vec![TreeOp::AddNode {
481
                node: f,
482
                kind: NodeKind::File,
483
                mode: Mode::TEXT,
484
                parent: NodeId::ROOT,
485
                name: "f".into(),
486
            }],
487
            vec![Edit::Insert {
488
                node: f,
489
                after: AtomRef::Start,
490
                before: AtomRef::End,
491
                lines: vec![b"a\n".to_vec(), b"b\n".to_vec()],
492
            }],
493
            "one",
494
        );
495
        let h1 = st.add(c1.clone()).unwrap();
496
        let c2 = change(
497
            vec![h1],
498
            vec![],
499
            vec![
500
                Edit::Delete {
501
                    node: f,
502
                    atoms: vec![AtomRange {
503
                        change: h1,
504
                        from: 0,
505
                        to: 1,
506
                    }],
507
                },
508
                Edit::Insert {
509
                    node: f,
510
                    after: AtomRef::Atom {
511
                        change: h1,
512
                        index: 1,
513
                    },
514
                    before: AtomRef::End,
515
                    lines: vec![b"c\n".to_vec()],
516
                },
517
            ],
518
            "two",
519
        );
520
        let h2 = st.add(c2.clone()).unwrap();
521
        let c3 = change(
522
            vec![h2],
523
            vec![TreeOp::SetMode {
524
                node: f,
525
                old: OpRef {
526
                    change: h1,
527
                    index: 0,
528
                },
529
                mode: Mode {
530
                    exec: true,
531
                    opaque: false,
532
                },
533
            }],
534
            vec![Edit::Insert {
535
                node: f,
536
                after: AtomRef::Atom {
537
                    change: h2,
538
                    index: 0,
539
                },
540
                before: AtomRef::End,
541
                lines: vec![b"d\n".to_vec()],
542
            }],
543
            "three",
544
        );
545
        let h3 = st.add(c3.clone()).unwrap();
546
        let before = st.materialize();
547
548
        // Rewrite c1's message; everything downstream follows.
549
        let mut n1 = c1.clone();
550
        n1.message = "one, described".into();
551
        let all: HashMap<ChangeHash, Change> =
552
            [(h1, c1.clone()), (h2, c2.clone()), (h3, c3.clone())]
553
                .into_iter()
554
                .collect();
555
        let load = |h: &ChangeHash| all.get(h).cloned();
556
        let rewritten = propagate(&st, h1, Some(&n1), &Correspondence::identity(&c1), &load)
557
            .unwrap()
558
            .changes;
559
        assert_eq!(
560
            rewritten.iter().map(|(h, _)| *h).collect::<Vec<_>>(),
561
            vec![h2, h3]
562
        );
563
        let mut st2 = State::new();
564
        st2.add(n1.clone()).unwrap();
565
        for (_, c) in &rewritten {
566
            st2.add(c.clone()).unwrap();
567
        }
568
        assert_eq!(st2.len(), 3);
569
        let after = st2.materialize();
570
        assert_eq!(after.files["f"].mode, before.files["f"].mode);
571
        match (&after.files["f"].content, &before.files["f"].content) {
572
            (FileContent::Text { bytes: a, .. }, FileContent::Text { bytes: b, .. }) => {
573
                assert_eq!(a, b)
574
            }
575
            _ => panic!(),
576
        }
577
        assert!(
578
            matches!(&after.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"b\nc\nd\n")
579
        );
580
        // ChangeIds survive; hashes change; deps point at the new hashes.
581
        assert_eq!(rewritten[0].1.change_id, c2.change_id);
582
        assert_eq!(rewritten[0].1.deps, vec![n1.hash()]);
583
        assert_ne!(rewritten[0].1.hash(), h2);
584
        assert_eq!(rewritten[1].1.deps, vec![rewritten[0].1.hash()]);
585
    }
586
587
    #[test]
588
    fn missing_counterpart_is_reanchored() {
589
        let mut st = State::new();
590
        let f = NodeId::random();
591
        let c1 = change(
592
            vec![],
593
            vec![TreeOp::AddNode {
594
                node: f,
595
                kind: NodeKind::File,
596
                mode: Mode::TEXT,
597
                parent: NodeId::ROOT,
598
                name: "f".into(),
599
            }],
600
            vec![Edit::Insert {
601
                node: f,
602
                after: AtomRef::Start,
603
                before: AtomRef::End,
604
                lines: vec![b"a\n".to_vec(), b"b\n".to_vec()],
605
            }],
606
            "one",
607
        );
608
        let h1 = st.add(c1.clone()).unwrap();
609
        let c2 = change(
610
            vec![h1],
611
            vec![],
612
            vec![Edit::Insert {
613
                node: f,
614
                after: AtomRef::Atom {
615
                    change: h1,
616
                    index: 1,
617
                },
618
                before: AtomRef::End,
619
                lines: vec![b"c\n".to_vec()],
620
            }],
621
            "two",
622
        );
623
        let h2 = st.add(c2.clone()).unwrap();
624
        let all: HashMap<ChangeHash, Change> = [(h1, c1.clone()), (h2, c2)].into_iter().collect();
625
        let load = |h: &ChangeHash| all.get(h).cloned();
626
        // The rewritten c1 drops line "b": atom 1 has no counterpart, so c2's
627
        // insert after "b" is re-anchored after "a", the nearest survivor.
628
        let mut n1 = c1.clone();
629
        n1.edits = vec![Edit::Insert {
630
            node: f,
631
            after: AtomRef::Start,
632
            before: AtomRef::End,
633
            lines: vec![b"a\n".to_vec()],
634
        }];
635
        let corr = Correspondence {
636
            atoms: [((f, 0), 0)].into_iter().collect(),
637
            ops: [(0, 0)].into_iter().collect(),
638
        };
639
        let p = propagate(&st, h1, Some(&n1), &corr, &load).unwrap();
640
        assert_eq!(p.notes.len(), 1, "{:?}", p.notes);
641
        let n1h = n1.hash();
642
        assert!(
643
            matches!(p.changes[0].1.edits[0], Edit::Insert { after: AtomRef::Atom { change, index: 0 }, before: AtomRef::End, .. } if change == n1h)
644
        );
645
        let mut st2 = State::new();
646
        st2.add(n1.clone()).unwrap();
647
        st2.add(p.changes[0].1.clone()).unwrap();
648
        let m = st2.materialize();
649
        assert!(!m.is_conflicted());
650
        assert!(
651
            matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"a\nc\n")
652
        );
653
        assert_eq!(
654
            propagate(&st, ObjectRef([9; 32]), Some(&n1), &corr, &load).unwrap_err(),
655
            RewriteError::Missing(ObjectRef([9; 32]))
656
        );
657
658
        // Removing c1 altogether: f vanishes with it, so c2's edit is dropped.
659
        let p = propagate(&st, h1, None, &Correspondence::default(), &load).unwrap();
660
        assert_eq!(p.notes.len(), 1, "{:?}", p.notes);
661
        assert!(p.changes[0].1.edits.is_empty() && p.changes[0].1.deps.is_empty());
662
663
        // Rewriting c1 to keep the file but none of its lines: c2's line goes
664
        // to c1's chain anchors (START, END).
665
        let mut n2 = c1.clone();
666
        n2.edits = vec![];
667
        let corr = Correspondence {
668
            atoms: HashMap::new(),
669
            ops: [(0, 0)].into_iter().collect(),
670
        };
671
        let p = propagate(&st, h1, Some(&n2), &corr, &load).unwrap();
672
        assert!(matches!(
673
            p.changes[0].1.edits[0],
674
            Edit::Insert {
675
                after: AtomRef::Start,
676
                before: AtomRef::End,
677
                ..
678
            }
679
        ));
680
        let mut st3 = State::new();
681
        st3.add(n2.clone()).unwrap();
682
        st3.add(p.changes[0].1.clone()).unwrap();
683
        let m = st3.materialize();
684
        assert!(!m.is_conflicted());
685
        assert!(
686
            matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"c\n")
687
        );
688
    }
689
690
    #[test]
691
    fn removal_rewires_dependencies_or_fails_on_references() {
692
        let mut st = State::new();
693
        let f = NodeId::random();
694
        let g = NodeId::random();
695
        let c1 = change(
696
            vec![],
697
            vec![TreeOp::AddNode {
698
                node: f,
699
                kind: NodeKind::File,
700
                mode: Mode::TEXT,
701
                parent: NodeId::ROOT,
702
                name: "f".into(),
703
            }],
704
            vec![],
705
            "one",
706
        );
707
        let h1 = st.add(c1.clone()).unwrap();
708
        // c2 merely depends on c1 explicitly (touches another file).
709
        let c2 = change(
710
            vec![h1],
711
            vec![TreeOp::AddNode {
712
                node: g,
713
                kind: NodeKind::File,
714
                mode: Mode::TEXT,
715
                parent: NodeId::ROOT,
716
                name: "g".into(),
717
            }],
718
            vec![],
719
            "two",
720
        );
721
        let h2 = st.add(c2.clone()).unwrap();
722
        let c3 = change(
723
            vec![h2],
724
            vec![],
725
            vec![Edit::Insert {
726
                node: g,
727
                after: AtomRef::Start,
728
                before: AtomRef::End,
729
                lines: vec![b"x\n".to_vec()],
730
            }],
731
            "three",
732
        );
733
        let h3 = st.add(c3.clone()).unwrap();
734
        let mut all: HashMap<ChangeHash, Change> =
735
            [(h1, c1), (h2, c2), (h3, c3)].into_iter().collect();
736
        let load_all = all.clone();
737
        let load = move |h: &ChangeHash| load_all.get(h).cloned();
738
        // Removing c2 (an explicit-only dependency of nothing that references it) works: c3 inherits h1.
739
        let out = propagate(&st, h2, None, &Correspondence::default(), &load)
740
            .unwrap()
741
            .changes;
742
        assert_eq!(out.len(), 1);
743
        assert_eq!(out[0].0, h3);
744
        assert_eq!(out[0].1.deps, vec![h1]);
745
        // Removing c1 fails: c2 does not reference c1 (only an explicit dep), but
746
        // nothing else does either, so it succeeds and c2 inherits no deps.
747
        let out = propagate(&st, h1, None, &Correspondence::default(), &load)
748
            .unwrap()
749
            .changes;
750
        assert!(out.iter().any(|(h, c)| *h == h2 && c.deps.is_empty()));
751
        // Removing c2, which created g: c3's edit of g and c4's removal of g's
752
        // placement are dropped with notes, since the file no longer exists.
753
        let c4 = change(
754
            vec![h2],
755
            vec![TreeOp::RemoveNode {
756
                node: g,
757
                placement: OpRef {
758
                    change: h2,
759
                    index: 0,
760
                },
761
            }],
762
            vec![],
763
            "four",
764
        );
765
        let h4 = st.add(c4.clone()).unwrap();
766
        all.insert(h4, c4);
767
        let load2 = |h: &ChangeHash| all.get(h).cloned();
768
        let p = propagate(&st, h2, None, &Correspondence::default(), &load2).unwrap();
769
        assert_eq!(p.notes.len(), 2, "{:?}", p.notes);
770
        for (_, c) in &p.changes {
771
            assert!(c.edits.is_empty() && c.tree_ops.is_empty());
772
        }
773
    }
774
}