OXYGENHUB

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

803 of 1033 lines run, 53 of 132 functions · run #358 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.
176×
    fn rewritable_head(&self, hash: &ChangeHash) -> Result<Change> {
186×
        let heads = self.bookmark_heads()?;
196×
        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));
216×
        }
226×
        let c = self.store.get_change(hash)?;
236×
        if c.kind == ChangeKind::WorkingCopy {
240×
            return fail("that is the working copy; commit it instead");
256×
        }
266×
        Ok(c)
276×
    }
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.
8612×
    fn rewrite_change(
8712×
        &self,
8812×
        old: &ChangeHash,
8912×
        new: Option<&Change>,
9012×
        corr: &Correspondence,
9112×
        command: &str,
9212×
    ) -> Result<Option<ChangeHash>> {
9312×
        self.rewrite_change_noted(old, new, corr, command)
9412×
            .map(|(h, _)| h)
9512×
    }
96
97
    /// Like `rewrite_change`, also returning notes about reified references.
9814×
    fn rewrite_change_noted(
9914×
        &self,
10014×
        old: &ChangeHash,
10114×
        new: Option<&Change>,
10214×
        corr: &Correspondence,
10314×
        command: &str,
10414×
    ) -> Result<(Option<ChangeHash>, Vec<String>)> {
10514×
        let _wc = self.lock_working_copy()?;
10614×
        let (op_ref, op) = self.head_op()?;
10714×
        let bookmark = self.config.bookmark.clone();
10814×
        let heads = self.heads_of(&op.view, &bookmark)?;
10914×
        let (wc_hash, _) = self.working_copy()?;
11014×
        let mut all = heads.clone();
11114×
        all.push(wc_hash);
11214×
        let state = self.load_state_history(&all)?;
11314×
        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
            ));
12314×
        }
12414×
        let old_change = self.store.get_change(old)?;
12514×
        if old_change.kind == ChangeKind::WorkingCopy {
1260×
            return fail("that is the working copy; commit it instead");
12714×
        }
12845×
        let load = |h: &ChangeHash| self.store.get_change(h).ok();
12914×
        let propagation =
13014×
            propagate(&state, *old, new, corr, &load).map_err(|e| RepoError::Msg(e.to_string()))?;
13114×
        let rewritten = propagation.changes;
132
13314×
        let mut mapping: HashMap<ChangeHash, ChangeHash> = HashMap::new();
13414×
        let new_hash = match new {
13512×
            Some(c) => {
13612×
                let h = self.store_signed(c)?;
13712×
                mapping.insert(*old, h);
13812×
                Some(h)
139
            }
1402×
            None => None,
141
        };
14231×
        for (h, c) in &rewritten {
14331×
            let nh = if c.kind == ChangeKind::WorkingCopy {
14413×
                self.store.put_change(c)?
145
            } else {
14618×
                self.store_signed(c)?
147
            };
14831×
            mapping.insert(*h, nh);
149
        }
15014×
        let remap = |set: &[ChangeHash]| -> Vec<ChangeHash> {
15114×
            let mut out = Vec::new();
15219×
            for h in set {
15319×
                if h == old && new_hash.is_none() {
1540×
                    out.extend(old_change.deps.iter().copied());
15519×
                } else {
15619×
                    out.push(*mapping.get(h).unwrap_or(h));
15719×
                }
158
            }
15914×
            out
16014×
        };
16114×
        let candidates = remap(&heads);
16214×
        let st = self.load_state_history(&candidates)?;
16314×
        let new_heads = st.maximal(&candidates);
16414×
        let new_wc = mapping.get(&wc_hash).copied().unwrap_or(wc_hash);
165
16614×
        let mut view = op.view.clone();
16714×
        view.bookmarks.insert(
16814×
            bookmark,
16914×
            self.store.put_state(&StateObject::new(new_heads))?,
170
        );
17114×
        view.workspaces.insert(self.workspace, new_wc);
17214×
        self.record_op(&[op_ref], command, view)?;
17314×
        self.drop_manifest();
17414×
        Ok((new_hash, propagation.notes))
17514×
    }
176
177
    /// Change the message of any change of the bookmark; dependents are rewritten.
1785×
    pub fn describe(&self, hash: &ChangeHash, message: &str) -> Result<ChangeHash> {
1795×
        if message.trim().is_empty() {
1800×
            return fail("a message is required");
1815×
        }
1825×
        let mut c = self.store.get_change(hash)?;
1835×
        if c.kind == ChangeKind::WorkingCopy {
1840×
            return fail("that is the working copy; commit it instead");
1855×
        }
1865×
        if c.message == message.trim_end() {
1870×
            return Ok(*hash);
1885×
        }
1895×
        let corr = Correspondence::identity(&c);
1905×
        c.message = message.trim_end().to_string();
1915×
        c.hlc = Hlc::now();
1925×
        let id = c.change_id;
1935×
        Ok(self
1945×
            .rewrite_change(
1955×
                hash,
1965×
                Some(&c),
1975×
                &corr,
1985×
                &format!("oxy describe {} -m {message:?}", id.short(8)),
1990×
            )?
2005×
            .unwrap())
2015×
    }
202
203
    /// Fold only `paths` of the working copy into the head it sits on
204
    /// (oxygen#118): they are committed as a change of their own, on the
205
    /// head, which is then squashed into it; the rest stays uncommitted
206
    /// work. A new `message` replaces the head's.
2071×
    pub fn amend_paths(&self, message: Option<&str>, paths: &[String]) -> Result<ChangeHash> {
2081×
        let snap = self.snapshot("oxy amend")?;
2091×
        let base_heads = snap.base.maximal(&snap.wc.deps);
2101×
        let [head] = base_heads.as_slice() else {
2110×
            return fail(format!(
212
                "the working copy sits on {} heads; check out the single change to amend",
2130×
                base_heads.len()
214
            ));
215
        };
2161×
        let head = *head;
2171×
        self.rewritable_head(&head)?;
2181×
        drop(snap);
2191×
        let (part, _) = self.commit_selection_partial(
2201×
            "oxy amend: the paths to fold in",
2211×
            &[head],
2221×
            &Selection::Paths(paths.to_vec()),
2230×
        )?;
2241×
        let folded = self.squash(&part, &head)?;
2251×
        match message {
2261×
            Some(m) => self.describe(&folded, m),
2270×
            None => Ok(folded),
228
        }
2291×
    }
