OXYGENHUB
# da58bbbbc4d9 oxygen / crates / oxy-core / src / repo / workspace.rs
6.3 KiB · Rust · last changed Multiple workspaces: oxy workspace add|list|forget, secondary .oxy file, per-workspace snapshot lock (spec 0.6i) 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")).ok().map(|s| PathBuf::from(s.trim()))
59
    }
60
61
    /// Every workspace this repository knows about.
62
    pub fn workspaces(&self) -> Result<Vec<WorkspaceInfo>> {
63
        let view = self.view()?;
64
        let mut ids: BTreeSet<WorkspaceId> = view.workspaces.keys().copied().collect();
65
        if let Ok(rd) = fs::read_dir(self.oxy.join("workspaces")) {
66
            for e in rd.flatten() {
67
                if let Some(id) = WorkspaceId::from_hex(&e.file_name().to_string_lossy()) {
68
                    if e.path().join("path").exists() {
69
                        ids.insert(id);
70
                    }
71
                }
72
            }
73
        }
74
        let mut out = Vec::new();
75
        for id in ids {
76
            let path = if id == self.workspace {
77
                self.root.clone()
78
            } else {
79
                self.workspace_path(&id).unwrap_or_else(|| PathBuf::from("(unknown)"))
80
            };
81
            out.push(WorkspaceInfo { id, path, working_copy: view.workspaces.get(&id).copied(), current: id == self.workspace });
82
        }
83
        Ok(out)
84
    }
85
86
    /// Create a workspace at `dir` checked out on `heads` (default the
87
    /// bookmark's), and return a `Repo` rooted there.
88
    pub fn workspace_add(&self, dir: &Path, heads: Option<&[ChangeHash]>) -> Result<Repo> {
89
        if dir.exists() && fs::read_dir(dir)?.next().is_some() {
90
            return fail(format!("{} exists and is not empty", dir.display()));
91
        }
92
        fs::create_dir_all(dir)?;
93
        let dir = dir.canonicalize()?;
94
        if dir == self.root || dir.starts_with(&self.root) || self.root.starts_with(&dir) {
95
            return fail("a workspace cannot be inside another workspace of the same repository");
96
        }
97
        let heads = match heads {
98
            Some(h) => h.to_vec(),
99
            None => self.bookmark_heads()?,
100
        };
101
        let id = WorkspaceId::random();
102
        let wc = self.new_working_copy(heads.clone(), "");
103
        let wc_hash = self.store.put_change(&wc)?;
104
        let (op_ref, op) = self.head_op()?;
105
        let mut view = op.view.clone();
106
        view.workspaces.insert(id, wc_hash);
107
        self.record_op(&[op_ref], &format!("oxy workspace add {}", dir.display()), view)?;
108
        self.register_workspace(&id, &dir)?;
109
        write_atomic(&dir.join(OXY_DIR), format!("oxydir: {}\nworkspace: {}\n", self.oxy.display(), id.to_hex()).as_bytes())?;
110
        let repo = Repo::open(&dir)?.with_home(self.keys_dir.parent().unwrap_or(&self.keys_dir));
111
        repo.checkout(&heads, true, "oxy workspace add (checkout)")?;
112
        Ok(repo)
113
    }
114
115
    /// Drop a workspace from the view and registry; its files stay.
116
    pub fn workspace_forget(&self, id: &WorkspaceId) -> Result<()> {
117
        if *id == self.workspace {
118
            return fail("cannot forget the workspace you are in");
119
        }
120
        let (op_ref, op) = self.head_op()?;
121
        let mut view = op.view.clone();
122
        if view.workspaces.remove(id).is_none() && self.workspace_path(id).is_none() {
123
            return fail(format!("no workspace {}", id.short(8)));
124
        }
125
        self.record_op(&[op_ref], &format!("oxy workspace forget {}", id.short(8)), view)?;
126
        let _ = fs::remove_dir_all(self.workspace_dir(id));
127
        Ok(())
128
    }
129
130
    /// Take this workspace's snapshot lock (spec §12.1).
131
    pub(crate) fn lock_snapshot(&self) -> Result<SnapshotLock> {
132
        let dir = self.workspace_dir(&self.workspace);
133
        fs::create_dir_all(&dir)?;
134
        let path = dir.join("lock");
135
        for _ in 0..50 {
136
            match fs::OpenOptions::new().write(true).create_new(true).open(&path) {
137
                Ok(mut f) => {
138
                    let _ = write!(f, "{}", std::process::id());
139
                    return Ok(SnapshotLock { path });
140
                }
141
                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
142
                    let stale = fs::metadata(&path)
143
                        .and_then(|m| m.modified())
144
                        .map(|t| t.elapsed().map(|d| d.as_secs() > 60).unwrap_or(false))
145
                        .unwrap_or(true);
146
                    if stale {
147
                        let _ = fs::remove_file(&path);
148
                        continue;
149
                    }
150
                    std::thread::sleep(std::time::Duration::from_millis(100));
151
                }
152
                Err(e) => return Err(e.into()),
153
            }
154
        }
155
        fail("another oxy process is snapshotting this workspace")
156
    }
157
}