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