OXYGENHUB
# e8b4e156f2fa oxygen / crates / oxy-core / src / repo / history.rs
6.0 KiB · Rust · last changed History editing for heads: oxy describe, oxy amend, oxy abandon (spec §8.4, heads only) 1 month ago source blame history raw
1
//! History editing (spec §8.4): describe, amend, abandon.
2
//!
3
//! Only heads can be rewritten so far. A head has no dependents, so
4
//! rewriting it needs no propagation; rewriting a change with dependents
5
//! (translating their atom and placement references) is not implemented.
6
//! The rewritten change keeps its ChangeId and gets a new hash; the old
7
//! object stays in the store until garbage collection.
8
9
use super::*;
10
11
impl Repo {
12
    /// The bookmark head `hash`, or an error explaining why it cannot be rewritten.
13
    fn rewritable_head(&self, hash: &ChangeHash) -> Result<Change> {
14
        let heads = self.bookmark_heads()?;
15
        if !heads.contains(hash) {
16
            return fail(format!(
17
                "change #{} is not a head of {}; rewriting changes that others depend on is not implemented yet",
18
                hash.short(12),
19
                self.config.bookmark
20
            ));
21
        }
22
        let c = self.store.get_change(hash)?;
23
        if c.kind == ChangeKind::WorkingCopy {
24
            return fail("that is the working copy; commit it instead");
25
        }
26
        Ok(c)
27
    }
28
29
    /// Replace head `old` with `new` (or with nothing) on the bookmark and
30
    /// restart the working copy on the resulting base. Files on disk are not
31
    /// touched: the next snapshot diffs them against the new base.
32
    fn replace_head(&self, old: &ChangeHash, new: Option<&Change>, command: &str) -> Result<Option<ChangeHash>> {
33
        let new_hash = match new {
34
            Some(c) => Some(self.store_signed(c)?),
35
            None => None,
36
        };
37
        let old_change = self.store.get_change(old)?;
38
        let (op_ref, op) = self.head_op()?;
39
        let bookmark = self.config.bookmark.clone();
40
        let heads = self.heads_of(&op.view, &bookmark)?;
41
        let replace = |set: &[ChangeHash]| -> Vec<ChangeHash> {
42
            let mut out: Vec<ChangeHash> = set.iter().filter(|h| *h != old).copied().collect();
43
            match new_hash {
44
                Some(n) => out.push(n),
45
                None => out.extend(old_change.deps.iter().copied()),
46
            }
47
            out
48
        };
49
        let candidates = replace(&heads);
50
        let st = self.load_state(&candidates)?;
51
        let new_heads = st.maximal(&candidates);
52
53
        let (_, wc) = self.working_copy()?;
54
        let base = self.load_state(&wc.deps)?;
55
        let wc_base = base.maximal(&wc.deps);
56
        let wc_base = if wc_base.contains(old) {
57
            let c = replace(&wc_base);
58
            let st = self.load_state(&c)?;
59
            st.maximal(&c)
60
        } else {
61
            wc_base
62
        };
63
        let fresh = self.new_working_copy(wc_base, &wc.message);
64
        let wc_hash = self.store.put_change(&fresh)?;
65
66
        let mut view = op.view.clone();
67
        view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?);
68
        view.workspaces.insert(self.workspace, wc_hash);
69
        self.record_op(&[op_ref], command, view)?;
70
        Ok(new_hash)
71
    }
72
73
    /// Change the message of a head.
74
    pub fn describe(&self, hash: &ChangeHash, message: &str) -> Result<ChangeHash> {
75
        if message.trim().is_empty() {
76
            return fail("a message is required");
77
        }
78
        let mut c = self.rewritable_head(hash)?;
79
        if c.message == message.trim_end() {
80
            return Ok(*hash);
81
        }
82
        c.message = message.trim_end().to_string();
83
        c.hlc = Hlc::now();
84
        Ok(self.replace_head(hash, Some(&c), &format!("oxy describe {} -m {message:?}", c.change_id.short(8)))?.unwrap())
85
    }
86
87
    /// Fold the working copy into the head it sits on: the head is rebuilt
88
    /// as the difference between the state beneath it and the files on disk.
89
    pub fn amend(&self, message: Option<&str>) -> Result<ChangeHash> {
90
        let snap = self.snapshot("oxy amend")?;
91
        if snap.statuses.iter().any(|s| s.status == ChangeStatus::ConflictEdited) {
92
            return fail("a file still contains conflict markers");
93
        }
94
        let base_heads = snap.base.maximal(&snap.wc.deps);
95
        let [head] = base_heads.as_slice() else {
96
            return fail(format!("the working copy sits on {} heads; check out the single change to amend", base_heads.len()));
97
        };
98
        let head = *head;
99
        let old = self.rewritable_head(&head)?;
100
        if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() && message.is_none() {
101
            return fail("nothing to amend: the working copy is clean and no new message was given");
102
        }
103
        // Rebuild against the state beneath the head.
104
        let beneath = self.load_state(&old.deps)?;
105
        let beneath_snapshot = beneath.materialize();
106
        let reuse = added_node_paths(&old, &beneath_snapshot);
107
        let built = self.build_against(&beneath, &beneath_snapshot, reuse)?;
108
        if built.tree_ops.is_empty() && built.edits.is_empty() {
109
            return fail("amending would leave the change empty; use `oxy abandon` to drop it");
110
        }
111
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
112
        deps.extend(built.contributors.iter().copied());
113
        let mut new = Change {
114
            change_id: old.change_id,
115
            deps: vec![],
116
            author: self.author(),
117
            hlc: Hlc::now(),
118
            message: message.map(|m| m.trim_end().to_string()).unwrap_or(old.message.clone()),
119
            tree_ops: built.tree_ops,
120
            edits: built.edits,
121
            kind: if built.resolves || old.kind == ChangeKind::Resolution { ChangeKind::Resolution } else { ChangeKind::Normal },
122
        };
123
        deps.extend(referenced_changes(&new, &beneath));
124
        new.deps = deps.into_iter().collect();
125
        new.normalize();
126
        Ok(self.replace_head(&head, Some(&new), &format!("oxy amend {}", old.change_id.short(8)))?.unwrap())
127
    }
128
129
    /// Drop a head from the bookmark. Its content stays on disk as
130
    /// uncommitted work if the working copy sat on it.
131
    pub fn abandon(&self, hash: &ChangeHash) -> Result<()> {
132
        let c = self.rewritable_head(hash)?;
133
        self.replace_head(hash, None, &format!("oxy abandon {}", c.change_id.short(8)))?;
134
        Ok(())
135
    }
136
}