OXYGENHUB
# c792033af344 oxygen / crates / oxy-core / src / repo / history.rs
40.1 KiB · Rust · last changed Squash and rebase (spec 0.8 section 8.5): squash folds a change into a dependency with dependents following its atoms; rebase re-derives a change against another base with content-matched correspondence; correspondences keyed by node; spec 0.8a and docs 1 month ago source blame history raw
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.
17
    fn rewritable_head(&self, hash: &ChangeHash) -> Result<Change> {
18
        let heads = self.bookmark_heads()?;
19
        if !heads.contains(hash) {
20
            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));
21
        }
22
        let c = self.store.get_change(hash)?;
23
        if c.kind == ChangeKind::WorkingCopy {
24
            return fail("that is the working copy; commit it instead");
25
        }
26
        Ok(c)
27
    }
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.
32
    fn replace_head(&self, old: &ChangeHash, new: Option<&Change>, command: &str) -> Result<Option<ChangeHash>> {
33
        let new_hash = match new {
34
            Some(c) => Some(self.store_signed(c)?),
35
            None => None,
36
        };
37
        let old_change = self.store.get_change(old)?;
38
        let (op_ref, op) = self.head_op()?;
39
        let bookmark = self.config.bookmark.clone();
40
        let heads = self.heads_of(&op.view, &bookmark)?;
41
        let replace = |set: &[ChangeHash]| -> Vec<ChangeHash> {
42
            let mut out: Vec<ChangeHash> = set.iter().filter(|h| *h != old).copied().collect();
43
            match new_hash {
44
                Some(n) => out.push(n),
45
                None => out.extend(old_change.deps.iter().copied()),
46
            }
47
            out
48
        };
49
        let candidates = replace(&heads);
50
        let st = self.load_state(&candidates)?;
51
        let new_heads = st.maximal(&candidates);
52
53
        let (_, wc) = self.working_copy()?;
54
        let base = self.load_state(&wc.deps)?;
55
        let wc_base = base.maximal(&wc.deps);
56
        let wc_base = if wc_base.contains(old) {
57
            let c = replace(&wc_base);
58
            let st = self.load_state(&c)?;
59
            st.maximal(&c)
60
        } else {
61
            wc_base
62
        };
63
        let fresh = self.new_working_copy(wc_base, &wc.message);
64
        let wc_hash = self.store.put_change(&fresh)?;
65
66
        let mut view = op.view.clone();
67
        view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?);
68
        view.workspaces.insert(self.workspace, wc_hash);
69
        self.record_op(&[op_ref], command, view)?;
70
        self.drop_manifest();
71
        Ok(new_hash)
72
    }
73
74
    /// Replace `old` by `new` (or remove it) anywhere in the bookmark's state,
75
    /// rewriting every dependent, including the working copy. Returns the
76
    /// new hash of `old`, if any.
77
    fn rewrite_change(&self, old: &ChangeHash, new: Option<&Change>, corr: &Correspondence, command: &str) -> Result<Option<ChangeHash>> {
78
        self.rewrite_change_noted(old, new, corr, command).map(|(h, _)| h)
79
    }
80
81
    /// Like `rewrite_change`, also returning notes about reified references.
82
    fn rewrite_change_noted(&self, old: &ChangeHash, new: Option<&Change>, corr: &Correspondence, command: &str) -> Result<(Option<ChangeHash>, Vec<String>)> {
83
        let (op_ref, op) = self.head_op()?;
84
        let bookmark = self.config.bookmark.clone();
85
        let heads = self.heads_of(&op.view, &bookmark)?;
86
        let (wc_hash, _) = self.working_copy()?;
87
        let mut all = heads.clone();
88
        all.push(wc_hash);
89
        let state = self.load_state(&all)?;
90
        if !state.closure(&wc_hash).contains(old) && !heads.contains(old) && !state.heads().contains(old) && !state.contains(old) {
91
            return fail(format!("change #{} is not in {} or under the working copy", old.short(12), bookmark));
92
        }
93
        let old_change = self.store.get_change(old)?;
94
        if old_change.kind == ChangeKind::WorkingCopy {
95
            return fail("that is the working copy; commit it instead");
96
        }
97
        let load = |h: &ChangeHash| self.store.get_change(h).ok();
98
        let propagation = propagate(&state, *old, new, corr, &load).map_err(|e| RepoError::Msg(e.to_string()))?;
99
        let rewritten = propagation.changes;
100
101
        let mut mapping: HashMap<ChangeHash, ChangeHash> = HashMap::new();
102
        let new_hash = match new {
103
            Some(c) => {
104
                let h = self.store_signed(c)?;
105
                mapping.insert(*old, h);
106
                Some(h)
107
            }
108
            None => None,
109
        };
110
        for (h, c) in &rewritten {
111
            let nh = if c.kind == ChangeKind::WorkingCopy { self.store.put_change(c)? } else { self.store_signed(c)? };
112
            mapping.insert(*h, nh);
113
        }
114
        let remap = |set: &[ChangeHash]| -> Vec<ChangeHash> {
115
            let mut out = Vec::new();
116
            for h in set {
117
                if h == old && new_hash.is_none() {
118
                    out.extend(old_change.deps.iter().copied());
119
                } else {
120
                    out.push(*mapping.get(h).unwrap_or(h));
121
                }
122
            }
123
            out
124
        };
125
        let candidates = remap(&heads);
126
        let st = self.load_state(&candidates)?;
127
        let new_heads = st.maximal(&candidates);
128
        let new_wc = mapping.get(&wc_hash).copied().unwrap_or(wc_hash);
129
130
        let mut view = op.view.clone();
131
        view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?);
