OXYGENHUB
# 6f8aeb996558 oxygen / crates / oxy-core / src / repo / history.rs
46.6 KiB · Rust · last changed Store large inserts as text blobs (change format 2) 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(
33
        &self,
34
        old: &ChangeHash,
35
        new: Option<&Change>,
36
        command: &str,
37
    ) -> Result<Option<ChangeHash>> {
38
        let new_hash = match new {
39
            Some(c) => Some(self.store_signed(c)?),
40
            None => None,
41
        };
42
        let old_change = self.store.get_change(old)?;
43
        let (op_ref, op) = self.head_op()?;
44
        let bookmark = self.config.bookmark.clone();
45
        let heads = self.heads_of(&op.view, &bookmark)?;
46
        let replace = |set: &[ChangeHash]| -> Vec<ChangeHash> {
47
            let mut out: Vec<ChangeHash> = set.iter().filter(|h| *h != old).copied().collect();
48
            match new_hash {
49
                Some(n) => out.push(n),
50
                None => out.extend(old_change.deps.iter().copied()),
51
            }
52
            out
53
        };
54
        let candidates = replace(&heads);
55
        let st = self.load_state(&candidates)?;
56
        let new_heads = st.maximal(&candidates);
57
58
        let (_, wc) = self.working_copy()?;
59
        let base = self.load_state(&wc.deps)?;
60
        let wc_base = base.maximal(&wc.deps);
61
        let wc_base = if wc_base.contains(old) {
62
            let c = replace(&wc_base);
63
            let st = self.load_state(&c)?;
64
            st.maximal(&c)
65
        } else {
66
            wc_base
67
        };
68
        let fresh = self.new_working_copy(wc_base, &wc.message);
69
        let wc_hash = self.store.put_change(&fresh)?;
70
71
        let mut view = op.view.clone();
72
        view.bookmarks.insert(
73
            bookmark,
74
            self.store.put_state(&StateObject::new(new_heads))?,
75
        );
76
        view.workspaces.insert(self.workspace, wc_hash);
77
        self.record_op(&[op_ref], command, view)?;
78
        self.drop_manifest();
79
        Ok(new_hash)
80
    }
81
82
    /// Replace `old` by `new` (or remove it) anywhere in the bookmark's state,
83
    /// rewriting every dependent, including the working copy. Returns the
84
    /// new hash of `old`, if any.
85
    fn rewrite_change(
86
        &self,
87
        old: &ChangeHash,
88
        new: Option<&Change>,
89
        corr: &Correspondence,
90
        command: &str,
91
    ) -> Result<Option<ChangeHash>> {
92
        self.rewrite_change_noted(old, new, corr, command)
93
            .map(|(h, _)| h)
94
    }
95
96
    /// Like `rewrite_change`, also returning notes about reified references.
97
    fn rewrite_change_noted(
98
        &self,
99
        old: &ChangeHash,
100
        new: Option<&Change>,
101
        corr: &Correspondence,
102
        command: &str,
103
    ) -> Result<(Option<ChangeHash>, Vec<String>)> {
104
        let (op_ref, op) = self.head_op()?;
105
        let bookmark = self.config.bookmark.clone();
106
        let heads = self.heads_of(&op.view, &bookmark)?;
107
        let (wc_hash, _) = self.working_copy()?;
108
        let mut all = heads.clone();
109
        all.push(wc_hash);
110
        let state = self.load_state(&all)?;
111
        if !state.closure(&wc_hash).contains(old)
112
            && !heads.contains(old)
113
            && !state.heads().contains(old)
114
            && !state.contains(old)
115
        {
116
            return fail(format!(
117
                "change #{} is not in {} or under the working copy",
118
                old.short(12),
119
                bookmark
120
            ));
121
        }
122
        let old_change = self.store.get_change(old)?;
123
        if old_change.kind == ChangeKind::WorkingCopy {
124
            return fail("that is the working copy; commit it instead");
125
        }
126
        let load = |h: &ChangeHash| self.store.get_change(h).ok();
127
        let propagation =
128
            propagate(&state, *old, new, corr, &load).map_err(|e| RepoError::Msg(e.to_string()))?;
129
        let rewritten = propagation.changes;
130
131
        let mut mapping: HashMap<ChangeHash, ChangeHash> = HashMap::new();
132
        let new_hash = match new {
133
            Some(c) => {
134
                let h = self.store_signed(c)?;
135
                mapping.insert(*old, h);
136
                Some(h)
137
            }
138
            None => None,
139
        };
140
        for (h, c) in &rewritten {
141
            let nh = if c.kind == ChangeKind::WorkingCopy {
142
                self.store.put_change(c)?
143
            } else {
144
                self.store_signed(c)?
145
            };
146
            mapping.insert(*h, nh);
147
        }
148
        let remap = |set: &[ChangeHash]| -> Vec<ChangeHash> {
149
            let mut out = Vec::new();
150
            for h in set {
151
                if h == old && new_hash.is_none() {
152
                    out.extend(old_change.deps.iter().copied());
153
                } else {
154
                    out.push(*mapping.get(h).unwrap_or(h));
155
                }
156
            }
157
            out
158
        };
159
        let candidates = remap(&heads);
160
        let st = self.load_state(&candidates)?;
161
        let new_heads = st.maximal(&candidates);
162
        let new_wc = mapping.get(&wc_hash).copied().unwrap_or(wc_hash);
163
164
        let mut view = op.view.clone();
165
        view.bookmarks.insert(
166
            bookmark,
167
            self.store.put_state(&StateObject::new(new_heads))?,
168
        );
169
        view.workspaces.insert(self.workspace, new_wc);
170
        self.record_op(&[op_ref], command, view)?;
171
        self.drop_manifest();
172
        Ok((new_hash, propagation.notes))
173
    }
