OXYGENHUB
# 252e8cb23fbc oxygen / crates / oxy-core / src / repo / history.rs
9.6 KiB · Rust · last changed Checkpoints: serialized states per bookmark, automatic every core.checkpoint-interval changes, oxy checkpoint, checkpoint-accelerated load_state; states keep change metadata only 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::rewrite::{propagate, Correspondence};
12
13
impl Repo {
14
    /// The bookmark head `hash`, or an error explaining why it cannot be rewritten.
15
    fn rewritable_head(&self, hash: &ChangeHash) -> Result<Change> {
16
        let heads = self.bookmark_heads()?;
17
        if !heads.contains(hash) {
18
            return fail(format!(
19
                "change #{} is not a head of {}; rewriting changes that others depend on is not implemented yet",
20
                hash.short(12),
21
                self.config.bookmark
22
            ));
23
        }
24
        let c = self.store.get_change(hash)?;
25
        if c.kind == ChangeKind::WorkingCopy {
26
            return fail("that is the working copy; commit it instead");
27
        }
28
        Ok(c)
29
    }
30
31
    /// Replace head `old` with `new` (or with nothing) on the bookmark and
32
    /// restart the working copy on the resulting base. Files on disk are not
33
    /// touched: the next snapshot diffs them against the new base.
34
    fn replace_head(&self, old: &ChangeHash, new: Option<&Change>, command: &str) -> Result<Option<ChangeHash>> {
35
        let new_hash = match new {
36
            Some(c) => Some(self.store_signed(c)?),
37
            None => None,
38
        };
39
        let old_change = self.store.get_change(old)?;
40
        let (op_ref, op) = self.head_op()?;
41
        let bookmark = self.config.bookmark.clone();
42
        let heads = self.heads_of(&op.view, &bookmark)?;
43
        let replace = |set: &[ChangeHash]| -> Vec<ChangeHash> {
44
            let mut out: Vec<ChangeHash> = set.iter().filter(|h| *h != old).copied().collect();
45
            match new_hash {
46
                Some(n) => out.push(n),
47
                None => out.extend(old_change.deps.iter().copied()),
48
            }
49
            out
50
        };
51
        let candidates = replace(&heads);
52
        let st = self.load_state(&candidates)?;
53
        let new_heads = st.maximal(&candidates);
54
55
        let (_, wc) = self.working_copy()?;
56
        let base = self.load_state(&wc.deps)?;
57
        let wc_base = base.maximal(&wc.deps);
58
        let wc_base = if wc_base.contains(old) {
59
            let c = replace(&wc_base);
60
            let st = self.load_state(&c)?;
61
            st.maximal(&c)
62
        } else {
63
            wc_base
64
        };
65
        let fresh = self.new_working_copy(wc_base, &wc.message);
66
        let wc_hash = self.store.put_change(&fresh)?;
67
68
        let mut view = op.view.clone();
69
        view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?);
70
        view.workspaces.insert(self.workspace, wc_hash);
71
        self.record_op(&[op_ref], command, view)?;
72
        Ok(new_hash)
73
    }
74
75
    /// Replace `old` by `new` (or remove it) anywhere in the bookmark's state,
76
    /// rewriting every dependent, including the working copy. Returns the
77
    /// new hash of `old`, if any.
78
    fn rewrite_change(&self, old: &ChangeHash, new: Option<&Change>, corr: &Correspondence, command: &str) -> Result<Option<ChangeHash>> {
79
        let (op_ref, op) = self.head_op()?;
80
        let bookmark = self.config.bookmark.clone();
81
        let heads = self.heads_of(&op.view, &bookmark)?;
82
        let (wc_hash, _) = self.working_copy()?;
83
        let mut all = heads.clone();
84
        all.push(wc_hash);
85
        let state = self.load_state(&all)?;
86
        if !state.closure(&wc_hash).contains(old) && !heads.contains(old) && !state.heads().contains(old) && !state.contains(old) {
87
            return fail(format!("change #{} is not in {} or under the working copy", old.short(12), bookmark));
88
        }
89
        let old_change = self.store.get_change(old)?;
90
        if old_change.kind == ChangeKind::WorkingCopy {
91
            return fail("that is the working copy; commit it instead");
92
        }
93
        let load = |h: &ChangeHash| self.store.get_change(h).ok();
94
        let rewritten = propagate(&state, *old, new, corr, &load).map_err(|e| RepoError::Msg(format!("{e}; rewriting through such references is not implemented yet")))?;
95
96
        let mut mapping: HashMap<ChangeHash, ChangeHash> = HashMap::new();
97
        let new_hash = match new {
98
            Some(c) => {
99
                let h = self.store_signed(c)?;
100
                mapping.insert(*old, h);
101
                Some(h)
102
            }
103
            None => None,
104
        };
105
        for (h, c) in &rewritten {
106
            let nh = if c.kind == ChangeKind::WorkingCopy { self.store.put_change(c)? } else { self.store_signed(c)? };
107
            mapping.insert(*h, nh);
108
        }
109
        let remap = |set: &[ChangeHash]| -> Vec<ChangeHash> {
110
            let mut out = Vec::new();
111
            for h in set {
112
                if h == old && new_hash.is_none() {
113
                    out.extend(old_change.deps.iter().copied());
114
                } else {
115
                    out.push(*mapping.get(h).unwrap_or(h));
116
                }
117
            }
118
            out
119
        };
120
        let candidates = remap(&heads);
121
        let st = self.load_state(&candidates)?;
122
        let new_heads = st.maximal(&candidates);
123
        let new_wc = mapping.get(&wc_hash).copied().unwrap_or(wc_hash);
124
125
        let mut view = op.view.clone();
126
        view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?);
