OXYGENHUB

Coverage / crates/oxy-core/src/repo/ops.rs 76.5%

137 of 179 lines run, 14 of 32 functions · run #377 on main · run missed

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.
801×
    pub fn at_op(mut self, spec: &str) -> Result<Repo> {
811×
        let (r, _) = self.resolve_op(spec)?;
821×
        self.at_op = Some(r);
831×
        Ok(self)
841×
    }
85
86
    /// Whether this repository is read at an earlier operation (`--at-op`).
878×
    pub fn is_at_op(&self) -> bool {
888×
        self.at_op.is_some()
898×
    }
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`.
936×
    pub fn resolve_op(&self, spec: &str) -> Result<(ObjectRef, Op)> {
946×
        let spec = spec.trim();
956×
        let log = self.op_log()?;
966×
        if let Some(rest) = spec.strip_prefix('@') {
971×
            if rest.chars().all(|c| c == '-') {
981×
                return log
991×
                    .into_iter()
1001×
                    .nth(rest.len())
1011×
                    .ok_or_else(|| RepoError::Msg(format!("the operation log has no {spec}")));
1020×
            }
1035×
        }
104203×
        if spec.is_empty() || !spec.chars().all(|c| c.is_ascii_hexdigit()) {
1051×
            return fail(format!(
106
                "{spec:?} is not an operation: give an ID from oxy op log, @ or @-"
107
            ));
1084×
        }
1094×
        let spec = spec.to_ascii_lowercase();
1104×
        let mut found = log
1114×
            .into_iter()
11225×
            .filter(|(r, _)| r.to_hex().starts_with(&spec));
1134×
        match (found.next(), found.next()) {
1144×
            (Some(one), None) => Ok(one),
1150×
            (None, _) => fail(format!("no operation {spec} in oxy op log")),
1160×
            (Some(_), Some(_)) => fail(format!(
117
                "operation {spec} is ambiguous: give more of its ID"
118
            )),
119
        }
1206×
    }
121
122
    /// What changed from `before` to `after`, for this workspace's working
123
    /// copy and every reference.
12426×
    pub fn view_changes(&self, before: &View, after: &View) -> Result<Vec<ViewChange>> {
12526×
        let mut out = Vec::new();
12634×
        let heads = |st: &ObjectRef| -> Result<usize> { Ok(self.store.get_state(st)?.heads.len()) };
12726×
        let names: BTreeSet<&String> = before
12826×
            .bookmarks
12926×
            .keys()
13026×
            .chain(after.bookmarks.keys())
13126×
            .collect();
13231×
        for name in names {
13331×
            match (before.bookmarks.get(name), after.bookmarks.get(name)) {
1340×
                (None, Some(a)) => out.push(ViewChange::BookmarkCreated {
1350×
                    name: name.clone(),
1360×
                    heads: heads(a)?,
137
                }),
1381×
                (Some(_), None) => out.push(ViewChange::BookmarkDeleted { name: name.clone() }),
13930×
                (Some(b), Some(a)) if a != b => out.push(ViewChange::BookmarkMoved {
14017×
                    name: name.clone(),
14117×
                    from: heads(b)?,
14217×
                    to: heads(a)?,
143
                }),
14413×
                _ => {}
145
            }
146
        }
14726×
        for (remote, bookmarks) in &after.remotes {
14811×
            for (name, st) in bookmarks {
14911×
                if before.remotes.get(remote).and_then(|b| b.get(name)) != Some(st) {
1501×
                    out.push(ViewChange::RemoteMoved {
1511×
                        remote: remote.clone(),
1521×
                        name: name.clone(),
1531×
                    });
15410×
                }
155
            }
156
        }
15726×
        for name in after.tags.keys().filter(|t| !before.tags.contains_key(*t)) {
1580×
            out.push(ViewChange::TagCreated { name: name.clone() });
1590×
        }
16026×
        for name in before.tags.keys().filter(|t| !after.tags.contains_key(*t)) {
1610×
            out.push(ViewChange::TagDeleted { name: name.clone() });
1620×
        }
16326×
        if before.policy != after.policy {
1640×
            out.push(ViewChange::Policy);
16526×
        }
16626×
        if let Some(w) = after.workspaces.get(&self.workspace) {
16726×
            if before.workspaces.get(&self.workspace) != Some(w) {
16823×
                out.push(ViewChange::WorkingCopy { change: *w });
16923×
            }
1700×
        }
17126×
        Ok(out)
17226×
    }
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.
1791×
    pub fn op_restore(&self, spec: &str, discard: bool) -> Result<RestoreReport> {
1801×
        let _wc = self.lock_working_copy()?;
1811×
        let (target_ref, target) = self.resolve_op(spec)?;
1821×
        let command = format!("oxy op restore {}", target_ref.short(12));
183
        // Refuse before anything moves, as the checkout below would.
1841×
        if !discard && !self.clean_by_manifest()? {
1850×
            let snap = self.snapshot_status(&command)?;
1860×
            if !snap.statuses.is_empty() {
1870×
                return fail(format!(
188
                    "the working copy has uncommitted changes ({}): commit them, or drop them with --discard",
1890×
                    plural_paths(snap.statuses.len())
190
                ));
1910×
            }
1921×
        }
1931×
        let (head_ref, head) = self.head_op()?;
1941×
        let mut view = head.view.clone();
1951×
        view.bookmarks = target.view.bookmarks.clone();
1961×
        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.
1991×
        let base = match target
2001×
            .view
2011×
            .workspaces
2021×
            .get(&self.workspace)
2031×
            .map(|w| self.store.get_change(w))
204
        {
2051×
            Some(Ok(wc)) => self.load_state_history(&wc.deps)?.maximal(&wc.deps),
206
            Some(Err(StoreError::NotFound(_))) | None => {
2070×
                match target.view.bookmarks.get(&self.config.bookmark) {
2080×
                    Some(st) => self.store.get_state(st)?.heads,
2090×
                    None => self.bookmark_heads()?,
210
                }
211
            }
2120×
            Some(Err(e)) => return Err(e.into()),
213
        };
2141×
        let checkout = self.checkout(&base, discard, &format!("{command} (checkout)"))?;
2151×
        Ok(RestoreReport {
2161×
            op: target_ref,
2171×
            command: target.command,
2181×
            checkout,
2191×
        })
2201×
    }
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.
2262×
    pub fn op_revert(&self, spec: &str) -> Result<RevertReport> {
2272×
        let _wc = self.lock_working_copy()?;
2282×
        let (target_ref, target) = self.resolve_op(spec)?;
2292×
        if target.is_snapshot() {
2300×
            return fail(
231
                "that operation only recorded the working copy: it moved no bookmark to revert",
232
            );
2332×
        }
2342×
        let Some(parent) = target.parents.first() else {
2350×
            return fail("that is the first operation: there is nothing before it");
236
        };
2372×
        let before = Op::decode(&self.store.get_typed(parent, ObjectType::Op)?)?.view;
2382×
        let (head_ref, head) = self.head_op()?;
2392×
        let mut view = head.view.clone();
2402×
        let (mut reverted, mut kept) = (Vec::new(), Vec::new());
2412×
        let names: BTreeSet<&String> = before
2422×
            .bookmarks
2432×
            .keys()
2442×
            .chain(target.view.bookmarks.keys())
2452×
            .collect();
2464×
        for name in names {
2474×
            let (was, became) = (before.bookmarks.get(name), target.view.bookmarks.get(name));
2484×
            if was == became {
2492×
                continue;
2502×
            }
2512×
            if head.view.bookmarks.get(name) != became {
2521×
                kept.push(name.clone());
2531×
                continue;
2541×
            }
2551×
            match was {
2560×
                Some(st) => view.bookmarks.insert(name.clone(), *st),
2571×
                None => view.bookmarks.remove(name),
258
            };
2591×
            reverted.push(name.clone());
260
        }
2612×
        if reverted.is_empty() {
2621×
            return fail(if kept.is_empty() {
2630×
                "that operation moved no bookmark: nothing to revert".to_string()
264
            } else {
2651×
                format!(
266
                    "every bookmark that operation moved has moved again since ({}); oxy op restore goes back to an operation instead",
2671×
                    kept.join(", ")
268
                )
269
            });
2701×
        }
2711×
        let command = format!("oxy op revert {}", target_ref.short(12));
2721×
        let current = self.config.bookmark.clone();
2731×
        let mut old = self.heads_of(&head.view, &current)?;
2741×
        old.sort();
2751×
        self.record_op(&[head_ref], &command, view.clone())?;
2761×
        let mut checked_out = false;
2771×
        if reverted.contains(&current) {
2780×
            let snap = self.snapshot(&command)?;
2790×
            let clean = snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty();
2800×
            let mut base = snap.base.maximal(&snap.wc.deps);
2810×
            base.sort();
2820×
            if clean && base == old {
2830×
                let new = self.heads_of(&view, &current)?;
2840×
                self.checkout(&new, false, &format!("{command} (checkout)"))?;
2850×
                checked_out = true;
2860×
            }
2871×
        }
2881×
        Ok(RevertReport {
2891×
            op: target_ref,
2901×
            command: target.command,
2911×
            reverted,
2921×
            kept,
2931×
            checked_out,
2941×
        })
2952×
    }
296
}
297
2980×
fn plural_paths(n: usize) -> String {
2990×
    format!("{n} path{}", if n == 1 { "" } else { "s" })
3000×
}