OXYGENHUB
# 23b217d75848 oxygen / crates / oxy-core / src / repo / history.rs
22.7 KiB · Rust · last changed History editing: oxy amend --into folds the working copy into an older change with dependents rewritten; oxy split; graph: delete/insert conflicts only between commuting changes (spec 0.6l) 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
//! `amend` works on heads, where the amended change is rebuilt from the files
6
//! on disk. A rewritten change keeps its ChangeId and gets a new hash; old
7
//! objects stay in the store until garbage collection. Rewritten changes by
8
//! other authors lose their signatures, as with any rewrite of others' work.
9
10
use super::*;
11
use crate::objects::{AtomRange, AtomRef};
12
use crate::rewrite::{propagate, Correspondence};
13
14
impl Repo {
15
    /// The bookmark head `hash`, or an error explaining why it cannot be rewritten.
16
    fn rewritable_head(&self, hash: &ChangeHash) -> Result<Change> {
17
        let heads = self.bookmark_heads()?;
18
        if !heads.contains(hash) {
19
            return fail(format!(
20
                "change #{} is not a head of {}; rewriting changes that others depend on is not implemented yet",
21
                hash.short(12),
22
                self.config.bookmark
23
            ));
24
        }
25
        let c = self.store.get_change(hash)?;
26
        if c.kind == ChangeKind::WorkingCopy {
27
            return fail("that is the working copy; commit it instead");
28
        }
29
        Ok(c)
30
    }
31
32
    /// Replace head `old` with `new` (or with nothing) on the bookmark and
33
    /// restart the working copy on the resulting base. Files on disk are not
34
    /// touched: the next snapshot diffs them against the new base.
35
    fn replace_head(&self, old: &ChangeHash, new: Option<&Change>, command: &str) -> Result<Option<ChangeHash>> {
36
        let new_hash = match new {
37
            Some(c) => Some(self.store_signed(c)?),
38
            None => None,
39
        };
40
        let old_change = self.store.get_change(old)?;
41
        let (op_ref, op) = self.head_op()?;
42
        let bookmark = self.config.bookmark.clone();
43
        let heads = self.heads_of(&op.view, &bookmark)?;
44
        let replace = |set: &[ChangeHash]| -> Vec<ChangeHash> {
45
            let mut out: Vec<ChangeHash> = set.iter().filter(|h| *h != old).copied().collect();
46
            match new_hash {
47
                Some(n) => out.push(n),
48
                None => out.extend(old_change.deps.iter().copied()),
49
            }
50
            out
51
        };
52
        let candidates = replace(&heads);
53
        let st = self.load_state(&candidates)?;
54
        let new_heads = st.maximal(&candidates);
55
56
        let (_, wc) = self.working_copy()?;
57
        let base = self.load_state(&wc.deps)?;
58
        let wc_base = base.maximal(&wc.deps);
59
        let wc_base = if wc_base.contains(old) {
60
            let c = replace(&wc_base);
61
            let st = self.load_state(&c)?;
62
            st.maximal(&c)
63
        } else {
64
            wc_base
65
        };
66
        let fresh = self.new_working_copy(wc_base, &wc.message);
67
        let wc_hash = self.store.put_change(&fresh)?;
68
69
        let mut view = op.view.clone();
70
        view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?);
71
        view.workspaces.insert(self.workspace, wc_hash);
72
        self.record_op(&[op_ref], command, view)?;
73
        self.drop_manifest();
74
        Ok(new_hash)
75
    }
76
77
    /// Replace `old` by `new` (or remove it) anywhere in the bookmark's state,
78
    /// rewriting every dependent, including the working copy. Returns the
79
    /// new hash of `old`, if any.