174
175
    /// Change the message of any change of the bookmark; dependents are rewritten.
176
    pub fn describe(&self, hash: &ChangeHash, message: &str) -> Result<ChangeHash> {
177
        if message.trim().is_empty() {
178
            return fail("a message is required");
179
        }
180
        let mut c = self.store.get_change(hash)?;
181
        if c.kind == ChangeKind::WorkingCopy {
182
            return fail("that is the working copy; commit it instead");
183
        }
184
        if c.message == message.trim_end() {
185
            return Ok(*hash);
186
        }
187
        let corr = Correspondence::identity(&c);
188
        c.message = message.trim_end().to_string();
189
        c.hlc = Hlc::now();
190
        let id = c.change_id;
191
        Ok(self
192
            .rewrite_change(
193
                hash,
194
                Some(&c),
195
                &corr,
196
                &format!("oxy describe {} -m {message:?}", id.short(8)),
197
            )?
198
            .unwrap())
199
    }
200
201
    /// Fold the working copy into the head it sits on: the head is rebuilt
202
    /// as the difference between the state beneath it and the files on disk.
203
    pub fn amend(&self, message: Option<&str>) -> Result<ChangeHash> {
204
        let snap = self.snapshot("oxy amend")?;
205
        if snap
206
            .statuses
207
            .iter()
208
            .any(|s| s.status == ChangeStatus::ConflictEdited)
209
        {
210
            return fail("a file still contains conflict markers");
211
        }
212
        let base_heads = snap.base.maximal(&snap.wc.deps);
213
        let [head] = base_heads.as_slice() else {
214
            return fail(format!(
215
                "the working copy sits on {} heads; check out the single change to amend",
216
                base_heads.len()
217
            ));
218
        };
219
        let head = *head;
220
        let old = self.rewritable_head(&head)?;
221
        if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() && message.is_none() {
222
            return fail(
223
                "nothing to amend: the working copy is clean and no new message was given",
224
            );
225
        }
226
        // Rebuild against the state beneath the head.
227
        let beneath = self.load_state(&old.deps)?;
228
        let beneath_snapshot = beneath.materialize();
229
        let reuse = added_node_paths(&old, &beneath_snapshot);
230
        let built = self.build_against(&beneath, &beneath_snapshot, reuse)?;
231
        if built.tree_ops.is_empty() && built.edits.is_empty() {
232
            return fail("amending would leave the change empty; use `oxy abandon` to drop it");
233
        }
234
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
235
        deps.extend(built.contributors.iter().copied());
236
        let mut new = Change {
237
            version: crate::objects::CHANGE_VERSION,
238
            change_id: old.change_id,
239
            deps: vec![],
240
            author: self.author(),
241
            hlc: Hlc::now(),
242
            message: message
243
                .map(|m| m.trim_end().to_string())
244
                .unwrap_or(old.message.clone()),
245
            tree_ops: built.tree_ops,
246
            edits: built.edits,
247
            kind: if built.resolves || old.kind == ChangeKind::Resolution {
248
                ChangeKind::Resolution
249
            } else {
250
                ChangeKind::Normal
251
            },
252
        };
253
        deps.extend(referenced_changes(&new, &beneath));
254
        new.deps = deps.into_iter().collect();
255
        new.normalize();
256
        Ok(self
257
            .replace_head(
258
                &head,
259
                Some(&new),
260
                &format!("oxy amend {}", old.change_id.short(8)),
261
            )?
262
            .unwrap())
263
    }
264
265
    /// Drop a change from the bookmark. Dependents inherit its dependencies;
266
    /// references into it are reified by re-anchoring (spec §8.4) and the
267
    /// adjustments are returned as notes. A dropped head's content stays on
268
    /// disk as uncommitted work if the working copy sat on it.
269
    pub fn abandon(&self, hash: &ChangeHash) -> Result<Vec<String>> {
270
        let c = self.store.get_change(hash)?;
271
        if c.kind == ChangeKind::WorkingCopy {
272
            return fail("that is the working copy; commit it instead");
273
        }
274
        let heads = self.bookmark_heads()?;
275
        if heads.contains(hash) {
276
            self.replace_head(hash, None, &format!("oxy abandon {}", c.change_id.short(8)))?;
277
            Ok(vec![])
278
        } else {
279
            let (_, notes) = self.rewrite_change_noted(
280
                hash,
281
                None,
282
                &Correspondence::default(),
283
                &format!("oxy abandon {}", c.change_id.short(8)),
284
            )?;
285
            Ok(notes)
286
        }
287
    }
