OXYGENHUB
# 3b1dc8a974a3 oxygen / crates / oxy-core / src / repo / bookmarks.rs
8.2 KiB · Rust · last changed Unbound hosted repositories adopt the first pusher's id 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
42
            .join("workspaces")
43
            .join(self.workspace.to_hex())
44
            .join("bookmark")
45
    }
46
47
    /// Every bookmark with its heads and remote-tracking difference.
48
    pub fn bookmarks(&self) -> Result<Vec<BookmarkInfo>> {
49
        let view = self.view()?;
50
        let mut out = Vec::new();
51
        for (name, st) in &view.bookmarks {
52
            let heads = self.store.get_state(st)?.heads;
53
            let (ahead, behind) = match view.remotes.get("origin").and_then(|b| b.get(name)) {
54
                Some(rs) => {
55
                    let remote_heads = self.store.get_state(rs)?.heads;
56
                    (
57
                        self.negotiate(&remote_heads, &heads).map(|v| v.len()).ok(),
58
                        self.negotiate(&heads, &remote_heads).map(|v| v.len()).ok(),
59
                    )
60
                }
61
                None => (None, None),
62
            };
63
            out.push(BookmarkInfo {
64
                name: name.clone(),
65
                heads,
66
                current: *name == self.config.bookmark,
67
                ahead,
68
                behind,
69
            });
70
        }
71
        Ok(out)
72
    }
73
74
    /// Create `name` at `heads` (default: the working copy's base heads).
75
    pub fn bookmark_create(
76
        &self,
77
        name: &str,
78
        heads: Option<&[ChangeHash]>,
79
    ) -> Result<Vec<ChangeHash>> {
80
        if !valid_reference_name(name) {
81
            return fail(format!("{name:?} is not a valid bookmark name"));
82
        }
83
        let (op_ref, op) = self.head_op()?;
84
        if op.view.bookmarks.contains_key(name) {
85
            return fail(format!("bookmark {name} already exists"));
86
        }
87
        let heads = match heads {
88
            Some(h) => self.load_state(h)?.maximal(h),
89
            None => {
90
                let (_, wc) = self.working_copy()?;
91
                self.load_state(&wc.deps)?.maximal(&wc.deps)
92
            }
93
        };
94
        let mut view = op.view.clone();
95
        view.bookmarks.insert(
96
            name.to_string(),
97
            self.store.put_state(&StateObject::new(heads.clone()))?,
98
        );
99
        self.record_op(&[op_ref], &format!("oxy bookmark create {name}"), view)?;
100
        Ok(heads)
101
    }
102
103
    /// Point an existing bookmark at `heads`.
104
    pub fn bookmark_set(&self, name: &str, heads: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
105
        let (op_ref, op) = self.head_op()?;
106
        if !op.view.bookmarks.contains_key(name) {
107
            return fail(format!(
108
                "no bookmark {name}; create it with `oxy bookmark create`"
109
            ));
110
        }
111
        let heads = self.load_state(heads)?.maximal(heads);
112
        let mut view = op.view.clone();
113
        view.bookmarks.insert(
114
            name.to_string(),
115
            self.store.put_state(&StateObject::new(heads.clone()))?,
116
        );
117
        self.record_op(&[op_ref], &format!("oxy bookmark set {name}"), view)?;
118
        if name == self.config.bookmark {
119
            self.drop_manifest();
120
        }
121
        Ok(heads)
122
    }
123
124
    /// Remove a bookmark; its changes stay until unreachable (spec §21).