230
231
    /// Fold the working copy into the head it sits on: the head is rebuilt
232
    /// as the difference between the state beneath it and the files on disk.
2333×
    pub fn amend(&self, message: Option<&str>) -> Result<ChangeHash> {
2343×
        let snap = self.snapshot("oxy amend")?;
2353×
        if snap
2363×
            .statuses
2373×
            .iter()
2383×
            .any(|s| s.status == ChangeStatus::ConflictEdited)
239
        {
2400×
            return fail("a file still contains conflict markers");
2413×
        }
2423×
        let base_heads = snap.base.maximal(&snap.wc.deps);
2433×
        let [head] = base_heads.as_slice() else {
2440×
            return fail(format!(
245
                "the working copy sits on {} heads; check out the single change to amend",
2460×
                base_heads.len()
247
            ));
248
        };
2493×
        let head = *head;
2503×
        let old = self.rewritable_head(&head)?;
2513×
        if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() && message.is_none() {
2521×
            return fail(
253
                "nothing to amend: the working copy is clean and no new message was given",
254
            );
2552×
        }
256
        // Rebuild against the state beneath the head.
2572×
        let beneath = self.load_state(&old.deps)?;
2582×
        let beneath_snapshot = beneath.materialize();
2592×
        let reuse = added_node_paths(&old, &beneath_snapshot);
2602×
        let built = self.build_against(&beneath, &beneath_snapshot, reuse)?;
2612×
        if built.tree_ops.is_empty() && built.edits.is_empty() {
2620×
            return fail("amending would leave the change empty; use `oxy abandon` to drop it");
2632×
        }
2642×
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
2652×
        deps.extend(built.contributors.iter().copied());
2662×
        let mut new = Change {
267
            version: crate::objects::CHANGE_VERSION,
2682×
            change_id: old.change_id,
2692×
            deps: vec![],
2702×
            author: self.author(),
2712×
            hlc: Hlc::now(),
2722×
            message: message
2732×
                .map(|m| m.trim_end().to_string())
2742×
                .unwrap_or(old.message.clone()),
2752×
            tree_ops: built.tree_ops,
2762×
            edits: built.edits,
2772×
            kind: if built.resolves || old.kind == ChangeKind::Resolution {
2780×
                ChangeKind::Resolution
279
            } else {
2802×
                ChangeKind::Normal
281
            },
282
        };
2832×
        deps.extend(referenced_changes(&new, &beneath));
2842×
        new.deps = deps.into_iter().collect();
2852×
        new.normalize();
2862×
        Ok(self
2872×
            .replace_head(
2882×
                &head,
2892×
                Some(&new),
2902×
                &format!("oxy amend {}", old.change_id.short(8)),
2910×
            )?
2922×
            .unwrap())
2933×
    }
294
295
    /// Drop a change from the bookmark. Dependents inherit its dependencies;
296
    /// references into it are reified by re-anchoring (spec §8.4) and the
297
    /// adjustments are returned as notes. A dropped head's content stays on
298
    /// disk as uncommitted work if the working copy sat on it.
2994×
    pub fn abandon(&self, hash: &ChangeHash) -> Result<Vec<String>> {
3004×
        let c = self.store.get_change(hash)?;
3014×
        if c.kind == ChangeKind::WorkingCopy {
3020×
            return fail("that is the working copy; commit it instead");
3034×
        }
3044×
        let heads = self.bookmark_heads()?;
3054×
        if heads.contains(hash) {
3062×
            self.replace_head(hash, None, &format!("oxy abandon {}", c.change_id.short(8)))?;
3072×
            Ok(vec![])
308
        } else {
3092×
            let (_, notes) = self.rewrite_change_noted(
3102×
                hash,
3112×
                None,
3122×
                &Correspondence::default(),
3132×
                &format!("oxy abandon {}", c.change_id.short(8)),
3140×
            )?;
3152×
            Ok(notes)
316
        }
3174×
    }