80
    fn rewrite_change(&self, old: &ChangeHash, new: Option<&Change>, corr: &Correspondence, command: &str) -> Result<Option<ChangeHash>> {
81
        let (op_ref, op) = self.head_op()?;
82
        let bookmark = self.config.bookmark.clone();
83
        let heads = self.heads_of(&op.view, &bookmark)?;
84
        let (wc_hash, _) = self.working_copy()?;
85
        let mut all = heads.clone();
86
        all.push(wc_hash);
87
        let state = self.load_state(&all)?;
88
        if !state.closure(&wc_hash).contains(old) && !heads.contains(old) && !state.heads().contains(old) && !state.contains(old) {
89
            return fail(format!("change #{} is not in {} or under the working copy", old.short(12), bookmark));
90
        }
91
        let old_change = self.store.get_change(old)?;
92
        if old_change.kind == ChangeKind::WorkingCopy {
93
            return fail("that is the working copy; commit it instead");
94
        }
95
        let load = |h: &ChangeHash| self.store.get_change(h).ok();
96
        let rewritten = propagate(&state, *old, new, corr, &load).map_err(|e| RepoError::Msg(format!("{e}; rewriting through such references is not implemented yet")))?;
97
98
        let mut mapping: HashMap<ChangeHash, ChangeHash> = HashMap::new();
99
        let new_hash = match new {
100
            Some(c) => {
101
                let h = self.store_signed(c)?;
102
                mapping.insert(*old, h);
103
                Some(h)
104
            }
105
            None => None,
106
        };
107
        for (h, c) in &rewritten {
108
            let nh = if c.kind == ChangeKind::WorkingCopy { self.store.put_change(c)? } else { self.store_signed(c)? };
109
            mapping.insert(*h, nh);
110
        }
111
        let remap = |set: &[ChangeHash]| -> Vec<ChangeHash> {
112
            let mut out = Vec::new();
113
            for h in set {
114
                if h == old && new_hash.is_none() {
115
                    out.extend(old_change.deps.iter().copied());
116
                } else {
117
                    out.push(*mapping.get(h).unwrap_or(h));
118
                }
119
            }
120
            out
121
        };
122
        let candidates = remap(&heads);
123
        let st = self.load_state(&candidates)?;
124
        let new_heads = st.maximal(&candidates);
125
        let new_wc = mapping.get(&wc_hash).copied().unwrap_or(wc_hash);
126
127
        let mut view = op.view.clone();
128
        view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?);
129
        view.workspaces.insert(self.workspace, new_wc);
130
        self.record_op(&[op_ref], command, view)?;
131
        self.drop_manifest();
132
        Ok(new_hash)
133
    }
134
135
    /// Change the message of any change of the bookmark; dependents are rewritten.
136
    pub fn describe(&self, hash: &ChangeHash, message: &str) -> Result<ChangeHash> {
137
        if message.trim().is_empty() {
138
            return fail("a message is required");
139
        }
140
        let mut c = self.store.get_change(hash)?;
141
        if c.kind == ChangeKind::WorkingCopy {
142
            return fail("that is the working copy; commit it instead");
143
        }
144
        if c.message == message.trim_end() {
145
            return Ok(*hash);
146
        }
147
        let corr = Correspondence::identity(&c);
148
        c.message = message.trim_end().to_string();
149
        c.hlc = Hlc::now();
150
        let id = c.change_id;
151
        Ok(self.rewrite_change(hash, Some(&c), &corr, &format!("oxy describe {} -m {message:?}", id.short(8)))?.unwrap())
152
    }
153
154
    /// Fold the working copy into the head it sits on: the head is rebuilt
155
    /// as the difference between the state beneath it and the files on disk.
