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