318
}
319
320
/// Translate a reference to change `target`'s own atom or op into a self
321
/// reference, and a working-copy self reference into one offset past the
322
/// target's own count; other references pass through.
3236×
fn fold_atom(
3246×
    r: AtomRef,
3256×
    target: &ChangeHash,
3266×
    wc_offset: &HashMap<NodeId, u32>,
3276×
    node: &NodeId,
3286×
) -> Option<AtomRef> {
3291×
    match r {
3304×
        AtomRef::Atom { change, index } if change == *target => Some(AtomRef::Atom {
3313×
            change: ObjectRef::ZERO,
3323×
            index,
3333×
        }),
3341×
        AtomRef::Atom { change, index } if change.is_zero() => Some(AtomRef::Atom {
3350×
            change: ObjectRef::ZERO,
3360×
            index: index + wc_offset.get(node).copied().unwrap_or(0),
3370×
        }),
338
        // References into the target's closure were validated by the caller and stay as they are.
3393×
        other => Some(other),
340
    }
3416×
}
342
343
impl Repo {
344
    /// Fold the working copy's uncommitted edits into any change of the
345
    /// bookmark (spec §8.4). The edits must reference only atoms, placements
346
    /// and nodes that exist in the target's own closure; dependents are
347
    /// rewritten with an identity correspondence, since the target's own
348
    /// atoms keep their indices.
3492×
    pub fn amend_into(&self, target: &ChangeHash, message: Option<&str>) -> Result<ChangeHash> {
3502×
        let _wc = self.lock_working_copy()?;
3512×
        let snap = self.snapshot("oxy amend")?;
3522×
        if snap
3532×
            .statuses
3542×
            .iter()
3552×
            .any(|s| s.status == ChangeStatus::ConflictEdited)
356
        {
3570×
            return fail("a file still contains conflict markers");
3582×
        }
3592×
        let wc = &snap.wc;
3602×
        if wc.tree_ops.is_empty() && wc.edits.is_empty() && message.is_none() {
3610×
            return fail(
362
                "nothing to amend: the working copy is clean and no new message was given",
363
            );
3642×
        }
3652×
        let old = self.store.get_change(target)?;
3662×
        if old.kind == ChangeKind::WorkingCopy {
3670×
            return fail("that is the working copy; commit it instead");
3682×
        }
369
        // Every reference must live in the target's closure (or be the target).
3702×
        let closure = self.load_state_history(&[*target])?;
3714×
        let ok_change = |h: &ChangeHash| *h == *target || closure.contains(h);
3723×
        let ok_node = |n: &NodeId| n.is_root() || closure.tree().introducer(n).is_some();
3732×
        let too_new = |what: &str| {
3741×
            RepoError::Msg(format!(
3751×
                "{what} was introduced after change #{}; amend into a later change instead",
3761×
                target.short(12)
3771×
            ))
3781×
        };
3792×
        for op in &wc.tree_ops {
3800×
            match op {
3810×
                TreeOp::AddNode { parent, .. } => {
3820×
                    if !ok_node(parent) {
3830×
                        return Err(too_new("the parent directory"));
3840×
                    }
385
                }
3860×
                TreeOp::RemoveNode { node, placement } => {
3870×
                    if !ok_node(node) || !ok_change(&placement.change) {
3880×
                        return Err(too_new("a removed file"));
3890×
                    }
390
                }
391
                TreeOp::Move {
3920×
                    node, old, parent, ..
393
                } => {
3940×
                    if !ok_node(node) || !ok_change(&old.change) || !ok_node(parent) {
3950×
                        return Err(too_new("a moved file"));
3960×
                    }
397
                }
3980×
                TreeOp::SetMode { node, old, .. } => {
3990×
                    if !ok_node(node) || !ok_change(&old.change) {
4000×
                        return Err(too_new("a file whose mode changed"));
4010×
                    }
402
                }
403
            }
404
        }
4052×
        let wc_added: BTreeSet<NodeId> = wc
4062×
            .tree_ops
4072×
            .iter()
4082×
            .filter_map(|o| {
4090×
                if let TreeOp::AddNode { node, .. } = o {
4100×
                    Some(*node)
411
                } else {
4120×
                    None
413
                }
4140×
            })
4152×
            .collect();
4163×
        for e in &wc.edits {
4173×
            let n = e.node();
4183×
            if !wc_added.contains(&n) && !ok_node(&n) {
4190×
                return Err(too_new("an edited file"));
4203×
            }
4213×
            let refs: Vec<ChangeHash> = match e {
4221×
                Edit::Insert { after, before, .. } => [after, before]
4231×
                    .iter()
4242×
                    .filter_map(|r| {
4252×
                        if let AtomRef::Atom { change, .. } = r {
4262×
                            Some(*change)
427
                        } else {
4280×
                            None
429
                        }
4302×
                    })
4311×
                    .collect(),
4322×
                Edit::Delete { atoms, .. } => atoms.iter().map(|r| r.change).collect(),
4330×
                Edit::Replace { .. } => vec![],
434
            };
4354×
            if refs.iter().any(|h| !h.is_zero() && !ok_change(h)) {
4361×
                return Err(too_new("an edited line"));
4372×
            }
438
        }
439
440
        // Compose: the target's operations first, then the working copy's
441
        // with references to the target turned into self references and its
442
        // own atom indices shifted past the target's.
4431×
        let mut per_node: HashMap<NodeId, u32> = HashMap::new();
4442×
        for e in &old.edits {
4452×
            if let Edit::Insert { node, lines, .. } = e {
4461×
                *per_node.entry(*node).or_default() += lines.len() as u32;
4471×
            }
448
        }
4491×
        let op_offset = old.tree_ops.len() as u32;
4501×
        let fold_op = |r: OpRef| -> OpRef {
4510×
            if r.change == *target {
4520×
                OpRef {
4530×
                    change: ObjectRef::ZERO,
4540×
                    index: r.index,
4550×
                }
4560×
            } else if r.change.is_zero() {
4570×
                OpRef {
4580×
                    change: ObjectRef::ZERO,
4590×
                    index: r.index + op_offset,
4600×
                }
461
            } else {
4620×
                r
463
            }
4640×
        };
4651×
        let mut tree_ops = old.tree_ops.clone();
4661×
        for op in &wc.tree_ops {
4670×
            tree_ops.push(match op {
4680×
                TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode {
4690×
                    node: *node,
4700×
                    placement: fold_op(*placement),
4710×
                },
472
                TreeOp::Move {
4730×
                    node,
4740×
                    old,
4750×
                    parent,
4760×
                    name,
4770×
                } => TreeOp::Move {
4780×
                    node: *node,
4790×
                    old: fold_op(*old),
4800×
                    parent: *parent,
4810×
                    name: name.clone(),
4820×
                },
4830×
                TreeOp::SetMode { node, old, mode } => TreeOp::SetMode {
4840×
                    node: *node,
4850×
                    old: fold_op(*old),
4860×
                    mode: *mode,
4870×
                },
4880×
                add => add.clone(),
489
            });
490
        }
4911×
        let mut edits = old.edits.clone();
4922×
        for e in &wc.edits {
4932×
            edits.push(match e {
494
                Edit::Insert {
4951×
                    node,
4961×
                    after,
4971×
                    before,
4981×
                    lines,
499
                } => Edit::Insert {
5001×
                    node: *node,
5011×
                    after: fold_atom(*after, target, &per_node, node)
5021×
                        .ok_or_else(|| too_new("an anchor line"))?,
5031×
                    before: fold_atom(*before, target, &per_node, node)
5041×
                        .ok_or_else(|| too_new("an anchor line"))?,
5051×
                    lines: lines.clone(),
506
                },
5071×
                Edit::Delete { node, atoms } => Edit::Delete {
5081×
                    node: *node,
5091×
                    atoms: atoms
5101×
                        .iter()
5111×
                        .map(|r| {
5121×
                            if r.change == *target {
5131×
                                AtomRange {
5141×
                                    change: ObjectRef::ZERO,
5151×
                                    from: r.from,
5161×
                                    to: r.to,
5171×
                                }
5180×
                            } else if r.change.is_zero() {
5190×
                                let off = per_node.get(node).copied().unwrap_or(0);
5200×
                                AtomRange {
5210×
                                    change: ObjectRef::ZERO,
5220×
                                    from: r.from + off,
5230×
                                    to: r.to + off,
5240×
                                }
525
                            } else {
5260×
                                *r
527
                            }
5281×
                        })
5291×
                        .collect(),
530
                },
5310×
                other => other.clone(),
532
            });
533
        }
5341×
        let mut new = Change {
535
            version: crate::objects::CHANGE_VERSION,
5361×
            change_id: old.change_id,
5371×
            deps: vec![],
5381×
            author: self.author(),
5391×
            hlc: Hlc::now(),
5401×
            message: message
5411×
                .map(|m| m.trim_end().to_string())
5421×
                .unwrap_or(old.message.clone()),
5431×
            tree_ops,
5441×
            edits,
5451×
            kind: old.kind,
546
        };
5471×
        let beneath = self.load_state(&old.deps)?;
5481×
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
5491×
        deps.extend(
5501×
            referenced_changes(&new, &beneath)
5511×
                .into_iter()
5521×
                .filter(|h| *h != *target),
553
        );
5541×
        deps.extend(
5551×
            wc.deps
5561×
                .iter()
5573×
                .filter(|d| closure.contains(d) && **d != *target)
5581×
                .copied(),
559
        );
5601×
        new.deps = deps.into_iter().collect();
5611×
        new.normalize();
562
563
        // Rewrite dependents (identity correspondence), then start a fresh,
564
        // empty working copy: its edits now live in the amended change.
5651×
        let corr = Correspondence::identity(&old);
5661×
        let new_hash = self
5671×
            .rewrite_change(
5681×
                target,
5691×
                Some(&new),
5701×
                &corr,
5711×
                &format!("oxy amend --into {}", old.change_id.short(8)),
5720×
            )?
5731×
            .unwrap();
5741×
        let (op_ref, op) = self.head_op()?;
5751×
        let heads = self.heads_of(&op.view, &self.config.bookmark)?;
5761×
        let fresh = self.new_working_copy(heads, "");
5771×
        let wc_hash = self.store.put_change(&fresh)?;
5781×
        let mut view = op.view.clone();
5791×
        view.workspaces.insert(self.workspace, wc_hash);
5801×
        self.record_op(&[op_ref], "oxy amend (working copy)", view)?;
5811×
        self.drop_manifest();
5821×
        Ok(new_hash)
5832×
    }
584
585
    /// Split a change into two: one holding its effect on `paths`, the
586
    /// other the rest (keeping the ChangeId). A head is rebuilt from its
587
    /// materialized files; any other change is split by node, which is
588
    /// exact, and is refused when a dependent refers to the split-off
589
    /// nodes. The working copy is unaffected.
5904×
    pub fn split(&self, hash: &ChangeHash, paths: &[String]) -> Result<(ChangeHash, ChangeHash)> {
5914×
        if paths.is_empty() {
5920×
            return fail("name at least one path to split out");
5934×
        }
5944×
        if !self.bookmark_heads()?.contains(hash) {
5952×
            return self.split_by_node(hash, paths);
5962×
        }
5972×
        let old = self.rewritable_head(hash)?;
5982×
        let beneath = self.load_state(&old.deps)?;
5992×
        let beneath_snapshot = beneath.materialize();
6002×
        let full = self.load_state(&[*hash])?;
6012×
        let full_snapshot = full.materialize();
6023×
        let selected = |p: &str| {
6033×
            paths
6043×
                .iter()
6053×
                .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/'))))
6063×
        };
607
        // Entries of the intermediate state: selected paths as the head
608
        // leaves them, everything else as beneath.
6092×
        let entries_of = |snap: &Snapshot,
610
                          keep_from_full: &dyn Fn(&str) -> bool|
6113×
         -> Result<BTreeMap<String, FsEntry>> {
6123×
            let mut out = BTreeMap::new();
6137×
            for (p, _) in snap.files.iter().chain(full_snapshot.files.iter()) {
6147×
                let take_full = keep_from_full(p);
6157×
                let src = if take_full {
6165×
                    full_snapshot.files.get(p)
617
                } else {
6182×
                    snap.files.get(p)
619
                };
6207×
                let Some(src) = src else { continue };
6215×
                if out.contains_key(p) {
6222×
                    continue;
6233×
                }
6243×
                let content = match &src.content {
6250×
                    FileContent::Dir => continue,
6263×
                    FileContent::Text { bytes, .. } => bytes.clone(),
6270×
                    FileContent::Opaque { blobs } => match blobs.first() {
6280×
                        Some(b) => self.read_blob(b)?,
6290×
                        None => continue,
630
                    },
631
                };
6323×
                out.insert(p.clone(), FsEntry::new(src.kind, src.mode.exec, content));
633
            }
6343×
            Ok(out)
6353×
        };
6363×
        let mid_entries = entries_of(&beneath_snapshot, &|p| selected(p))?;
6372×
        let first =
6382×
            self.build_from_entries(&beneath, &beneath_snapshot, HashMap::new(), &mid_entries)?;
6392×
        if first.tree_ops.is_empty() && first.edits.is_empty() {
6401×
            return fail("the named paths are not touched by that change");
6411×
        }
6421×
        let mut c1 = Change {
6431×
            version: crate::objects::CHANGE_VERSION,
6441×
            change_id: ChangeId::random(),
6451×
            deps: old.deps.clone(),
6461×
            author: old.author,
6471×
            hlc: Hlc::now(),
6481×
            message: format!(
6491×
                "{} (part 1: {})",
6501×
                old.message.lines().next().unwrap_or(""),
6511×
                paths.join(", ")
6521×
            ),
6531×
            tree_ops: first.tree_ops,
6541×
            edits: first.edits,
6551×
            kind: ChangeKind::Normal,
6561×
        };
6571×
        let mut d1: BTreeSet<ChangeHash> = referenced_changes(&c1, &beneath);
6581×
        d1.extend(old.deps.iter().copied());
6591×
        c1.deps = d1.into_iter().collect();
6601×
        c1.normalize();
6611×
        let h1 = self.store_signed(&c1)?;
6621×
        let mut mid = beneath.clone();
6631×
        mid.add(c1)?;
6641×
        let mid_snapshot = mid.materialize();
6651×
        let rest_entries = entries_of(&full_snapshot, &|_| true)?;
6661×
        let rest = self.build_from_entries(&mid, &mid_snapshot, HashMap::new(), &rest_entries)?;
6671×
        if rest.tree_ops.is_empty() && rest.edits.is_empty() {
6680×
            return fail(
669
                "everything in that change is under the named paths; nothing would remain",
670
            );
6711×
        }
6721×
        let mut c2 = Change {
6731×
            version: crate::objects::CHANGE_VERSION,
6741×
            change_id: old.change_id,
6751×
            deps: vec![],
6761×
            author: old.author,
6771×
            hlc: Hlc::now(),
6781×
            message: old.message.clone(),
6791×
            tree_ops: rest.tree_ops,
6801×
            edits: rest.edits,
6811×
            kind: ChangeKind::Normal,
6821×
        };
6831×
        let mut d2: BTreeSet<ChangeHash> = referenced_changes(&c2, &mid);
6841×
        d2.extend(old.deps.iter().copied());
6851×
        c2.deps = d2.into_iter().collect();
6861×
        c2.normalize();
6871×
        let h2 = self
6881×
            .replace_head(
6891×
                hash,
6901×
                Some(&c2),
6911×
                &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")),
6920×
            )?
6931×
            .unwrap();
6941×
        self.add_to_bookmark(h1)?;
6951×
        Ok((h1, h2))
6964×
    }
697
698
    /// Put `h` into the bookmark's state (a head unless something already
699
    /// depends on it) and under the working copy.
7002×
    fn add_to_bookmark(&self, h: ChangeHash) -> Result<()> {
7012×
        let (op_ref, op) = self.head_op()?;
7022×
        let bookmark = self.config.bookmark.clone();
7032×
        let mut heads = self.heads_of(&op.view, &bookmark)?;
7042×
        heads.push(h);
7052×
        let st = self.load_state_history(&heads)?;
7062×
        let heads = st.maximal(&heads);
7072×
        let mut view = op.view.clone();
7082×
        view.bookmarks.insert(
7092×
            bookmark,
7102×
            self.store.put_state(&StateObject::new(heads.clone()))?,
711
        );
7122×
        let (_, wc) = self.working_copy()?;
7132×
        let mut base = wc.deps.clone();
7142×
        base.push(h);
7152×
        let wc_base = self.load_state_history(&base)?.maximal(&base);
7162×
        let fresh = self.new_working_copy(wc_base, &wc.message);
7172×
        view.workspaces
7182×
            .insert(self.workspace, self.store.put_change(&fresh)?);
7192×
        self.record_op(&[op_ref], "oxy split (heads)", view)?;
7202×
        self.drop_manifest();
7212×
        Ok(())
7222×
    }
723
724
    /// Split a non-head change by node: the operations and edits on nodes
725
    /// under `paths` become a new change, the rest keeps the ChangeId with
726
    /// every remaining atom at its old index, so dependents are rewritten
727
    /// exactly. Refused when a dependent touches a split-off node.
7282×
    fn split_by_node(
7292×
        &self,
7302×
        hash: &ChangeHash,
7312×
        paths: &[String],
7322×
    ) -> Result<(ChangeHash, ChangeHash)> {
7332×
        let old = self.store.get_change(hash)?;
7342×
        if old.kind == ChangeKind::WorkingCopy {
7350×
            return fail("that is the working copy; commit it instead");
7362×
        }
7376×
        let selected = |p: &str| {
7386×
            paths
7396×
                .iter()
7406×
                .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/'))))
7416×
        };
7422×
        let beneath = self.load_state(&old.deps)?;
7432×
        let full = self.load_state(&[*hash])?;
7442×
        let mut removed: BTreeSet<NodeId> = BTreeSet::new();
7454×
        for snap in [beneath.materialize(), full.materialize()] {
7466×
            for (p, f) in &snap.files {
7476×
                if selected(p) {
7485×
                    removed.insert(f.node);
7495×
                }
750
            }
751
        }
752
        // Nodes the change itself adds under a removed directory.
7536×
        for op in &old.tree_ops {
7546×
            if let TreeOp::AddNode { node, parent, .. } = op {
7556×
                if removed.contains(parent) {
7563×
                    removed.insert(*node);
7573×
                }
7580×
            }
759
        }
7602×
        if removed.is_empty() {
7610×
            return fail("the named paths are not touched by that change");
7622×
        }
763
        // Dependents must not touch the split-off nodes: their references
764
        // would have to move to the new change, which a rewrite cannot express.
7652×
        let heads = self.bookmark_heads()?;
7662×
        let (wc_hash, _) = self.working_copy()?;
7672×
        let mut all = heads.clone();
7682×
        all.push(wc_hash);
7692×
        let state = self.load_state_history(&all)?;
7707×
        for (h, meta) in state.changes() {
7717×
            if !meta.deps.contains(hash) {
7725×
                continue;
7732×
            }
7742×
            let d = self.store.get_change(h)?;
7752×
            if d.tree_ops.iter().any(|op| removed.contains(&op.node()))
7763×
                || d.edits.iter().any(|e| removed.contains(&e.node()))
777
            {
7781×
                return fail(format!(
779
                    "change {} depends on the split-off paths of {}; split it first",
7801×
                    d.change_id.short(8),
7811×
                    old.change_id.short(8)
782
                ));
7831×
            }
784
        }
7851×
        let mut c1 = Change {
786
            version: crate::objects::CHANGE_VERSION,
7871×
            change_id: ChangeId::random(),
7881×
            deps: old.deps.clone(),
7891×
            author: old.author,
7901×
            hlc: Hlc::now(),
7911×
            message: format!(
792
                "{} (part 1: {})",
7931×
                old.message.lines().next().unwrap_or(""),
7941×
                paths.join(", ")
795
            ),
7961×
            tree_ops: old
7971×
                .tree_ops
7981×
                .iter()
7993×
                .filter(|op| removed.contains(&op.node()))
8001×
                .cloned()
8011×
                .collect(),
8021×
            edits: old
8031×
                .edits
8041×
                .iter()
8052×
                .filter(|e| removed.contains(&e.node()))
8061×
                .cloned()
8071×
                .collect(),
8081×
            kind: ChangeKind::Normal,
809
        };
8101×
        let mut c2 = old.clone();
8111×
        c2.hlc = Hlc::now();
8121×
        let mut ops_map: HashMap<u32, u32> = HashMap::new();
8131×
        let mut kept_ops = Vec::new();
8143×
        for (i, op) in old.tree_ops.iter().enumerate() {
8153×
            if !removed.contains(&op.node()) {
8161×
                ops_map.insert(i as u32, kept_ops.len() as u32);
8171×
                kept_ops.push(op.clone());
8182×
            }
819
        }
8201×
        c2.tree_ops = kept_ops;
8212×
        c2.edits.retain(|e| !removed.contains(&e.node()));
8221×
        if c1.tree_ops.is_empty() && c1.edits.is_empty() {
8230×
            return fail("the named paths are not touched by that change");
8241×
        }
8251×
        if c2.tree_ops.is_empty() && c2.edits.is_empty() {
8260×
            return fail(
827
                "everything in that change is under the named paths; use `oxy abandon` instead",
828
            );
8291×
        }
8301×
        let mut d1: BTreeSet<ChangeHash> = referenced_changes(&c1, &beneath);
8311×
        d1.extend(old.deps.iter().copied());
8321×
        c1.deps = d1.into_iter().collect();
8331×
        c1.normalize();
8341×
        let mut d2: BTreeSet<ChangeHash> = referenced_changes(&c2, &beneath);
8351×
        d2.extend(old.deps.iter().copied());
8361×
        c2.deps = d2.into_iter().collect();
8371×
        c2.normalize();
8381×
        let corr = Correspondence {
8391×
            atoms: Correspondence::identity(&old).atoms,
8401×
            ops: ops_map,
8411×
        };
8421×
        let h2 = self
8431×
            .rewrite_change(
8441×
                hash,
8451×
                Some(&c2),
8461×
                &corr,
8471×
                &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")),
8480×
            )?
8491×
            .unwrap();
8501×
        let h1 = self.store_signed(&c1)?;
8511×
        self.add_to_bookmark(h1)?;
8521×
        Ok((h1, h2))
8532×
    }
854
}
855
856
impl Repo {
857
    /// Fold change `c` into its dependency `into` (spec §8.5): the result
858
    /// keeps `into`'s ChangeId, dependents of both are rewritten, and `c`
859
    /// leaves the state.
8603×
    pub fn squash(&self, c: &ChangeHash, into: &ChangeHash) -> Result<ChangeHash> {
8613×
        if c == into {
8620×
            return fail("a change cannot be squashed into itself");
8633×
        }
8643×
        let src = self.store.get_change(c)?;
8653×
        let dst = self.store.get_change(into)?;
8663×
        if src.kind == ChangeKind::WorkingCopy {
8670×
            return fail("that is the working copy; use `oxy amend --into`");
8683×
        }
8693×
        if dst.kind == ChangeKind::WorkingCopy {
8700×
            return fail("cannot squash into the working copy");
8713×
        }
8723×
        let closure = self.load_state_history(&[*into])?;
8733×
        let outside: Vec<String> = src
8743×
            .deps
8753×
            .iter()
8764×
            .filter(|d| *d != into && !closure.contains(d))
8773×
            .map(|d| d.short(12))
8783×
            .collect();
8793×
        if !outside.is_empty() {
8801×
            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)));
