1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//! Where wsb keeps what it knows about a worktree and never commits.
//!
//! A worktree's `.wsb/` is committed, all of it ([`wsb_prj::settings`]). What
//! wsb keeps beside it for itself lives elsewhere: its test catalogue and
//! workbench layout in the workshop's one database ([`crate::store`]), keyed
//! by [`LocalState::key`], and its files — logs, the lock its writers take —
//! in a folder under `$PRJ/.state`:
//!
//! * a worktree of a workspace in the workshop:
//! `$PRJ/.state/workspaces/<project>/worktrees/<worktree>/`, kept by name,
//! so a worktree removed and made again under the same name finds it;
//! * a checkout outside the workshop, or on a machine with no `$PRJ`:
//! `<machine dir>/outside/<key>/`, keyed by the checkout's resolved path,
//! because such a path only means anything on the machine it is on.
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
/// The folder of a machine's state that holds checkouts outside the workshop.
pub const OUTSIDE_DIR: &str = "outside";
/// One worktree's local state.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalState {
dir: PathBuf,
key: String,
}
impl LocalState {
/// The local state of the worktree checked out at `worktree_root`.
///
/// A path in a workspace but in none of its worktrees is refused: it is
/// no worktree, and filing state for it would invent one.
pub fn of(worktree_root: &Path) -> Result<Self> {
let (dir, key) = match wsb_prj::Workshop::from_env() {
Ok(workshop) => match workshop.locate(worktree_root) {
Ok(wsb_prj::Location {
workspace,
worktree: Some(worktree),
}) => (workspace.worktree_state_dir(&worktree), format!("{}/{worktree}", workspace.name())),
Ok(wsb_prj::Location { workspace, worktree: None }) => bail!(
"{} is in the workspace of {} but in none of its worktrees, so it has no local state",
worktree_root.display(),
workspace.name()
),
Err(wsb_prj::WorkshopError::Outside { .. }) => outside(worktree_root)?,
Err(other) => return Err(other.into()),
},
Err(wsb_prj::WorkshopError::NotSet) => outside(worktree_root)?,
Err(other) => return Err(other.into()),
};
Ok(Self { dir, key })
}
/// What the workshop's database names this worktree by:
/// `<workspace>/<worktree>` for one of the workshop's,
/// `outside/<digest>` for a checkout elsewhere.
pub fn key(&self) -> &str {
&self.key
}
/// The folder itself.
pub fn dir(&self) -> &Path {
&self.dir
}
/// The logs of what ran in the worktree.
pub fn logs(&self) -> PathBuf {
self.dir.join("logs")
}
/// The build folder the stamp uses when `stamp.json` names none.
pub fn build(&self) -> PathBuf {
self.dir.join("build")
}
/// The lock wsb's writers of the worktree take
/// ([`crate::worktree_lock`]).
pub fn lock(&self) -> PathBuf {
self.dir.join("worktree.lock")
}
/// The folder each process writing the worktree registers in.
pub fn writers(&self) -> PathBuf {
self.dir.join("worktree.writers")
}
}
/// `<machine dir>/outside/<digest>` for a checkout outside the workshop, and
/// its key.
fn outside(checkout: &Path) -> Result<(PathBuf, String)> {
let resolved = crate::platform::canonicalize(checkout)
.with_context(|| format!("resolving {}", checkout.display()))?;
let machine = crate::daemon::machine_dir().map_err(anyhow::Error::msg)?;
let digest = wsb_prj::short_digest(&resolved.to_string_lossy());
Ok((machine.join(OUTSIDE_DIR).join(&digest), format!("{OUTSIDE_DIR}/{digest}")))
}
#[cfg(test)]
mod tests;