132
        view.workspaces.insert(self.workspace, new_wc);
133
        self.record_op(&[op_ref], command, view)?;
134
        self.drop_manifest();
135
        Ok((new_hash, propagation.notes))
136
    }
137
138
    /// Change the message of any change of the bookmark; dependents are rewritten.
139
    pub fn describe(&self, hash: &ChangeHash, message: &str) -> Result<ChangeHash> {
140
        if message.trim().is_empty() {
141
            return fail("a message is required");
142
        }
143
        let mut c = self.store.get_change(hash)?;
144
        if c.kind == ChangeKind::WorkingCopy {
145
            return fail("that is the working copy; commit it instead");
146
        }
147
        if c.message == message.trim_end() {
148
            return Ok(*hash);
149
        }
150
        let corr = Correspondence::identity(&c);
151
        c.message = message.trim_end().to_string();
152
        c.hlc = Hlc::now();
153
        let id = c.change_id;
154
        Ok(self.rewrite_change(hash, Some(&c), &corr, &format!("oxy describe {} -m {message:?}", id.short(8)))?.unwrap())
155
    }
156
157
    /// Fold the working copy into the head it sits on: the head is rebuilt
158
    /// as the difference between the state beneath it and the files on disk.
159
    pub fn amend(&self, message: Option<&str>) -> Result<ChangeHash> {
160
        let snap = self.snapshot("oxy amend")?;
161
        if snap.statuses.iter().any(|s| s.status == ChangeStatus::ConflictEdited) {
162
            return fail("a file still contains conflict markers");
163
        }
164
        let base_heads = snap.base.maximal(&snap.wc.deps);
165
        let [head] = base_heads.as_slice() else {
166
            return fail(format!("the working copy sits on {} heads; check out the single change to amend", base_heads.len()));
167
        };
168
        let head = *head;
169
        let old = self.rewritable_head(&head)?;
170
        if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() && message.is_none() {
171
            return fail("nothing to amend: the working copy is clean and no new message was given");
172
        }
173
        // Rebuild against the state beneath the head.
174
        let beneath = self.load_state(&old.deps)?;
175
        let beneath_snapshot = beneath.materialize();
176
        let reuse = added_node_paths(&old, &beneath_snapshot);
177
        let built = self.build_against(&beneath, &beneath_snapshot, reuse)?;
178
        if built.tree_ops.is_empty() && built.edits.is_empty() {
179
            return fail("amending would leave the change empty; use `oxy abandon` to drop it");
180
        }
181
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
182
        deps.extend(built.contributors.iter().copied());
183
        let mut new = Change {
184
            change_id: old.change_id,
185
            deps: vec![],
186
            author: self.author(),
187
            hlc: Hlc::now(),
188
            message: message.map(|m| m.trim_end().to_string()).unwrap_or(old.message.clone()),
189
            tree_ops: built.tree_ops,
190
            edits: built.edits,
191
            kind: if built.resolves || old.kind == ChangeKind::Resolution { ChangeKind::Resolution } else { ChangeKind::Normal },
192
        };
193
        deps.extend(referenced_changes(&new, &beneath));
194
        new.deps = deps.into_iter().collect();
195
        new.normalize();
196
        Ok(self.replace_head(&head, Some(&new), &format!("oxy amend {}", old.change_id.short(8)))?.unwrap())
197
    }
198
199
    /// Drop a change from the bookmark. Dependents inherit its dependencies;
200
    /// references into it are reified by re-anchoring (spec §8.4) and the
201
    /// adjustments are returned as notes. A dropped head's content stays on
202
    /// disk as uncommitted work if the working copy sat on it.
203
    pub fn abandon(&self, hash: &ChangeHash) -> Result<Vec<String>> {
204
        let c = self.store.get_change(hash)?;
205
        if c.kind == ChangeKind::WorkingCopy {
206
            return fail("that is the working copy; commit it instead");
207
        }
208
        let heads = self.bookmark_heads()?;
209
        if heads.contains(hash) {
210
            self.replace_head(hash, None, &format!("oxy abandon {}", c.change_id.short(8)))?;
211
            Ok(vec![])
212
        } else {
213
            let (_, notes) = self.rewrite_change_noted(hash, None, &Correspondence::default(), &format!("oxy abandon {}", c.change_id.short(8)))?;
214
            Ok(notes)
215
        }
216
    }