288
}
289
290
/// Translate a reference to change `target`'s own atom or op into a self
291
/// reference, and a working-copy self reference into one offset past the
292
/// target's own count; other references pass through.
293
fn fold_atom(
294
    r: AtomRef,
295
    target: &ChangeHash,
296
    wc_offset: &HashMap<NodeId, u32>,
297
    node: &NodeId,
298
) -> Option<AtomRef> {
299
    match r {
300
        AtomRef::Atom { change, index } if change == *target => Some(AtomRef::Atom {
301
            change: ObjectRef::ZERO,
302
            index,
303
        }),
304
        AtomRef::Atom { change, index } if change.is_zero() => Some(AtomRef::Atom {
305
            change: ObjectRef::ZERO,
306
            index: index + wc_offset.get(node).copied().unwrap_or(0),
307
        }),
308
        // References into the target's closure were validated by the caller and stay as they are.
309
        other => Some(other),
310
    }
311
}
312
313
impl Repo {
314
    /// Fold the working copy's uncommitted edits into any change of the
315
    /// bookmark (spec §8.4). The edits must reference only atoms, placements
316
    /// and nodes that exist in the target's own closure; dependents are
317
    /// rewritten with an identity correspondence, since the target's own
318
    /// atoms keep their indices.
319
    pub fn amend_into(&self, target: &ChangeHash, message: Option<&str>) -> Result<ChangeHash> {
320
        let snap = self.snapshot("oxy amend")?;
321
        if snap
322
            .statuses
323
            .iter()
324
            .any(|s| s.status == ChangeStatus::ConflictEdited)
325
        {
326
            return fail("a file still contains conflict markers");
327
        }
328
        let wc = &snap.wc;
329
        if wc.tree_ops.is_empty() && wc.edits.is_empty() && message.is_none() {
330
            return fail(
331
                "nothing to amend: the working copy is clean and no new message was given",
332
            );
333
        }
334
        let old = self.store.get_change(target)?;
335
        if old.kind == ChangeKind::WorkingCopy {
336
            return fail("that is the working copy; commit it instead");
337
        }
338
        // Every reference must live in the target's closure (or be the target).
339
        let closure = self.load_state(&[*target])?;
340
        let ok_change = |h: &ChangeHash| *h == *target || closure.contains(h);
341
        let ok_node = |n: &NodeId| n.is_root() || closure.tree().introducer(n).is_some();
342
        let too_new = |what: &str| {
343
            RepoError::Msg(format!(
344
                "{what} was introduced after change #{}; amend into a later change instead",
345
                target.short(12)
346
            ))
347
        };
348
        for op in &wc.tree_ops {
349
            match op {
350
                TreeOp::AddNode { parent, .. } => {
351
                    if !ok_node(parent) {
352
                        return Err(too_new("the parent directory"));
353
                    }
354
                }
355
                TreeOp::RemoveNode { node, placement } => {
356
                    if !ok_node(node) || !ok_change(&placement.change) {
357
                        return Err(too_new("a removed file"));
358
                    }
359
                }
360
                TreeOp::Move {
361
                    node, old, parent, ..
362
                } => {
363
                    if !ok_node(node) || !ok_change(&old.change) || !ok_node(parent) {
364
                        return Err(too_new("a moved file"));
365
                    }
366
                }
367
                TreeOp::SetMode { node, old, .. } => {
368
                    if !ok_node(node) || !ok_change(&old.change) {
369
                        return Err(too_new("a file whose mode changed"));
370
                    }
371
                }
372
            }
373
        }
374
        let wc_added: BTreeSet<NodeId> = wc
375
            .tree_ops
376
            .iter()
377
            .filter_map(|o| {
378
                if let TreeOp::AddNode { node, .. } = o {
379
                    Some(*node)
380
                } else {
381
                    None
382
                }
383
            })
384
            .collect();
385
        for e in &wc.edits {
386
            let n = e.node();
387
            if !wc_added.contains(&n) && !ok_node(&n) {
388
                return Err(too_new("an edited file"));
389
            }
390
            let refs: Vec<ChangeHash> = match e {
391
                Edit::Insert { after, before, .. } => [after, before]
392
                    .iter()
393
                    .filter_map(|r| {
394
                        if let AtomRef::Atom { change, .. } = r {
395
                            Some(*change)
396
                        } else {
397
                            None
398
                        }
399
                    })
400
                    .collect(),
401
                Edit::Delete { atoms, .. } => atoms.iter().map(|r| r.change).collect(),
402
                Edit::Replace { .. } => vec![],
403
            };
404
            if refs.iter().any(|h| !h.is_zero() && !ok_change(h)) {
405
                return Err(too_new("an edited line"));
406
            }
407
        }
408
409
        // Compose: the target's operations first, then the working copy's
410
        // with references to the target turned into self references and its
411
        // own atom indices shifted past the target's.
412
        let mut per_node: HashMap<NodeId, u32> = HashMap::new();
413
        for e in &old.edits {
414
            if let Edit::Insert { node, lines, .. } = e {
415
                *per_node.entry(*node).or_default() += lines.len() as u32;
416
            }
417
        }
418
        let op_offset = old.tree_ops.len() as u32;
419
        let fold_op = |r: OpRef| -> OpRef {
420
            if r.change == *target {
421
                OpRef {
422
                    change: ObjectRef::ZERO,
423
                    index: r.index,
424
                }
425
            } else if r.change.is_zero() {
426
                OpRef {
427
                    change: ObjectRef::ZERO,
428
                    index: r.index + op_offset,
429
                }
430
            } else {
431
                r
432
            }
433
        };
434
        let mut tree_ops = old.tree_ops.clone();
435
        for op in &wc.tree_ops {
436
            tree_ops.push(match op {
437
                TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode {
438
                    node: *node,
439
                    placement: fold_op(*placement),
440
                },
441
                TreeOp::Move {
442
                    node,
443
                    old,
444
                    parent,
445
                    name,
446
                } => TreeOp::Move {
447
                    node: *node,
448
                    old: fold_op(*old),
449
                    parent: *parent,
450
                    name: name.clone(),
451
                },
452
                TreeOp::SetMode { node, old, mode } => TreeOp::SetMode {
453
                    node: *node,
454
                    old: fold_op(*old),
455
                    mode: *mode,
456
                },
457
                add => add.clone(),
458
            });
459
        }
460
        let mut edits = old.edits.clone();
461
        for e in &wc.edits {
462
            edits.push(match e {
463
                Edit::Insert {
464
                    node,
465
                    after,
466
                    before,
467
                    lines,
468
                } => Edit::Insert {
469
                    node: *node,
470
                    after: fold_atom(*after, target, &per_node, node)
471
                        .ok_or_else(|| too_new("an anchor line"))?,
472
                    before: fold_atom(*before, target, &per_node, node)
473
                        .ok_or_else(|| too_new("an anchor line"))?,
474
                    lines: lines.clone(),
475
                },
476
                Edit::Delete { node, atoms } => Edit::Delete {
477
                    node: *node,
478
                    atoms: atoms
479
                        .iter()
480
                        .map(|r| {
481
                            if r.change == *target {
482
                                AtomRange {
483
                                    change: ObjectRef::ZERO,
484
                                    from: r.from,
485
                                    to: r.to,
486
                                }
487
                            } else if r.change.is_zero() {
488
                                let off = per_node.get(node).copied().unwrap_or(0);
489
                                AtomRange {
490
                                    change: ObjectRef::ZERO,
491
                                    from: r.from + off,
492
                                    to: r.to + off,
493
                                }
494
                            } else {
495
                                *r
496
                            }
497
                        })
498
                        .collect(),
499
                },
500
                other => other.clone(),
501
            });
502
        }
503
        let mut new = Change {
504
            version: crate::objects::CHANGE_VERSION,
505
            change_id: old.change_id,
506
            deps: vec![],
507
            author: self.author(),
508
            hlc: Hlc::now(),
509
            message: message
510
                .map(|m| m.trim_end().to_string())
511
                .unwrap_or(old.message.clone()),
512
            tree_ops,
513
            edits,
514
            kind: old.kind,
515
        };
516
        let beneath = self.load_state(&old.deps)?;
517
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
518
        deps.extend(
519
            referenced_changes(&new, &beneath)
520
                .into_iter()
521
                .filter(|h| *h != *target),
522
        );
523
        deps.extend(
524
            wc.deps
525
                .iter()
526
                .filter(|d| closure.contains(d) && **d != *target)
527
                .copied(),
528
        );
529
        new.deps = deps.into_iter().collect();
530
        new.normalize();
531
532
        // Rewrite dependents (identity correspondence), then start a fresh,
533
        // empty working copy: its edits now live in the amended change.
534
        let corr = Correspondence::identity(&old);
535
        let new_hash = self
536
            .rewrite_change(
537
                target,
538
                Some(&new),
539
                &corr,
540
                &format!("oxy amend --into {}", old.change_id.short(8)),
541
            )?
542
            .unwrap();
543
        let (op_ref, op) = self.head_op()?;
544
        let heads = self.heads_of(&op.view, &self.config.bookmark)?;
545
        let fresh = self.new_working_copy(heads, "");
546
        let wc_hash = self.store.put_change(&fresh)?;
547
        let mut view = op.view.clone();
548
        view.workspaces.insert(self.workspace, wc_hash);
549
        self.record_op(&[op_ref], "oxy amend (working copy)", view)?;
550
        self.drop_manifest();
551
        Ok(new_hash)
552
    }
553
554
    /// Split a change into two: one holding its effect on `paths`, the
555
    /// other the rest (keeping the ChangeId). A head is rebuilt from its
556
    /// materialized files; any other change is split by node, which is
557
    /// exact, and is refused when a dependent refers to the split-off
558
    /// nodes. The working copy is unaffected.
559
    pub fn split(&self, hash: &ChangeHash, paths: &[String]) -> Result<(ChangeHash, ChangeHash)> {
560
        if paths.is_empty() {
561
            return fail("name at least one path to split out");
562
        }
563
        if !self.bookmark_heads()?.contains(hash) {
564
            return self.split_by_node(hash, paths);
565
        }
566
        let old = self.rewritable_head(hash)?;
567
        let beneath = self.load_state(&old.deps)?;
568
        let beneath_snapshot = beneath.materialize();
569
        let full = self.load_state(&[*hash])?;
570
        let full_snapshot = full.materialize();
571
        let selected = |p: &str| {
572
            paths
573
                .iter()
574
                .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/'))))
575
        };
576
        // Entries of the intermediate state: selected paths as the head
577
        // leaves them, everything else as beneath.
578
        let entries_of = |snap: &Snapshot,
579
                          keep_from_full: &dyn Fn(&str) -> bool|
580
         -> Result<BTreeMap<String, FsEntry>> {
581
            let mut out = BTreeMap::new();
582
            for (p, _) in snap.files.iter().chain(full_snapshot.files.iter()) {
583
                let take_full = keep_from_full(p);
584
                let src = if take_full {
585
                    full_snapshot.files.get(p)
586
                } else {
587
                    snap.files.get(p)
588
                };
589
                let Some(src) = src else { continue };
590
                if out.contains_key(p) {
591
                    continue;
592
                }
593
                let content = match &src.content {
594
                    FileContent::Dir => continue,
595
                    FileContent::Text { bytes, .. } => bytes.clone(),
596
                    FileContent::Opaque { blobs } => match blobs.first() {
597
                        Some(b) => self.read_blob(b)?,
598
                        None => continue,
599
                    },
600
                };
601
                out.insert(p.clone(), FsEntry::new(src.kind, src.mode.exec, content));
602
            }
603
            Ok(out)
604
        };
605
        let mid_entries = entries_of(&beneath_snapshot, &|p| selected(p))?;
606
        let first =
607
            self.build_from_entries(&beneath, &beneath_snapshot, HashMap::new(), &mid_entries)?;
608
        if first.tree_ops.is_empty() && first.edits.is_empty() {
609
            return fail("the named paths are not touched by that change");
610
        }
611
        let mut c1 = Change {
612
            version: crate::objects::CHANGE_VERSION,
613
            change_id: ChangeId::random(),
614
            deps: old.deps.clone(),
615
            author: old.author,
616
            hlc: Hlc::now(),
617
            message: format!(
618
                "{} (part 1: {})",
619
                old.message.lines().next().unwrap_or(""),
620
                paths.join(", ")
621
            ),
622
            tree_ops: first.tree_ops,
623
            edits: first.edits,
624
            kind: ChangeKind::Normal,
625
        };
626
        let mut d1: BTreeSet<ChangeHash> = referenced_changes(&c1, &beneath);
627
        d1.extend(old.deps.iter().copied());
628
        c1.deps = d1.into_iter().collect();
629
        c1.normalize();
630
        let h1 = self.store_signed(&c1)?;
631
        let mut mid = beneath.clone();
632
        mid.add(c1)?;
633
        let mid_snapshot = mid.materialize();
634
        let rest_entries = entries_of(&full_snapshot, &|_| true)?;
635
        let rest = self.build_from_entries(&mid, &mid_snapshot, HashMap::new(), &rest_entries)?;
636
        if rest.tree_ops.is_empty() && rest.edits.is_empty() {
637
            return fail(
638
                "everything in that change is under the named paths; nothing would remain",
639
            );
640
        }
641
        let mut c2 = Change {
642
            version: crate::objects::CHANGE_VERSION,
643
            change_id: old.change_id,
644
            deps: vec![],
645
            author: old.author,
646
            hlc: Hlc::now(),
647
            message: old.message.clone(),
648
            tree_ops: rest.tree_ops,
649
            edits: rest.edits,
650
            kind: ChangeKind::Normal,
651
        };
652
        let mut d2: BTreeSet<ChangeHash> = referenced_changes(&c2, &mid);
653
        d2.extend(old.deps.iter().copied());
654
        c2.deps = d2.into_iter().collect();
655
        c2.normalize();
656
        let h2 = self
657
            .replace_head(
658
                hash,
659
                Some(&c2),
660
                &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")),
661
            )?
662
            .unwrap();
663
        self.add_to_bookmark(h1)?;
664
        Ok((h1, h2))
665
    }
666
667
    /// Put `h` into the bookmark's state (a head unless something already
668
    /// depends on it) and under the working copy.
669
    fn add_to_bookmark(&self, h: ChangeHash) -> Result<()> {
670
        let (op_ref, op) = self.head_op()?;
671
        let bookmark = self.config.bookmark.clone();
672
        let mut heads = self.heads_of(&op.view, &bookmark)?;
673
        heads.push(h);
674
        let st = self.load_state(&heads)?;
675
        let heads = st.maximal(&heads);
676
        let mut view = op.view.clone();
677
        view.bookmarks.insert(
678
            bookmark,
679
            self.store.put_state(&StateObject::new(heads.clone()))?,
680
        );
681
        let (_, wc) = self.working_copy()?;
682
        let mut base = wc.deps.clone();
683
        base.push(h);
684
        let wc_base = self.load_state(&base)?.maximal(&base);
685
        let fresh = self.new_working_copy(wc_base, &wc.message);
686
        view.workspaces
687
            .insert(self.workspace, self.store.put_change(&fresh)?);
688
        self.record_op(&[op_ref], "oxy split (heads)", view)?;
689
        self.drop_manifest();
690
        Ok(())
691
    }
692
693
    /// Split a non-head change by node: the operations and edits on nodes
694
    /// under `paths` become a new change, the rest keeps the ChangeId with
695
    /// every remaining atom at its old index, so dependents are rewritten
696
    /// exactly. Refused when a dependent touches a split-off node.
697
    fn split_by_node(
698
        &self,
699
        hash: &ChangeHash,
700
        paths: &[String],
701
    ) -> Result<(ChangeHash, ChangeHash)> {
702
        let old = self.store.get_change(hash)?;
703
        if old.kind == ChangeKind::WorkingCopy {
704
            return fail("that is the working copy; commit it instead");
705
        }
706
        let selected = |p: &str| {
707
            paths
708
                .iter()
709
                .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/'))))
710
        };
711
        let beneath = self.load_state(&old.deps)?;
712
        let full = self.load_state(&[*hash])?;
713
        let mut removed: BTreeSet<NodeId> = BTreeSet::new();
714
        for snap in [beneath.materialize(), full.materialize()] {
715
            for (p, f) in &snap.files {
716
                if selected(p) {
717
                    removed.insert(f.node);
718
                }
719
            }
720
        }
721
        // Nodes the change itself adds under a removed directory.
722
        for op in &old.tree_ops {
723
            if let TreeOp::AddNode { node, parent, .. } = op {
724
                if removed.contains(parent) {
725
                    removed.insert(*node);
726
                }
727
            }
728
        }
729
        if removed.is_empty() {
730
            return fail("the named paths are not touched by that change");
731
        }
732
        // Dependents must not touch the split-off nodes: their references
733
        // would have to move to the new change, which a rewrite cannot express.
734
        let heads = self.bookmark_heads()?;
735
        let (wc_hash, _) = self.working_copy()?;
736
        let mut all = heads.clone();
737
        all.push(wc_hash);
738
        let state = self.load_state(&all)?;
739
        for (h, meta) in state.changes() {
740
            if !meta.deps.contains(hash) {
741
                continue;
742
            }
743
            let d = self.store.get_change(h)?;
744
            if d.tree_ops.iter().any(|op| removed.contains(&op.node()))
745
                || d.edits.iter().any(|e| removed.contains(&e.node()))
746
            {
747
                return fail(format!(
748
                    "change {} depends on the split-off paths of {}; split it first",
749
                    d.change_id.short(8),
750
                    old.change_id.short(8)
751
                ));
752
            }
753
        }
754
        let mut c1 = Change {
755
            version: crate::objects::CHANGE_VERSION,
756
            change_id: ChangeId::random(),
757
            deps: old.deps.clone(),
758
            author: old.author,
759
            hlc: Hlc::now(),
760
            message: format!(
761
                "{} (part 1: {})",
762
                old.message.lines().next().unwrap_or(""),
763
                paths.join(", ")
764
            ),
765
            tree_ops: old
766
                .tree_ops
767
                .iter()
768
                .filter(|op| removed.contains(&op.node()))
769
                .cloned()
770
                .collect(),
771
            edits: old
772
                .edits
773
                .iter()
774
                .filter(|e| removed.contains(&e.node()))
775
                .cloned()
776
                .collect(),
777
            kind: ChangeKind::Normal,
778
        };
779
        let mut c2 = old.clone();
780
        c2.hlc = Hlc::now();
781
        let mut ops_map: HashMap<u32, u32> = HashMap::new();
782
        let mut kept_ops = Vec::new();
783
        for (i, op) in old.tree_ops.iter().enumerate() {
784
            if !removed.contains(&op.node()) {
785
                ops_map.insert(i as u32, kept_ops.len() as u32);
786
                kept_ops.push(op.clone());
787
            }
788
        }
789
        c2.tree_ops = kept_ops;
790
        c2.edits.retain(|e| !removed.contains(&e.node()));
791
        if c1.tree_ops.is_empty() && c1.edits.is_empty() {
792
            return fail("the named paths are not touched by that change");
793
        }
794
        if c2.tree_ops.is_empty() && c2.edits.is_empty() {
795
            return fail(
796
                "everything in that change is under the named paths; use `oxy abandon` instead",
797
            );
798
        }
799
        let mut d1: BTreeSet<ChangeHash> = referenced_changes(&c1, &beneath);
800
        d1.extend(old.deps.iter().copied());
801
        c1.deps = d1.into_iter().collect();
802
        c1.normalize();
803
        let mut d2: BTreeSet<ChangeHash> = referenced_changes(&c2, &beneath);
804
        d2.extend(old.deps.iter().copied());
805
        c2.deps = d2.into_iter().collect();
806
        c2.normalize();
807
        let corr = Correspondence {
808
            atoms: Correspondence::identity(&old).atoms,
809
            ops: ops_map,
810
        };
811
        let h2 = self
812
            .rewrite_change(
813
                hash,
814
                Some(&c2),
815
                &corr,
816
                &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")),
817
            )?
818
            .unwrap();
819
        let h1 = self.store_signed(&c1)?;
820
        self.add_to_bookmark(h1)?;
821
        Ok((h1, h2))
822
    }
823
}
824
825
impl Repo {
826
    /// Fold change `c` into its dependency `into` (spec §8.5): the result
827
    /// keeps `into`'s ChangeId, dependents of both are rewritten, and `c`
828
    /// leaves the state.
829
    pub fn squash(&self, c: &ChangeHash, into: &ChangeHash) -> Result<ChangeHash> {
830
        if c == into {
831
            return fail("a change cannot be squashed into itself");
832
        }
833
        let src = self.store.get_change(c)?;
834
        let dst = self.store.get_change(into)?;
835
        if src.kind == ChangeKind::WorkingCopy {
836
            return fail("that is the working copy; use `oxy amend --into`");
837
        }
838
        if dst.kind == ChangeKind::WorkingCopy {
839
            return fail("cannot squash into the working copy");
840
        }
841
        let closure = self.load_state(&[*into])?;
842
        let outside: Vec<String> = src
843
            .deps
844
            .iter()
845
            .filter(|d| *d != into && !closure.contains(d))
846
            .map(|d| d.short(12))
847
            .collect();
848
        if !outside.is_empty() {
849
            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)));
