OXYGENHUB
# c792033af344 oxygen / crates / oxy-core / src / repo / bookmarks.rs
7.5 KiB · Rust · last changed Bookmarks: any number per repository, per-workspace current bookmark (bookmark create|set|delete|list|switch), oxy merge as union of heads, bookmark names resolve everywhere, push -b (spec 0.8 section 10.1) 1 month ago source blame history raw
1
//! Bookmarks (spec §10.1): named states, any number per repository, with a
2
//! current bookmark per workspace. Merging is the union of two bookmarks'
3
//! heads: no merge change, conflicts reified.
4
5
use super::*;
6
7
/// One bookmark as `oxy bookmark list` shows it.
8
#[derive(Clone, Debug)]
9
pub struct BookmarkInfo {
10
    pub name: String,
11
    pub heads: Vec<ChangeHash>,
12
    /// The workspace's current bookmark.
13
    pub current: bool,
14
    /// Changes ahead of / behind `origin`'s copy, when tracked.
15
    pub ahead: Option<usize>,
16
    pub behind: Option<usize>,
17
}
18
19
#[derive(Clone, Debug)]
20
pub struct MergeReport {
21
    pub heads: Vec<ChangeHash>,
22
    pub changed: bool,
23
    pub checked_out: bool,
24
}
25
26
/// Bookmark and tag names (spec §10.1): UTF-8, 1–255 bytes, no control
27
/// characters, no leading `-`, and none of `..`, `//`, `@{`.
28
pub fn valid_reference_name(name: &str) -> bool {
29
    !name.is_empty()
30
        && name.len() <= 255
31
        && !name.starts_with('-')
32
        && !name.chars().any(|c| c.is_control())
33
        && !name.contains("..")
34
        && !name.contains("//")
35
        && !name.contains("@{")
36
        && name != "policy"
37
}
38
39
impl Repo {
40
    fn workspace_bookmark_path(&self) -> PathBuf {
41
        self.oxy.join("workspaces").join(self.workspace.to_hex()).join("bookmark")
42
    }
43
44
    /// Every bookmark with its heads and remote-tracking difference.
45
    pub fn bookmarks(&self) -> Result<Vec<BookmarkInfo>> {
46
        let view = self.view()?;
47
        let mut out = Vec::new();
48
        for (name, st) in &view.bookmarks {
49
            let heads = self.store.get_state(st)?.heads;
50
            let (ahead, behind) = match view.remotes.get("origin").and_then(|b| b.get(name)) {
51
                Some(rs) => {
52
                    let remote_heads = self.store.get_state(rs)?.heads;
53
                    (self.negotiate(&remote_heads, &heads).map(|v| v.len()).ok(), self.negotiate(&heads, &remote_heads).map(|v| v.len()).ok())
54
                }
55
                None => (None, None),
56
            };
57
            out.push(BookmarkInfo { name: name.clone(), heads, current: *name == self.config.bookmark, ahead, behind });
58
        }
59
        Ok(out)
60
    }
61
62
    /// Create `name` at `heads` (default: the working copy's base heads).
63
    pub fn bookmark_create(&self, name: &str, heads: Option<&[ChangeHash]>) -> Result<Vec<ChangeHash>> {
64
        if !valid_reference_name(name) {
65
            return fail(format!("{name:?} is not a valid bookmark name"));
66
        }
67
        let (op_ref, op) = self.head_op()?;
68
        if op.view.bookmarks.contains_key(name) {
69
            return fail(format!("bookmark {name} already exists"));
70
        }
71
        let heads = match heads {
72
            Some(h) => self.load_state(h)?.maximal(h),
73
            None => {
74
                let (_, wc) = self.working_copy()?;
75
                self.load_state(&wc.deps)?.maximal(&wc.deps)
76
            }
77
        };
78
        let mut view = op.view.clone();
79
        view.bookmarks.insert(name.to_string(), self.store.put_state(&StateObject::new(heads.clone()))?);
80
        self.record_op(&[op_ref], &format!("oxy bookmark create {name}"), view)?;
81
        Ok(heads)
82
    }
83
84
    /// Point an existing bookmark at `heads`.
85
    pub fn bookmark_set(&self, name: &str, heads: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
86
        let (op_ref, op) = self.head_op()?;
87
        if !op.view.bookmarks.contains_key(name) {
88
            return fail(format!("no bookmark {name}; create it with `oxy bookmark create`"));
89
        }
90
        let heads = self.load_state(heads)?.maximal(heads);
91
        let mut view = op.view.clone();
92
        view.bookmarks.insert(name.to_string(), self.store.put_state(&StateObject::new(heads.clone()))?);
93
        self.record_op(&[op_ref], &format!("oxy bookmark set {name}"), view)?;
94
        if name == self.config.bookmark {
95
            self.drop_manifest();
96
        }
97
        Ok(heads)
98
    }
99
100
    /// Remove a bookmark; its changes stay until unreachable (spec §21).
101
    pub fn bookmark_delete(&self, name: &str) -> Result<()> {
102
        if name == self.config.bookmark {
103
            return fail(format!("{name} is this workspace's current bookmark; switch to another first"));
104
        }
105
        for w in self.workspaces()? {
106
            if !w.current && self.workspace_bookmark_of(&w.id).as_deref() == Some(name) {
107
                return fail(format!("{name} is the current bookmark of workspace {} at {}", w.id.short(8), w.path.display()));
108
            }
109
        }
110
        let (op_ref, op) = self.head_op()?;
111
        let mut view = op.view.clone();
112
        if view.bookmarks.remove(name).is_none() {
113
            return fail(format!("no bookmark {name}"));
114
        }
115
        self.record_op(&[op_ref], &format!("oxy bookmark delete {name}"), view)?;
116
        Ok(())
117
    }
118
119
    fn workspace_bookmark_of(&self, id: &WorkspaceId) -> Option<String> {
120
        fs::read_to_string(self.oxy.join("workspaces").join(id.to_hex()).join("bookmark")).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
121
    }
122
123
    /// Make `name` this workspace's current bookmark and check it out.
124
    pub fn bookmark_switch(&mut self, name: &str, discard: bool) -> Result<CheckoutReport> {
125
        let view = self.view()?;
126
        let Some(st) = view.bookmarks.get(name) else { return fail(format!("no bookmark {name}")) };
127
        let heads = self.store.get_state(st)?.heads;
128
        let report = self.checkout(&heads, discard, &format!("oxy bookmark switch {name}"))?;
129
        fs::create_dir_all(self.workspace_bookmark_path().parent().unwrap())?;
130
        write_atomic(&self.workspace_bookmark_path(), name.as_bytes())?;
131
        self.config.bookmark = name.to_string();
132
        Ok(report)
133
    }
134
135
    /// Record `name` as the current bookmark of a new workspace.
136
    pub(crate) fn set_workspace_bookmark(&self, id: &WorkspaceId, name: &str) -> Result<()> {
137
        let dir = self.oxy.join("workspaces").join(id.to_hex());
138
        fs::create_dir_all(&dir)?;
139
        write_atomic(&dir.join("bookmark"), name.as_bytes())
140
    }
141
142
    /// Union the heads of bookmark `name` into the current bookmark (spec
143
    /// §10.1). The working copy follows when it was clean on the old heads.
144
    pub fn merge(&self, name: &str) -> Result<MergeReport> {
145
        let (op_ref, op) = self.head_op()?;
146
        let current = self.config.bookmark.clone();
147
        if name == current {
148
            return fail(format!("{name} is the current bookmark"));
149
        }
150
        let Some(st) = op.view.bookmarks.get(name) else { return fail(format!("no bookmark {name}")) };
151
        let other = self.store.get_state(st)?.heads;
152
        let local = self.heads_of(&op.view, &current)?;
153
        let mut all = local.clone();
154
        all.extend(other);
155
        let mut heads = self.load_state(&all)?.maximal(&all);
156
        heads.sort();
157
        let mut sorted_local = local.clone();
158
        sorted_local.sort();
159
        if heads == sorted_local {
160
            return Ok(MergeReport { heads, changed: false, checked_out: false });
161
        }
162
        let mut view = op.view.clone();
163
        view.bookmarks.insert(current, self.store.put_state(&StateObject::new(heads.clone()))?);
164
        self.record_op(&[op_ref], &format!("oxy merge {name}"), view)?;
165
        let snap = self.snapshot("oxy merge")?;
166
        let clean = snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty();
167
        let mut base = snap.base.maximal(&snap.wc.deps);
168
        base.sort();
169
        let mut checked_out = false;
170
        if clean && base == sorted_local {
171
            self.checkout(&heads, false, "oxy merge (checkout)")?;
172
            checked_out = true;
173
        }
174
        Ok(MergeReport { heads, changed: true, checked_out })
175
    }
176
}