217
}
218
219
/// Translate a reference to change `target`'s own atom or op into a self
220
/// reference, and a working-copy self reference into one offset past the
221
/// target's own count; other references pass through.
222
fn fold_atom(r: AtomRef, target: &ChangeHash, wc_offset: &HashMap<NodeId, u32>, node: &NodeId) -> Option<AtomRef> {
223
    match r {
224
        AtomRef::Atom { change, index } if change == *target => Some(AtomRef::Atom { change: ObjectRef::ZERO, index }),
225
        AtomRef::Atom { change, index } if change.is_zero() => Some(AtomRef::Atom { change: ObjectRef::ZERO, index: index + wc_offset.get(node).copied().unwrap_or(0) }),
226
        // References into the target's closure were validated by the caller and stay as they are.
227
        other => Some(other),
228
    }
229
}
230
231
impl Repo {
232
    /// Fold the working copy's uncommitted edits into any change of the
233
    /// bookmark (spec §8.4). The edits must reference only atoms, placements
234
    /// and nodes that exist in the target's own closure; dependents are
235
    /// rewritten with an identity correspondence, since the target's own
236
    /// atoms keep their indices.
237
    pub fn amend_into(&self, target: &ChangeHash, message: Option<&str>) -> Result<ChangeHash> {
238
        let snap = self.snapshot("oxy amend")?;
239
        if snap.statuses.iter().any(|s| s.status == ChangeStatus::ConflictEdited) {
240
            return fail("a file still contains conflict markers");
241
        }
242
        let wc = &snap.wc;
243
        if wc.tree_ops.is_empty() && wc.edits.is_empty() && message.is_none() {
244
            return fail("nothing to amend: the working copy is clean and no new message was given");
245
        }
246
        let old = self.store.get_change(target)?;
247
        if old.kind == ChangeKind::WorkingCopy {
248
            return fail("that is the working copy; commit it instead");
249
        }
250
        // Every reference must live in the target's closure (or be the target).
251
        let closure = self.load_state(&[*target])?;
252
        let ok_change = |h: &ChangeHash| *h == *target || closure.contains(h);
253
        let ok_node = |n: &NodeId| n.is_root() || closure.tree().introducer(n).is_some();
254
        let too_new = |what: &str| RepoError::Msg(format!("{what} was introduced after change #{}; amend into a later change instead", target.short(12)));
255
        for op in &wc.tree_ops {
256
            match op {
257
                TreeOp::AddNode { parent, .. } => {
258
                    if !ok_node(parent) {
259
                        return Err(too_new("the parent directory"));
260
                    }
261
                }
262
                TreeOp::RemoveNode { node, placement } => {
263
                    if !ok_node(node) || !ok_change(&placement.change) {
264
                        return Err(too_new("a removed file"));
265
                    }
266
                }
267
                TreeOp::Move { node, old, parent, .. } => {
268
                    if !ok_node(node) || !ok_change(&old.change) || !ok_node(parent) {
269
                        return Err(too_new("a moved file"));
270
                    }
271
                }
272
                TreeOp::SetMode { node, old, .. } => {
273
                    if !ok_node(node) || !ok_change(&old.change) {
274
                        return Err(too_new("a file whose mode changed"));
275
                    }
276
                }
277
            }
278
        }
279
        let wc_added: BTreeSet<NodeId> = wc.tree_ops.iter().filter_map(|o| if let TreeOp::AddNode { node, .. } = o { Some(*node) } else { None }).collect();
280
        for e in &wc.edits {
281
            let n = e.node();
282
            if !wc_added.contains(&n) && !ok_node(&n) {
283
                return Err(too_new("an edited file"));
284
            }
285
            let refs: Vec<ChangeHash> = match e {
286
                Edit::Insert { after, before, .. } => [after, before].iter().filter_map(|r| if let AtomRef::Atom { change, .. } = r { Some(*change) } else { None }).collect(),
287
                Edit::Delete { atoms, .. } => atoms.iter().map(|r| r.change).collect(),
288
                Edit::Replace { .. } => vec![],
289
            };
290
            if refs.iter().any(|h| !h.is_zero() && !ok_change(h)) {
291
                return Err(too_new("an edited line"));
292
            }
293
        }
294
295
        // Compose: the target's operations first, then the working copy's
296
        // with references to the target turned into self references and its
297
        // own atom indices shifted past the target's.
298
        let mut per_node: HashMap<NodeId, u32> = HashMap::new();
299
        for e in &old.edits {
300
            if let Edit::Insert { node, lines, .. } = e {
301
                *per_node.entry(*node).or_default() += lines.len() as u32;
302
            }
303
        }
304
        let op_offset = old.tree_ops.len() as u32;
305
        let fold_op = |r: OpRef| -> OpRef {
306
            if r.change == *target {
307
                OpRef { change: ObjectRef::ZERO, index: r.index }
308
            } else if r.change.is_zero() {
309
                OpRef { change: ObjectRef::ZERO, index: r.index + op_offset }
310
            } else {
311
                r
312
            }
313
        };
314
        let mut tree_ops = old.tree_ops.clone();
315
        for op in &wc.tree_ops {
316
            tree_ops.push(match op {
317
                TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode { node: *node, placement: fold_op(*placement) },
318
                TreeOp::Move { node, old, parent, name } => TreeOp::Move { node: *node, old: fold_op(*old), parent: *parent, name: name.clone() },
319
                TreeOp::SetMode { node, old, mode } => TreeOp::SetMode { node: *node, old: fold_op(*old), mode: *mode },
320
                add => add.clone(),
321
            });
322
        }
323
        let mut edits = old.edits.clone();
324
        for e in &wc.edits {
325
            edits.push(match e {
326
                Edit::Insert { node, after, before, lines } => Edit::Insert {
327
                    node: *node,
328
                    after: fold_atom(*after, target, &per_node, node).ok_or_else(|| too_new("an anchor line"))?,
329
                    before: fold_atom(*before, target, &per_node, node).ok_or_else(|| too_new("an anchor line"))?,
330
                    lines: lines.clone(),
331
                },
332
                Edit::Delete { node, atoms } => Edit::Delete {
333
                    node: *node,
334
                    atoms: atoms
335
                        .iter()
336
                        .map(|r| {
337
                            if r.change == *target {
338
                                AtomRange { change: ObjectRef::ZERO, from: r.from, to: r.to }
339
                            } else if r.change.is_zero() {
340
                                let off = per_node.get(node).copied().unwrap_or(0);
341
                                AtomRange { change: ObjectRef::ZERO, from: r.from + off, to: r.to + off }
342
                            } else {
343
                                *r
344
                            }
345
                        })
346
                        .collect(),
347
                },
348
                other => other.clone(),
349
            });
350
        }
351
        let mut new = Change {
352
            change_id: old.change_id,
353
            deps: vec![],
354
            author: self.author(),
355
            hlc: Hlc::now(),
356
            message: message.map(|m| m.trim_end().to_string()).unwrap_or(old.message.clone()),
357
            tree_ops,
358
            edits,
359
            kind: old.kind,
360
        };
361
        let beneath = self.load_state(&old.deps)?;
362
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
363
        deps.extend(referenced_changes(&new, &beneath).into_iter().filter(|h| *h != *target));
364
        deps.extend(wc.deps.iter().filter(|d| closure.contains(d) && **d != *target).copied());
365
        new.deps = deps.into_iter().collect();
366
        new.normalize();
367
368
        // Rewrite dependents (identity correspondence), then start a fresh,
369
        // empty working copy: its edits now live in the amended change.
370
        let corr = Correspondence::identity(&old);
371
        let new_hash = self.rewrite_change(target, Some(&new), &corr, &format!("oxy amend --into {}", old.change_id.short(8)))?.unwrap();
372
        let (op_ref, op) = self.head_op()?;
373
        let heads = self.heads_of(&op.view, &self.config.bookmark)?;
374
        let fresh = self.new_working_copy(heads, "");
375
        let wc_hash = self.store.put_change(&fresh)?;
376
        let mut view = op.view.clone();
377
        view.workspaces.insert(self.workspace, wc_hash);
378
        self.record_op(&[op_ref], "oxy amend (working copy)", view)?;
379
        self.drop_manifest();
380
        Ok(new_hash)
381
    }
382
383
    /// Split a change into two: one holding its effect on `paths`, the
384
    /// other the rest (keeping the ChangeId). A head is rebuilt from its
385
    /// materialized files; any other change is split by node, which is
386
    /// exact, and is refused when a dependent refers to the split-off
387
    /// nodes. The working copy is unaffected.
388
    pub fn split(&self, hash: &ChangeHash, paths: &[String]) -> Result<(ChangeHash, ChangeHash)> {
389
        if paths.is_empty() {
390
            return fail("name at least one path to split out");
391
        }
392
        if !self.bookmark_heads()?.contains(hash) {
393
            return self.split_by_node(hash, paths);
394
        }
395
        let old = self.rewritable_head(hash)?;
396
        let beneath = self.load_state(&old.deps)?;
397
        let beneath_snapshot = beneath.materialize();
398
        let full = self.load_state(&[*hash])?;
399
        let full_snapshot = full.materialize();
400
        let selected = |p: &str| paths.iter().any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/'))));
401
        // Entries of the intermediate state: selected paths as the head
402
        // leaves them, everything else as beneath.
403
        let entries_of = |snap: &Snapshot, keep_from_full: &dyn Fn(&str) -> bool| -> Result<BTreeMap<String, FsEntry>> {
404
            let mut out = BTreeMap::new();
405
            for (p, _) in snap.files.iter().chain(full_snapshot.files.iter()) {
406
                let take_full = keep_from_full(p);
407
                let src = if take_full { full_snapshot.files.get(p) } else { snap.files.get(p) };
408
                let Some(src) = src else { continue };
409
                if out.contains_key(p) {
410
                    continue;
411
                }
412
                let content = match &src.content {
413
                    FileContent::Dir => continue,
414
                    FileContent::Text { bytes, .. } => bytes.clone(),
415
                    FileContent::Opaque { blobs } => match blobs.first() {
416
                        Some(b) => self.read_blob(b)?,
417
                        None => continue,
418
                    },
419
                };
420
                out.insert(p.clone(), FsEntry::new(src.kind, src.mode.exec, content));
421
            }
422
            Ok(out)
423
        };
424
        let mid_entries = entries_of(&beneath_snapshot, &|p| selected(p))?;
425
        let first = self.build_from_entries(&beneath, &beneath_snapshot, HashMap::new(), &mid_entries)?;
426
        if first.tree_ops.is_empty() && first.edits.is_empty() {
427
            return fail("the named paths are not touched by that change");
428
        }
429
        let mut c1 = Change {
430
            change_id: ChangeId::random(),
431
            deps: old.deps.clone(),
432
            author: old.author,
433
            hlc: Hlc::now(),
434
            message: format!("{} (part 1: {})", old.message.lines().next().unwrap_or(""), paths.join(", ")),
435
            tree_ops: first.tree_ops,
436
            edits: first.edits,
437
            kind: ChangeKind::Normal,
438
        };
439
        let mut d1: BTreeSet<ChangeHash> = referenced_changes(&c1, &beneath);
440
        d1.extend(old.deps.iter().copied());
441
        c1.deps = d1.into_iter().collect();
442
        c1.normalize();
443
        let h1 = self.store_signed(&c1)?;
444
        let mut mid = beneath.clone();
445
        mid.add(c1)?;
446
        let mid_snapshot = mid.materialize();
447
        let rest_entries = entries_of(&full_snapshot, &|_| true)?;
448
        let rest = self.build_from_entries(&mid, &mid_snapshot, HashMap::new(), &rest_entries)?;
449
        if rest.tree_ops.is_empty() && rest.edits.is_empty() {
450
            return fail("everything in that change is under the named paths; nothing would remain");
451
        }
452
        let mut c2 = Change {
453
            change_id: old.change_id,
454
            deps: vec![],
455
            author: old.author,
456
            hlc: Hlc::now(),
457
            message: old.message.clone(),
458
            tree_ops: rest.tree_ops,
459
            edits: rest.edits,
460
            kind: ChangeKind::Normal,
461
        };
462
        let mut d2: BTreeSet<ChangeHash> = referenced_changes(&c2, &mid);
463
        d2.extend(old.deps.iter().copied());
464
        c2.deps = d2.into_iter().collect();
465
        c2.normalize();
466
        let h2 = self.replace_head(hash, Some(&c2), &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")))?.unwrap();
467
        self.add_to_bookmark(h1)?;
468
        Ok((h1, h2))
469
    }
470
471
    /// Put `h` into the bookmark's state (a head unless something already
472
    /// depends on it) and under the working copy.
473
    fn add_to_bookmark(&self, h: ChangeHash) -> Result<()> {
474
        let (op_ref, op) = self.head_op()?;
475
        let bookmark = self.config.bookmark.clone();
476
        let mut heads = self.heads_of(&op.view, &bookmark)?;
477
        heads.push(h);
478
        let st = self.load_state(&heads)?;
479
        let heads = st.maximal(&heads);
480
        let mut view = op.view.clone();
481
        view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(heads.clone()))?);
482
        let (_, wc) = self.working_copy()?;
483
        let mut base = wc.deps.clone();
484
        base.push(h);
485
        let wc_base = self.load_state(&base)?.maximal(&base);
486
        let fresh = self.new_working_copy(wc_base, &wc.message);
487
        view.workspaces.insert(self.workspace, self.store.put_change(&fresh)?);
488
        self.record_op(&[op_ref], "oxy split (heads)", view)?;
489
        self.drop_manifest();
490
        Ok(())
491
    }
