OXYGENHUB

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

134 of 173 lines run, 12 of 30 functions · run #259 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 `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.
661×
    pub fn at_op(mut self, spec: &str) -> Result<Repo> {
671×
        let (r, _) = self.resolve_op(spec)?;
681×
        self.at_op = Some(r);
691×
        Ok(self)
701×
    }
71
72
    /// Whether this repository is read at an earlier operation (`--at-op`).
738×
    pub fn is_at_op(&self) -> bool {
748×
        self.at_op.is_some()
758×
    }
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`.
796×
    pub fn resolve_op(&self, spec: &str) -> Result<(ObjectRef, Op)> {
806×
        let spec = spec.trim();
816×
        let log = self.op_log()?;
826×
        if let Some(rest) = spec.strip_prefix('@') {
831×
            if rest.chars().all(|c| c == '-') {
841×
                return log
851×
                    .into_iter()
861×
                    .nth(rest.len())
871×
                    .ok_or_else(|| RepoError::Msg(format!("the operation log has no {spec}")));
880×
            }
895×
        }
90203×
        if spec.is_empty() || !spec.chars().all(|c| c.is_ascii_hexdigit()) {
911×
            return fail(format!(
92
                "{spec:?} is not an operation: give an ID from oxy op log, @ or @-"
93
            ));
944×
        }
954×
        let spec = spec.to_ascii_lowercase();
964×
        let mut found = log
974×
            .into_iter()
9825×
            .filter(|(r, _)| r.to_hex().starts_with(&spec));
994×
        match (found.next(), found.next()) {
1004×
            (Some(one), None) => Ok(one),
1010×
            (None, _) => fail(format!("no operation {spec} in oxy op log")),
1020×
            (Some(_), Some(_)) => fail(format!(
103
                "operation {spec} is ambiguous: give more of its ID"
104
            )),
105
        }
1066×
    }
107
108
    /// What changed from `before` to `after`, for this workspace's working
109
    /// copy and every reference.
11011×
    pub fn view_changes(&self, before: &View, after: &View) -> Result<Vec<ViewChange>> {
11111×
        let mut out = Vec::new();
11211×
        let heads = |st: &ObjectRef| -> Result<usize> { Ok(self.store.get_state(st)?.heads.len()) };
11311×
        let names: BTreeSet<&String> = before
11411×
            .bookmarks
11511×
            .keys()
11611×
            .chain(after.bookmarks.keys())
11711×
            .collect();
11812×
        for name in names {
11912×
            match (before.bookmarks.get(name), after.bookmarks.get(name)) {
1200×
                (None, Some(a)) => out.push(ViewChange::BookmarkCreated {
1210×
                    name: name.clone(),
1220×
                    heads: heads(a)?,
123
                }),
1240×
                (Some(_), None) => out.push(ViewChange::BookmarkDeleted { name: name.clone() }),
12512×
                (Some(b), Some(a)) if a != b => out.push(ViewChange::BookmarkMoved {
1263×
                    name: name.clone(),
1273×
                    from: heads(b)?,
1283×
                    to: heads(a)?,
129
                }),
1309×
                _ => {}
131
            }
132
        }
13311×
        for (remote, bookmarks) in &after.remotes {
13410×
            for (name, st) in bookmarks {
13510×
                if before.remotes.get(remote).and_then(|b| b.get(name)) != Some(st) {
1361×
                    out.push(ViewChange::RemoteMoved {
1371×
                        remote: remote.clone(),
1381×
                        name: name.clone(),
1391×
                    });
1409×
                }
141
            }
142
        }
14311×
        for name in after.tags.keys().filter(|t| !before.tags.contains_key(*t)) {
1440×
            out.push(ViewChange::TagCreated { name: name.clone() });
1450×
        }
14611×
        for name in before.tags.keys().filter(|t| !after.tags.contains_key(*t)) {
1470×
            out.push(ViewChange::TagDeleted { name: name.clone() });
1480×
        }
14911×
        if before.policy != after.policy {
1500×
            out.push(ViewChange::Policy);
15111×
        }
15211×
        if let Some(w) = after.workspaces.get(&self.workspace) {
15311×
            if before.workspaces.get(&self.workspace) != Some(w) {
1549×
                out.push(ViewChange::WorkingCopy { change: *w });
1559×
            }
1560×
        }
15711×
        Ok(out)
15811×
    }
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.
1651×
    pub fn op_restore(&self, spec: &str, discard: bool) -> Result<RestoreReport> {
1661×
        let _wc = self.lock_working_copy()?;
1671×
        let (target_ref, target) = self.resolve_op(spec)?;
1681×
        let command = format!("oxy op restore {}", target_ref.short(12));
169
        // Refuse before anything moves, as the checkout below would.
1701×
        if !discard && !self.clean_by_manifest()? {
1710×
            let snap = self.snapshot_status(&command)?;
1720×
            if !snap.statuses.is_empty() {
1730×
                return fail(format!(
174
                    "the working copy has uncommitted changes ({}): commit them, or drop them with --discard",
1750×
                    plural_paths(snap.statuses.len())
176
                ));
1770×
            }
1781×
        }
1791×
        let (head_ref, head) = self.head_op()?;
1801×
        let mut view = head.view.clone();
1811×
        view.bookmarks = target.view.bookmarks.clone();
1821×
        self.record_op(&[head_ref], &command, view)?;
1831×
        let base = match target.view.workspaces.get(&self.workspace) {
1841×
            Some(w) => {
1851×
                let wc = self.store.get_change(w)?;
1861×
                self.load_state_history(&wc.deps)?.maximal(&wc.deps)
187
            }
1880×
            None => self.bookmark_heads()?,
189
        };
1901×
        let checkout = self.checkout(&base, discard, &format!("{command} (checkout)"))?;
1911×
        Ok(RestoreReport {
1921×
            op: target_ref,
1931×
            command: target.command,
1941×
            checkout,
1951×
        })
1961×
    }
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.
2022×
    pub fn op_revert(&self, spec: &str) -> Result<RevertReport> {
2032×
        let _wc = self.lock_working_copy()?;
2042×
        let (target_ref, target) = self.resolve_op(spec)?;
2052×
        if target.is_snapshot() {
2060×
            return fail(
207
                "that operation only recorded the working copy: it moved no bookmark to revert",
208
            );
2092×
        }
2102×
        let Some(parent) = target.parents.first() else {
2110×
            return fail("that is the first operation: there is nothing before it");
212
        };
2132×
        let before = Op::decode(&self.store.get_typed(parent, ObjectType::Op)?)?.view;
2142×
        let (head_ref, head) = self.head_op()?;
2152×
        let mut view = head.view.clone();
2162×
        let (mut reverted, mut kept) = (Vec::new(), Vec::new());
2172×
        let names: BTreeSet<&String> = before
2182×
            .bookmarks
2192×
            .keys()
2202×
            .chain(target.view.bookmarks.keys())
2212×
            .collect();
2224×
        for name in names {
2234×
            let (was, became) = (before.bookmarks.get(name), target.view.bookmarks.get(name));
2244×
            if was == became {
2252×
                continue;
2262×
            }
2272×
            if head.view.bookmarks.get(name) != became {
2281×
                kept.push(name.clone());
2291×
                continue;
2301×
            }
2311×
            match was {
2320×
                Some(st) => view.bookmarks.insert(name.clone(), *st),
2331×
                None => view.bookmarks.remove(name),
234
            };
2351×
            reverted.push(name.clone());
236
        }
2372×
        if reverted.is_empty() {
2381×
            return fail(if kept.is_empty() {
2390×
                "that operation moved no bookmark: nothing to revert".to_string()
240
            } else {
2411×
                format!(
242
                    "every bookmark that operation moved has moved again since ({}); oxy op restore goes back to an operation instead",
2431×
                    kept.join(", ")
244
                )
245
            });
2461×
        }
2471×
        let command = format!("oxy op revert {}", target_ref.short(12));
2481×
        let current = self.config.bookmark.clone();
2491×
        let mut old = self.heads_of(&head.view, &current)?;
2501×
        old.sort();
2511×
        self.record_op(&[head_ref], &command, view.clone())?;
2521×
        let mut checked_out = false;
2531×
        if reverted.contains(&current) {
2540×
            let snap = self.snapshot(&command)?;
2550×
            let clean = snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty();
2560×
            let mut base = snap.base.maximal(&snap.wc.deps);
2570×
            base.sort();
2580×
            if clean && base == old {
2590×
                let new = self.heads_of(&view, &current)?;
2600×
                self.checkout(&new, false, &format!("{command} (checkout)"))?;
2610×
                checked_out = true;
2620×
            }
2631×
        }
2641×
        Ok(RevertReport {
2651×
            op: target_ref,
2661×
            command: target.command,
2671×
            reverted,
2681×
            kept,
2691×
            checked_out,
2701×
        })
2712×
    }
272
}
273
2740×
fn plural_paths(n: usize) -> String {
2750×
    format!("{n} path{}", if n == 1 { "" } else { "s" })
2760×
}