850
        }
851
        let ok_change = |h: &ChangeHash| *h == *into || closure.contains(h);
852
        let ok_node = |n: &NodeId| n.is_root() || closure.tree().introducer(n).is_some();
853
        let too_new = |what: &str| {
854
            RepoError::Msg(format!(
855
                "{what} was introduced after change #{}; squash into a later change instead",
856
                into.short(12)
857
            ))
858
        };
859
        let src_added: BTreeSet<NodeId> = src
860
            .tree_ops
861
            .iter()
862
            .filter_map(|o| {
863
                if let TreeOp::AddNode { node, .. } = o {
864
                    Some(*node)
865
                } else {
866
                    None
867
                }
868
            })
869
            .collect();
870
        for op in &src.tree_ops {
871
            match op {
872
                TreeOp::AddNode { parent, .. } => {
873
                    if !ok_node(parent) && !src_added.contains(parent) {
874
                        return Err(too_new("the parent directory"));
875
                    }
876
                }
877
                TreeOp::RemoveNode { node, placement } => {
878
                    if !ok_node(node) || !ok_change(&placement.change) {
879
                        return Err(too_new("a removed file"));
880
                    }
881
                }
882
                TreeOp::Move {
883
                    node, old, parent, ..
884
                } => {
885
                    if !ok_node(node)
886
                        || !ok_change(&old.change)
887
                        || (!ok_node(parent) && !src_added.contains(parent))
888
                    {
889
                        return Err(too_new("a moved file"));
890
                    }
891
                }
892
                TreeOp::SetMode { node, old, .. } => {
893
                    if !ok_node(node) || !ok_change(&old.change) {
894
                        return Err(too_new("a file whose mode changed"));
895
                    }
896
                }
897
            }
898
        }