492
493
    /// Split a non-head change by node: the operations and edits on nodes
494
    /// under `paths` become a new change, the rest keeps the ChangeId with
495
    /// every remaining atom at its old index, so dependents are rewritten
496
    /// exactly. Refused when a dependent touches a split-off node.
497
    fn split_by_node(&self, hash: &ChangeHash, paths: &[String]) -> Result<(ChangeHash, ChangeHash)> {
498
        let old = self.store.get_change(hash)?;
499
        if old.kind == ChangeKind::WorkingCopy {
500
            return fail("that is the working copy; commit it instead");
501
        }
502
        let selected = |p: &str| paths.iter().any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/'))));
503
        let beneath = self.load_state(&old.deps)?;
504
        let full = self.load_state(&[*hash])?;
505
        let mut removed: BTreeSet<NodeId> = BTreeSet::new();
506
        for snap in [beneath.materialize(), full.materialize()] {
507
            for (p, f) in &snap.files {
508
                if selected(p) {
509
                    removed.insert(f.node);
510
                }
511
            }
512
        }
513
        // Nodes the change itself adds under a removed directory.
514
        for op in &old.tree_ops {
515
            if let TreeOp::AddNode { node, parent, .. } = op {
516
                if removed.contains(parent) {
517
                    removed.insert(*node);
518
                }
519
            }
520
        }
521
        if removed.is_empty() {
522
            return fail("the named paths are not touched by that change");
523
        }
524
        // Dependents must not touch the split-off nodes: their references
525
        // would have to move to the new change, which a rewrite cannot express.
526
        let heads = self.bookmark_heads()?;
527
        let (wc_hash, _) = self.working_copy()?;
528
        let mut all = heads.clone();
529
        all.push(wc_hash);
530
        let state = self.load_state(&all)?;
531
        for (h, meta) in state.changes() {
532
            if !meta.deps.contains(hash) {
533
                continue;
534
            }
535
            let d = self.store.get_change(h)?;
536
            if d.tree_ops.iter().any(|op| removed.contains(&op.node())) || d.edits.iter().any(|e| removed.contains(&e.node())) {
537
                return fail(format!("change {} depends on the split-off paths of {}; split it first", d.change_id.short(8), old.change_id.short(8)));
538
            }
539
        }
540
        let mut c1 = Change {
541
            change_id: ChangeId::random(),
542
            deps: old.deps.clone(),
543
            author: old.author,
544
            hlc: Hlc::now(),
545
            message: format!("{} (part 1: {})", old.message.lines().next().unwrap_or(""), paths.join(", ")),
546
            tree_ops: old.tree_ops.iter().filter(|op| removed.contains(&op.node())).cloned().collect(),
547
            edits: old.edits.iter().filter(|e| removed.contains(&e.node())).cloned().collect(),
548
            kind: ChangeKind::Normal,
549
        };
550
        let mut c2 = old.clone();
551
        c2.hlc = Hlc::now();
552
        let mut ops_map: HashMap<u32, u32> = HashMap::new();
553
        let mut kept_ops = Vec::new();
554
        for (i, op) in old.tree_ops.iter().enumerate() {
555
            if !removed.contains(&op.node()) {
556
                ops_map.insert(i as u32, kept_ops.len() as u32);
557
                kept_ops.push(op.clone());
558
            }
559
        }
560
        c2.tree_ops = kept_ops;
561
        c2.edits.retain(|e| !removed.contains(&e.node()));
562
        if c1.tree_ops.is_empty() && c1.edits.is_empty() {
563
            return fail("the named paths are not touched by that change");
564
        }
565
        if c2.tree_ops.is_empty() && c2.edits.is_empty() {
566
            return fail("everything in that change is under the named paths; use `oxy abandon` instead");
567
        }
568
        let mut d1: BTreeSet<ChangeHash> = referenced_changes(&c1, &beneath);
569
        d1.extend(old.deps.iter().copied());
570
        c1.deps = d1.into_iter().collect();
571
        c1.normalize();
572
        let mut d2: BTreeSet<ChangeHash> = referenced_changes(&c2, &beneath);
573
        d2.extend(old.deps.iter().copied());
574
        c2.deps = d2.into_iter().collect();
575
        c2.normalize();
576
        let corr = Correspondence { atoms: Correspondence::identity(&old).atoms, ops: ops_map };
577
        let h2 = self.rewrite_change(hash, Some(&c2), &corr, &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")))?.unwrap();
578
        let h1 = self.store_signed(&c1)?;
579
        self.add_to_bookmark(h1)?;
580
        Ok((h1, h2))
581
    }
582
}
583
584
impl Repo {
585
    /// Fold change `c` into its dependency `into` (spec §8.5): the result
586
    /// keeps `into`'s ChangeId, dependents of both are rewritten, and `c`
587
    /// leaves the state.
588
    pub fn squash(&self, c: &ChangeHash, into: &ChangeHash) -> Result<ChangeHash> {
589
        if c == into {
590
            return fail("a change cannot be squashed into itself");
591
        }
592
        let src = self.store.get_change(c)?;
593
        let dst = self.store.get_change(into)?;
594
        if src.kind == ChangeKind::WorkingCopy {
595
            return fail("that is the working copy; use `oxy amend --into`");
596
        }
597
        if dst.kind == ChangeKind::WorkingCopy {
598
            return fail("cannot squash into the working copy");
599
        }
600
        let closure = self.load_state(&[*into])?;
601
        let outside: Vec<String> = src.deps.iter().filter(|d| *d != into && !closure.contains(d)).map(|d| d.short(12)).collect();
602
        if !outside.is_empty() {
603
            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)));