156
    pub fn amend(&self, message: Option<&str>) -> Result<ChangeHash> {
157
        let snap = self.snapshot("oxy amend")?;
158
        if snap.statuses.iter().any(|s| s.status == ChangeStatus::ConflictEdited) {
159
            return fail("a file still contains conflict markers");
160
        }
161
        let base_heads = snap.base.maximal(&snap.wc.deps);
162
        let [head] = base_heads.as_slice() else {
163
            return fail(format!("the working copy sits on {} heads; check out the single change to amend", base_heads.len()));
164
        };
165
        let head = *head;
166
        let old = self.rewritable_head(&head)?;
167
        if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() && message.is_none() {
168
            return fail("nothing to amend: the working copy is clean and no new message was given");
169
        }
170
        // Rebuild against the state beneath the head.
171
        let beneath = self.load_state(&old.deps)?;
172
        let beneath_snapshot = beneath.materialize();
173
        let reuse = added_node_paths(&old, &beneath_snapshot);
174
        let built = self.build_against(&beneath, &beneath_snapshot, reuse)?;
175
        if built.tree_ops.is_empty() && built.edits.is_empty() {
176
            return fail("amending would leave the change empty; use `oxy abandon` to drop it");
177
        }
178
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
179
        deps.extend(built.contributors.iter().copied());
180
        let mut new = Change {
181
            change_id: old.change_id,
182
            deps: vec![],
183
            author: self.author(),
184
            hlc: Hlc::now(),
185
            message: message.map(|m| m.trim_end().to_string()).unwrap_or(old.message.clone()),
186
            tree_ops: built.tree_ops,
187
            edits: built.edits,
188
            kind: if built.resolves || old.kind == ChangeKind::Resolution { ChangeKind::Resolution } else { ChangeKind::Normal },
189
        };
190
        deps.extend(referenced_changes(&new, &beneath));
191
        new.deps = deps.into_iter().collect();
192
        new.normalize();
193
        Ok(self.replace_head(&head, Some(&new), &format!("oxy amend {}", old.change_id.short(8)))?.unwrap())
194
    }
195
196
    /// Drop a change from the bookmark. Dependents that merely depend on it
197
    /// inherit its dependencies; one that references its atoms or placements
198
    /// cannot be rewritten yet. A dropped head's content stays on disk as
199
    /// uncommitted work if the working copy sat on it.
200
    pub fn abandon(&self, hash: &ChangeHash) -> Result<()> {
201
        let c = self.store.get_change(hash)?;
202
        if c.kind == ChangeKind::WorkingCopy {
203
            return fail("that is the working copy; commit it instead");
204
        }
205
        let heads = self.bookmark_heads()?;
206
        if heads.contains(hash) {
207
            self.replace_head(hash, None, &format!("oxy abandon {}", c.change_id.short(8)))?;
208
        } else {
209
            self.rewrite_change(hash, None, &Correspondence::default(), &format!("oxy abandon {}", c.change_id.short(8)))?;
210
        }
211
        Ok(())
212
    }