899
        for e in &src.edits {
900
            let n = e.node();
901
            if !src_added.contains(&n) && !ok_node(&n) {
902
                return Err(too_new("an edited file"));
903
            }
904
            let refs: Vec<ChangeHash> = match e {
905
                Edit::Insert { after, before, .. } => [after, before]
906
                    .iter()
907
                    .filter_map(|r| {
908
                        if let AtomRef::Atom { change, .. } = r {
909
                            Some(*change)
910
                        } else {
911
                            None
912
                        }
913
                    })
914
                    .collect(),
915
                Edit::Delete { atoms, .. } => atoms.iter().map(|r| r.change).collect(),
916
                Edit::Replace { .. } => vec![],
917
            };
918
            if refs.iter().any(|h| !h.is_zero() && !ok_change(h)) {
919
                return Err(too_new("an edited line"));
920
            }
921
        }
922
        // Compose dst then src, with src's references to dst turned into
923
        // self references and its own atoms shifted past dst's.
924
        let mut per_node: HashMap<NodeId, u32> = HashMap::new();
925
        for e in &dst.edits {
926
            if let Edit::Insert { node, lines, .. } = e {
927
                *per_node.entry(*node).or_default() += lines.len() as u32;
928
            }
929
        }
930
        let op_offset = dst.tree_ops.len() as u32;