604
        }
605
        let ok_change = |h: &ChangeHash| *h == *into || closure.contains(h);
606
        let ok_node = |n: &NodeId| n.is_root() || closure.tree().introducer(n).is_some();
607
        let too_new = |what: &str| RepoError::Msg(format!("{what} was introduced after change #{}; squash into a later change instead", into.short(12)));
608
        let src_added: BTreeSet<NodeId> = src.tree_ops.iter().filter_map(|o| if let TreeOp::AddNode { node, .. } = o { Some(*node) } else { None }).collect();
609
        for op in &src.tree_ops {
610
            match op {
611
                TreeOp::AddNode { parent, .. } => {
612
                    if !ok_node(parent) && !src_added.contains(parent) {
613
                        return Err(too_new("the parent directory"));
614
                    }
615
                }
616
                TreeOp::RemoveNode { node, placement } => {
617
                    if !ok_node(node) || !ok_change(&placement.change) {
618
                        return Err(too_new("a removed file"));
619
                    }
620
                }
621
                TreeOp::Move { node, old, parent, .. } => {
622
                    if !ok_node(node) || !ok_change(&old.change) || (!ok_node(parent) && !src_added.contains(parent)) {
623
                        return Err(too_new("a moved file"));
624
                    }
625
                }
626
                TreeOp::SetMode { node, old, .. } => {
627
                    if !ok_node(node) || !ok_change(&old.change) {
628
                        return Err(too_new("a file whose mode changed"));
629
                    }
630
                }
631
            }
632
        }