8812×
        }
8822×
        let ok_change = |h: &ChangeHash| *h == *into || closure.contains(h);
8832×
        let ok_node = |n: &NodeId| n.is_root() || closure.tree().introducer(n).is_some();
8842×
        let too_new = |what: &str| {
8850×
            RepoError::Msg(format!(
8860×
                "{what} was introduced after change #{}; squash into a later change instead",
8870×
                into.short(12)
8880×
            ))
8890×
        };
8902×
        let src_added: BTreeSet<NodeId> = src
8912×
            .tree_ops
8922×
            .iter()
8932×
            .filter_map(|o| {
8940×
                if let TreeOp::AddNode { node, .. } = o {
8950×
                    Some(*node)
896
                } else {
8970×
                    None
898
                }
8990×
            })
9002×
            .collect();
9012×
        for op in &src.tree_ops {
9020×
            match op {
9030×
                TreeOp::AddNode { parent, .. } => {
9040×
                    if !ok_node(parent) && !src_added.contains(parent) {
9050×
                        return Err(too_new("the parent directory"));
9060×
                    }
907
                }
9080×
                TreeOp::RemoveNode { node, placement } => {
9090×
                    if !ok_node(node) || !ok_change(&placement.change) {
9100×
                        return Err(too_new("a removed file"));
9110×
                    }
912
                }
913
                TreeOp::Move {
9140×
                    node, old, parent, ..
915
                } => {
9160×
                    if !ok_node(node)
9170×
                        || !ok_change(&old.change)
9180×
                        || (!ok_node(parent) && !src_added.contains(parent))
919
                    {
9200×
                        return Err(too_new("a moved file"));
9210×
                    }
922
                }
9230×
                TreeOp::SetMode { node, old, .. } => {
9240×
                    if !ok_node(node) || !ok_change(&old.change) {
9250×
                        return Err(too_new("a file whose mode changed"));
9260×
                    }
927
                }
928
            }
929
        }