127
        view.workspaces.insert(self.workspace, new_wc);
128
        self.record_op(&[op_ref], command, view)?;
129
        Ok(new_hash)
130
    }
131
132
    /// Change the message of any change of the bookmark; dependents are rewritten.
133
    pub fn describe(&self, hash: &ChangeHash, message: &str) -> Result<ChangeHash> {
134
        if message.trim().is_empty() {
135
            return fail("a message is required");
136
        }
137
        let mut c = self.store.get_change(hash)?;
138
        if c.kind == ChangeKind::WorkingCopy {
139
            return fail("that is the working copy; commit it instead");
140
        }
141
        if c.message == message.trim_end() {
142
            return Ok(*hash);
143
        }
144
        let corr = Correspondence::identity(&c);
145
        c.message = message.trim_end().to_string();
146
        c.hlc = Hlc::now();
147
        let id = c.change_id;
148
        Ok(self.rewrite_change(hash, Some(&c), &corr, &format!("oxy describe {} -m {message:?}", id.short(8)))?.unwrap())
149
    }
150
151
    /// Fold the working copy into the head it sits on: the head is rebuilt
152
    /// as the difference between the state beneath it and the files on disk.
153
    pub fn amend(&self, message: Option<&str>) -> Result<ChangeHash> {
154
        let snap = self.snapshot("oxy amend")?;
155
        if snap.statuses.iter().any(|s| s.status == ChangeStatus::ConflictEdited) {
156
            return fail("a file still contains conflict markers");
157
        }
158
        let base_heads = snap.base.maximal(&snap.wc.deps);
159
        let [head] = base_heads.as_slice() else {
160
            return fail(format!("the working copy sits on {} heads; check out the single change to amend", base_heads.len()));
161
        };
162
        let head = *head;
163
        let old = self.rewritable_head(&head)?;
164
        if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() && message.is_none() {
165
            return fail("nothing to amend: the working copy is clean and no new message was given");
166
        }
167
        // Rebuild against the state beneath the head.
168
        let beneath = self.load_state(&old.deps)?;
169
        let beneath_snapshot = beneath.materialize();
170
        let reuse = added_node_paths(&old, &beneath_snapshot);
171
        let built = self.build_against(&beneath, &beneath_snapshot, reuse)?;
172
        if built.tree_ops.is_empty() && built.edits.is_empty() {
173
            return fail("amending would leave the change empty; use `oxy abandon` to drop it");
174
        }
175
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
176
        deps.extend(built.contributors.iter().copied());
177
        let mut new = Change {
178
            change_id: old.change_id,
179
            deps: vec![],
180
            author: self.author(),
181
            hlc: Hlc::now(),
182
            message: message.map(|m| m.trim_end().to_string()).unwrap_or(old.message.clone()),
183
            tree_ops: built.tree_ops,
184
            edits: built.edits,
185
            kind: if built.resolves || old.kind == ChangeKind::Resolution { ChangeKind::Resolution } else { ChangeKind::Normal },
186
        };
187
        deps.extend(referenced_changes(&new, &beneath));
188
        new.deps = deps.into_iter().collect();
189
        new.normalize();
190
        Ok(self.replace_head(&head, Some(&new), &format!("oxy amend {}", old.change_id.short(8)))?.unwrap())
191
    }
192
193
    /// Drop a change from the bookmark. Dependents that merely depend on it
194
    /// inherit its dependencies; one that references its atoms or placements
195
    /// cannot be rewritten yet. A dropped head's content stays on disk as
196
    /// uncommitted work if the working copy sat on it.
197
    pub fn abandon(&self, hash: &ChangeHash) -> Result<()> {
198
        let c = self.store.get_change(hash)?;
199
        if c.kind == ChangeKind::WorkingCopy {
200
            return fail("that is the working copy; commit it instead");
201
        }
202
        let heads = self.bookmark_heads()?;
203
        if heads.contains(hash) {
204
            self.replace_head(hash, None, &format!("oxy abandon {}", c.change_id.short(8)))?;
205
        } else {
206
            self.rewrite_change(hash, None, &Correspondence::default(), &format!("oxy abandon {}", c.change_id.short(8)))?;
207
        }
208
        Ok(())
209
    }
210
}