931
        let fold_op = |r: OpRef| -> OpRef {
932
            if r.change == *into {
933
                OpRef {
934
                    change: ObjectRef::ZERO,
935
                    index: r.index,
936
                }
937
            } else if r.change.is_zero() {
938
                OpRef {
939
                    change: ObjectRef::ZERO,
940
                    index: r.index + op_offset,
941
                }
942
            } else {
943
                r
944
            }
945
        };
946
        let mut tree_ops = dst.tree_ops.clone();
947
        for op in &src.tree_ops {
948
            tree_ops.push(match op {
949
                TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode {
950
                    node: *node,
951
                    placement: fold_op(*placement),
952
                },
953
                TreeOp::Move {
954
                    node,
955
                    old,
956
                    parent,
957
                    name,
958
                } => TreeOp::Move {
959
                    node: *node,
960
                    old: fold_op(*old),
961
                    parent: *parent,
962
                    name: name.clone(),
963
                },
964
                TreeOp::SetMode { node, old, mode } => TreeOp::SetMode {
965
                    node: *node,
966
                    old: fold_op(*old),
967
                    mode: *mode,
968
                },
969
                add => add.clone(),
970
            });
971
        }
972
        let mut edits = dst.edits.clone();
973
        for e in &src.edits {
974
            edits.push(match e {
975
                Edit::Insert {
976
                    node,
977
                    after,
978
                    before,
979
                    lines,
980
                } => Edit::Insert {
981
                    node: *node,
982
                    after: fold_atom(*after, into, &per_node, node)
983
                        .ok_or_else(|| too_new("an anchor line"))?,
984
                    before: fold_atom(*before, into, &per_node, node)
985
                        .ok_or_else(|| too_new("an anchor line"))?,
986
                    lines: lines.clone(),
987
                },
988
                Edit::Delete { node, atoms } => Edit::Delete {
989
                    node: *node,
990
                    atoms: atoms
991
                        .iter()
992
                        .map(|r| {
993
                            if r.change == *into {
994
                                AtomRange {
995
                                    change: ObjectRef::ZERO,
996
                                    from: r.from,
997
                                    to: r.to,
998
                                }
999
                            } else if r.change.is_zero() {
1000
                                let off = per_node.get(node).copied().unwrap_or(0);
1001
                                AtomRange {
1002
                                    change: ObjectRef::ZERO,
1003
                                    from: r.from + off,
1004
                                    to: r.to + off,
1005
                                }
1006
                            } else {
1007
                                *r
1008
                            }
1009
                        })
1010
                        .collect(),
1011
                },
1012
                other => other.clone(),
1013
            });
1014
        }
