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