OXYGENHUB

Coverage / crates/oxy-core/src/rewrite.rs 91.6%

536 of 585 lines run, 25 of 43 functions · run #302 on main · run missed

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`.
3045×
    pub fn identity(change: &Change) -> Correspondence {
3145×
        let mut atoms = HashMap::new();
3245×
        let mut per_node: HashMap<crate::ids::NodeId, u32> = HashMap::new();
3347×
        for e in &change.edits {
3447×
            if let Edit::Insert { node, lines, .. } = e {
3542×
                let idx = per_node.entry(*node).or_default();
3646×
                for _ in lines.iter() {
3746×
                    atoms.insert((*node, *idx), *idx);
3846×
                    *idx += 1;
3946×
                }
405×
            }
41
        }
4245×
        let ops = (0..change.tree_ops.len() as u32).map(|i| (i, i)).collect();
4345×
        Correspondence { atoms, ops }
4445×
    }
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.
491×
    pub fn by_content(old: &Change, new: &Change) -> Correspondence {
502×
        let lines_of = |c: &Change| -> HashMap<crate::ids::NodeId, Vec<Vec<u8>>> {
512×
            let mut m: HashMap<crate::ids::NodeId, Vec<Vec<u8>>> = HashMap::new();
522×
            for e in &c.edits {
532×
                if let Edit::Insert { node, lines, .. } = e {
542×
                    m.entry(*node)
552×
                        .or_default()
562×
                        .extend(lines.iter().map(<[u8]>::to_vec));
572×
                }
58
            }
592×
            m
602×
        };
611×
        let (a, b) = (lines_of(old), lines_of(new));
621×
        let mut atoms = HashMap::new();
631×
        for (node, olds) in &a {
641×
            let Some(news) = b.get(node) else { continue };
651×
            let mut j = 0usize;
661×
            for (i, line) in olds.iter().enumerate() {
671×
                while j < news.len() && news[j] != *line {
680×
                    j += 1;
690×
                }
701×
                if j < news.len() {
711×
                    atoms.insert((*node, i as u32), j as u32);
721×
                    j += 1;
731×
                }
74
            }
75
        }
761×
        let mut ops = HashMap::new();
771×
        for (i, op) in old.tree_ops.iter().enumerate() {
780×
            if let Some(j) = new.tree_ops.iter().position(|n| {
790×
                n.node() == op.node() && std::mem::discriminant(n) == std::mem::discriminant(op)
800×
            }) {
810×
                ops.insert(i as u32, j as u32);
820×
            }
83
        }
841×
        Correspondence { atoms, ops }
851×
    }
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
11218×
fn chains_of(c: &Change) -> Vec<Chain> {
11318×
    let mut per_node: HashMap<crate::ids::NodeId, u32> = HashMap::new();
11418×
    let mut out = Vec::new();
11524×
    for e in &c.edits {
116
        if let Edit::Insert {
11719×
            node,
11819×
            after,
11919×
            before,
12019×
            lines,
12124×
        } = e
12219×
        {
12319×
            let idx = per_node.entry(*node).or_default();
12419×
            out.push(Chain {
12519×
                node: *node,
12619×
                first: *idx,
12719×
                last: *idx + lines.len() as u32 - 1,
12819×
                after: *after,
12919×
                before: *before,
13019×
            });
13119×
            *idx += lines.len() as u32;
13219×
        }
133
    }
13418×
    out
13518×
}
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).
15219×
pub fn propagate(
15319×
    state: &State,
15419×
    old: ChangeHash,
15519×
    new: Option<&Change>,
15619×
    corr: &Correspondence,
15719×
    load: &dyn Fn(&ChangeHash) -> Option<Change>,
15819×
) -> Result<Propagation, RewriteError> {
15919×
    let old_meta = state.get(&old).ok_or(RewriteError::Missing(old))?;
16018×
    let old_full = load(&old);
16118×
    let mut mapping: HashMap<ChangeHash, Replacement> = HashMap::new();
16218×
    mapping.insert(
16318×
        old,
16418×
        match new {
16512×
            Some(n) => Replacement::Change(n.hash()),
1666×
            None => Replacement::Removed(old_meta.deps.clone()),
167
        },
168
    );
16918×
    let mut corrs: HashMap<ChangeHash, Correspondence> = HashMap::new();
17018×
    corrs.insert(old, corr.clone());
171
    // Chains of the rewritten change, for re-anchoring; vanished nodes.
17218×
    let chains: HashMap<ChangeHash, Vec<Chain>> = old_full
17318×
        .as_ref()
17418×
        .map(|c| (old, chains_of(c)))
17518×
        .into_iter()
17618×
        .collect();
17718×
    let mut vanished_nodes: BTreeSet<crate::ids::NodeId> = BTreeSet::new();
17818×
    if let Some(c) = &old_full {
17926×
        for (i, op) in c.tree_ops.iter().enumerate() {
18026×
            if let TreeOp::AddNode { node, .. } = op {
18126×
                let survives = new.is_some() && corr.ops.contains_key(&(i as u32));
18226×
                if !survives {
18313×
                    vanished_nodes.insert(*node);
18413×
                }
1850×
            }
186
        }
1870×
    }
18818×
    let mut notes = Vec::new();
189
190
    // Dependents in topological order: every change whose closure has `old`,
191
    // sorted so that dependencies come first.
19218×
    let mut dependents: Vec<ChangeHash> = state
19318×
        .changes()
19472×
        .filter(|(h, _)| **h != old && state.closure(h).contains(&old))
19518×
        .map(|(h, _)| *h)
19618×
        .collect();
19788×
    dependents.sort_by_key(|h| (state.closure(h).len(), *h));
198
19918×
    let mut out = Vec::new();
20037×
    for h in dependents {
20137×
        let c = load(&h).ok_or(RewriteError::Missing(h))?;
20237×
        let rewritten = rewrite_one(
20337×
            &h,
20437×
            &c,
20537×
            &mapping,
20637×
            &corrs,
20737×
            &chains,
20837×
            &vanished_nodes,
20937×
            &mut notes,
210
        );
21137×
        let rh = rewritten.hash();
21237×
        corrs.insert(h, Correspondence::identity(&rewritten));
21337×
        mapping.insert(h, Replacement::Change(rh));
21437×
        out.push((h, rewritten));
215
    }
21618×
    Ok(Propagation {
21718×
        changes: out,
21818×
        notes,
21918×
    })
22019×
}
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.
2252×
fn reanchor(
2262×
    change: &ChangeHash,
2272×
    index: u32,
2282×
    want_after: bool,
2292×
    mapping: &HashMap<ChangeHash, Replacement>,
2302×
    corrs: &HashMap<ChangeHash, Correspondence>,
2312×
    chains: &HashMap<ChangeHash, Vec<Chain>>,
2322×
) -> AtomRef {
2332×
    let corr = corrs.get(change);
2342×
    let chain = chains
2352×
        .get(change)
2362×
        .and_then(|cs| cs.iter().find(|ch| ch.first <= index && index <= ch.last));
2372×
    if let (Some(Replacement::Change(nh)), Some(corr), Some(ch)) =
2382×
        (mapping.get(change), corr, chain)
239
    {
2402×
        let candidates: Box<dyn Iterator<Item = u32>> = if want_after {
2412×
            Box::new((ch.first..index).rev())
242
        } else {
2430×
            Box::new(index + 1..=ch.last)
244
        };
2452×
        for j in candidates {
2462×
            if let Some(nj) = corr.atoms.get(&(ch.node, j)) {
2471×
                return AtomRef::Atom {
2481×
                    change: *nh,
2491×
                    index: *nj,
2501×
                };
2511×
            }
252
        }
2530×
    }
2541×
    match chain {
2551×
        Some(ch) => {
2561×
            if want_after {
2571×
                ch.after
258
            } else {
2590×
                ch.before
260
            }
261
        }
262
        None => {
2630×
            if want_after {
2640×
                AtomRef::Start
265
            } else {
2660×
                AtomRef::End
267
            }
268
        }
269
    }
2702×
}
271
27237×
fn rewrite_one(
27337×
    hash: &ChangeHash,
27437×
    c: &Change,
27537×
    mapping: &HashMap<ChangeHash, Replacement>,
27637×
    corrs: &HashMap<ChangeHash, Correspondence>,
27737×
    chains: &HashMap<ChangeHash, Vec<Chain>>,
27837×
    vanished_nodes: &BTreeSet<crate::ids::NodeId>,
27937×
    notes: &mut Vec<String>,
28037×
) -> Change {
28137×
    let short = hash.short(12);
282
    // An anchor: translate, or re-anchor when its counterpart is gone.
28337×
    let map_anchor = |r: AtomRef,
284
                      node: &crate::ids::NodeId,
285
                      want_after: bool,
286
                      notes: &mut Vec<String>|
28762×
     -> AtomRef {
28862×
        match r {
28921×
            AtomRef::Atom { change, index } => match mapping.get(&change) {
29020×
                Some(Replacement::Change(nh)) => match corrs[&change].atoms.get(&(*node, index)) {
29118×
                    Some(idx) => AtomRef::Atom {
29218×
                        change: *nh,
29318×
                        index: *idx,
29418×
                    },
295
                    None => {
2962×
                        notes.push(format!("change {short}: an anchor line of the rewritten change vanished; re-anchored to its nearest neighbour"));
2972×
                        reanchor(&change, index, want_after, mapping, corrs, chains)
298
                    }
299
                },
300
                Some(Replacement::Removed(_)) => {
3010×
                    notes.push(format!("change {short}: an anchor line of the removed change vanished; re-anchored to its neighbours"));
3020×
                    reanchor(&change, index, want_after, mapping, corrs, chains)
303
                }
3041×
                None => r,
305
            },
30641×
            other => other,
307
        }
30862×
    };
309
    // A tree-operation reference: translate, or `None` when gone.
31037×
    let map_op = |r: OpRef| -> Option<OpRef> {
3111×
        match mapping.get(&r.change) {
3121×
            Some(Replacement::Change(nh)) => corrs[&r.change].ops.get(&r.index).map(|idx| OpRef {
3131×
                change: *nh,
3141×
                index: *idx,
3151×
            }),
3160×
            Some(Replacement::Removed(_)) => None,
3170×
            None => Some(r),
318
        }
3191×
    };
32037×
    let mut out = c.clone();
32137×
    let mut deps: BTreeSet<ChangeHash> = BTreeSet::new();
32255×
    for d in &c.deps {
32355×
        match mapping.get(d) {
32432×
            Some(Replacement::Change(nh)) => {
32532×
                deps.insert(*nh);
32632×
            }
3277×
            Some(Replacement::Removed(inherited)) => deps.extend(inherited.iter().copied()),
32816×
            None => {
32916×
                deps.insert(*d);
33016×
            }
331
        }
332
    }
33337×
    out.deps = deps.into_iter().collect();
33437×
    let mut ops = Vec::new();
33537×
    for op in &out.tree_ops {
33613×
        let node = op.node();
33713×
        if vanished_nodes.contains(&node)
33811×
            || matches!(op, TreeOp::AddNode { parent, .. } if vanished_nodes.contains(parent))
339
        {
3401×
            notes.push(format!("change {short}: an operation on a file that vanished with the rewritten change was dropped"));
3411×
            continue;
34212×
        }
34312×
        match op {
3440×
            TreeOp::RemoveNode { node, placement } => match map_op(*placement) {
3450×
                Some(p) => ops.push(TreeOp::RemoveNode {
3460×
                    node: *node,
3470×
                    placement: p,
3480×
                }),
3490×
                None => notes.push(format!(
350
                    "change {short}: removal of a placement that vanished was dropped"
351
                )),
352
            },
353
            TreeOp::Move {
3540×
                node,
3550×
                old,
3560×
                parent,
3570×
                name,
3580×
            } => match map_op(*old) {
3590×
                Some(o) => ops.push(TreeOp::Move {
3600×
                    node: *node,
3610×
                    old: o,
3620×
                    parent: *parent,
3630×
                    name: name.clone(),
3640×
                }),
3650×
                None => notes.push(format!(
366
                    "change {short}: a move of {name} lost its origin and was dropped"
367
                )),
368
            },
3691×
            TreeOp::SetMode { node, old, mode } => match map_op(*old) {
3701×
                Some(o) => ops.push(TreeOp::SetMode {
3711×
                    node: *node,
3721×
                    old: o,
3731×
                    mode: *mode,
3741×
                }),
3750×
                None => notes.push(format!(
376
                    "change {short}: a mode change lost its origin and was dropped"
377
                )),
378
            },
37911×
            add @ TreeOp::AddNode { .. } => ops.push(add.clone()),
380
        }
381
    }
38237×
    out.tree_ops = ops;
38337×
    let mut edits = Vec::new();
38437×
    for e in &out.edits {
38537×
        if vanished_nodes.contains(&e.node()) {
3864×
            notes.push(format!("change {short}: an edit of a file that vanished with the rewritten change was dropped"));
3874×
            continue;
38833×
        }
38933×
        match e {
390
            Edit::Insert {
39131×
                node,
39231×
                after,
39331×
                before,
39431×
                lines,
39531×
            } => edits.push(Edit::Insert {
39631×
                node: *node,
39731×
                after: map_anchor(*after, node, true, notes),
39831×
                before: map_anchor(*before, node, false, notes),
39931×
                lines: lines.clone(),
40031×
            }),
4012×
            Edit::Delete { node, atoms } => {
4022×
                let mut mapped: Vec<(ChangeHash, u32)> = Vec::new();
4032×
                let mut dropped = 0;
4042×
                for r in atoms.iter() {
4052×
                    for i in r.from..r.to {
4062×
                        match mapping.get(&r.change) {
4072×
                            Some(Replacement::Change(nh)) => {
4082×
                                match corrs[&r.change].atoms.get(&(*node, i)) {
4092×
                                    Some(idx) => mapped.push((*nh, *idx)),
4100×
                                    None => dropped += 1,
411
                                }
412
                            }
4130×
                            Some(Replacement::Removed(_)) => dropped += 1,
4140×
                            None => mapped.push((r.change, i)),
415
                        }
416
                    }
417
                }
4182×
                if dropped > 0 {
4190×
                    notes.push(format!(
4200×
                        "change {short}: {dropped} deletion(s) of lines that vanished were dropped"
4210×
                    ));
4222×
                }
4232×
                let mut ranges: Vec<AtomRange> = Vec::new();
4242×
                for (ch, i) in mapped {
4252×
                    match ranges.last_mut() {
4260×
                        Some(r) if r.change == ch && r.to == i => r.to = i + 1,
4272×
                        _ => ranges.push(AtomRange {
4282×
                            change: ch,
4292×
                            from: i,
4302×
                            to: i + 1,
4312×
                        }),
432
                    }
433
                }
4342×
                if !ranges.is_empty() {
4352×
                    edits.push(Edit::Delete {
4362×
                        node: *node,
4372×
                        atoms: ranges,
4382×
                    });
4392×
                }
440
            }
4410×
            other => edits.push(other.clone()),
442
        }
443
    }
44437×
    out.edits = edits;
44537×
    out.normalize();
44637×
    out
44737×
}
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
4579×
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>, msg: &str) -> Change {
4589×
        let mut c = Change {
4599×
            version: crate::objects::CHANGE_VERSION,
4609×
            change_id: ChangeId::random(),
4619×
            deps,
4629×
            author: ObjectRef::ZERO,
4639×
            hlc: Hlc {
4649×
                wall_ms: 1,
4659×
                logical: 0,
4669×
            },
4679×
            message: msg.into(),
4689×
            tree_ops,
4699×
            edits,
4709×
            kind: ChangeKind::Normal,
4719×
        };
4729×
        c.normalize();
4739×
        c
4749×
    }
475
476
    #[test]
4771×
    fn describe_propagates_through_dependents() {
4781×
        let mut st = State::new();
4791×
        let f = NodeId::random();
4801×
        let c1 = change(
4811×
            vec![],
4821×
            vec![TreeOp::AddNode {
4831×
                node: f,
4841×
                kind: NodeKind::File,
4851×
                mode: Mode::TEXT,
4861×
                parent: NodeId::ROOT,
4871×
                name: "f".into(),
4881×
            }],
4891×
            vec![Edit::Insert {
4901×
                node: f,
4911×
                after: AtomRef::Start,
4921×
                before: AtomRef::End,
4931×
                lines: vec![b"a\n".to_vec(), b"b\n".to_vec()].into(),
4941×
            }],
4951×
            "one",
496
        );
4971×
        let h1 = st.add(c1.clone()).unwrap();
4981×
        let c2 = change(
4991×
            vec![h1],
5001×
            vec![],
5011×
            vec![
5021×
                Edit::Delete {
5031×
                    node: f,
5041×
                    atoms: vec![AtomRange {
5051×
                        change: h1,
5061×
                        from: 0,
5071×
                        to: 1,
5081×
                    }],
5091×
                },
5101×
                Edit::Insert {
5111×
                    node: f,
5121×
                    after: AtomRef::Atom {
5131×
                        change: h1,
5141×
                        index: 1,
5151×
                    },
5161×
                    before: AtomRef::End,
5171×
                    lines: vec![b"c\n".to_vec()].into(),
5181×
                },
519
            ],
5201×
            "two",
521
        );
5221×
        let h2 = st.add(c2.clone()).unwrap();
5231×
        let c3 = change(
5241×
            vec![h2],
5251×
            vec![TreeOp::SetMode {
5261×
                node: f,
5271×
                old: OpRef {
5281×
                    change: h1,
5291×
                    index: 0,
5301×
                },
5311×
                mode: Mode {
5321×
                    exec: true,
5331×
                    opaque: false,
5341×
                },
5351×
            }],
5361×
            vec![Edit::Insert {
5371×
                node: f,
5381×
                after: AtomRef::Atom {
5391×
                    change: h2,
5401×
                    index: 0,
5411×
                },
5421×
                before: AtomRef::End,
5431×
                lines: vec![b"d\n".to_vec()].into(),
5441×
            }],
5451×
            "three",
546
        );
5471×
        let h3 = st.add(c3.clone()).unwrap();
5481×
        let before = st.materialize();
549
550
        // Rewrite c1's message; everything downstream follows.
5511×
        let mut n1 = c1.clone();
5521×
        n1.message = "one, described".into();
5531×
        let all: HashMap<ChangeHash, Change> =
5541×
            [(h1, c1.clone()), (h2, c2.clone()), (h3, c3.clone())]
5551×
                .into_iter()
5561×
                .collect();
5573×
        let load = |h: &ChangeHash| all.get(h).cloned();
5581×
        let rewritten = propagate(&st, h1, Some(&n1), &Correspondence::identity(&c1), &load)
5591×
            .unwrap()
5601×
            .changes;
5611×
        assert_eq!(
5621×
            rewritten.iter().map(|(h, _)| *h).collect::<Vec<_>>(),
5631×
            vec![h2, h3]
564
        );
5651×
        let mut st2 = State::new();
5661×
        st2.add(n1.clone()).unwrap();
5672×
        for (_, c) in &rewritten {
5682×
            st2.add(c.clone()).unwrap();
5692×
        }
5701×
        assert_eq!(st2.len(), 3);
5711×
        let after = st2.materialize();
5721×
        assert_eq!(after.files["f"].mode, before.files["f"].mode);
5731×
        match (&after.files["f"].content, &before.files["f"].content) {
5741×
            (FileContent::Text { bytes: a, .. }, FileContent::Text { bytes: b, .. }) => {
5751×
                assert_eq!(a, b)
576
            }
5770×
            _ => panic!(),
578
        }
5791×
        assert!(
5801×
            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.
5831×
        assert_eq!(rewritten[0].1.change_id, c2.change_id);
5841×
        assert_eq!(rewritten[0].1.deps, vec![n1.hash()]);
5851×
        assert_ne!(rewritten[0].1.hash(), h2);
5861×
        assert_eq!(rewritten[1].1.deps, vec![rewritten[0].1.hash()]);
5871×
    }
588
589
    #[test]
5901×
    fn missing_counterpart_is_reanchored() {
5911×
        let mut st = State::new();
5921×
        let f = NodeId::random();
5931×
        let c1 = change(
5941×
            vec![],
5951×
            vec![TreeOp::AddNode {
5961×
                node: f,
5971×
                kind: NodeKind::File,
5981×
                mode: Mode::TEXT,
5991×
                parent: NodeId::ROOT,
6001×
                name: "f".into(),
6011×
            }],
6021×
            vec![Edit::Insert {
6031×
                node: f,
6041×
                after: AtomRef::Start,
6051×
                before: AtomRef::End,
6061×
                lines: vec![b"a\n".to_vec(), b"b\n".to_vec()].into(),
6071×
            }],
6081×
            "one",
609
        );
6101×
        let h1 = st.add(c1.clone()).unwrap();
6111×
        let c2 = change(
6121×
            vec![h1],
6131×
            vec![],
6141×
            vec![Edit::Insert {
6151×
                node: f,
6161×
                after: AtomRef::Atom {
6171×
                    change: h1,
6181×
                    index: 1,
6191×
                },
6201×
                before: AtomRef::End,
6211×
                lines: vec![b"c\n".to_vec()].into(),
6221×
            }],
6231×
            "two",
624
        );
6251×
        let h2 = st.add(c2.clone()).unwrap();
6261×
        let all: HashMap<ChangeHash, Change> = [(h1, c1.clone()), (h2, c2)].into_iter().collect();
6276×
        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.
6301×
        let mut n1 = c1.clone();
6311×
        n1.edits = vec![Edit::Insert {
6321×
            node: f,
6331×
            after: AtomRef::Start,
6341×
            before: AtomRef::End,
6351×
            lines: vec![b"a\n".to_vec()].into(),
6361×
        }];
6371×
        let corr = Correspondence {
6381×
            atoms: [((f, 0), 0)].into_iter().collect(),
6391×
            ops: [(0, 0)].into_iter().collect(),
6401×
        };
6411×
        let p = propagate(&st, h1, Some(&n1), &corr, &load).unwrap();
6421×
        assert_eq!(p.notes.len(), 1, "{:?}", p.notes);
6431×
        let n1h = n1.hash();
6441×
        assert!(
6451×
            matches!(p.changes[0].1.edits[0], Edit::Insert { after: AtomRef::Atom { change, index: 0 }, before: AtomRef::End, .. } if change == n1h)
646
        );
6471×
        let mut st2 = State::new();
6481×
        st2.add(n1.clone()).unwrap();
6491×
        st2.add(p.changes[0].1.clone()).unwrap();
6501×
        let m = st2.materialize();
6511×
        assert!(!m.is_conflicted());
6521×
        assert!(
6531×
            matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"a\nc\n")
654
        );
6551×
        assert_eq!(
6561×
            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.
6611×
        let p = propagate(&st, h1, None, &Correspondence::default(), &load).unwrap();
6621×
        assert_eq!(p.notes.len(), 1, "{:?}", p.notes);
6631×
        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).
6671×
        let mut n2 = c1.clone();
6681×
        n2.edits = vec![];
6691×
        let corr = Correspondence {
6701×
            atoms: HashMap::new(),
6711×
            ops: [(0, 0)].into_iter().collect(),
6721×
        };
6731×
        let p = propagate(&st, h1, Some(&n2), &corr, &load).unwrap();
6741×
        assert!(matches!(
6751×
            p.changes[0].1.edits[0],
676
            Edit::Insert {
677
                after: AtomRef::Start,
678
                before: AtomRef::End,
679
                ..
680
            }
681
        ));
6821×
        let mut st3 = State::new();
6831×
        st3.add(n2.clone()).unwrap();
6841×
        st3.add(p.changes[0].1.clone()).unwrap();
6851×
        let m = st3.materialize();
6861×
        assert!(!m.is_conflicted());
6871×
        assert!(
6881×
            matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"c\n")
689
        );
6901×
    }
691
692
    #[test]
6931×
    fn removal_rewires_dependencies_or_fails_on_references() {
6941×
        let mut st = State::new();
6951×
        let f = NodeId::random();
6961×
        let g = NodeId::random();
6971×
        let c1 = change(
6981×
            vec![],
6991×
            vec![TreeOp::AddNode {
7001×
                node: f,
7011×
                kind: NodeKind::File,
7021×
                mode: Mode::TEXT,
7031×
                parent: NodeId::ROOT,
7041×
                name: "f".into(),
7051×
            }],
7061×
            vec![],
7071×
            "one",
708
        );
7091×
        let h1 = st.add(c1.clone()).unwrap();
710
        // c2 merely depends on c1 explicitly (touches another file).
7111×
        let c2 = change(
7121×
            vec![h1],
7131×
            vec![TreeOp::AddNode {
7141×
                node: g,
7151×
                kind: NodeKind::File,
7161×
                mode: Mode::TEXT,
7171×
                parent: NodeId::ROOT,
7181×
                name: "g".into(),
7191×
            }],
7201×
            vec![],
7211×
            "two",
722
        );
7231×
        let h2 = st.add(c2.clone()).unwrap();
7241×
        let c3 = change(
7251×
            vec![h2],
7261×
            vec![],
7271×
            vec![Edit::Insert {
7281×
                node: g,
7291×
                after: AtomRef::Start,
7301×
                before: AtomRef::End,
7311×
                lines: vec![b"x\n".to_vec()].into(),
7321×
            }],
7331×
            "three",
734
        );
7351×
        let h3 = st.add(c3.clone()).unwrap();
7361×
        let mut all: HashMap<ChangeHash, Change> =
7371×
            [(h1, c1), (h2, c2), (h3, c3)].into_iter().collect();
7381×
        let load_all = all.clone();
7395×
        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.
7411×
        let out = propagate(&st, h2, None, &Correspondence::default(), &load)
7421×
            .unwrap()
7431×
            .changes;
7441×
        assert_eq!(out.len(), 1);
7451×
        assert_eq!(out[0].0, h3);
7461×
        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.
7491×
        let out = propagate(&st, h1, None, &Correspondence::default(), &load)
7501×
            .unwrap()
7511×
            .changes;
7521×
        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.
7551×
        let c4 = change(
7561×
            vec![h2],
7571×
            vec![TreeOp::RemoveNode {
7581×
                node: g,
7591×
                placement: OpRef {
7601×
                    change: h2,
7611×
                    index: 0,
7621×
                },
7631×
            }],
7641×
            vec![],
7651×
            "four",
766
        );
7671×
        let h4 = st.add(c4.clone()).unwrap();
7681×
        all.insert(h4, c4);
7693×
        let load2 = |h: &ChangeHash| all.get(h).cloned();
7701×
        let p = propagate(&st, h2, None, &Correspondence::default(), &load2).unwrap();
7711×
        assert_eq!(p.notes.len(), 2, "{:?}", p.notes);
7722×
        for (_, c) in &p.changes {
7732×
            assert!(c.edits.is_empty() && c.tree_ops.is_empty());
774
        }
7751×
    }
776
}