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