125
    pub fn bookmark_delete(&self, name: &str) -> Result<()> {
126
        if name == self.config.bookmark {
127
            return fail(format!(
128
                "{name} is this workspace's current bookmark; switch to another first"
129
            ));
130
        }
131
        for w in self.workspaces()? {
132
            if !w.current && self.workspace_bookmark_of(&w.id).as_deref() == Some(name) {
133
                return fail(format!(
134
                    "{name} is the current bookmark of workspace {} at {}",
135
                    w.id.short(8),
136
                    w.path.display()
137
                ));
138
            }
139
        }
140
        let (op_ref, op) = self.head_op()?;
141
        let mut view = op.view.clone();
142
        if view.bookmarks.remove(name).is_none() {
143
            return fail(format!("no bookmark {name}"));
144
        }
145
        self.record_op(&[op_ref], &format!("oxy bookmark delete {name}"), view)?;
146
        Ok(())
147
    }
148
149
    fn workspace_bookmark_of(&self, id: &WorkspaceId) -> Option<String> {
150
        fs::read_to_string(
151
            self.oxy
152
                .join("workspaces")
153
                .join(id.to_hex())
154
                .join("bookmark"),
155
        )
156
        .ok()
157
        .map(|s| s.trim().to_string())
158
        .filter(|s| !s.is_empty())
159
    }
160
161
    /// Make `name` this workspace's current bookmark and check it out.
162
    pub fn bookmark_switch(&mut self, name: &str, discard: bool) -> Result<CheckoutReport> {
163
        let view = self.view()?;
164
        let Some(st) = view.bookmarks.get(name) else {
165
            return fail(format!("no bookmark {name}"));
166
        };
167
        let heads = self.store.get_state(st)?.heads;
168
        let report = self.checkout(&heads, discard, &format!("oxy bookmark switch {name}"))?;
169
        fs::create_dir_all(self.workspace_bookmark_path().parent().unwrap())?;
170
        write_atomic(&self.workspace_bookmark_path(), name.as_bytes())?;
171
        self.config.bookmark = name.to_string();
172
        Ok(report)
173
    }
174
175
    /// Record `name` as the current bookmark of a new workspace.
176
    pub(crate) fn set_workspace_bookmark(&self, id: &WorkspaceId, name: &str) -> Result<()> {
177
        let dir = self.oxy.join("workspaces").join(id.to_hex());
178
        fs::create_dir_all(&dir)?;
179
        write_atomic(&dir.join("bookmark"), name.as_bytes())
180
    }
181
182
    /// Union the heads of bookmark `name` into the current bookmark (spec
183
    /// §10.1). The working copy follows when it was clean on the old heads.
184
    pub fn merge(&self, name: &str) -> Result<MergeReport> {
185
        let (op_ref, op) = self.head_op()?;
186
        let current = self.config.bookmark.clone();
187
        if name == current {
188
            return fail(format!("{name} is the current bookmark"));
189
        }
190
        let Some(st) = op.view.bookmarks.get(name) else {
191
            return fail(format!("no bookmark {name}"));
192
        };
193
        let other = self.store.get_state(st)?.heads;
194
        let local = self.heads_of(&op.view, &current)?;
195
        let mut all = local.clone();
196
        all.extend(other);
197
        let mut heads = self.load_state(&all)?.maximal(&all);
198
        heads.sort();
199
        let mut sorted_local = local.clone();
200
        sorted_local.sort();
201
        if heads == sorted_local {
202
            return Ok(MergeReport {
203
                heads,
204
                changed: false,
205
                checked_out: false,
206
            });
207
        }
208
        let mut view = op.view.clone();
209
        view.bookmarks.insert(
210
            current,
211
            self.store.put_state(&StateObject::new(heads.clone()))?,
212
        );
213
        self.record_op(&[op_ref], &format!("oxy merge {name}"), view)?;
214
        let snap = self.snapshot("oxy merge")?;
215
        let clean = snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty();
216
        let mut base = snap.base.maximal(&snap.wc.deps);
217
        base.sort();
218
        let mut checked_out = false;
219
        if clean && base == sorted_local {
220
            self.checkout(&heads, false, "oxy merge (checkout)")?;
221
            checked_out = true;
222
        }
223
        Ok(MergeReport {
224
            heads,
225
            changed: true,
226
            checked_out,
227
        })
228
    }
229
}