9302×
        for e in &src.edits {
9312×
            let n = e.node();
9322×
            if !src_added.contains(&n) && !ok_node(&n) {
9330×
                return Err(too_new("an edited file"));
9342×
            }
9352×
            let refs: Vec<ChangeHash> = match e {
9362×
                Edit::Insert { after, before, .. } => [after, before]
9372×
                    .iter()
9384×
                    .filter_map(|r| {
9394×
                        if let AtomRef::Atom { change, .. } = r {
9402×
                            Some(*change)
941
                        } else {
9422×
                            None
943
                        }
9444×
                    })
9452×
                    .collect(),
9460×
                Edit::Delete { atoms, .. } => atoms.iter().map(|r| r.change).collect(),
9470×
                Edit::Replace { .. } => vec![],
948
            };
9492×
            if refs.iter().any(|h| !h.is_zero() && !ok_change(h)) {
9500×
                return Err(too_new("an edited line"));
9512×
            }
952
        }
953
        // Compose dst then src, with src's references to dst turned into
954
        // self references and its own atoms shifted past dst's.
9552×
        let mut per_node: HashMap<NodeId, u32> = HashMap::new();
9562×
        for e in &dst.edits {
9572×
            if let Edit::Insert { node, lines, .. } = e {
9582×
                *per_node.entry(*node).or_default() += lines.len() as u32;
9592×
            }
960
        }
