OXYGENHUB
# 4476cdbb0443 oxygen / crates / oxy-core / src / repo / ops.rs
11.1 KiB · Rust · last changed Snapshots: large new files left out and named; past working copies expire 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
        // A working copy past `core.snapshot-retention` may be gone: then
198
        // the bookmark as it was.
199
        let base = match target
200
            .view
201
            .workspaces
202
            .get(&self.workspace)
203
            .map(|w| self.store.get_change(w))
204
        {
205
            Some(Ok(wc)) => self.load_state_history(&wc.deps)?.maximal(&wc.deps),
206
            Some(Err(StoreError::NotFound(_))) | None => {
207
                match target.view.bookmarks.get(&self.config.bookmark) {
208
                    Some(st) => self.store.get_state(st)?.heads,
209
                    None => self.bookmark_heads()?,
210
                }
211
            }
212
            Some(Err(e)) => return Err(e.into()),
213
        };
214
        let checkout = self.checkout(&base, discard, &format!("{command} (checkout)"))?;
215
        Ok(RestoreReport {
216
            op: target_ref,
217
            command: target.command,
218
            checkout,
219
        })
220
    }
221
222
    /// Undo operation `spec` alone, keeping everything after it: each
223
    /// bookmark it moved goes back to where it was before, unless it has
224
    /// moved again since. The working copy follows the current bookmark
225
    /// when it was clean and on its heads, as after a pull.
226
    pub fn op_revert(&self, spec: &str) -> Result<RevertReport> {
227
        let _wc = self.lock_working_copy()?;
228
        let (target_ref, target) = self.resolve_op(spec)?;
229
        if target.is_snapshot() {
230
            return fail(
231
                "that operation only recorded the working copy: it moved no bookmark to revert",
232
            );
233
        }
234
        let Some(parent) = target.parents.first() else {
235
            return fail("that is the first operation: there is nothing before it");
236
        };
237
        let before = Op::decode(&self.store.get_typed(parent, ObjectType::Op)?)?.view;
238
        let (head_ref, head) = self.head_op()?;
239
        let mut view = head.view.clone();
240
        let (mut reverted, mut kept) = (Vec::new(), Vec::new());
241
        let names: BTreeSet<&String> = before
242
            .bookmarks
243
            .keys()
244
            .chain(target.view.bookmarks.keys())
245
            .collect();
246
        for name in names {
247
            let (was, became) = (before.bookmarks.get(name), target.view.bookmarks.get(name));
248
            if was == became {
249
                continue;
250
            }
251
            if head.view.bookmarks.get(name) != became {
252
                kept.push(name.clone());
253
                continue;
254
            }
255
            match was {
256
                Some(st) => view.bookmarks.insert(name.clone(), *st),
257
                None => view.bookmarks.remove(name),
258
            };
259
            reverted.push(name.clone());
260
        }
261
        if reverted.is_empty() {
262
            return fail(if kept.is_empty() {
263
                "that operation moved no bookmark: nothing to revert".to_string()
264
            } else {
265
                format!(
266
                    "every bookmark that operation moved has moved again since ({}); oxy op restore goes back to an operation instead",
267
                    kept.join(", ")
268
                )
269
            });
270
        }
271
        let command = format!("oxy op revert {}", target_ref.short(12));
272
        let current = self.config.bookmark.clone();
273
        let mut old = self.heads_of(&head.view, &current)?;
274
        old.sort();
275
        self.record_op(&[head_ref], &command, view.clone())?;
276
        let mut checked_out = false;
277
        if reverted.contains(&current) {
278
            let snap = self.snapshot(&command)?;
279
            let clean = snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty();
280
            let mut base = snap.base.maximal(&snap.wc.deps);
281
            base.sort();
282
            if clean && base == old {
283
                let new = self.heads_of(&view, &current)?;
284
                self.checkout(&new, false, &format!("{command} (checkout)"))?;
285
                checked_out = true;
286
            }
287
        }
288
        Ok(RevertReport {
289
            op: target_ref,
290
            command: target.command,
291
            reverted,
292
            kept,
293
            checked_out,
294
        })
295
    }
296
}
297
298
fn plural_paths(n: usize) -> String {
299
    format!("{n} path{}", if n == 1 { "" } else { "s" })
300
}