1015
        let mut new = Change {
1016
            version: crate::objects::CHANGE_VERSION,
1017
            change_id: dst.change_id,
1018
            deps: vec![],
1019
            author: dst.author,
1020
            hlc: Hlc::now(),
1021
            message: dst.message.clone(),
1022
            tree_ops,
1023
            edits,
1024
            kind: if dst.kind == ChangeKind::Resolution || src.kind == ChangeKind::Resolution {
1025
                ChangeKind::Resolution
1026
            } else {
1027
                ChangeKind::Normal
1028
            },
1029
        };
1030
        let beneath = self.load_state(&dst.deps)?;
1031
        let mut deps: BTreeSet<ChangeHash> = dst.deps.iter().copied().collect();
1032
        deps.extend(
1033
            referenced_changes(&new, &beneath)
1034
                .into_iter()
1035
                .filter(|h| *h != *into && *h != *c),
1036
        );
1037
        deps.extend(src.deps.iter().filter(|d| **d != *into).copied());
1038
        new.deps = deps.into_iter().collect();
1039
        new.normalize();
1040
1041
        // Dependents of dst keep their indices; dependents of src follow its
1042
        // atoms and operations to their shifted places in the new change.
1043
        let cmd = format!(
1044
            "oxy squash {} --into {}",
1045
            src.change_id.short(8),
1046
            dst.change_id.short(8)
1047
        );
1048
        let dst_new = self
1049
            .rewrite_change(into, Some(&new), &Correspondence::identity(&dst), &cmd)?
1050
            .unwrap();
1051
        // src was rewritten along with the other dependents of dst; find it by ChangeId.
1052
        let (wc_hash, _) = self.working_copy()?;
1053
        let mut all = self.bookmark_heads()?;
1054
        all.push(wc_hash);
1055
        let state = self.load_state(&all)?;
1056
        let src_now = state
1057
            .changes()
1058
            .find(|(_, m)| m.change_id == src.change_id)
1059
            .map(|(h, _)| *h)
1060
            .unwrap_or(*c);
1061
        let src_change = self.store.get_change(&src_now)?;
1062
        let mut corr = Correspondence {
1063
            atoms: HashMap::new(),
1064
            ops: HashMap::new(),
1065
        };
1066
        let mut counts: HashMap<NodeId, u32> = HashMap::new();
1067
        for e in &src_change.edits {
1068
            if let Edit::Insert { node, lines, .. } = e {
1069
                let off = per_node.get(node).copied().unwrap_or(0);
1070
                let idx = counts.entry(*node).or_default();
1071
                for _ in lines {
1072
                    corr.atoms.insert((*node, *idx), *idx + off);
1073
                    *idx += 1;
1074
                }
1075
            }
1076
        }