9612×
        let op_offset = dst.tree_ops.len() as u32;
9622×
        let fold_op = |r: OpRef| -> OpRef {
9630×
            if r.change == *into {
9640×
                OpRef {
9650×
                    change: ObjectRef::ZERO,
9660×
                    index: r.index,
9670×
                }
9680×
            } else if r.change.is_zero() {
9690×
                OpRef {
9700×
                    change: ObjectRef::ZERO,
9710×
                    index: r.index + op_offset,
9720×
                }
973
            } else {
9740×
                r
975
            }
9760×
        };
9772×
        let mut tree_ops = dst.tree_ops.clone();
9782×
        for op in &src.tree_ops {
9790×
            tree_ops.push(match op {
9800×
                TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode {
9810×
                    node: *node,
9820×
                    placement: fold_op(*placement),
9830×
                },
984
                TreeOp::Move {
9850×
                    node,
9860×
                    old,
9870×
                    parent,
9880×
                    name,
9890×
                } => TreeOp::Move {
9900×
                    node: *node,
9910×
                    old: fold_op(*old),
9920×
                    parent: *parent,
9930×
                    name: name.clone(),
9940×
                },
9950×
                TreeOp::SetMode { node, old, mode } => TreeOp::SetMode {
9960×
                    node: *node,
9970×
                    old: fold_op(*old),
9980×
                    mode: *mode,
9990×
                },
10000×
                add => add.clone(),
1001
            });
1002
        }
