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