use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::{self, Error, Result};
use crate::{home, ids, lock};
#[derive(Debug)]
pub struct Lease {
record: PathBuf,
_held: lock::Guard,
}
impl Drop for Lease {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.record);
}
}
#[derive(Debug, Serialize, Deserialize)]
struct Record {
identity: String,
branch: String,
run_root: PathBuf,
worktree: PathBuf,
}
fn lease_of(run_root: &Path) -> String {
format!("publication:{}", run_root.display())
}
fn checked(record: Record, path: &Path) -> Result<Record> {
let problem = if !record.run_root.is_absolute() {
Some("its run root is not an absolute path")
} else if !record.worktree.is_absolute() {
Some("its workspace is not an absolute path")
} else if !record.worktree.starts_with(&record.run_root) {
Some("its workspace is not inside its run root")
} else {
None
};
match problem {
None => Ok(record),
Some(problem) => Err(Error::Invalid {
reason: format!(
"the publication record at {} is not one onevcs wrote: {problem} (run root \
{}, workspace {}). Remove it; a publication that is running rewrites \
nothing there, and one that is not is what it would have reported",
path.display(),
record.run_root.display(),
record.worktree.display(),
),
}),
}
}
fn branch_dir(identity: &str, branch: &str) -> Result<PathBuf> {
Ok(home::publishing_dir()?.join(ids::digest(&format!("{identity}\n{branch}"))))
}
impl Lease {
pub fn take(identity: &str, branch: &str, run_root: &Path, worktree: &Path) -> Result<Self> {
let run_root = std::path::absolute(run_root)
.map_err(error::at("resolve the publication run root", run_root))?;
let lease = lease_of(&run_root);
let held = lock::try_shared(&lease)?.ok_or_else(|| Error::Invalid {
reason: format!(
"the publication lease for {} is already held; nothing else should hold the \
lease of a run root this command just cut, so the state root is being written \
to by something other than onevcs",
run_root.display(),
),
})?;
let worktree = std::path::absolute(worktree)
.map_err(error::at("resolve the publication workspace", worktree))?;
let record = branch_dir(identity, branch)?.join(format!("{}.json", ids::digest(&lease)));
let document = serde_json::to_string(&Record {
identity: identity.to_owned(),
branch: branch.to_owned(),
run_root,
worktree,
})
.map_err(|error| error::invalid(error.to_string()))?;
home::atomic_write(&record, &document)?;
Ok(Self {
record,
_held: held,
})
}
}
pub fn running(identity: &str, branch: &str) -> Result<Option<PathBuf>> {
let dir = branch_dir(identity, branch)?;
let entries = match std::fs::read_dir(&dir) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error::at("list the publications in", &dir)(error)),
};
let mut records: Vec<PathBuf> = entries
.map(|entry| entry.map(|entry| entry.path()))
.collect::<std::io::Result<_>>()
.map_err(error::at("list the publications in", &dir))?;
records.retain(|path| {
path.extension()
.is_some_and(|extension| extension == "json")
&& !path
.file_name()
.is_some_and(|name| name.to_string_lossy().starts_with('.'))
});
records.sort();
for path in records {
let raw = match std::fs::read_to_string(&path) {
Ok(raw) => raw,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(error::at("read the publication record at", &path)(error)),
};
let record: Record = serde_json::from_str(&raw)
.map_err(error::at("read the publication record at", &path))?;
if record.identity != identity || record.branch != branch {
continue;
}
let record = checked(record, &path)?;
if lock::is_occupied(&lease_of(&record.run_root))? {
return Ok(Some(record.worktree));
}
}
Ok(None)
}