10032×
        let mut edits = dst.edits.clone();
10042×
        for e in &src.edits {
10052×
            edits.push(match e {
1006
                Edit::Insert {
10072×
                    node,
10082×
                    after,
10092×
                    before,
10102×
                    lines,
1011
                } => Edit::Insert {
10122×
                    node: *node,
10132×
                    after: fold_atom(*after, into, &per_node, node)
10142×
                        .ok_or_else(|| too_new("an anchor line"))?,
10152×
                    before: fold_atom(*before, into, &per_node, node)
10162×
                        .ok_or_else(|| too_new("an anchor line"))?,
10172×
                    lines: lines.clone(),
1018
                },
10190×
                Edit::Delete { node, atoms } => Edit::Delete {
10200×
                    node: *node,
10210×
                    atoms: atoms
10220×
                        .iter()
10230×
                        .map(|r| {
10240×
                            if r.change == *into {
10250×
                                AtomRange {
10260×
                                    change: ObjectRef::ZERO,
10270×
                                    from: r.from,
10280×
                                    to: r.to,
10290×
                                }
10300×
                            } else if r.change.is_zero() {
10310×
                                let off = per_node.get(node).copied().unwrap_or(0);
10320×
                                AtomRange {
10330×
                                    change: ObjectRef::ZERO,
10340×
                                    from: r.from + off,
10350×
                                    to: r.to + off,
10360×
                                }
1037
                            } else {
10380×
                                *r
1039
                            }
10400×
                        })
10410×
                        .collect(),
1042
                },
10430×
                other => other.clone(),
1044
            });
1045
        }
10462×
        let mut new = Change {
1047
            version: crate::objects::CHANGE_VERSION,
10482×
            change_id: dst.change_id,
10492×
            deps: vec![],
10502×
            author: dst.author,
10512×
            hlc: Hlc::now(),
10522×
            message: dst.message.clone(),
10532×
            tree_ops,
10542×
            edits,
10552×
            kind: if dst.kind == ChangeKind::Resolution || src.kind == ChangeKind::Resolution {
10560×
                ChangeKind::Resolution
1057
            } else {
10582×
                ChangeKind::Normal
1059
            },
1060
        };
10612×
        let beneath = self.load_state(&dst.deps)?;
10622×
        let mut deps: BTreeSet<ChangeHash> = dst.deps.iter().copied().collect();
10632×
        deps.extend(
10642×
            referenced_changes(&new, &beneath)
10652×
                .into_iter()
10662×
                .filter(|h| *h != *into && *h != *c),
1067
        );
10682×
        deps.extend(src.deps.iter().filter(|d| **d != *into).copied());
10692×
        new.deps = deps.into_iter().collect();
10702×
        new.normalize();
1071
1072
        // Dependents of dst keep their indices; dependents of src follow its
1073
        // atoms and operations to their shifted places in the new change.
10742×
        let cmd = format!(
1075
            "oxy squash {} --into {}",
10762×
            src.change_id.short(8),
10772×
            dst.change_id.short(8)
1078
        );
10792×
        let dst_new = self
10802×
            .rewrite_change(into, Some(&new), &Correspondence::identity(&dst), &cmd)?
10812×
            .unwrap();
1082
        // src was rewritten along with the other dependents of dst; find it by ChangeId.
10832×
        let (wc_hash, _) = self.working_copy()?;
10842×
        let mut all = self.bookmark_heads()?;
10852×
        all.push(wc_hash);
10862×
        let state = self.load_state_history(&all)?;
10872×
        let src_now = state
10882×
            .changes()
10899×
            .find(|(_, m)| m.change_id == src.change_id)
10902×
            .map(|(h, _)| *h)
10912×
            .unwrap_or(*c);
10922×
        let src_change = self.store.get_change(&src_now)?;
10932×
        let mut corr = Correspondence {
10942×
            atoms: HashMap::new(),
10952×
            ops: HashMap::new(),
10962×
        };
10972×
        let mut counts: HashMap<NodeId, u32> = HashMap::new();
10982×
        for e in &src_change.edits {
10992×
            if let Edit::Insert { node, lines, .. } = e {
11002×
                let off = per_node.get(node).copied().unwrap_or(0);
11012×
                let idx = counts.entry(*node).or_default();
11022×
                for _ in lines.iter() {
11032×
                    corr.atoms.insert((*node, *idx), *idx + off);
11042×
                    *idx += 1;
11052×
                }
11060×
            }
1107
        }