213
}
214
215
/// Translate a reference to change `target`'s own atom or op into a self
216
/// reference, and a working-copy self reference into one offset past the
217
/// target's own count; other references pass through.
218
fn fold_atom(r: AtomRef, target: &ChangeHash, wc_offset: &HashMap<NodeId, u32>, node: &NodeId) -> Option<AtomRef> {
219
    match r {
220
        AtomRef::Atom { change, index } if change == *target => Some(AtomRef::Atom { change: ObjectRef::ZERO, index }),
221
        AtomRef::Atom { change, index } if change.is_zero() => Some(AtomRef::Atom { change: ObjectRef::ZERO, index: index + wc_offset.get(node).copied().unwrap_or(0) }),
222
        // References into the target's closure were validated by the caller and stay as they are.
223
        other => Some(other),
224
    }
225
}
226
227
impl Repo {
228
    /// Fold the working copy's uncommitted edits into any change of the
229
    /// bookmark (spec §8.4). The edits must reference only atoms, placements
230
    /// and nodes that exist in the target's own closure; dependents are
231
    /// rewritten with an identity correspondence, since the target's own
232
    /// atoms keep their indices.
233
    pub fn amend_into(&self, target: &ChangeHash, message: Option<&str>) -> Result<ChangeHash> {
234
        let snap = self.snapshot("oxy amend")?;
235
        if snap.statuses.iter().any(|s| s.status == ChangeStatus::ConflictEdited) {
236
            return fail("a file still contains conflict markers");
237
        }
238
        let wc = &snap.wc;
239
        if wc.tree_ops.is_empty() && wc.edits.is_empty() && message.is_none() {
240
            return fail("nothing to amend: the working copy is clean and no new message was given");
241
        }
242
        let old = self.store.get_change(target)?;
243
        if old.kind == ChangeKind::WorkingCopy {
244
            return fail("that is the working copy; commit it instead");
245
        }
246
        // Every reference must live in the target's closure (or be the target).
247
        let closure = self.load_state(&[*target])?;
248
        let ok_change = |h: &ChangeHash| *h == *target || closure.contains(h);
249
        let ok_node = |n: &NodeId| n.is_root() || closure.tree().introducer(n).is_some();
250
        let too_new = |what: &str| RepoError::Msg(format!("{what} was introduced after change #{}; amend into a later change instead", target.short(12)));
251
        for op in &wc.tree_ops {
252
            match op {
253
                TreeOp::AddNode { parent, .. } => {
254
                    if !ok_node(parent) {
255
                        return Err(too_new("the parent directory"));
256
                    }
257
                }
258
                TreeOp::RemoveNode { node, placement } => {
259
                    if !ok_node(node) || !ok_change(&placement.change) {
260
                        return Err(too_new("a removed file"));
261
                    }
262
                }
263
                TreeOp::Move { node, old, parent, .. } => {
264
                    if !ok_node(node) || !ok_change(&old.change) || !ok_node(parent) {
265
                        return Err(too_new("a moved file"));
266
                    }
267
                }
268
                TreeOp::SetMode { node, old, .. } => {
269
                    if !ok_node(node) || !ok_change(&old.change) {
270
                        return Err(too_new("a file whose mode changed"));
271
                    }
272
                }
273
            }
274
        }
275
        let wc_added: BTreeSet<NodeId> = wc.tree_ops.iter().filter_map(|o| if let TreeOp::AddNode { node, .. } = o { Some(*node) } else { None }).collect();
276
        for e in &wc.edits {
277
            let n = e.node();
278
            if !wc_added.contains(&n) && !ok_node(&n) {
279
                return Err(too_new("an edited file"));
280
            }
281
            let refs: Vec<ChangeHash> = match e {
282
                Edit::Insert { after, before, .. } => [after, before].iter().filter_map(|r| if let AtomRef::Atom { change, .. } = r { Some(*change) } else { None }).collect(),
283
                Edit::Delete { atoms, .. } => atoms.iter().map(|r| r.change).collect(),
284
                Edit::Replace { .. } => vec![],
285
            };
286
            if refs.iter().any(|h| !h.is_zero() && !ok_change(h)) {
287
                return Err(too_new("an edited line"));
288
            }
289
        }
290
291
        // Compose: the target's operations first, then the working copy's
292
        // with references to the target turned into self references and its
293
        // own atom indices shifted past the target's.
294
        let mut per_node: HashMap<NodeId, u32> = HashMap::new();
295
        for e in &old.edits {
296
            if let Edit::Insert { node, lines, .. } = e {
297
                *per_node.entry(*node).or_default() += lines.len() as u32;
298
            }
299
        }
300
        let op_offset = old.tree_ops.len() as u32;
301
        let fold_op = |r: OpRef| -> OpRef {
302
            if r.change == *target {
303
                OpRef { change: ObjectRef::ZERO, index: r.index }
304
            } else if r.change.is_zero() {
305
                OpRef { change: ObjectRef::ZERO, index: r.index + op_offset }
306
            } else {
307
                r
308
            }
309
        };
310
        let mut tree_ops = old.tree_ops.clone();
311
        for op in &wc.tree_ops {
312
            tree_ops.push(match op {
313
                TreeOp::RemoveNode { node, placement } => TreeOp::RemoveNode { node: *node, placement: fold_op(*placement) },
314
                TreeOp::Move { node, old, parent, name } => TreeOp::Move { node: *node, old: fold_op(*old), parent: *parent, name: name.clone() },
315
                TreeOp::SetMode { node, old, mode } => TreeOp::SetMode { node: *node, old: fold_op(*old), mode: *mode },
316
                add => add.clone(),
317
            });
318
        }
319
        let mut edits = old.edits.clone();
320
        for e in &wc.edits {
321
            edits.push(match e {
322
                Edit::Insert { node, after, before, lines } => Edit::Insert {
323
                    node: *node,
324
                    after: fold_atom(*after, target, &per_node, node).ok_or_else(|| too_new("an anchor line"))?,
325
                    before: fold_atom(*before, target, &per_node, node).ok_or_else(|| too_new("an anchor line"))?,
326
                    lines: lines.clone(),
327
                },
328
                Edit::Delete { node, atoms } => Edit::Delete {
329
                    node: *node,
330
                    atoms: atoms
331
                        .iter()
332
                        .map(|r| {
333
                            if r.change == *target {
334
                                AtomRange { change: ObjectRef::ZERO, from: r.from, to: r.to }
335
                            } else if r.change.is_zero() {
336
                                let off = per_node.get(node).copied().unwrap_or(0);
337
                                AtomRange { change: ObjectRef::ZERO, from: r.from + off, to: r.to + off }
338
                            } else {
339
                                *r
340
                            }
341
                        })
342
                        .collect(),
343
                },
344
                other => other.clone(),
345
            });
346
        }
347
        let mut new = Change {
348
            change_id: old.change_id,
349
            deps: vec![],
350
            author: self.author(),
351
            hlc: Hlc::now(),
352
            message: message.map(|m| m.trim_end().to_string()).unwrap_or(old.message.clone()),
353
            tree_ops,
354
            edits,
355
            kind: old.kind,
356
        };
357
        let beneath = self.load_state(&old.deps)?;
358
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
359
        deps.extend(referenced_changes(&new, &beneath).into_iter().filter(|h| *h != *target));
360
        deps.extend(wc.deps.iter().filter(|d| closure.contains(d) && **d != *target).copied());
361
        new.deps = deps.into_iter().collect();
362
        new.normalize();
363
364
        // Rewrite dependents (identity correspondence), then start a fresh,
365
        // empty working copy: its edits now live in the amended change.
366
        let corr = Correspondence::identity(&old);
367
        let new_hash = self.rewrite_change(target, Some(&new), &corr, &format!("oxy amend --into {}", old.change_id.short(8)))?.unwrap();
368
        let (op_ref, op) = self.head_op()?;
369
        let heads = self.heads_of(&op.view, &self.config.bookmark)?;
370
        let fresh = self.new_working_copy(heads, "");
371
        let wc_hash = self.store.put_change(&fresh)?;
372
        let mut view = op.view.clone();
373
        view.workspaces.insert(self.workspace, wc_hash);
374
        self.record_op(&[op_ref], "oxy amend (working copy)", view)?;
375
        self.drop_manifest();
376
        Ok(new_hash)
377
    }
378
379
    /// Split a head into two changes: one holding the head's effect on
380
    /// `paths`, the other the rest (keeping the head's ChangeId). The
381
    /// working copy is unaffected.
382
    pub fn split(&self, hash: &ChangeHash, paths: &[String]) -> Result<(ChangeHash, ChangeHash)> {
383
        let old = self.rewritable_head(hash)?;
384
        if paths.is_empty() {
385
            return fail("name at least one path to split out");
386
        }
387
        let beneath = self.load_state(&old.deps)?;
388
        let beneath_snapshot = beneath.materialize();
389
        let full = self.load_state(&[*hash])?;
390
        let full_snapshot = full.materialize();
391
        let selected = |p: &str| paths.iter().any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/'))));
392
        // Entries of the intermediate state: selected paths as the head
393
        // leaves them, everything else as beneath.
394
        let entries_of = |snap: &Snapshot, keep_from_full: &dyn Fn(&str) -> bool| -> Result<BTreeMap<String, FsEntry>> {
395
            let mut out = BTreeMap::new();
396
            for (p, _) in snap.files.iter().chain(full_snapshot.files.iter()) {
397
                let take_full = keep_from_full(p);
398
                let src = if take_full { full_snapshot.files.get(p) } else { snap.files.get(p) };
399
                let Some(src) = src else { continue };
400
                if out.contains_key(p) {
401
                    continue;
402
                }
403
                let content = match &src.content {
404
                    FileContent::Dir => continue,
405
                    FileContent::Text { bytes, .. } => bytes.clone(),
406
                    FileContent::Opaque { blobs } => match blobs.first() {
407
                        Some(b) => self.read_blob(b)?,
408
                        None => continue,
409
                    },
410
                };
411
                out.insert(p.clone(), FsEntry { kind: src.kind, exec: src.mode.exec, content });
412
            }
413
            Ok(out)
414
        };
415
        let mid_entries = entries_of(&beneath_snapshot, &|p| selected(p))?;
416
        let first = self.build_from_entries(&beneath, &beneath_snapshot, HashMap::new(), &mid_entries)?;
417
        if first.tree_ops.is_empty() && first.edits.is_empty() {
418
            return fail("the named paths are not touched by that change");
419
        }
420
        let mut c1 = Change {
421
            change_id: ChangeId::random(),
422
            deps: old.deps.clone(),
423
            author: old.author,
424
            hlc: Hlc::now(),
425
            message: format!("{} (part 1: {})", old.message.lines().next().unwrap_or(""), paths.join(", ")),
426
            tree_ops: first.tree_ops,
427
            edits: first.edits,
428
            kind: ChangeKind::Normal,
429
        };
430
        let mut d1: BTreeSet<ChangeHash> = referenced_changes(&c1, &beneath);
431
        d1.extend(old.deps.iter().copied());
432
        c1.deps = d1.into_iter().collect();
433
        c1.normalize();
434
        let h1 = self.store_signed(&c1)?;
435
        let mut mid = beneath.clone();
436
        mid.add(c1)?;
437
        let mid_snapshot = mid.materialize();
438
        let rest_entries = entries_of(&full_snapshot, &|_| true)?;
439
        let rest = self.build_from_entries(&mid, &mid_snapshot, HashMap::new(), &rest_entries)?;
440
        if rest.tree_ops.is_empty() && rest.edits.is_empty() {
441
            return fail("everything in that change is under the named paths; nothing would remain");
442
        }
443
        let mut c2 = Change {
444
            change_id: old.change_id,
445
            deps: vec![],
446
            author: old.author,
447
            hlc: Hlc::now(),
448
            message: old.message.clone(),
449
            tree_ops: rest.tree_ops,
450
            edits: rest.edits,
451
            kind: ChangeKind::Normal,
452
        };
453
        let mut d2: BTreeSet<ChangeHash> = referenced_changes(&c2, &mid);
454
        d2.extend(old.deps.iter().copied());
455
        c2.deps = d2.into_iter().collect();
456
        c2.normalize();
457
        let h2 = self.replace_head(hash, Some(&c2), &format!("oxy split {} {}", old.change_id.short(8), paths.join(" ")))?.unwrap();
458
        // c1 may be a head of its own if c2 does not depend on it.
459
        let (op_ref, op) = self.head_op()?;
460
        let bookmark = self.config.bookmark.clone();
461
        let mut heads = self.heads_of(&op.view, &bookmark)?;
462
        heads.push(h1);
463
        let st = self.load_state(&heads)?;
464
        let heads = st.maximal(&heads);
465
        let mut view = op.view.clone();
466
        view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(heads.clone()))?);
467
        let (_, wc) = self.working_copy()?;
468
        let mut base = wc.deps.clone();
469
        base.push(h1);
470
        let wc_base = self.load_state(&base)?.maximal(&base);
471
        let fresh = self.new_working_copy(wc_base, &wc.message);
472
        view.workspaces.insert(self.workspace, self.store.put_change(&fresh)?);
473
        self.record_op(&[op_ref], "oxy split (heads)", view)?;
474
        self.drop_manifest();
475
        Ok((h1, h2))
476
    }
477
}