633
        for e in &src.edits {
634
            let n = e.node();
635
            if !src_added.contains(&n) && !ok_node(&n) {
636
                return Err(too_new("an edited file"));
637
            }
638
            let refs: Vec<ChangeHash> = match e {
639
                Edit::Insert { after, before, .. } => [after, before].iter().filter_map(|r| if let AtomRef::Atom { change, .. } = r { Some(*change) } else { None }).collect(),
640
                Edit::Delete { atoms, .. } => atoms.iter().map(|r| r.change).collect(),
641
                Edit::Replace { .. } => vec![],
642
            };
643
            if refs.iter().any(|h| !h.is_zero() && !ok_change(h)) {
644
                return Err(too_new("an edited line"));
645
            }
646
        }
647
        // Compose dst then src, with src's references to dst turned into
648
        // self references and its own atoms shifted past dst's.
649
        let mut per_node: HashMap<NodeId, u32> = HashMap::new();
650
        for e in &dst.edits {
651
            if let Edit::Insert { node, lines, .. } = e {
652
                *per_node.entry(*node).or_default() += lines.len() as u32;
653
            }
654
        }
655
        let op_offset = dst.tree_ops.len() as u32;
656
        let fold_op = |r: OpRef| -> OpRef {
657
            if r.change == *into {
658
                OpRef { change: ObjectRef::ZERO, index: r.index }
659
            } else if r.change.is_zero() {
660
                OpRef { change: ObjectRef::ZERO, index: r.index + op_offset }
661
            } else {
662
                r
663
            }
664
        };