11082×
        for i in 0..src_change.tree_ops.len() as u32 {
11090×
            corr.ops.insert(i, i + op_offset);
11100×
        }
11112×
        let final_new = self.store.get_change(&dst_new)?;
11122×
        self.rewrite_change(&src_now, Some(&final_new), &corr, &cmd)?;
11132×
        Ok(dst_new)
11143×
    }
1115
1116
    /// Re-express change `c` against the state `onto` names (spec §8.5): its
1117
    /// effect on the files it touches is re-derived as edits against that
1118
    /// state. Keeps the ChangeId; dependents follow by content.
11194×
    pub fn rebase(&self, c: &ChangeHash, onto: &[ChangeHash]) -> Result<ChangeHash> {
11204×
        let old = self.store.get_change(c)?;
11214×
        if old.kind == ChangeKind::WorkingCopy {
11220×
            return fail("that is the working copy; check out the new base instead");
11234×
        }
11244×
        let target = self.load_state(onto)?;
11254×
        if target.contains(c) {
11261×
            return fail(format!(
1127
                "#{} is already part of the target state",
11281×
                c.short(12)
1129
            ));
11303×
        }
11313×
        let onto = target.maximal(onto);
11323×
        let target_snapshot = target.materialize();
1133
        // c applied to the target: the model does this by union, since c
1134
        // commutes with everything it does not depend on. What the union
1135
        // shows is what the re-derived change must produce.
11363×
        let mut with_c = target.clone();
11373×
        if with_c.add(old.clone()).is_err() {
11381×
            return fail(format!(
1139
                "the target state lacks dependencies of #{}; rebase onto a state that has them",
11401×
                c.short(12)
1141
            ));
11422×
        }
11432×
        let after = with_c.materialize();
1144
        // The paths whose file differs between the target and the target with c.
11452×
        let mut touched: BTreeSet<String> = BTreeSet::new();
11464×
        for p in target_snapshot.files.keys().chain(after.files.keys()) {
11474×
            match (target_snapshot.files.get(p), after.files.get(p)) {
11480×
                (Some(a), Some(b))
11494×
                    if a.kind == b.kind && a.mode == b.mode && a.content == b.content => {}
11504×
                _ => {
11514×
                    touched.insert(p.clone());
11524×
                }
1153
            }
1154
        }
11552×
        for p in &touched {
11562×
            if target_snapshot
11572×
                .files
11582×
                .get(p)
11592×
                .is_some_and(|f| f.is_conflicted())
11602×
                || after.files.get(p).is_some_and(|f| f.is_conflicted())
1161
            {
11621×
                return Err(RepoError::Conflicted(format!(
11631×
                    "{p} would be conflicted, and a rebased change cannot hold a conflict. \
11641×
                     To resolve it in the working copy instead, on the bookmark holding #{} \
11651×
                     pass --into-working-copy (the target is merged in), fix the marked files \
11661×
                     and oxy commit",
11671×
                    c.short(12)
11681×
                )));
11691×
            }
1170
        }
1171
        // Entries: the target's files, with touched paths as c leaves them.
11721×
        let mut entries: BTreeMap<String, FsEntry> = BTreeMap::new();
11731×
        let mut reuse: HashMap<String, (NodeId, NodeKind)> = HashMap::new();
11741×
        let content_of = |f: &crate::state::FileState| -> Result<Option<Vec<u8>>> {
11751×
            Ok(match &f.content {
11760×
                FileContent::Dir => None,
11771×
                FileContent::Text { bytes, .. } => Some(bytes.clone()),
11780×
                FileContent::Opaque { blobs } => match blobs.first() {
11790×
                    Some(b) => Some(self.read_blob(b)?),
11800×
                    None => None,
1181
                },
1182
            })
11831×
        };
11841×
        for (p, f) in &target_snapshot.files {
11851×
            if touched.contains(p) {
11861×
                continue;
11870×
            }
11880×
            if let Some(content) = content_of(f)? {
11890×
                entries.insert(p.clone(), FsEntry::new(f.kind, f.mode.exec, content));
11900×
            }
1191
        }
11921×
        for p in &touched {
11931×
            if let Some(f) = after.files.get(p) {
11941×
                if let Some(content) = content_of(f)? {
11951×
                    entries.insert(p.clone(), FsEntry::new(f.kind, f.mode.exec, content));
11961×
                }
11971×
                if !target_snapshot.files.contains_key(p) {
11980×
                    reuse.insert(p.clone(), (f.node, f.kind));
11991×
                }
12000×
            }
1201
        }
12021×
        let built = self.build_from_entries(&target, &target_snapshot, reuse, &entries)?;
12031×
        if built.tree_ops.is_empty() && built.edits.is_empty() {
12040×
            return fail(
1205
                "rebasing would leave the change empty; the target already has its effect",
1206
            );
12071×
        }
12081×
        let mut new = Change {
1209
            version: crate::objects::CHANGE_VERSION,
12101×
            change_id: old.change_id,
12111×
            deps: vec![],
12121×
            author: old.author,
12131×
            hlc: Hlc::now(),
12141×
            message: old.message.clone(),
12151×
            tree_ops: built.tree_ops,
12161×
            edits: built.edits,
12171×
            kind: if built.resolves {
12180×
                ChangeKind::Resolution
1219
            } else {
12201×
                ChangeKind::Normal
1221
            },
1222
        };
12231×
        let mut deps: BTreeSet<ChangeHash> = onto.iter().copied().collect();
12241×
        deps.extend(referenced_changes(&new, &target));
12251×
        deps.extend(built.contributors.iter().copied());
12261×
        new.deps = deps.into_iter().collect();
12271×
        new.normalize();
12281×
        let corr = Correspondence::by_content(&old, &new);
12291×
        let cmd = format!(
1230
            "oxy rebase {} --onto {}",
12311×
            old.change_id.short(8),
12321×
            onto.iter()
12331×
                .map(|h| h.short(12))
12341×
                .collect::<Vec<_>>()
12351×
                .join(" ")
1236
        );
12371×
        Ok(self.rewrite_change(c, Some(&new), &corr, &cmd)?.unwrap())
12384×
    }
1239
}