OXYGENHUB
# 51d4862a16d8 oxygen / crates / oxy-core / src / repo / ops.rs
10.7 KiB · Rust · last changed undo: this workspace's last command, saying what it undid 25 days ago source blame history raw
1
//! Working with the operation log (spec §12.1): naming an operation,
2
//! reading the repository as it was after one (`--at-op`), going back to
3
//! one (`op restore`), undoing one alone (`op revert`), and saying what an
4
//! operation changed (`op log`).
5
6
use super::*;
7
8
/// One reference change between two views.
9
#[derive(Clone, Debug, PartialEq, Eq)]
10
pub enum ViewChange {
11
    BookmarkCreated {
12
        name: String,
13
        heads: usize,
14
    },
15
    BookmarkDeleted {
16
        name: String,
17
    },
18
    BookmarkMoved {
19
        name: String,
20
        from: usize,
21
        to: usize,
22
    },
23
    RemoteMoved {
24
        remote: String,
25
        name: String,
26
    },
27
    TagCreated {
28
        name: String,
29
    },
30
    TagDeleted {
31
        name: String,
32
    },
33
    Policy,
34
    /// This workspace's working copy is now `change`.
35
    WorkingCopy {
36
        change: ChangeHash,
37
    },
38
}
39
40
/// What `undo` did (oxygen#99).
41
#[derive(Debug)]
42
pub struct Undone {
43
    /// The command undone.
44
    pub command: String,
45
    /// What that command had changed, now put back.
46
    pub changes: Vec<ViewChange>,
47
    /// Newer operations from other workspaces, left as they are.
48
    pub others_kept: usize,
49
    /// `(bookmark, remote)`: changes the command added to a bookmark that
50
    /// the remote already holds; undoing here does not take them back there.
51
    pub pushed: Vec<(String, String)>,
52
}
53
54
/// What `op restore` did.
55
#[derive(Debug)]
56
pub struct RestoreReport {
57
    /// The operation restored to.
58
    pub op: ObjectRef,
59
    pub command: String,
60
    pub checkout: CheckoutReport,
61
}
62
63
/// What `op revert` did.
64
#[derive(Debug)]
65
pub struct RevertReport {
66
    pub op: ObjectRef,
67
    pub command: String,
68
    /// Bookmarks put back as they were before the operation.
69
    pub reverted: Vec<String>,
70
    /// Bookmarks the operation moved that have moved again since: left as
71
    /// they are.
72
    pub kept: Vec<String>,
73
    /// Whether the working copy followed the current bookmark.
74
    pub checked_out: bool,
75
}
76
77
impl Repo {
78
    /// This repository read as it was after operation `spec` (`--at-op`).
79
    /// Commands that would record an operation are refused.
80
    pub fn at_op(mut self, spec: &str) -> Result<Repo> {
81
        let (r, _) = self.resolve_op(spec)?;
82
        self.at_op = Some(r);
83
        Ok(self)
84
    }
85
86
    /// Whether this repository is read at an earlier operation (`--at-op`).
87
    pub fn is_at_op(&self) -> bool {
88
        self.at_op.is_some()
89
    }
90
91
    /// The operation `spec` names: `@` for the current one, `@-` for the
92
    /// one before it, or a prefix of an operation ID from `oxy op log`.
93
    pub fn resolve_op(&self, spec: &str) -> Result<(ObjectRef, Op)> {
94
        let spec = spec.trim();
95
        let log = self.op_log()?;
96
        if let Some(rest) = spec.strip_prefix('@') {
97
            if rest.chars().all(|c| c == '-') {
98
                return log
99
                    .into_iter()
100
                    .nth(rest.len())
101
                    .ok_or_else(|| RepoError::Msg(format!("the operation log has no {spec}")));
102
            }
103
        }
104
        if spec.is_empty() || !spec.chars().all(|c| c.is_ascii_hexdigit()) {
105
            return fail(format!(
106
                "{spec:?} is not an operation: give an ID from oxy op log, @ or @-"
107
            ));
108
        }
109
        let spec = spec.to_ascii_lowercase();
110
        let mut found = log
111
            .into_iter()
112
            .filter(|(r, _)| r.to_hex().starts_with(&spec));
113
        match (found.next(), found.next()) {
114
            (Some(one), None) => Ok(one),
115
            (None, _) => fail(format!("no operation {spec} in oxy op log")),
116
            (Some(_), Some(_)) => fail(format!(
117
                "operation {spec} is ambiguous: give more of its ID"
118
            )),
119
        }
120
    }
121
122
    /// What changed from `before` to `after`, for this workspace's working
123
    /// copy and every reference.
124
    pub fn view_changes(&self, before: &View, after: &View) -> Result<Vec<ViewChange>> {
125
        let mut out = Vec::new();
126
        let heads = |st: &ObjectRef| -> Result<usize> { Ok(self.store.get_state(st)?.heads.len()) };
127
        let names: BTreeSet<&String> = before
128
            .bookmarks
129
            .keys()
130
            .chain(after.bookmarks.keys())
131
            .collect();
132
        for name in names {
133
            match (before.bookmarks.get(name), after.bookmarks.get(name)) {
134
                (None, Some(a)) => out.push(ViewChange::BookmarkCreated {
135
                    name: name.clone(),
136
                    heads: heads(a)?,
137
                }),
138
                (Some(_), None) => out.push(ViewChange::BookmarkDeleted { name: name.clone() }),
139
                (Some(b), Some(a)) if a != b => out.push(ViewChange::BookmarkMoved {
140
                    name: name.clone(),
141
                    from: heads(b)?,
142
                    to: heads(a)?,
143
                }),
144
                _ => {}
145
            }
146
        }
147
        for (remote, bookmarks) in &after.remotes {
148
            for (name, st) in bookmarks {
149
                if before.remotes.get(remote).and_then(|b| b.get(name)) != Some(st) {
150
                    out.push(ViewChange::RemoteMoved {
151
                        remote: remote.clone(),
152
                        name: name.clone(),
153
                    });
154
                }
155
            }
156
        }
157
        for name in after.tags.keys().filter(|t| !before.tags.contains_key(*t)) {
158
            out.push(ViewChange::TagCreated { name: name.clone() });
159
        }
160
        for name in before.tags.keys().filter(|t| !after.tags.contains_key(*t)) {
161
            out.push(ViewChange::TagDeleted { name: name.clone() });
162
        }
163
        if before.policy != after.policy {
164
            out.push(ViewChange::Policy);
165
        }
166
        if let Some(w) = after.workspaces.get(&self.workspace) {
167
            if before.workspaces.get(&self.workspace) != Some(w) {
168
                out.push(ViewChange::WorkingCopy { change: *w });
169
            }
170
        }
171
        Ok(out)
172
    }
173
174
    /// Go back to how the bookmarks and this working copy were after
175
    /// operation `spec`, recorded as a new operation, so it can be undone
176
    /// in turn. The working copy is checked out at the base it had then;
177
    /// uncommitted work stops it unless `discard`. Tags and the trust
178
    /// policy are left as they are: neither goes backwards.
179
    pub fn op_restore(&self, spec: &str, discard: bool) -> Result<RestoreReport> {
180
        let _wc = self.lock_working_copy()?;
181
        let (target_ref, target) = self.resolve_op(spec)?;
182
        let command = format!("oxy op restore {}", target_ref.short(12));
183
        // Refuse before anything moves, as the checkout below would.
184
        if !discard && !self.clean_by_manifest()? {
185
            let snap = self.snapshot_status(&command)?;
186
            if !snap.statuses.is_empty() {
187
                return fail(format!(
188
                    "the working copy has uncommitted changes ({}): commit them, or drop them with --discard",
189
                    plural_paths(snap.statuses.len())
190
                ));
191
            }
192
        }
193
        let (head_ref, head) = self.head_op()?;
194
        let mut view = head.view.clone();
195
        view.bookmarks = target.view.bookmarks.clone();
196
        self.record_op(&[head_ref], &command, view)?;
197
        let base = match target.view.workspaces.get(&self.workspace) {
198
            Some(w) => {
199
                let wc = self.store.get_change(w)?;
200
                self.load_state_history(&wc.deps)?.maximal(&wc.deps)
201
            }
202
            None => self.bookmark_heads()?,
203
        };
204
        let checkout = self.checkout(&base, discard, &format!("{command} (checkout)"))?;
205
        Ok(RestoreReport {
206
            op: target_ref,
207
            command: target.command,
208
            checkout,
209
        })
210
    }
211
212
    /// Undo operation `spec` alone, keeping everything after it: each
213
    /// bookmark it moved goes back to where it was before, unless it has
214
    /// moved again since. The working copy follows the current bookmark
215
    /// when it was clean and on its heads, as after a pull.
216
    pub fn op_revert(&self, spec: &str) -> Result<RevertReport> {
217
        let _wc = self.lock_working_copy()?;
218
        let (target_ref, target) = self.resolve_op(spec)?;
219
        if target.is_snapshot() {
220
            return fail(
221
                "that operation only recorded the working copy: it moved no bookmark to revert",
222
            );
223
        }
224
        let Some(parent) = target.parents.first() else {
225
            return fail("that is the first operation: there is nothing before it");
226
        };
227
        let before = Op::decode(&self.store.get_typed(parent, ObjectType::Op)?)?.view;
228
        let (head_ref, head) = self.head_op()?;
229
        let mut view = head.view.clone();
230
        let (mut reverted, mut kept) = (Vec::new(), Vec::new());
231
        let names: BTreeSet<&String> = before
232
            .bookmarks
233
            .keys()
234
            .chain(target.view.bookmarks.keys())
235
            .collect();
236
        for name in names {
237
            let (was, became) = (before.bookmarks.get(name), target.view.bookmarks.get(name));
238
            if was == became {
239
                continue;
240
            }
241
            if head.view.bookmarks.get(name) != became {
242
                kept.push(name.clone());
243
                continue;
244
            }
245
            match was {
246
                Some(st) => view.bookmarks.insert(name.clone(), *st),
247
                None => view.bookmarks.remove(name),
248
            };
249
            reverted.push(name.clone());
250
        }
251
        if reverted.is_empty() {
252
            return fail(if kept.is_empty() {
253
                "that operation moved no bookmark: nothing to revert".to_string()
254
            } else {
255
                format!(
256
                    "every bookmark that operation moved has moved again since ({}); oxy op restore goes back to an operation instead",
257
                    kept.join(", ")
258
                )
259
            });
260
        }
261
        let command = format!("oxy op revert {}", target_ref.short(12));
262
        let current = self.config.bookmark.clone();
263
        let mut old = self.heads_of(&head.view, &current)?;
264
        old.sort();
265
        self.record_op(&[head_ref], &command, view.clone())?;
266
        let mut checked_out = false;
267
        if reverted.contains(&current) {
268
            let snap = self.snapshot(&command)?;
269
            let clean = snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty();
270
            let mut base = snap.base.maximal(&snap.wc.deps);
271
            base.sort();
272
            if clean && base == old {
273
                let new = self.heads_of(&view, &current)?;
274
                self.checkout(&new, false, &format!("{command} (checkout)"))?;
275
                checked_out = true;
276
            }
277
        }
278
        Ok(RevertReport {
279
            op: target_ref,
280
            command: target.command,
281
            reverted,
282
            kept,
283
            checked_out,
284
        })
285
    }
286
}
287
288
fn plural_paths(n: usize) -> String {
289
    format!("{n} path{}", if n == 1 { "" } else { "s" })
290
}