665
        let mut tree_ops = dst.tree_ops.clone();
666
        for op in &src.tree_ops {
667
            tree_ops.push(match op {
668
                TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode { node: *node, placement: fold_op(*placement) },
669
                TreeOp::Move { node, old, parent, name } => TreeOp::Move { node: *node, old: fold_op(*old), parent: *parent, name: name.clone() },
670
                TreeOp::SetMode { node, old, mode } => TreeOp::SetMode { node: *node, old: fold_op(*old), mode: *mode },
671
                add => add.clone(),
672
            });
673
        }
674
        let mut edits = dst.edits.clone();
675
        for e in &src.edits {
676
            edits.push(match e {
677
                Edit::Insert { node, after, before, lines } => Edit::Insert {
678
                    node: *node,
679
                    after: fold_atom(*after, into, &per_node, node).ok_or_else(|| too_new("an anchor line"))?,
680
                    before: fold_atom(*before, into, &per_node, node).ok_or_else(|| too_new("an anchor line"))?,
681
                    lines: lines.clone(),
682
                },
683
                Edit::Delete { node, atoms } => Edit::Delete {
684
                    node: *node,
685
                    atoms: atoms
686
                        .iter()
687
                        .map(|r| {
688
                            if r.change == *into {
689
                                AtomRange { change: ObjectRef::ZERO, from: r.from, to: r.to }
690
                            } else if r.change.is_zero() {
691
                                let off = per_node.get(node).copied().unwrap_or(0);
692
                                AtomRange { change: ObjectRef::ZERO, from: r.from + off, to: r.to + off }
693
                            } else {
694
                                *r
695
                            }
696
                        })
697
                        .collect(),
698
                },
699
                other => other.clone(),
700
            });
701
        }
702
        let mut new = Change {
703
            change_id: dst.change_id,
704
            deps: vec![],
705
            author: dst.author,
706
            hlc: Hlc::now(),
707
            message: dst.message.clone(),
708
            tree_ops,
709
            edits,
710
            kind: if dst.kind == ChangeKind::Resolution || src.kind == ChangeKind::Resolution { ChangeKind::Resolution } else { ChangeKind::Normal },
711
        };
712
        let beneath = self.load_state(&dst.deps)?;
713
        let mut deps: BTreeSet<ChangeHash> = dst.deps.iter().copied().collect();
714
        deps.extend(referenced_changes(&new, &beneath).into_iter().filter(|h| *h != *into && *h != *c));
715
        deps.extend(src.deps.iter().filter(|d| **d != *into).copied());
716
        new.deps = deps.into_iter().collect();
717
        new.normalize();
718
719
        // Dependents of dst keep their indices; dependents of src follow its
720
        // atoms and operations to their shifted places in the new change.
721
        let cmd = format!("oxy squash {} --into {}", src.change_id.short(8), dst.change_id.short(8));
722
        let dst_new = self.rewrite_change(into, Some(&new), &Correspondence::identity(&dst), &cmd)?.unwrap();
723
        // src was rewritten along with the other dependents of dst; find it by ChangeId.
724
        let (wc_hash, _) = self.working_copy()?;
725
        let mut all = self.bookmark_heads()?;
726
        all.push(wc_hash);
727
        let state = self.load_state(&all)?;
728
        let src_now = state.changes().find(|(_, m)| m.change_id == src.change_id).map(|(h, _)| *h).unwrap_or(*c);
729
        let src_change = self.store.get_change(&src_now)?;
