OXYGENHUB
# 30924fc6c7d6 oxygen / crates / oxy-core / src / repo / workspace.rs
6.8 KiB · Rust · last changed Unbound hosted repositories adopt the first pusher's id 1 month ago source blame history raw
1
//! Multiple workspaces (spec §12.2): extra checkout directories sharing one
2
//! `.oxy`. A secondary workspace holds a `.oxy` *file* of the form
3
//! `oxydir: <path>\nworkspace: <id>`; the repository lists workspaces under
4
//! `.oxy/workspaces/<id>/path`. Each workspace has its own working-copy
5
//! change, manifest, and short-lived snapshot lock.
6
7
use super::*;
8
9
#[derive(Debug, Clone, PartialEq, Eq)]
10
pub struct WorkspaceInfo {
11
    pub id: WorkspaceId,
12
    pub path: PathBuf,
13
    /// Working-copy change hash from the current view, if the workspace is registered there.
14
    pub working_copy: Option<ChangeHash>,
15
    pub current: bool,
16
}
17
18
/// Held while a workspace is snapshotted; a stale lock (older than a
19
/// minute, its process gone) is taken over.
20
pub struct SnapshotLock {
21
    path: PathBuf,
22
}
23
24
impl Drop for SnapshotLock {
25
    fn drop(&mut self) {
26
        let _ = fs::remove_file(&self.path);
27
    }
28
}
29
30
/// Parse a secondary workspace's `.oxy` file.
31
pub(crate) fn parse_workspace_file(text: &str) -> Option<(PathBuf, WorkspaceId)> {
32
    let mut dir = None;
33
    let mut id = None;
34
    for line in text.lines() {
35
        if let Some(v) = line.strip_prefix("oxydir:") {
36
            dir = Some(PathBuf::from(v.trim()));
37
        } else if let Some(v) = line.strip_prefix("workspace:") {
38
            id = WorkspaceId::from_hex(v.trim());
39
        }
40
    }
41
    Some((dir?, id?))
42
}
43
44
impl Repo {
45
    fn workspace_dir(&self, id: &WorkspaceId) -> PathBuf {
46
        self.oxy.join("workspaces").join(id.to_hex())
47
    }
48
49
    /// Record where a workspace lives.
50
    pub(crate) fn register_workspace(&self, id: &WorkspaceId, path: &Path) -> Result<()> {
51
        let dir = self.workspace_dir(id);
52
        fs::create_dir_all(&dir)?;
53
        write_atomic(&dir.join("path"), path.to_string_lossy().as_bytes())
54
    }
55
56
    /// The directory of a workspace, if registered.
57
    pub fn workspace_path(&self, id: &WorkspaceId) -> Option<PathBuf> {
58
        fs::read_to_string(self.workspace_dir(id).join("path"))
59
            .ok()
60
            .map(|s| PathBuf::from(s.trim()))
61
    }
62
63
    /// Every workspace this repository knows about.
64
    pub fn workspaces(&self) -> Result<Vec<WorkspaceInfo>> {
65
        let view = self.view()?;
66
        let mut ids: BTreeSet<WorkspaceId> = view.workspaces.keys().copied().collect();
67
        if let Ok(rd) = fs::read_dir(self.oxy.join("workspaces")) {
68
            for e in rd.flatten() {
69
                if let Some(id) = WorkspaceId::from_hex(&e.file_name().to_string_lossy()) {
70
                    if e.path().join("path").exists() {
71
                        ids.insert(id);
72
                    }
73
                }
74
            }
75
        }
76
        let mut out = Vec::new();
77
        for id in ids {
78
            let path = if id == self.workspace {
79
                self.root.clone()
80
            } else {
81
                self.workspace_path(&id)
82
                    .unwrap_or_else(|| PathBuf::from("(unknown)"))
83
            };
84
            out.push(WorkspaceInfo {
85
                id,
86
                path,
87
                working_copy: view.workspaces.get(&id).copied(),
88
                current: id == self.workspace,
89
            });
90
        }
91
        Ok(out)
92
    }
93
94
    /// Create a workspace at `dir` checked out on `heads` (default the
95
    /// bookmark's), and return a `Repo` rooted there.
96
    pub fn workspace_add(&self, dir: &Path, heads: Option<&[ChangeHash]>) -> Result<Repo> {
97
        if dir.exists() && fs::read_dir(dir)?.next().is_some() {
98
            return fail(format!("{} exists and is not empty", dir.display()));
99
        }
100
        fs::create_dir_all(dir)?;
101
        let dir = dir.canonicalize()?;
102
        if dir == self.root || dir.starts_with(&self.root) || self.root.starts_with(&dir) {
103
            return fail("a workspace cannot be inside another workspace of the same repository");
104
        }
105
        let heads = match heads {
106
            Some(h) => h.to_vec(),
107
            None => self.bookmark_heads()?,
108
        };
109
        let id = WorkspaceId::random();
110
        let wc = self.new_working_copy(heads.clone(), "");
111
        let wc_hash = self.store.put_change(&wc)?;
112
        let (op_ref, op) = self.head_op()?;
113
        let mut view = op.view.clone();
114
        view.workspaces.insert(id, wc_hash);
115
        self.record_op(
116
            &[op_ref],
117
            &format!("oxy workspace add {}", dir.display()),
118
            view,
119
        )?;
120
        self.register_workspace(&id, &dir)?;
121
        self.set_workspace_bookmark(&id, &self.config.bookmark)?;
122
        write_atomic(
123
            &dir.join(OXY_DIR),
124
            format!(
125
                "oxydir: {}\nworkspace: {}\n",
126
                self.oxy.display(),
127
                id.to_hex()
128
            )
129
            .as_bytes(),
130
        )?;
131
        let repo = Repo::open(&dir)?.with_home(self.keys_dir.parent().unwrap_or(&self.keys_dir));
132
        repo.checkout(&heads, true, "oxy workspace add (checkout)")?;
133
        Ok(repo)
134
    }
135
136
    /// Drop a workspace from the view and registry; its files stay.
137
    pub fn workspace_forget(&self, id: &WorkspaceId) -> Result<()> {
138
        if *id == self.workspace {
139
            return fail("cannot forget the workspace you are in");
140
        }
141
        let (op_ref, op) = self.head_op()?;
142
        let mut view = op.view.clone();
143
        if view.workspaces.remove(id).is_none() && self.workspace_path(id).is_none() {
144
            return fail(format!("no workspace {}", id.short(8)));
145
        }
146
        self.record_op(
147
            &[op_ref],
148
            &format!("oxy workspace forget {}", id.short(8)),
149
            view,
150
        )?;
151
        let _ = fs::remove_dir_all(self.workspace_dir(id));
152
        Ok(())
153
    }
154
155
    /// Take this workspace's snapshot lock (spec §12.1).
156
    pub(crate) fn lock_snapshot(&self) -> Result<SnapshotLock> {
157
        let dir = self.workspace_dir(&self.workspace);
158
        fs::create_dir_all(&dir)?;
159
        let path = dir.join("lock");
160
        for _ in 0..50 {
161
            match fs::OpenOptions::new()
162
                .write(true)
163
                .create_new(true)
164
                .open(&path)
165
            {
166
                Ok(mut f) => {
167
                    let _ = write!(f, "{}", std::process::id());
168
                    return Ok(SnapshotLock { path });
169
                }
170
                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
171
                    let stale = fs::metadata(&path)
172
                        .and_then(|m| m.modified())
173
                        .map(|t| t.elapsed().map(|d| d.as_secs() > 60).unwrap_or(false))
174
                        .unwrap_or(true);
175
                    if stale {
176
                        let _ = fs::remove_file(&path);
177
                        continue;
178
                    }
179
                    std::thread::sleep(std::time::Duration::from_millis(100));
180
                }
181
                Err(e) => return Err(e.into()),
182
            }
183
        }
184
        fail("another oxy process is snapshotting this workspace")
185
    }
186
}