1077
        for i in 0..src_change.tree_ops.len() as u32 {
1078
            corr.ops.insert(i, i + op_offset);
1079
        }
1080
        let final_new = self.store.get_change(&dst_new)?;
1081
        self.rewrite_change(&src_now, Some(&final_new), &corr, &cmd)?;
1082
        Ok(dst_new)
1083
    }
1084
1085
    /// Re-express change `c` against the state `onto` names (spec §8.5): its
1086
    /// effect on the files it touches is re-derived as edits against that
1087
    /// state. Keeps the ChangeId; dependents follow by content.
1088
    pub fn rebase(&self, c: &ChangeHash, onto: &[ChangeHash]) -> Result<ChangeHash> {
1089
        let old = self.store.get_change(c)?;
1090
        if old.kind == ChangeKind::WorkingCopy {
1091
            return fail("that is the working copy; check out the new base instead");
1092
        }
1093
        let target = self.load_state(onto)?;
1094
        if target.contains(c) {
1095
            return fail(format!(
1096
                "#{} is already part of the target state",
1097
                c.short(12)
1098
            ));
1099
        }
1100
        let onto = target.maximal(onto);
1101
        let target_snapshot = target.materialize();
1102
        // c applied to the target: the model does this by union, since c
1103
        // commutes with everything it does not depend on. What the union
1104
        // shows is what the re-derived change must produce.
1105
        let mut with_c = target.clone();
1106
        if with_c.add(old.clone()).is_err() {
1107
            return fail(format!(
1108
                "the target state lacks dependencies of #{}; rebase onto a state that has them",
1109
                c.short(12)
1110
            ));
1111
        }
1112
        let after = with_c.materialize();
1113
        // The paths whose file differs between the target and the target with c.
1114
        let mut touched: BTreeSet<String> = BTreeSet::new();
1115
        for p in target_snapshot.files.keys().chain(after.files.keys()) {
1116
            match (target_snapshot.files.get(p), after.files.get(p)) {
1117
                (Some(a), Some(b))
1118
                    if a.kind == b.kind && a.mode == b.mode && a.content == b.content => {}
1119
                _ => {
1120
                    touched.insert(p.clone());
1121
                }
1122
            }
1123
        }
1124
        for p in &touched {
1125
            if target_snapshot
1126
                .files
1127
                .get(p)
1128
                .is_some_and(|f| f.is_conflicted())
1129
                || after.files.get(p).is_some_and(|f| f.is_conflicted())
1130
            {
1131
                return fail(format!("{p} would be conflicted; resolve the target state or the conflict with #{} first", c.short(12)));
1132
            }
1133
        }
1134
        // Entries: the target's files, with touched paths as c leaves them.
1135
        let mut entries: BTreeMap<String, FsEntry> = BTreeMap::new();
1136
        let mut reuse: HashMap<String, (NodeId, NodeKind)> = HashMap::new();
1137
        let content_of = |f: &crate::state::FileState| -> Result<Option<Vec<u8>>> {
1138
            Ok(match &f.content {
1139
                FileContent::Dir => None,
1140
                FileContent::Text { bytes, .. } => Some(bytes.clone()),
1141
                FileContent::Opaque { blobs } => match blobs.first() {
1142
                    Some(b) => Some(self.read_blob(b)?),
1143
                    None => None,
1144
                },
1145
            })
1146
        };
1147
        for (p, f) in &target_snapshot.files {
1148
            if touched.contains(p) {
1149
                continue;
1150
            }
1151
            if let Some(content) = content_of(f)? {
1152
                entries.insert(p.clone(), FsEntry::new(f.kind, f.mode.exec, content));
1153
            }
1154
        }
1155
        for p in &touched {
1156
            if let Some(f) = after.files.get(p) {
1157
                if let Some(content) = content_of(f)? {
1158
                    entries.insert(p.clone(), FsEntry::new(f.kind, f.mode.exec, content));
1159
                }
1160
                if !target_snapshot.files.contains_key(p) {
1161
                    reuse.insert(p.clone(), (f.node, f.kind));
1162
                }
1163
            }
1164
        }
1165
        let built = self.build_from_entries(&target, &target_snapshot, reuse, &entries)?;
1166
        if built.tree_ops.is_empty() && built.edits.is_empty() {
1167
            return fail(
1168
                "rebasing would leave the change empty; the target already has its effect",
1169
            );
1170
        }
1171
        let mut new = Change {
1172
            version: crate::objects::CHANGE_VERSION,
1173
            change_id: old.change_id,
1174
            deps: vec![],
1175
            author: old.author,
1176
            hlc: Hlc::now(),
1177
            message: old.message.clone(),
1178
            tree_ops: built.tree_ops,
1179
            edits: built.edits,
1180
            kind: if built.resolves {
1181
                ChangeKind::Resolution
1182
            } else {
1183
                ChangeKind::Normal
1184
            },
1185
        };
1186
        let mut deps: BTreeSet<ChangeHash> = onto.iter().copied().collect();
1187
        deps.extend(referenced_changes(&new, &target));
1188
        deps.extend(built.contributors.iter().copied());
1189
        new.deps = deps.into_iter().collect();
1190
        new.normalize();
1191
        let corr = Correspondence::by_content(&old, &new);
1192
        let cmd = format!(
1193
            "oxy rebase {} --onto {}",
1194
            old.change_id.short(8),
1195
            onto.iter()
1196
                .map(|h| h.short(12))
1197
                .collect::<Vec<_>>()
1198
                .join(" ")
1199
        );
1200
        Ok(self.rewrite_change(c, Some(&new), &corr, &cmd)?.unwrap())
1201
    }
1202
}