OXYGENHUB

Coverage / crates/oxy-core/src/repo/history.rs 77.7%

788 of 1014 lines run, 52 of 130 functions · run #327 on main · run missed

1
//! History editing (spec §8.4): describe, amend, abandon.
2
//!
3
//! `describe` and `abandon` work on any change of the bookmark: dependents
4
//! are rewritten through `crate::rewrite`, with their references translated
5
//! and orphaned references reified as re-anchoring, reported as notes.
6
//! `amend` works on heads, where the amended change is rebuilt from the files
7
//! on disk. A rewritten change keeps its ChangeId and gets a new hash; old
8
//! objects stay in the store until garbage collection. Rewritten changes by
9
//! other authors lose their signatures, as with any rewrite of others' work.
10
11
use super::*;
12
use crate::objects::{AtomRange, AtomRef};
13
use crate::rewrite::{propagate, Correspondence};
14
15
impl Repo {
16
    /// The bookmark head `hash`, or an error explaining why it cannot be rewritten.
175×
    fn rewritable_head(&self, hash: &ChangeHash) -> Result<Change> {
185×
        let heads = self.bookmark_heads()?;
195×
        if !heads.contains(hash) {
200×
            return fail(format!("change #{} is not a head of {}; amend works on heads only (describe, split, and abandon work anywhere)", hash.short(12), self.config.bookmark));
215×
        }
225×
        let c = self.store.get_change(hash)?;
235×
        if c.kind == ChangeKind::WorkingCopy {
240×
            return fail("that is the working copy; commit it instead");
255×
        }
265×
        Ok(c)
275×
    }
28
29
    /// Replace head `old` with `new` (or with nothing) on the bookmark and
30
    /// restart the working copy on the resulting base. Files on disk are not
31
    /// touched: the next snapshot diffs them against the new base.
325×
    fn replace_head(
335×
        &self,
345×
        old: &ChangeHash,
355×
        new: Option<&Change>,
365×
        command: &str,
375×
    ) -> Result<Option<ChangeHash>> {
385×
        let _wc = self.lock_working_copy()?;
395×
        let new_hash = match new {
403×
            Some(c) => Some(self.store_signed(c)?),
412×
            None => None,
42
        };
435×
        let old_change = self.store.get_change(old)?;
445×
        let (op_ref, op) = self.head_op()?;
455×
        let bookmark = self.config.bookmark.clone();
465×
        let heads = self.heads_of(&op.view, &bookmark)?;
4710×
        let replace = |set: &[ChangeHash]| -> Vec<ChangeHash> {
4814×
            let mut out: Vec<ChangeHash> = set.iter().filter(|h| *h != old).copied().collect();
4910×
            match new_hash {
506×
                Some(n) => out.push(n),
514×
                None => out.extend(old_change.deps.iter().copied()),
52
            }
5310×
            out
5410×
        };
555×
        let candidates = replace(&heads);
565×
        let st = self.load_state_history(&candidates)?;
575×
        let new_heads = st.maximal(&candidates);
58
595×
        let (_, wc) = self.working_copy()?;
605×
        let base = self.load_state_history(&wc.deps)?;
615×
        let wc_base = base.maximal(&wc.deps);
625×
        let wc_base = if wc_base.contains(old) {
635×
            let c = replace(&wc_base);
645×
            let st = self.load_state_history(&c)?;
655×
            st.maximal(&c)
66
        } else {
670×
            wc_base
68
        };
695×
        let fresh = self.new_working_copy(wc_base, &wc.message);
705×
        let wc_hash = self.store.put_change(&fresh)?;
71
725×
        let mut view = op.view.clone();
735×
        view.bookmarks.insert(
745×
            bookmark,
755×
            self.store.put_state(&StateObject::new(new_heads))?,
76
        );
775×
        view.workspaces.insert(self.workspace, wc_hash);
785×
        self.record_op(&[op_ref], command, view)?;
795×
        self.drop_manifest();
805×
        Ok(new_hash)
815×
    }
82
83
    /// Replace `old` by `new` (or remove it) anywhere in the bookmark's state,
84
    /// rewriting every dependent, including the working copy. Returns the
85
    /// new hash of `old`, if any.
869×
    fn rewrite_change(
879×
        &self,
889×
        old: &ChangeHash,
899×
        new: Option<&Change>,
909×
        corr: &Correspondence,
919×
        command: &str,
929×
    ) -> Result<Option<ChangeHash>> {
939×
        self.rewrite_change_noted(old, new, corr, command)
949×
            .map(|(h, _)| h)
959×
    }
96
97
    /// Like `rewrite_change`, also returning notes about reified references.
9811×
    fn rewrite_change_noted(
9911×
        &self,
10011×
        old: &ChangeHash,
10111×
        new: Option<&Change>,
10211×
        corr: &Correspondence,
10311×
        command: &str,
10411×
    ) -> Result<(Option<ChangeHash>, Vec<String>)> {
10511×
        let _wc = self.lock_working_copy()?;
10611×
        let (op_ref, op) = self.head_op()?;
10711×
        let bookmark = self.config.bookmark.clone();
10811×
        let heads = self.heads_of(&op.view, &bookmark)?;
10911×
        let (wc_hash, _) = self.working_copy()?;
11011×
        let mut all = heads.clone();
11111×
        all.push(wc_hash);
11211×
        let state = self.load_state_history(&all)?;
11311×
        if !state.closure(&wc_hash).contains(old)
1141×
            && !heads.contains(old)
1151×
            && !state.heads().contains(old)
1161×
            && !state.contains(old)
117
        {
1180×
            return fail(format!(
119
                "change #{} is not in {} or under the working copy",
1200×
                old.short(12),
121
                bookmark
122
            ));
12311×
        }
12411×
        let old_change = self.store.get_change(old)?;
12511×
        if old_change.kind == ChangeKind::WorkingCopy {
1260×
            return fail("that is the working copy; commit it instead");
12711×
        }
12838×
        let load = |h: &ChangeHash| self.store.get_change(h).ok();
12911×
        let propagation =
13011×
            propagate(&state, *old, new, corr, &load).map_err(|e| RepoError::Msg(e.to_string()))?;
13111×
        let rewritten = propagation.changes;
132
13311×
        let mut mapping: HashMap<ChangeHash, ChangeHash> = HashMap::new();
13411×
        let new_hash = match new {
1359×
            Some(c) => {
1369×
                let h = self.store_signed(c)?;
1379×
                mapping.insert(*old, h);
1389×
                Some(h)
139
            }
1402×
            None => None,
141
        };
14227×
        for (h, c) in &rewritten {
14327×
            let nh = if c.kind == ChangeKind::WorkingCopy {
14410×
                self.store.put_change(c)?
145
            } else {
14617×
                self.store_signed(c)?
147
            };
14827×
            mapping.insert(*h, nh);
149
        }
15011×
        let remap = |set: &[ChangeHash]| -> Vec<ChangeHash> {
15111×
            let mut out = Vec::new();
15216×
            for h in set {
15316×
                if h == old && new_hash.is_none() {
1540×
                    out.extend(old_change.deps.iter().copied());
15516×
                } else {
15616×
                    out.push(*mapping.get(h).unwrap_or(h));
15716×
                }
158
            }
15911×
            out
16011×
        };
16111×
        let candidates = remap(&heads);
16211×
        let st = self.load_state_history(&candidates)?;
16311×
        let new_heads = st.maximal(&candidates);
16411×
        let new_wc = mapping.get(&wc_hash).copied().unwrap_or(wc_hash);
165
16611×
        let mut view = op.view.clone();
16711×
        view.bookmarks.insert(
16811×
            bookmark,
16911×
            self.store.put_state(&StateObject::new(new_heads))?,
170
        );
17111×
        view.workspaces.insert(self.workspace, new_wc);
17211×
        self.record_op(&[op_ref], command, view)?;
17311×
        self.drop_manifest();
17411×
        Ok((new_hash, propagation.notes))
17511×
    }
176
177
    /// Change the message of any change of the bookmark; dependents are rewritten.
1784×
    pub fn describe(&self, hash: &ChangeHash, message: &str) -> Result<ChangeHash> {
1794×
        if message.trim().is_empty() {
1800×
            return fail("a message is required");
1814×
        }
1824×
        let mut c = self.store.get_change(hash)?;
1834×
        if c.kind == ChangeKind::WorkingCopy {
1840×
            return fail("that is the working copy; commit it instead");
1854×
        }
1864×
        if c.message == message.trim_end() {
1870×
            return Ok(*hash);
1884×
        }
1894×
        let corr = Correspondence::identity(&c);
1904×
        c.message = message.trim_end().to_string();
1914×
        c.hlc = Hlc::now();
1924×
        let id = c.change_id;
1934×
        Ok(self
1944×
            .rewrite_change(
1954×
                hash,
1964×
                Some(&c),
1974×
                &corr,
1984×
                &format!("oxy describe {} -m {message:?}", id.short(8)),
1990×
            )?
2004×
            .unwrap())
2014×
    }
202
203
    /// Fold the working copy into the head it sits on: the head is rebuilt
204
    /// as the difference between the state beneath it and the files on disk.
2053×
    pub fn amend(&self, message: Option<&str>) -> Result<ChangeHash> {
2063×
        let snap = self.snapshot("oxy amend")?;
2073×
        if snap
2083×
            .statuses
2093×
            .iter()
2103×
            .any(|s| s.status == ChangeStatus::ConflictEdited)
211
        {
2120×
            return fail("a file still contains conflict markers");
2133×
        }
2143×
        let base_heads = snap.base.maximal(&snap.wc.deps);
2153×
        let [head] = base_heads.as_slice() else {
2160×
            return fail(format!(
217
                "the working copy sits on {} heads; check out the single change to amend",
2180×
                base_heads.len()
219
            ));
220
        };
2213×
        let head = *head;
2223×
        let old = self.rewritable_head(&head)?;
2233×
        if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() && message.is_none() {
2241×
            return fail(
225
                "nothing to amend: the working copy is clean and no new message was given",
226
            );
2272×
        }
228
        // Rebuild against the state beneath the head.
2292×
        let beneath = self.load_state(&old.deps)?;
2302×
        let beneath_snapshot = beneath.materialize();
2312×
        let reuse = added_node_paths(&old, &beneath_snapshot);
2322×
        let built = self.build_against(&beneath, &beneath_snapshot, reuse)?;
2332×
        if built.tree_ops.is_empty() && built.edits.is_empty() {
2340×
            return fail("amending would leave the change empty; use `oxy abandon` to drop it");
2352×
        }
2362×
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
2372×
        deps.extend(built.contributors.iter().copied());
2382×
        let mut new = Change {
239
            version: crate::objects::CHANGE_VERSION,
2402×
            change_id: old.change_id,
2412×
            deps: vec![],
2422×
            author: self.author(),
2432×
            hlc: Hlc::now(),
2442×
            message: message
2452×
                .map(|m| m.trim_end().to_string())
2462×
                .unwrap_or(old.message.clone()),
2472×
            tree_ops: built.tree_ops,
2482×
            edits: built.edits,
2492×
            kind: if built.resolves || old.kind == ChangeKind::Resolution {
2500×
                ChangeKind::Resolution
251
            } else {
2522×
                ChangeKind::Normal
253
            },
254
        };
2552×
        deps.extend(referenced_changes(&new, &beneath));
2562×
        new.deps = deps.into_iter().collect();
2572×
        new.normalize();
2582×
        Ok(self
2592×
            .replace_head(
2602×
                &head,
2612×
                Some(&new),
2622×
                &format!("oxy amend {}", old.change_id.short(8)),
2630×
            )?
2642×
            .unwrap())
2653×
    }
266
267
    /// Drop a change from the bookmark. Dependents inherit its dependencies;
268
    /// references into it are reified by re-anchoring (spec §8.4) and the
269
    /// adjustments are returned as notes. A dropped head's content stays on
270
    /// disk as uncommitted work if the working copy sat on it.
2714×
    pub fn abandon(&self, hash: &ChangeHash) -> Result<Vec<String>> {
2724×
        let c = self.store.get_change(hash)?;
2734×
        if c.kind == ChangeKind::WorkingCopy {
2740×
            return fail("that is the working copy; commit it instead");
2754×
        }
2764×
        let heads = self.bookmark_heads()?;
2774×
        if heads.contains(hash) {
2782×
            self.replace_head(hash, None, &format!("oxy abandon {}", c.change_id.short(8)))?;
2792×
            Ok(vec![])
280
        } else {
2812×
            let (_, notes) = self.rewrite_change_noted(
2822×
                hash,
2832×
                None,
2842×
                &Correspondence::default(),
2852×
                &format!("oxy abandon {}", c.change_id.short(8)),
2860×
            )?;
2872×
            Ok(notes)
288
        }
2894×
    }
290
}
291
292
/// Translate a reference to change `target`'s own atom or op into a self
293
/// reference, and a working-copy self reference into one offset past the
294
/// target's own count; other references pass through.
2954×
fn fold_atom(
2964×
    r: AtomRef,
2974×
    target: &ChangeHash,
2984×
    wc_offset: &HashMap<NodeId, u32>,
2994×
    node: &NodeId,
3004×
) -> Option<AtomRef> {
3011×
    match r {
3023×
        AtomRef::Atom { change, index } if change == *target => Some(AtomRef::Atom {
3032×
            change: ObjectRef::ZERO,
3042×
            index,
3052×
        }),
3061×
        AtomRef::Atom { change, index } if change.is_zero() => Some(AtomRef::Atom {
3070×
            change: ObjectRef::ZERO,
3080×
            index: index + wc_offset.get(node).copied().unwrap_or(0),
3090×
        }),
310
        // References into the target's closure were validated by the caller and stay as they are.
3112×
        other => Some(other),
312
    }
3134×
}
314
315
impl Repo {
316
    /// Fold the working copy's uncommitted edits into any change of the
317
    /// bookmark (spec §8.4). The edits must reference only atoms, placements
318
    /// and nodes that exist in the target's own closure; dependents are
319
    /// rewritten with an identity correspondence, since the target's own
320
    /// atoms keep their indices.
3212×
    pub fn amend_into(&self, target: &ChangeHash, message: Option<&str>) -> Result<ChangeHash> {
3222×
        let _wc = self.lock_working_copy()?;
3232×
        let snap = self.snapshot("oxy amend")?;
3242×
        if snap
3252×
            .statuses
3262×
            .iter()
3272×
            .any(|s| s.status == ChangeStatus::ConflictEdited)
328
        {
3290×
            return fail("a file still contains conflict markers");
3302×
        }
3312×
        let wc = &snap.wc;
3322×
        if wc.tree_ops.is_empty() && wc.edits.is_empty() && message.is_none() {
3330×
            return fail(
334
                "nothing to amend: the working copy is clean and no new message was given",
335
            );
3362×
        }
3372×
        let old = self.store.get_change(target)?;
3382×
        if old.kind == ChangeKind::WorkingCopy {
3390×
            return fail("that is the working copy; commit it instead");
3402×
        }
341
        // Every reference must live in the target's closure (or be the target).
3422×
        let closure = self.load_state_history(&[*target])?;
3434×
        let ok_change = |h: &ChangeHash| *h == *target || closure.contains(h);
3443×
        let ok_node = |n: &NodeId| n.is_root() || closure.tree().introducer(n).is_some();
3452×
        let too_new = |what: &str| {
3461×
            RepoError::Msg(format!(
3471×
                "{what} was introduced after change #{}; amend into a later change instead",
3481×
                target.short(12)
3491×
            ))
3501×
        };
3512×
        for op in &wc.tree_ops {
3520×
            match op {
3530×
                TreeOp::AddNode { parent, .. } => {
3540×
                    if !ok_node(parent) {
3550×
                        return Err(too_new("the parent directory"));
3560×
                    }
357
                }
3580×
                TreeOp::RemoveNode { node, placement } => {
3590×
                    if !ok_node(node) || !ok_change(&placement.change) {
3600×
                        return Err(too_new("a removed file"));
3610×
                    }
362
                }
363
                TreeOp::Move {
3640×
                    node, old, parent, ..
365
                } => {
3660×
                    if !ok_node(node) || !ok_change(&old.change) || !ok_node(parent) {
3670×
                        return Err(too_new("a moved file"));
3680×
                    }
369
                }
3700×
                TreeOp::SetMode { node, old, .. } => {
3710×
                    if !ok_node(node) || !ok_change(&old.change) {
3720×
                        return Err(too_new("a file whose mode changed"));
3730×
                    }
374
                }
375
            }
376
        }
3772×
        let wc_added: BTreeSet<NodeId> = wc
3782×
            .tree_ops
3792×
            .iter()
3802×
            .filter_map(|o| {
3810×
                if let TreeOp::AddNode { node, .. } = o {
3820×
                    Some(*node)
383
                } else {
3840×
                    None
385
                }
3860×
            })
3872×
            .collect();
3883×
        for e in &wc.edits {
3893×
            let n = e.node();
3903×
            if !wc_added.contains(&n) && !ok_node(&n) {
3910×
                return Err(too_new("an edited file"));
3923×
            }
3933×
            let refs: Vec<ChangeHash> = match e {
3941×
                Edit::Insert { after, before, .. } => [after, before]
3951×
                    .iter()
3962×
                    .filter_map(|r| {
3972×
                        if let AtomRef::Atom { change, .. } = r {
3982×
                            Some(*change)
399
                        } else {
4000×
                            None
401
                        }
4022×
                    })
4031×
                    .collect(),
4042×
                Edit::Delete { atoms, .. } => atoms.iter().map(|r| r.change).collect(),
4050×
                Edit::Replace { .. } => vec![],
406
            };
4074×
            if refs.iter().any(|h| !h.is_zero() && !ok_change(h)) {
4081×
                return Err(too_new("an edited line"));
4092×
            }
410
        }
411
412
        // Compose: the target's operations first, then the working copy's
413
        // with references to the target turned into self references and its
414
        // own atom indices shifted past the target's.
4151×
        let mut per_node: HashMap<NodeId, u32> = HashMap::new();
4162×
        for e in &old.edits {
4172×
            if let Edit::Insert { node, lines, .. } = e {
4181×
                *per_node.entry(*node).or_default() += lines.len() as u32;
4191×
            }
420
        }
4211×
        let op_offset = old.tree_ops.len() as u32;
4221×
        let fold_op = |r: OpRef| -> OpRef {
4230×
            if r.change == *target {
4240×
                OpRef {
4250×
                    change: ObjectRef::ZERO,
4260×
                    index: r.index,
4270×
                }
4280×
            } else if r.change.is_zero() {
4290×
                OpRef {
4300×
                    change: ObjectRef::ZERO,
4310×
                    index: r.index + op_offset,
4320×
                }
433
            } else {
4340×
                r
435
            }
4360×
        };
4371×
        let mut tree_ops = old.tree_ops.clone();
4381×
        for op in &wc.tree_ops {
4390×
            tree_ops.push(match op {
4400×
                TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode {
4410×
                    node: *node,
4420×
                    placement: fold_op(*placement),
4430×
                },
444
                TreeOp::Move {
4450×
                    node,
4460×
                    old,
4470×
                    parent,
4480×
                    name,
4490×
                } => TreeOp::Move {
4500×
                    node: *node,
4510×
                    old: fold_op(*old),
4520×
                    parent: *parent,
4530×
                    name: name.clone(),
4540×
                },
4550×
                TreeOp::SetMode { node, old, mode } => TreeOp::SetMode {
4560×
                    node: *node,
4570×
                    old: fold_op(*old),
4580×
                    mode: *mode,
4590×
                },
4600×
                add => add.clone(),
461
            });
462
        }
4631×
        let mut edits = old.edits.clone();
4642×
        for e in &wc.edits {
4652×
            edits.push(match e {
466
                Edit::Insert {
4671×
                    node,
4681×
                    after,
4691×
                    before,
4701×
                    lines,
471
                } => Edit::Insert {
4721×
                    node: *node,
4731×
                    after: fold_atom(*after, target, &per_node, node)
4741×
                        .ok_or_else(|| too_new("an anchor line"))?,
4751×
                    before: fold_atom(*before, target, &per_node, node)
4761×
                        .ok_or_else(|| too_new("an anchor line"))?,
4771×
                    lines: lines.clone(),
478
                },
4791×
                Edit::Delete { node, atoms } => Edit::Delete {
4801×
                    node: *node,
4811×
                    atoms: atoms
4821×
                        .iter()
4831×
                        .map(|r| {
4841×
                            if r.change == *target {
4851×
                                AtomRange {
4861×
                                    change: ObjectRef::ZERO,
4871×
                                    from: r.from,
4881×
                                    to: r.to,
4891×
                                }
4900×
                            } else if r.change.is_zero() {
4910×
                                let off = per_node.get(node).copied().unwrap_or(0);
4920×
                                AtomRange {
4930×
                                    change: ObjectRef::ZERO,
4940×
                                    from: r.from + off,
4950×
                                    to: r.to + off,
4960×
                                }
497
                            } else {
4980×
                                *r
499
                            }
5001×
                        })
5011×
                        .collect(),
502
                },
5030×
                other => other.clone(),
504
            });
505
        }
5061×
        let mut new = Change {
507
            version: crate::objects::CHANGE_VERSION,
5081×
            change_id: old.change_id,
5091×
            deps: vec![],
5101×
            author: self.author(),
5111×
            hlc: Hlc::now(),
5121×
            message: message
5131×
                .map(|m| m.trim_end().to_string())
5141×
                .unwrap_or(old.message.clone()),
5151×
            tree_ops,
5161×
            edits,
5171×
            kind: old.kind,
518
        };
5191×
        let beneath = self.load_state(&old.deps)?;
5201×
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
5211×
        deps.extend(
5221×
            referenced_changes(&new, &beneath)
5231×
                .into_iter()
5241×
                .filter(|h| *h != *target),
525
        );
5261×
        deps.extend(
5271×
            wc.deps
5281×
                .iter()
5293×
                .filter(|d| closure.contains(d) && **d != *target)
5301×
                .copied(),
531
        );
5321×
        new.deps = deps.into_iter().collect();
5331×
        new.normalize();
534
535
        // Rewrite dependents (identity correspondence), then start a fresh,
536
        // empty working copy: its edits now live in the amended change.
5371×
        let corr = Correspondence::identity(&old);
5381×
        let new_hash = self
5391×
            .rewrite_change(
5401×
                target,
5411×
                Some(&new),
5421×
                &corr,
5431×
                &format!("oxy amend --into {}", old.change_id.short(8)),
5440×
            )?
5451×
            .unwrap();
5461×
        let (op_ref, op) = self.head_op()?;
5471×
        let heads = self.heads_of(&op.view, &self.config.bookmark)?;
5481×
        let fresh = self.new_working_copy(heads, "");
5491×
        let wc_hash = self.store.put_change(&fresh)?;
5501×
        let mut view = op.view.clone();
5511×
        view.workspaces.insert(self.workspace, wc_hash);
5521×
        self.record_op(&[op_ref], "oxy amend (working copy)", view)?;
5531×
        self.drop_manifest();
5541×
        Ok(new_hash)
5552×
    }
556
557
    /// Split a change into two: one holding its effect on `paths`, the
558
    /// other the rest (keeping the ChangeId). A head is rebuilt from its
559
    /// materialized files; any other change is split by node, which is
560
    /// exact, and is refused when a dependent refers to the split-off
561
    /// nodes. The working copy is unaffected.
5624×
    pub fn split(&self, hash: &ChangeHash, paths: &[String]) -> Result<(ChangeHash, ChangeHash)> {
5634×
        if paths.is_empty() {
5640×
            return fail("name at least one path to split out");
5654×
        }
5664×
        if !self.bookmark_heads()?.contains(hash) {
5672×
            return self.split_by_node(hash, paths);
5682×
        }
5692×
        let old = self.rewritable_head(hash)?;
5702×
        let beneath = self.load_state(&old.deps)?;
5712×
        let beneath_snapshot = beneath.materialize();
5722×
        let full = self.load_state(&[*hash])?;
5732×
        let full_snapshot = full.materialize();
5743×
        let selected = |p: &str| {
5753×
            paths
5763×
                .iter()
5773×
                .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/'))))
5783×
        };
579
        // Entries of the intermediate state: selected paths as the head
580
        // leaves them, everything else as beneath.
5812×
        let entries_of = |snap: &Snapshot,
582
                          keep_from_full: &dyn Fn(&str) -> bool|
5833×
         -> Result<BTreeMap<String, FsEntry>> {
5843×
            let mut out = BTreeMap::new();
5857×
            for (p, _) in snap.files.iter().chain(full_snapshot.files.iter()) {
5867×
                let take_full = keep_from_full(p);
5877×
                let src = if take_full {
5885×
                    full_snapshot.files.get(p)
589
                } else {
5902×
                    snap.files.get(p)
591
                };
5927×
                let Some(src) = src else { continue };
5935×
                if out.contains_key(p) {
5942×
                    continue;
5953×
                }
5963×
                let content = match &src.content {
5970×
                    FileContent::Dir => continue,
5983×
                    FileContent::Text { bytes, .. } => bytes.clone(),
5990×
                    FileContent::Opaque { blobs } => match blobs.first() {
6000×
                        Some(b) => self.read_blob(b)?,
6010×
                        None => continue,
602
                    },
603
                };
6043×
                out.insert(p.clone(), FsEntry::new(src.kind, src.mode.exec, content));
605
            }
6063×
            Ok(out)
6073×
        };
6083×
        let mid_entries = entries_of(&beneath_snapshot, &|p| selected(p))?;
6092×
        let first =
6102×
            self.build_from_entries(&beneath, &beneath_snapshot, HashMap::new(), &mid_entries)?;
6112×
        if first.tree_ops.is_empty() && first.edits.is_empty() {
6121×
            return fail("the named paths are not touched by that change");
6131×
        }
6141×
        let mut c1 = Change {
6151×
            version: crate::objects::CHANGE_VERSION,
6161×
            change_id: ChangeId::random(),
6171×
            deps: old.deps.clone(),
6181×
            author: old.author,
6191×
            hlc: Hlc::now(),
6201×
            message: format!(
6211×
                "{} (part 1: {})",
6221×
                old.message.lines().next().unwrap_or(""),
6231×
                paths.join(", ")
6241×
            ),
6251×
            tree_ops: first.tree_ops,
6261×
            edits: first.edits,
6271×
            kind: ChangeKind::Normal,
6281×
        };
6291×
        let mut d1: BTreeSet<ChangeHash> = referenced_changes(&c1, &beneath);
6301×
        d1.extend(old.deps.iter().copied());
6311×
        c1.deps = d1.into_iter().collect();
6321×
        c1.normalize();
6331×
        let h1 = self.store_signed(&c1)?;
6341×
        let mut mid = beneath.clone();
6351×
        mid.add(c1)?;
6361×
        let mid_snapshot = mid.materialize();
6371×
        let rest_entries = entries_of(&full_snapshot, &|_| true)?;
6381×
        let rest = self.build_from_entries(&mid, &mid_snapshot, HashMap::new(), &rest_entries)?;
6391×
        if rest.tree_ops.is_empty() && rest.edits.is_empty() {
6400×
            return fail(
641
                "everything in that change is under the named paths; nothing would remain",
642
            );
6431×
        }
6441×
        let mut c2 = Change {
6451×
            version: crate::objects::CHANGE_VERSION,
6461×
            change_id: old.change_id,
6471×
            deps: vec![],
6481×
            author: old.author,
6491×
            hlc: Hlc::now(),
6501×
            message: old.message.clone(),
6511×
            tree_ops: rest.tree_ops,
6521×
            edits: rest.edits,
6531×
            kind: ChangeKind::Normal,
6541×
        };
6551×
        let mut d2: BTreeSet<ChangeHash> = referenced_changes(&c2, &mid);
6561×
        d2.extend(old.deps.iter().copied());
6571×
        c2.deps = d2.into_iter().collect();
6581×
        c2.normalize();
6591×
        let h2 = self
6601×
            .replace_head(
6611×
                hash,
6621×
                Some(&c2),
6631×
                &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")),
6640×
            )?
6651×
            .unwrap();
6661×
        self.add_to_bookmark(h1)?;
6671×
        Ok((h1, h2))
6684×
    }
669
670
    /// Put `h` into the bookmark's state (a head unless something already
671
    /// depends on it) and under the working copy.
6722×
    fn add_to_bookmark(&self, h: ChangeHash) -> Result<()> {
6732×
        let (op_ref, op) = self.head_op()?;
6742×
        let bookmark = self.config.bookmark.clone();
6752×
        let mut heads = self.heads_of(&op.view, &bookmark)?;
6762×
        heads.push(h);
6772×
        let st = self.load_state_history(&heads)?;
6782×
        let heads = st.maximal(&heads);
6792×
        let mut view = op.view.clone();
6802×
        view.bookmarks.insert(
6812×
            bookmark,
6822×
            self.store.put_state(&StateObject::new(heads.clone()))?,
683
        );
6842×
        let (_, wc) = self.working_copy()?;
6852×
        let mut base = wc.deps.clone();
6862×
        base.push(h);
6872×
        let wc_base = self.load_state_history(&base)?.maximal(&base);
6882×
        let fresh = self.new_working_copy(wc_base, &wc.message);
6892×
        view.workspaces
6902×
            .insert(self.workspace, self.store.put_change(&fresh)?);
6912×
        self.record_op(&[op_ref], "oxy split (heads)", view)?;
6922×
        self.drop_manifest();
6932×
        Ok(())
6942×
    }
695
696
    /// Split a non-head change by node: the operations and edits on nodes
697
    /// under `paths` become a new change, the rest keeps the ChangeId with
698
    /// every remaining atom at its old index, so dependents are rewritten
699
    /// exactly. Refused when a dependent touches a split-off node.
7002×
    fn split_by_node(
7012×
        &self,
7022×
        hash: &ChangeHash,
7032×
        paths: &[String],
7042×
    ) -> Result<(ChangeHash, ChangeHash)> {
7052×
        let old = self.store.get_change(hash)?;
7062×
        if old.kind == ChangeKind::WorkingCopy {
7070×
            return fail("that is the working copy; commit it instead");
7082×
        }
7096×
        let selected = |p: &str| {
7106×
            paths
7116×
                .iter()
7126×
                .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/'))))
7136×
        };
7142×
        let beneath = self.load_state(&old.deps)?;
7152×
        let full = self.load_state(&[*hash])?;
7162×
        let mut removed: BTreeSet<NodeId> = BTreeSet::new();
7174×
        for snap in [beneath.materialize(), full.materialize()] {
7186×
            for (p, f) in &snap.files {
7196×
                if selected(p) {
7205×
                    removed.insert(f.node);
7215×
                }
722
            }
723
        }
724
        // Nodes the change itself adds under a removed directory.
7256×
        for op in &old.tree_ops {
7266×
            if let TreeOp::AddNode { node, parent, .. } = op {
7276×
                if removed.contains(parent) {
7283×
                    removed.insert(*node);
7293×
                }
7300×
            }
731
        }
7322×
        if removed.is_empty() {
7330×
            return fail("the named paths are not touched by that change");
7342×
        }
735
        // Dependents must not touch the split-off nodes: their references
736
        // would have to move to the new change, which a rewrite cannot express.
7372×
        let heads = self.bookmark_heads()?;
7382×
        let (wc_hash, _) = self.working_copy()?;
7392×
        let mut all = heads.clone();
7402×
        all.push(wc_hash);
7412×
        let state = self.load_state_history(&all)?;
7424×
        for (h, meta) in state.changes() {
7434×
            if !meta.deps.contains(hash) {
7442×
                continue;
7452×
            }
7462×
            let d = self.store.get_change(h)?;
7472×
            if d.tree_ops.iter().any(|op| removed.contains(&op.node()))
7483×
                || d.edits.iter().any(|e| removed.contains(&e.node()))
749
            {
7501×
                return fail(format!(
751
                    "change {} depends on the split-off paths of {}; split it first",
7521×
                    d.change_id.short(8),
7531×
                    old.change_id.short(8)
754
                ));
7551×
            }
756
        }
7571×
        let mut c1 = Change {
758
            version: crate::objects::CHANGE_VERSION,
7591×
            change_id: ChangeId::random(),
7601×
            deps: old.deps.clone(),
7611×
            author: old.author,
7621×
            hlc: Hlc::now(),
7631×
            message: format!(
764
                "{} (part 1: {})",
7651×
                old.message.lines().next().unwrap_or(""),
7661×
                paths.join(", ")
767
            ),
7681×
            tree_ops: old
7691×
                .tree_ops
7701×
                .iter()
7713×
                .filter(|op| removed.contains(&op.node()))
7721×
                .cloned()
7731×
                .collect(),
7741×
            edits: old
7751×
                .edits
7761×
                .iter()
7772×
                .filter(|e| removed.contains(&e.node()))
7781×
                .cloned()
7791×
                .collect(),
7801×
            kind: ChangeKind::Normal,
781
        };
7821×
        let mut c2 = old.clone();
7831×
        c2.hlc = Hlc::now();
7841×
        let mut ops_map: HashMap<u32, u32> = HashMap::new();
7851×
        let mut kept_ops = Vec::new();
7863×
        for (i, op) in old.tree_ops.iter().enumerate() {
7873×
            if !removed.contains(&op.node()) {
7881×
                ops_map.insert(i as u32, kept_ops.len() as u32);
7891×
                kept_ops.push(op.clone());
7902×
            }
791
        }
7921×
        c2.tree_ops = kept_ops;
7932×
        c2.edits.retain(|e| !removed.contains(&e.node()));
7941×
        if c1.tree_ops.is_empty() && c1.edits.is_empty() {
7950×
            return fail("the named paths are not touched by that change");
7961×
        }
7971×
        if c2.tree_ops.is_empty() && c2.edits.is_empty() {
7980×
            return fail(
799
                "everything in that change is under the named paths; use `oxy abandon` instead",
800
            );
8011×
        }
8021×
        let mut d1: BTreeSet<ChangeHash> = referenced_changes(&c1, &beneath);
8031×
        d1.extend(old.deps.iter().copied());
8041×
        c1.deps = d1.into_iter().collect();
8051×
        c1.normalize();
8061×
        let mut d2: BTreeSet<ChangeHash> = referenced_changes(&c2, &beneath);
8071×
        d2.extend(old.deps.iter().copied());
8081×
        c2.deps = d2.into_iter().collect();
8091×
        c2.normalize();
8101×
        let corr = Correspondence {
8111×
            atoms: Correspondence::identity(&old).atoms,
8121×
            ops: ops_map,
8131×
        };
8141×
        let h2 = self
8151×
            .rewrite_change(
8161×
                hash,
8171×
                Some(&c2),
8181×
                &corr,
8191×
                &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")),
8200×
            )?
8211×
            .unwrap();
8221×
        let h1 = self.store_signed(&c1)?;
8231×
        self.add_to_bookmark(h1)?;
8241×
        Ok((h1, h2))
8252×
    }
826
}
827
828
impl Repo {
829
    /// Fold change `c` into its dependency `into` (spec §8.5): the result
830
    /// keeps `into`'s ChangeId, dependents of both are rewritten, and `c`
831
    /// leaves the state.
8322×
    pub fn squash(&self, c: &ChangeHash, into: &ChangeHash) -> Result<ChangeHash> {
8332×
        if c == into {
8340×
            return fail("a change cannot be squashed into itself");
8352×
        }
8362×
        let src = self.store.get_change(c)?;
8372×
        let dst = self.store.get_change(into)?;
8382×
        if src.kind == ChangeKind::WorkingCopy {
8390×
            return fail("that is the working copy; use `oxy amend --into`");
8402×
        }
8412×
        if dst.kind == ChangeKind::WorkingCopy {
8420×
            return fail("cannot squash into the working copy");
8432×
        }
8442×
        let closure = self.load_state_history(&[*into])?;
8452×
        let outside: Vec<String> = src
8462×
            .deps
8472×
            .iter()
8483×
            .filter(|d| *d != into && !closure.contains(d))
8492×
            .map(|d| d.short(12))
8502×
            .collect();
8512×
        if !outside.is_empty() {
8521×
            return fail(format!("{} depends on {} which #{} does not; squash those first or squash into a later change", src.change_id.short(8), outside.join(", "), into.short(12)));
8531×
        }
8541×
        let ok_change = |h: &ChangeHash| *h == *into || closure.contains(h);
8551×
        let ok_node = |n: &NodeId| n.is_root() || closure.tree().introducer(n).is_some();
8561×
        let too_new = |what: &str| {
8570×
            RepoError::Msg(format!(
8580×
                "{what} was introduced after change #{}; squash into a later change instead",
8590×
                into.short(12)
8600×
            ))
8610×
        };
8621×
        let src_added: BTreeSet<NodeId> = src
8631×
            .tree_ops
8641×
            .iter()
8651×
            .filter_map(|o| {
8660×
                if let TreeOp::AddNode { node, .. } = o {
8670×
                    Some(*node)
868
                } else {
8690×
                    None
870
                }
8710×
            })
8721×
            .collect();
8731×
        for op in &src.tree_ops {
8740×
            match op {
8750×
                TreeOp::AddNode { parent, .. } => {
8760×
                    if !ok_node(parent) && !src_added.contains(parent) {
8770×
                        return Err(too_new("the parent directory"));
8780×
                    }
879
                }
8800×
                TreeOp::RemoveNode { node, placement } => {
8810×
                    if !ok_node(node) || !ok_change(&placement.change) {
8820×
                        return Err(too_new("a removed file"));
8830×
                    }
884
                }
885
                TreeOp::Move {
8860×
                    node, old, parent, ..
887
                } => {
8880×
                    if !ok_node(node)
8890×
                        || !ok_change(&old.change)
8900×
                        || (!ok_node(parent) && !src_added.contains(parent))
891
                    {
8920×
                        return Err(too_new("a moved file"));
8930×
                    }
894
                }
8950×
                TreeOp::SetMode { node, old, .. } => {
8960×
                    if !ok_node(node) || !ok_change(&old.change) {
8970×
                        return Err(too_new("a file whose mode changed"));
8980×
                    }
899
                }
900
            }
901
        }
9021×
        for e in &src.edits {
9031×
            let n = e.node();
9041×
            if !src_added.contains(&n) && !ok_node(&n) {
9050×
                return Err(too_new("an edited file"));
9061×
            }
9071×
            let refs: Vec<ChangeHash> = match e {
9081×
                Edit::Insert { after, before, .. } => [after, before]
9091×
                    .iter()
9102×
                    .filter_map(|r| {
9112×
                        if let AtomRef::Atom { change, .. } = r {
9121×
                            Some(*change)
913
                        } else {
9141×
                            None
915
                        }
9162×
                    })
9171×
                    .collect(),
9180×
                Edit::Delete { atoms, .. } => atoms.iter().map(|r| r.change).collect(),
9190×
                Edit::Replace { .. } => vec![],
920
            };
9211×
            if refs.iter().any(|h| !h.is_zero() && !ok_change(h)) {
9220×
                return Err(too_new("an edited line"));
9231×
            }
924
        }
925
        // Compose dst then src, with src's references to dst turned into
926
        // self references and its own atoms shifted past dst's.
9271×
        let mut per_node: HashMap<NodeId, u32> = HashMap::new();
9281×
        for e in &dst.edits {
9291×
            if let Edit::Insert { node, lines, .. } = e {
9301×
                *per_node.entry(*node).or_default() += lines.len() as u32;
9311×
            }
932
        }
9331×
        let op_offset = dst.tree_ops.len() as u32;
9341×
        let fold_op = |r: OpRef| -> OpRef {
9350×
            if r.change == *into {
9360×
                OpRef {
9370×
                    change: ObjectRef::ZERO,
9380×
                    index: r.index,
9390×
                }
9400×
            } else if r.change.is_zero() {
9410×
                OpRef {
9420×
                    change: ObjectRef::ZERO,
9430×
                    index: r.index + op_offset,
9440×
                }
945
            } else {
9460×
                r
947
            }
9480×
        };
9491×
        let mut tree_ops = dst.tree_ops.clone();
9501×
        for op in &src.tree_ops {
9510×
            tree_ops.push(match op {
9520×
                TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode {
9530×
                    node: *node,
9540×
                    placement: fold_op(*placement),
9550×
                },
956
                TreeOp::Move {
9570×
                    node,
9580×
                    old,
9590×
                    parent,
9600×
                    name,
9610×
                } => TreeOp::Move {
9620×
                    node: *node,
9630×
                    old: fold_op(*old),
9640×
                    parent: *parent,
9650×
                    name: name.clone(),
9660×
                },
9670×
                TreeOp::SetMode { node, old, mode } => TreeOp::SetMode {
9680×
                    node: *node,
9690×
                    old: fold_op(*old),
9700×
                    mode: *mode,
9710×
                },
9720×
                add => add.clone(),
973
            });
974
        }
9751×
        let mut edits = dst.edits.clone();
9761×
        for e in &src.edits {
9771×
            edits.push(match e {
978
                Edit::Insert {
9791×
                    node,
9801×
                    after,
9811×
                    before,
9821×
                    lines,
983
                } => Edit::Insert {
9841×
                    node: *node,
9851×
                    after: fold_atom(*after, into, &per_node, node)
9861×
                        .ok_or_else(|| too_new("an anchor line"))?,
9871×
                    before: fold_atom(*before, into, &per_node, node)
9881×
                        .ok_or_else(|| too_new("an anchor line"))?,
9891×
                    lines: lines.clone(),
990
                },
9910×
                Edit::Delete { node, atoms } => Edit::Delete {
9920×
                    node: *node,
9930×
                    atoms: atoms
9940×
                        .iter()
9950×
                        .map(|r| {
9960×
                            if r.change == *into {
9970×
                                AtomRange {
9980×
                                    change: ObjectRef::ZERO,
9990×
                                    from: r.from,
10000×
                                    to: r.to,
10010×
                                }
10020×
                            } else if r.change.is_zero() {
10030×
                                let off = per_node.get(node).copied().unwrap_or(0);
10040×
                                AtomRange {
10050×
                                    change: ObjectRef::ZERO,
10060×
                                    from: r.from + off,
10070×
                                    to: r.to + off,
10080×
                                }
1009
                            } else {
10100×
                                *r
1011
                            }
10120×
                        })
10130×
                        .collect(),
1014
                },
10150×
                other => other.clone(),
1016
            });
1017
        }
10181×
        let mut new = Change {
1019
            version: crate::objects::CHANGE_VERSION,
10201×
            change_id: dst.change_id,
10211×
            deps: vec![],
10221×
            author: dst.author,
10231×
            hlc: Hlc::now(),
10241×
            message: dst.message.clone(),
10251×
            tree_ops,
10261×
            edits,
10271×
            kind: if dst.kind == ChangeKind::Resolution || src.kind == ChangeKind::Resolution {
10280×
                ChangeKind::Resolution
1029
            } else {
10301×
                ChangeKind::Normal
1031
            },
1032
        };
10331×
        let beneath = self.load_state(&dst.deps)?;
10341×
        let mut deps: BTreeSet<ChangeHash> = dst.deps.iter().copied().collect();
10351×
        deps.extend(
10361×
            referenced_changes(&new, &beneath)
10371×
                .into_iter()
10381×
                .filter(|h| *h != *into && *h != *c),
1039
        );
10401×
        deps.extend(src.deps.iter().filter(|d| **d != *into).copied());
10411×
        new.deps = deps.into_iter().collect();
10421×
        new.normalize();
1043
1044
        // Dependents of dst keep their indices; dependents of src follow its
1045
        // atoms and operations to their shifted places in the new change.
10461×
        let cmd = format!(
1047
            "oxy squash {} --into {}",
10481×
            src.change_id.short(8),
10491×
            dst.change_id.short(8)
1050
        );
10511×
        let dst_new = self
10521×
            .rewrite_change(into, Some(&new), &Correspondence::identity(&dst), &cmd)?
10531×
            .unwrap();
1054
        // src was rewritten along with the other dependents of dst; find it by ChangeId.
10551×
        let (wc_hash, _) = self.working_copy()?;
10561×
        let mut all = self.bookmark_heads()?;
10571×
        all.push(wc_hash);
10581×
        let state = self.load_state_history(&all)?;
10591×
        let src_now = state
10601×
            .changes()
10612×
            .find(|(_, m)| m.change_id == src.change_id)
10621×
            .map(|(h, _)| *h)
10631×
            .unwrap_or(*c);
10641×
        let src_change = self.store.get_change(&src_now)?;
10651×
        let mut corr = Correspondence {
10661×
            atoms: HashMap::new(),
10671×
            ops: HashMap::new(),
10681×
        };
10691×
        let mut counts: HashMap<NodeId, u32> = HashMap::new();
10701×
        for e in &src_change.edits {
10711×
            if let Edit::Insert { node, lines, .. } = e {
10721×
                let off = per_node.get(node).copied().unwrap_or(0);
10731×
                let idx = counts.entry(*node).or_default();
10741×
                for _ in lines.iter() {
10751×
                    corr.atoms.insert((*node, *idx), *idx + off);
10761×
                    *idx += 1;
10771×
                }
10780×
            }
1079
        }
10801×
        for i in 0..src_change.tree_ops.len() as u32 {
10810×
            corr.ops.insert(i, i + op_offset);
10820×
        }
10831×
        let final_new = self.store.get_change(&dst_new)?;
10841×
        self.rewrite_change(&src_now, Some(&final_new), &corr, &cmd)?;
10851×
        Ok(dst_new)
10862×
    }
1087
1088
    /// Re-express change `c` against the state `onto` names (spec §8.5): its
1089
    /// effect on the files it touches is re-derived as edits against that
1090
    /// state. Keeps the ChangeId; dependents follow by content.
10914×
    pub fn rebase(&self, c: &ChangeHash, onto: &[ChangeHash]) -> Result<ChangeHash> {
10924×
        let old = self.store.get_change(c)?;
10934×
        if old.kind == ChangeKind::WorkingCopy {
10940×
            return fail("that is the working copy; check out the new base instead");
10954×
        }
10964×
        let target = self.load_state(onto)?;
10974×
        if target.contains(c) {
10981×
            return fail(format!(
1099
                "#{} is already part of the target state",
11001×
                c.short(12)
1101
            ));
11023×
        }
11033×
        let onto = target.maximal(onto);
11043×
        let target_snapshot = target.materialize();
1105
        // c applied to the target: the model does this by union, since c
1106
        // commutes with everything it does not depend on. What the union
1107
        // shows is what the re-derived change must produce.
11083×
        let mut with_c = target.clone();
11093×
        if with_c.add(old.clone()).is_err() {
11101×
            return fail(format!(
1111
                "the target state lacks dependencies of #{}; rebase onto a state that has them",
11121×
                c.short(12)
1113
            ));
11142×
        }
11152×
        let after = with_c.materialize();
1116
        // The paths whose file differs between the target and the target with c.
11172×
        let mut touched: BTreeSet<String> = BTreeSet::new();
11184×
        for p in target_snapshot.files.keys().chain(after.files.keys()) {
11194×
            match (target_snapshot.files.get(p), after.files.get(p)) {
11200×
                (Some(a), Some(b))
11214×
                    if a.kind == b.kind && a.mode == b.mode && a.content == b.content => {}
11224×
                _ => {
11234×
                    touched.insert(p.clone());
11244×
                }
1125
            }
1126
        }
11272×
        for p in &touched {
11282×
            if target_snapshot
11292×
                .files
11302×
                .get(p)
11312×
                .is_some_and(|f| f.is_conflicted())
11322×
                || after.files.get(p).is_some_and(|f| f.is_conflicted())
1133
            {
11341×
                return Err(RepoError::Conflicted(format!(
11351×
                    "{p} would be conflicted, and a rebased change cannot hold a conflict. \
11361×
                     To resolve it in the working copy instead, on the bookmark holding #{} \
11371×
                     pass --into-working-copy (the target is merged in), fix the marked files \
11381×
                     and oxy commit",
11391×
                    c.short(12)
11401×
                )));
11411×
            }
1142
        }
1143
        // Entries: the target's files, with touched paths as c leaves them.
11441×
        let mut entries: BTreeMap<String, FsEntry> = BTreeMap::new();
11451×
        let mut reuse: HashMap<String, (NodeId, NodeKind)> = HashMap::new();
11461×
        let content_of = |f: &crate::state::FileState| -> Result<Option<Vec<u8>>> {
11471×
            Ok(match &f.content {
11480×
                FileContent::Dir => None,
11491×
                FileContent::Text { bytes, .. } => Some(bytes.clone()),
11500×
                FileContent::Opaque { blobs } => match blobs.first() {
11510×
                    Some(b) => Some(self.read_blob(b)?),
11520×
                    None => None,
1153
                },
1154
            })
11551×
        };
11561×
        for (p, f) in &target_snapshot.files {
11571×
            if touched.contains(p) {
11581×
                continue;
11590×
            }
11600×
            if let Some(content) = content_of(f)? {
11610×
                entries.insert(p.clone(), FsEntry::new(f.kind, f.mode.exec, content));
11620×
            }
1163
        }
11641×
        for p in &touched {
11651×
            if let Some(f) = after.files.get(p) {
11661×
                if let Some(content) = content_of(f)? {
11671×
                    entries.insert(p.clone(), FsEntry::new(f.kind, f.mode.exec, content));
11681×
                }
11691×
                if !target_snapshot.files.contains_key(p) {
11700×
                    reuse.insert(p.clone(), (f.node, f.kind));
11711×
                }
11720×
            }
1173
        }
11741×
        let built = self.build_from_entries(&target, &target_snapshot, reuse, &entries)?;
11751×
        if built.tree_ops.is_empty() && built.edits.is_empty() {
11760×
            return fail(
1177
                "rebasing would leave the change empty; the target already has its effect",
1178
            );
11791×
        }
11801×
        let mut new = Change {
1181
            version: crate::objects::CHANGE_VERSION,
11821×
            change_id: old.change_id,
11831×
            deps: vec![],
11841×
            author: old.author,
11851×
            hlc: Hlc::now(),
11861×
            message: old.message.clone(),
11871×
            tree_ops: built.tree_ops,
11881×
            edits: built.edits,
11891×
            kind: if built.resolves {
11900×
                ChangeKind::Resolution
1191
            } else {
11921×
                ChangeKind::Normal
1193
            },
1194
        };
11951×
        let mut deps: BTreeSet<ChangeHash> = onto.iter().copied().collect();
11961×
        deps.extend(referenced_changes(&new, &target));
11971×
        deps.extend(built.contributors.iter().copied());
11981×
        new.deps = deps.into_iter().collect();
11991×
        new.normalize();
12001×
        let corr = Correspondence::by_content(&old, &new);
12011×
        let cmd = format!(
1202
            "oxy rebase {} --onto {}",
12031×
            old.change_id.short(8),
12041×
            onto.iter()
12051×
                .map(|h| h.short(12))
12061×
                .collect::<Vec<_>>()
12071×
                .join(" ")
1208
        );
12091×
        Ok(self.rewrite_change(c, Some(&new), &corr, &cmd)?.unwrap())
12104×
    }
1211
}