730
        let mut corr = Correspondence { atoms: HashMap::new(), ops: HashMap::new() };
731
        let mut counts: HashMap<NodeId, u32> = HashMap::new();
732
        for e in &src_change.edits {
733
            if let Edit::Insert { node, lines, .. } = e {
734
                let off = per_node.get(node).copied().unwrap_or(0);
735
                let idx = counts.entry(*node).or_default();
736
                for _ in lines {
737
                    corr.atoms.insert((*node, *idx), *idx + off);
738
                    *idx += 1;
739
                }
740
            }
741
        }
742
        for i in 0..src_change.tree_ops.len() as u32 {
743
            corr.ops.insert(i, i + op_offset);
744
        }
745
        let final_new = self.store.get_change(&dst_new)?;
746
        self.rewrite_change(&src_now, Some(&final_new), &corr, &cmd)?;
747
        Ok(dst_new)
748
    }
749
750
    /// Re-express change `c` against the state `onto` names (spec §8.5): its
751
    /// effect on the files it touches is re-derived as edits against that
752
    /// state. Keeps the ChangeId; dependents follow by content.
753
    pub fn rebase(&self, c: &ChangeHash, onto: &[ChangeHash]) -> Result<ChangeHash> {
754
        let old = self.store.get_change(c)?;
755
        if old.kind == ChangeKind::WorkingCopy {
756
            return fail("that is the working copy; check out the new base instead");
757
        }
758
        let target = self.load_state(onto)?;
759
        if target.contains(c) {
760
            return fail(format!("#{} is already part of the target state", c.short(12)));
761
        }
762
        let onto = target.maximal(onto);
763
        let target_snapshot = target.materialize();
764
        let before = self.load_state(&old.deps)?.materialize();
765
        let after = self.load_state(&[*c])?.materialize();
766
        // The paths whose file differs between the state with and without c.
767
        let mut touched: BTreeSet<String> = BTreeSet::new();
768
        for p in before.files.keys().chain(after.files.keys()) {
769
            match (before.files.get(p), after.files.get(p)) {
770
                (Some(a), Some(b)) if a.kind == b.kind && a.mode == b.mode && a.content == b.content => {}
771
                _ => {
772
                    touched.insert(p.clone());
773
                }
774
            }
775
        }
776
        for p in &touched {
777
            if target_snapshot.files.get(p).is_some_and(|f| f.is_conflicted()) {
778
                return fail(format!("{p} is conflicted in the target state; resolve it first"));
779
            }
780
        }
781
        // Entries: the target's files, with touched paths as c leaves them.
782
        let mut entries: BTreeMap<String, FsEntry> = BTreeMap::new();
783
        let mut reuse: HashMap<String, (NodeId, NodeKind)> = HashMap::new();
784
        let content_of = |f: &crate::state::FileState| -> Result<Option<Vec<u8>>> {
785
            Ok(match &f.content {
786
                FileContent::Dir => None,
787
                FileContent::Text { bytes, .. } => Some(bytes.clone()),
788
                FileContent::Opaque { blobs } => match blobs.first() {
789
                    Some(b) => Some(self.read_blob(b)?),
790
                    None => None,
791
                },
792
            })
793
        };
794
        for (p, f) in &target_snapshot.files {
795
            if touched.contains(p) {
796
                continue;
797
            }
798
            if let Some(content) = content_of(f)? {
799
                entries.insert(p.clone(), FsEntry::new(f.kind, f.mode.exec, content));
800
            }
801
        }
802
        for p in &touched {
803
            if let Some(f) = after.files.get(p) {
804
                if let Some(content) = content_of(f)? {
805
                    entries.insert(p.clone(), FsEntry::new(f.kind, f.mode.exec, content));
806
                }
807
                if !target_snapshot.files.contains_key(p) {
808
                    reuse.insert(p.clone(), (f.node, f.kind));
809
                }
810
            }
811
        }
812
        let built = self.build_from_entries(&target, &target_snapshot, reuse, &entries)?;
813
        if built.tree_ops.is_empty() && built.edits.is_empty() {
814
            return fail("rebasing would leave the change empty; the target already has its effect");
815
        }
816
        let mut new = Change {
817
            change_id: old.change_id,
818
            deps: vec![],
819
            author: old.author,
820
            hlc: Hlc::now(),
821
            message: old.message.clone(),
822
            tree_ops: built.tree_ops,
823
            edits: built.edits,
824
            kind: if built.resolves { ChangeKind::Resolution } else { ChangeKind::Normal },
825
        };
826
        let mut deps: BTreeSet<ChangeHash> = onto.iter().copied().collect();
827
        deps.extend(referenced_changes(&new, &target));
828
        deps.extend(built.contributors.iter().copied());
829
        new.deps = deps.into_iter().collect();
830
        new.normalize();
831
        let corr = Correspondence::by_content(&old, &new);
832
        let cmd = format!("oxy rebase {} --onto {}", old.change_id.short(8), onto.iter().map(|h| h.short(12)).collect::<Vec<_>>().join(" "));
833
        Ok(self.rewrite_change(c, Some(&new), &corr, &cmd)?.unwrap())
834
    }
835
}
836