OXYGENHUB

Coverage / crates/oxy-core/src/repo/workspace.rs 86.7%

221 of 255 lines run, 37 of 54 functions · run #409 on main · run missed

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 lock file, taken by creating it exclusively and released on drop: a
19
/// workspace's working-copy lock, the operation head's lock. A stale one,
20
/// whose process is gone (or, where that cannot be told, older than a
21
/// minute), is taken over.
22
pub struct FileLock {
23
    path: PathBuf,
24
}
25
26
/// A workspace's working-copy lock (spec §12.1), held by everything that
27
/// reads or writes the working copy and records its working-copy change:
28
/// snapshot, checkout, bookmark switch, commit, discard, pull. It is
29
/// re-entrant within a thread, so a checkout that snapshots first holds it
30
/// once.
31
pub struct WorkingCopyLock {
32
    path: PathBuf,
33
    _file: Option<FileLock>,
34
}
35
36
/// The working-copy lock; its old name.
37
pub type SnapshotLock = WorkingCopyLock;
38
39
thread_local! {
40
    /// Working-copy locks this thread holds, with their depth.
41
    static HELD: std::cell::RefCell<HashMap<PathBuf, usize>> = Default::default();
42
}
43
44
impl Drop for WorkingCopyLock {
453267×
    fn drop(&mut self) {
463267×
        HELD.with(|h| {
473267×
            let mut h = h.borrow_mut();
483267×
            if let Some(n) = h.get_mut(&self.path) {
493266×
                *n -= 1;
503266×
                if *n == 0 {
511868×
                    h.remove(&self.path);
521868×
                }
531×
            }
543267×
        });
553267×
    }
56
}
57
58
/// Whether the process that wrote a lock file is gone. Where that cannot
59
/// be told (no `/proc`), a lock older than a minute counts as stale.
60296×
pub(crate) fn lock_is_stale(path: &Path) -> bool {
61296×
    let old = fs::metadata(path)
62296×
        .and_then(|m| m.modified())
63296×
        .map(|t| t.elapsed().map(|d| d.as_secs() > 60).unwrap_or(false))
64296×
        .unwrap_or(true);
65296×
    let proc_fs = Path::new("/proc/self").exists();
66296×
    match fs::read_to_string(path)
67296×
        .ok()
68296×
        .and_then(|s| s.trim().parse::<u32>().ok())
69
    {
70296×
        Some(pid) if proc_fs => !Path::new("/proc").join(pid.to_string()).exists(),
71
        // Being written this instant, or by something that is not oxy.
720×
        _ => old,
73
    }
74296×
}
75
76
impl Drop for FileLock {
775896×
    fn drop(&mut self) {
785896×
        let _ = fs::remove_file(&self.path);
795896×
    }
80
}
81
82
impl FileLock {
83
    /// Wait up to about five seconds for `path`; `busy` is the error after that.
844028×
    pub(crate) fn acquire(path: PathBuf, busy: &str) -> Result<FileLock> {
854028×
        Self::acquire_within(path, busy, std::time::Duration::from_secs(5))
864028×
    }
87
88
    /// Wait up to `wait` for `path`; `busy` is the error after that.
895896×
    pub(crate) fn acquire_within(
905896×
        path: PathBuf,
915896×
        busy: &str,
925896×
        wait: std::time::Duration,
935896×
    ) -> Result<FileLock> {
945896×
        let until = std::time::Instant::now() + wait;
95
        loop {
966191×
            match fs::OpenOptions::new()
976191×
                .write(true)
986191×
                .create_new(true)
996191×
                .open(&path)
100
            {
1015896×
                Ok(mut f) => {
1025896×
                    let _ = write!(f, "{}", std::process::id());
1035896×
                    return Ok(FileLock { path });
104
                }
105295×
                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
106295×
                    if lock_is_stale(&path) {
1071×
                        let _ = fs::remove_file(&path);
1081×
                        continue;
109294×
                    }
110294×
                    if std::time::Instant::now() >= until {
1110×
                        return fail(busy);
112294×
                    }
113294×
                    std::thread::sleep(std::time::Duration::from_millis(10));
114
                }
1150×
                Err(e) => return Err(e.into()),
116
            }
117
        }
1185896×
    }
