OXYGENHUB
# b2ef93bb2366 oxygen / crates / oxy-core / src / repo / history.rs
9.6 KiB · Rust · last changed Pack merging above 16 packs; modern CLI presentation (colour, spinners, --plain, NO_COLOR); oxy prompt with manifest-based dirty check and docs/prompt.md 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
        self.drop_manifest();
73
        Ok(new_hash)
74
    }
75
76
    /// Replace `old` by `new` (or remove it) anywhere in the bookmark's state,
77
    /// rewriting every dependent, including the working copy. Returns the
78
    /// new hash of `old`, if any.
79
    fn rewrite_change(&self, old: &ChangeHash, new: Option<&Change>, corr: &Correspondence, command: &str) -> Result<Option<ChangeHash>> {
80
        let (op_ref, op) = self.head_op()?;
81
        let bookmark = self.config.bookmark.clone();
82
        let heads = self.heads_of(&op.view, &bookmark)?;
83
        let (wc_hash, _) = self.working_copy()?;
84
        let mut all = heads.clone();
85
        all.push(wc_hash);
86
        let state = self.load_state(&all)?;
87
        if !state.closure(&wc_hash).contains(old) && !heads.contains(old) && !state.heads().contains(old) && !state.contains(old) {
88
            return fail(format!("change #{} is not in {} or under the working copy", old.short(12), bookmark));
89
        }
90
        let old_change = self.store.get_change(old)?;
91
        if old_change.kind == ChangeKind::WorkingCopy {
92
            return fail("that is the working copy; commit it instead");
93
        }
94
        let load = |h: &ChangeHash| self.store.get_change(h).ok();
95
        let rewritten = propagate(&state, *old, new, corr, &load).map_err(|e| RepoError::Msg(format!("{e}; rewriting through such references is not implemented yet")))?;
96
97
        let mut mapping: HashMap<ChangeHash, ChangeHash> = HashMap::new();
98
        let new_hash = match new {
99
            Some(c) => {
100
                let h = self.store_signed(c)?;
101
                mapping.insert(*old, h);
102
                Some(h)
103
            }
104
            None => None,
105
        };
106
        for (h, c) in &rewritten {
107
            let nh = if c.kind == ChangeKind::WorkingCopy { self.store.put_change(c)? } else { self.store_signed(c)? };
108
            mapping.insert(*h, nh);
109
        }
110
        let remap = |set: &[ChangeHash]| -> Vec<ChangeHash> {
111
            let mut out = Vec::new();
112
            for h in set {
113
                if h == old && new_hash.is_none() {
114
                    out.extend(old_change.deps.iter().copied());
115
                } else {
116
                    out.push(*mapping.get(h).unwrap_or(h));
117
                }
118
            }
119
            out
120
        };
121
        let candidates = remap(&heads);
122
        let st = self.load_state(&candidates)?;
123
        let new_heads = st.maximal(&candidates);
124
        let new_wc = mapping.get(&wc_hash).copied().unwrap_or(wc_hash);
125
126
        let mut view = op.view.clone();
127
        view.bookmarks.insert(bookmark, self.store.put_state(&StateObject::new(new_heads))?);
128
        view.workspaces.insert(self.workspace, new_wc);
129
        self.record_op(&[op_ref], command, view)?;
130
        self.drop_manifest();
131
        Ok(new_hash)
132
    }
133
134
    /// Change the message of any change of the bookmark; dependents are rewritten.
135
    pub fn describe(&self, hash: &ChangeHash, message: &str) -> Result<ChangeHash> {
136
        if message.trim().is_empty() {
137
            return fail("a message is required");
138
        }
139
        let mut c = self.store.get_change(hash)?;
140
        if c.kind == ChangeKind::WorkingCopy {
141
            return fail("that is the working copy; commit it instead");
142
        }
143
        if c.message == message.trim_end() {
144
            return Ok(*hash);
145
        }
146
        let corr = Correspondence::identity(&c);
147
        c.message = message.trim_end().to_string();
148
        c.hlc = Hlc::now();
149
        let id = c.change_id;
150
        Ok(self.rewrite_change(hash, Some(&c), &corr, &format!("oxy describe {} -m {message:?}", id.short(8)))?.unwrap())
151
    }
152
153
    /// Fold the working copy into the head it sits on: the head is rebuilt
154
    /// as the difference between the state beneath it and the files on disk.
155
    pub fn amend(&self, message: Option<&str>) -> Result<ChangeHash> {
156
        let snap = self.snapshot("oxy amend")?;
157
        if snap.statuses.iter().any(|s| s.status == ChangeStatus::ConflictEdited) {
158
            return fail("a file still contains conflict markers");
159
        }
160
        let base_heads = snap.base.maximal(&snap.wc.deps);
161
        let [head] = base_heads.as_slice() else {
162
            return fail(format!("the working copy sits on {} heads; check out the single change to amend", base_heads.len()));
163
        };
164
        let head = *head;
165
        let old = self.rewritable_head(&head)?;
166
        if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() && message.is_none() {
167
            return fail("nothing to amend: the working copy is clean and no new message was given");
168
        }
169
        // Rebuild against the state beneath the head.
170
        let beneath = self.load_state(&old.deps)?;
171
        let beneath_snapshot = beneath.materialize();
172
        let reuse = added_node_paths(&old, &beneath_snapshot);
173
        let built = self.build_against(&beneath, &beneath_snapshot, reuse)?;
174
        if built.tree_ops.is_empty() && built.edits.is_empty() {
175
            return fail("amending would leave the change empty; use `oxy abandon` to drop it");
176
        }
177
        let mut deps: BTreeSet<ChangeHash> = old.deps.iter().copied().collect();
178
        deps.extend(built.contributors.iter().copied());
179
        let mut new = Change {
180
            change_id: old.change_id,
181
            deps: vec![],
182
            author: self.author(),
183
            hlc: Hlc::now(),
184
            message: message.map(|m| m.trim_end().to_string()).unwrap_or(old.message.clone()),
185
            tree_ops: built.tree_ops,
186
            edits: built.edits,
187
            kind: if built.resolves || old.kind == ChangeKind::Resolution { ChangeKind::Resolution } else { ChangeKind::Normal },
188
        };
189
        deps.extend(referenced_changes(&new, &beneath));
190
        new.deps = deps.into_iter().collect();
191
        new.normalize();
192
        Ok(self.replace_head(&head, Some(&new), &format!("oxy amend {}", old.change_id.short(8)))?.unwrap())
193
    }
194
195
    /// Drop a change from the bookmark. Dependents that merely depend on it
196
    /// inherit its dependencies; one that references its atoms or placements
197
    /// cannot be rewritten yet. A dropped head's content stays on disk as
198
    /// uncommitted work if the working copy sat on it.
199
    pub fn abandon(&self, hash: &ChangeHash) -> Result<()> {
200
        let c = self.store.get_change(hash)?;
201
        if c.kind == ChangeKind::WorkingCopy {
202
            return fail("that is the working copy; commit it instead");
203
        }
204
        let heads = self.bookmark_heads()?;
205
        if heads.contains(hash) {
206
            self.replace_head(hash, None, &format!("oxy abandon {}", c.change_id.short(8)))?;
207
        } else {
208
            self.rewrite_change(hash, None, &Correspondence::default(), &format!("oxy abandon {}", c.change_id.short(8)))?;
209
        }
210
        Ok(())
211
    }
212
}