119
}
120
121
/// Parse a secondary workspace's `.oxy` file.
1229×
pub(crate) fn parse_workspace_file(text: &str) -> Option<(PathBuf, WorkspaceId)> {
1239×
    let mut dir = None;
1249×
    let mut id = None;
12518×
    for line in text.lines() {
12618×
        if let Some(v) = line.strip_prefix("oxydir:") {
1279×
            dir = Some(PathBuf::from(v.trim()));
1289×
        } else if let Some(v) = line.strip_prefix("workspace:") {
1299×
            id = WorkspaceId::from_hex(v.trim());
1309×
        }
131
    }
1329×
    Some((dir?, id?))
1339×
}
134
135
impl Repo {
1363858×
    fn workspace_dir(&self, id: &WorkspaceId) -> PathBuf {
1373858×
        self.oxy.join("workspaces").join(id.to_hex())
1383858×
    }
139
140
    /// Record where a workspace lives.
141586×
    pub(crate) fn register_workspace(&self, id: &WorkspaceId, path: &Path) -> Result<()> {
142586×
        let dir = self.workspace_dir(id);
143586×
        fs::create_dir_all(&dir)?;
144586×
        write_atomic(&dir.join("path"), path.to_string_lossy().as_bytes())
145586×
    }
146
147
    /// The directory of a workspace, if registered.
1484×
    pub fn workspace_path(&self, id: &WorkspaceId) -> Option<PathBuf> {
1494×
        fs::read_to_string(self.workspace_dir(id).join("path"))
1504×
            .ok()
1514×
            .map(|s| PathBuf::from(s.trim()))
1524×
    }
153
154
    /// Every workspace this repository knows about.
1556×
    pub fn workspaces(&self) -> Result<Vec<WorkspaceInfo>> {
1566×
        let view = self.view()?;
1576×
        let mut ids: BTreeSet<WorkspaceId> = view.workspaces.keys().copied().collect();
1586×
        if let Ok(rd) = fs::read_dir(self.oxy.join("workspaces")) {
1599×
            for e in rd.flatten() {
1609×
                if let Some(id) = WorkspaceId::from_hex(&e.file_name().to_string_lossy()) {
1619×
                    if e.path().join("path").exists() {
1629×
                        ids.insert(id);
1639×
                    }
1640×
                }
165
            }
1660×
        }
1676×
        let mut out = Vec::new();
1689×
        for id in ids {
1699×
            let path = if id == self.workspace {
1706×
                self.root.clone()
171
            } else {
1723×
                self.workspace_path(&id)
1733×
                    .unwrap_or_else(|| PathBuf::from("(unknown)"))
174
            };
1759×
            out.push(WorkspaceInfo {
1769×
                id,
1779×
                path,
1789×
                working_copy: view.workspaces.get(&id).copied(),
1799×
                current: id == self.workspace,
1809×
            });
181
        }
1826×
        Ok(out)
1836×
    }
184
185
    /// Create a workspace at `dir` checked out on `heads` (default the
186
    /// bookmark's), and return a `Repo` rooted there.
187
    /// `bookmark` is the new workspace's current bookmark (oxygen#60), by
188
    /// default this workspace's.
1896×
    pub fn workspace_add(&self, dir: &Path, heads: Option<&[ChangeHash]>) -> Result<Repo> {
1906×
        self.workspace_add_on(dir, heads, None)
1916×
    }
192
193
    /// [`Repo::workspace_add`] with the new workspace on `bookmark`.
1948×
    pub fn workspace_add_on(
1958×
        &self,
1968×
        dir: &Path,
1978×
        heads: Option<&[ChangeHash]>,
1988×
        bookmark: Option<&str>,
1998×
    ) -> Result<Repo> {
2008×
        if dir.exists() && fs::read_dir(dir)?.next().is_some() {
2010×
            return fail(format!("{} exists and is not empty", dir.display()));
2028×
        }
2038×
        fs::create_dir_all(dir)?;
2048×
        let dir = crate::portable::canonical(dir)?;
2058×
        if dir == self.root || dir.starts_with(&self.root) || self.root.starts_with(&dir) {
2061×
            return fail("a workspace cannot be inside another workspace of the same repository");
2077×
        }
2087×
        let heads = match heads {
2092×
            Some(h) => h.to_vec(),
2105×
            None => self.bookmark_heads()?,
211
        };
2127×
        let id = WorkspaceId::random();
213
        // On nothing, as the directory is, until the checkout: on `heads`,
214
        // the checkout's first snapshot found every file of the tree
215
        // deleted, rendering the whole tree to say so (195 s of a 467 s
216
        // `oxy workspace add` in the VS Code history, oxygen#137).
2177×
        let wc = self.new_working_copy(Vec::new(), "");
2187×
        let wc_hash = self.store.put_change(&wc)?;
2197×
        let (op_ref, op) = self.head_op()?;
2207×
        let mut view = op.view.clone();
2217×
        view.workspaces.insert(id, wc_hash);
2227×
        self.record_op(
2237×
            &[op_ref],
2247×
            &format!("oxy workspace add {}", dir.display()),
2257×
            view,
2260×
        )?;
2277×
        self.register_workspace(&id, &dir)?;
2287×
        self.set_workspace_bookmark(&id, bookmark.unwrap_or(&self.config.bookmark))?;
2297×
        write_atomic(
2307×
            &dir.join(OXY_DIR),
2317×
            format!(
2327×
                "oxydir: {}\nworkspace: {}\n",
2337×
                self.oxy.display(),
2347×
                id.to_hex()
2357×
            )
2367×
            .as_bytes(),
2370×
        )?;
2387×
        let repo = Repo::open(&dir)?.with_home(self.keys_dir.parent().unwrap_or(&self.keys_dir));
2397×
        repo.checkout(&heads, true, "oxy workspace add (checkout)")?;
2407×
        self.copy_subrepos_into(&repo);
2417×
        Ok(repo)
2428×
    }
243
244
    /// The new workspace's subrepos at their pins, from this workspace's
245
    /// own copies (oxygen#61): a local clone, its remote then pointed at
246
    /// the subrepo's URL, and the pin checked out, with no network unless
247
    /// this copy lacks the pinned changes. A subrepo this workspace does
248
    /// not have stays for `oxy submodule update --init`. Best effort.
2497×
    fn copy_subrepos_into(&self, new: &Repo) {
2507×
        let Ok(subs) = new.subrepos() else { return };
2517×
        let modules = new.modules_on_disk();
2527×
        for s in subs {
2531×
            let (src, dst) = (self.root.join(&s.path), new.root.join(&s.path));
2541×
            if dst.join(OXY_DIR).exists() || dst.join(".git").exists() {
2550×
                continue;
2561×
            }
2571×
            let url = modules.entries.get(&s.path).map(|m| m.url.clone());
2581×
            if let Some(parent) = dst.parent() {
2591×
                let _ = fs::create_dir_all(parent);
2601×
            }
2611×
            let _ = fs::remove_dir(&dst);
2621×
            if src.join(OXY_DIR).exists() {
2631×
                match Repo::clone(&src.to_string_lossy(), &dst) {
2641×
                    Ok(mut sub) => {
2651×
                        if let Some(u) = &url {
2661×
                            if sub.config.set("remote.origin.url", u).is_ok() {
2671×
                                let _ = sub.save_config();
2681×
                            }
2690×
                        }
270
                    }
2710×
                    Err(_) => continue,
272
                }
2730×
            } else if src.join(".git").exists() {
2740×
                let cloned = std::process::Command::new("git")
2750×
                    .args(["clone", "-q", "--local"])
2760×
                    .arg(&src)
2770×
                    .arg(&dst)
2780×
                    .status()
2790×
                    .is_ok_and(|st| st.success());
2800×
                if !cloned {
2810×
                    continue;
2820×
                }
2830×
                if let Some(u) = &url {
2840×
                    let _ = std::process::Command::new("git")
2850×
                        .arg("-C")
2860×
                        .arg(&dst)
2870×
                        .args(["remote", "set-url", "origin", u])
2880×
                        .status();
2890×
                }
290
            } else {
2910×
                continue;
292
            }
2931×
            let _ = new.sync_subrepo(&s.path, &s.pin, false);
294
        }
2957×
    }
296
297
    /// Drop a workspace from the view and registry; its files stay.
2982×
    pub fn workspace_forget(&self, id: &WorkspaceId) -> Result<()> {
2992×
        if *id == self.workspace {
3001×
            return fail("cannot forget the workspace you are in");
3011×
        }
3021×
        let (op_ref, op) = self.head_op()?;
3031×
        let mut view = op.view.clone();
3041×
        if view.workspaces.remove(id).is_none() && self.workspace_path(id).is_none() {
3050×
            return fail(format!("no workspace {}", id.short(8)));
3061×
        }
3071×
        self.record_op(
3081×
            &[op_ref],
3091×
            &format!("oxy workspace forget {}", id.short(8)),
3101×
            view,
3110×
        )?;
3121×
        let _ = fs::remove_dir_all(self.workspace_dir(id));
3131×
        Ok(())
3142×
    }
315
316
    /// Take this workspace's snapshot lock (spec §12.1).
3171618×
    pub(crate) fn lock_snapshot(&self) -> Result<SnapshotLock> {
3181618×
        self.lock_working_copy()
3191618×
    }
320
321
    /// Take this workspace's working-copy lock (spec §12.1), waiting up to
322
    /// a minute for another process's checkout or snapshot to finish.
3233267×
    pub fn lock_working_copy(&self) -> Result<WorkingCopyLock> {
3243267×
        let dir = self.workspace_dir(&self.workspace);
3253267×
        if self.bare {
326
            // No working copy to guard.
3271×
            return Ok(WorkingCopyLock {
3281×
                path: dir,
3291×
                _file: None,
3301×
            });
3313266×
        }
3323266×
        fs::create_dir_all(&dir)?;
3333266×
        let path = dir.join("lock");
3343266×
        let held = HELD.with(|h| {
3353266×
            let mut h = h.borrow_mut();
3363266×
            match h.get_mut(&path) {
3371398×
                Some(n) => {
3381398×
                    *n += 1;
3391398×
                    true
340
                }
3411868×
                None => false,
342
            }
3433266×
        });
3443266×
        if held {
3451398×
            return Ok(WorkingCopyLock { path, _file: None });
3461868×
        }
3471868×
        let file = FileLock::acquire_within(
3481868×
            path.clone(),
3491868×
            "another oxy process is using this working copy (a checkout or snapshot); try again when it finishes",
3501868×
            std::time::Duration::from_secs(60),
3510×
        )?;
3521868×
        HELD.with(|h| h.borrow_mut().insert(path.clone(), 1));
353
        // Whoever held the lock may have moved the operation head: what
354
        // is done under it starts from the head as it is now (oxygenhub#249).
3551868×
        self.forget_head();
3561868×
        Ok(WorkingCopyLock {
3571868×
            path,
3581868×
            _file: Some(file),
3591868×
        })
3603267×
    }
361
}