use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::{self, Error, Result};
use crate::session::{Lifecycle, SessionRequest, SessionToken};
use crate::store::{self, Resolution};
use crate::workspace::{self, ProcessStart, Record, Ref};
use crate::workspaces::{self, Bound, Overrides, Resolved};
use crate::{git, guidance, ids, lock, processes};
mod maintain;
pub use maintain::{
pool_maintain, IdentityMaintenance, IdentityOutcome, MaintainReport, SlotMaintenance,
SlotOutcome,
};
pub const SLOT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub(crate) struct Timestamp(String);
impl TryFrom<String> for Timestamp {
type Error = String;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
match ids::is_timestamp(&value) {
true => Ok(Timestamp(value)),
false => Err(format!(
"{value:?} is not a timestamp this build can order by (RFC3339, millisecond, \
UTC)"
)),
}
}
}
impl From<Timestamp> for String {
fn from(stamp: Timestamp) -> Self {
stamp.0
}
}
impl Timestamp {
fn now() -> Self {
Timestamp(ids::timestamp())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct SlotRecord {
pub version: u32,
pub number: u32,
pub identity: String,
pub execution_checkout: PathBuf,
pub created: Timestamp,
pub maintaining: Option<Claim>,
pub last_maintained: Option<Timestamp>,
pub last_outcome: Option<MaintenanceOutcome>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Claim {
pub pid: u32,
pub started: ProcessStart,
pub since: Timestamp,
}
impl Claim {
fn is_live(&self) -> bool {
workspace::process_started(self.pid) == Some(self.started)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum MaintenanceOutcome {
Succeeded,
Failed {
exit: Option<i32>,
},
TimedOut,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "kebab-case")]
pub enum SlotState {
Idle,
InUse {
session: SessionToken,
},
Maintaining {
pid: u32,
since: String,
},
Broken {
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SlotStatus {
pub number: u32,
pub path: PathBuf,
pub execution_checkout: PathBuf,
pub state: SlotState,
pub last_maintained: Option<String>,
pub last_outcome: Option<MaintenanceOutcome>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkspaceCapacity {
pub identity: String,
pub pool: u32,
pub slots: u32,
pub idle: u32,
pub in_use: u32,
pub maintaining: u32,
pub overflow: Bound,
pub overflow_in_use: u32,
pub admits: Bound,
pub admitted: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PoolStatus {
pub capacity: WorkspaceCapacity,
pub slots: Vec<SlotStatus>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PruneReport {
pub removed: Vec<u32>,
pub kept: Vec<(u32, String)>,
}
pub(crate) fn pool_dir(identity_root: &Path) -> PathBuf {
identity_root.join("pool")
}
fn placement_identity(pool: &Path) -> String {
format!("pool:{}", pool.display())
}
fn record_path(slot: &Path) -> PathBuf {
slot.join("slot.json")
}
fn read_record(
slot: &Path,
number: u32,
identity: &str,
) -> std::result::Result<SlotRecord, String> {
let path = record_path(slot);
let raw = std::fs::read_to_string(&path)
.map_err(|e| format!("its record at {} cannot be read: {e}", path.display()))?;
let record: SlotRecord = serde_json::from_str(&raw)
.map_err(|e| format!("its record at {} is malformed: {e}", path.display()))?;
if record.version != SLOT_VERSION {
return Err(format!(
"its record at {} declares version {}; this build reads version {SLOT_VERSION}",
path.display(),
record.version
));
}
if record.number != number {
return Err(format!(
"its record at {} is for slot {}, not for slot {number}",
path.display(),
record.number
));
}
if record.identity != identity {
return Err(format!(
"its record at {} belongs to {:?}, not to {identity:?}",
path.display(),
record.identity
));
}
if !record.execution_checkout.is_absolute() {
return Err(format!(
"its record at {} names an execution checkout at {}, which is not an absolute path",
path.display(),
record.execution_checkout.display()
));
}
Ok(record)
}
fn write_record(slot: &Path, record: &SlotRecord) -> Result<()> {
let path = record_path(slot);
let json = serde_json::to_string_pretty(record).map_err(error::at("serialize", &path))?;
crate::home::atomic_write(&path, &format!("{json}\n"))
}
#[derive(Debug, Clone)]
struct Surveyed {
number: u32,
dir: PathBuf,
record: Option<SlotRecord>,
state: SlotState,
}
impl Surveyed {
fn clone_dir(&self) -> PathBuf {
self.dir.join("clone")
}
fn worktree(&self) -> PathBuf {
self.dir.join("worktree")
}
fn lender(&self) -> Option<&Path> {
self.record
.as_ref()
.map(|record| record.execution_checkout.as_path())
}
fn takeable(&self) -> bool {
matches!(self.state, SlotState::Idle | SlotState::Broken { .. })
}
fn status(&self) -> SlotStatus {
SlotStatus {
number: self.number,
path: self.dir.clone(),
execution_checkout: self.lender().map(Path::to_path_buf).unwrap_or_default(),
state: self.state.clone(),
last_maintained: self
.record
.as_ref()
.and_then(|record| record.last_maintained.clone())
.map(String::from),
last_outcome: self.record.as_ref().and_then(|record| record.last_outcome),
}
}
}
struct Survey {
pool: PathBuf,
slots: Vec<Surveyed>,
open: Vec<Record>,
}
impl Survey {
fn overflow(&self) -> Vec<&Record> {
self.open
.iter()
.filter(|record| record.slot.is_none())
.collect()
}
fn count(&self, wanted: impl Fn(&SlotState) -> bool) -> u32 {
u32::try_from(self.slots.iter().filter(|slot| wanted(&slot.state)).count())
.unwrap_or(u32::MAX)
}
}
fn survey(identity: &str, pool: &Path) -> Result<Survey> {
let open: Vec<Record> = workspace::all()?
.into_iter()
.filter(|record| record.identity == identity && record.state == Lifecycle::Open)
.collect();
let mut slots = Vec::new();
let entries = match std::fs::read_dir(pool) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(Survey {
pool: pool.to_path_buf(),
slots,
open,
})
}
Err(e) => return Err(error::at("read the pool at", pool)(e)),
};
for entry in entries {
let entry = entry.map_err(error::at("read the pool at", pool))?;
let dir = entry.path();
let Some(number) = entry
.file_name()
.to_str()
.and_then(|name| name.parse::<u32>().ok())
.filter(|number| *number > 0)
else {
continue;
};
if !dir.is_dir() {
continue;
}
let (record, state) = state_of(&dir, number, identity, &open);
slots.push(Surveyed {
number,
dir,
record,
state,
});
}
slots.sort_by_key(|slot| slot.number);
Ok(Survey {
pool: pool.to_path_buf(),
slots,
open,
})
}
fn state_of(
dir: &Path,
number: u32,
identity: &str,
open: &[Record],
) -> (Option<SlotRecord>, SlotState) {
let record = match read_record(dir, number, identity) {
Ok(record) => record,
Err(reason) => return (None, SlotState::Broken { reason }),
};
let clone = dir.join("clone");
let worktree = dir.join("worktree");
if !git::is_repo(&clone) {
return (
Some(record),
SlotState::Broken {
reason: format!(
"its clone at {} is missing or not a repository",
clone.display()
),
},
);
}
if !git::is_repo(&worktree) {
return (
Some(record),
SlotState::Broken {
reason: format!(
"its worktree at {} is missing or not a repository",
worktree.display()
),
},
);
}
if let Some(holder) = open.iter().find(|held| held.run_root == dir) {
return (
Some(record),
SlotState::InUse {
session: SessionToken(holder.token.to_string()),
},
);
}
if let Some(claim) = record.maintaining.clone() {
if claim.is_live() {
return (
Some(record),
SlotState::Maintaining {
pid: claim.pid,
since: claim.since.into(),
},
);
}
let mut cleared = record.clone();
cleared.maintaining = None;
let _ = write_record(dir, &cleared);
return (Some(cleared), SlotState::Idle);
}
(Some(record), SlotState::Idle)
}
fn retained_branches(
clone: &Path,
lender: Option<&Path>,
) -> std::result::Result<Vec<String>, String> {
if !git::is_repo(clone) {
return Ok(Vec::new());
}
let branches = git::branches(clone).map_err(|e| e.to_string())?;
Ok(branches
.into_iter()
.filter(|branch| {
let reached = match (git::tip(clone, &format!("refs/heads/{branch}")), lender) {
(Some(tip), Some(lender)) => git::refs_reach(lender, &tip),
_ => false,
};
!reached
})
.collect())
}
fn keeps(slot: &Surveyed, judge: &Path) -> Option<String> {
match &slot.state {
SlotState::InUse { session } => {
return Some(format!("session {} is working in it", session.0));
}
SlotState::Maintaining { pid, since } => {
return Some(format!(
"a maintenance run (pid {pid}) has claimed it since {since}"
));
}
SlotState::Idle | SlotState::Broken { .. } => {}
}
let lender = slot.lender().unwrap_or(judge);
let retained = match retained_branches(&slot.clone_dir(), Some(lender)) {
Ok(retained) => retained,
Err(reason) => {
return Some(format!(
"its clone could not be asked what it retains, and a count nobody got is not \
a count of none: {reason}"
))
}
};
if retained.is_empty() {
return None;
}
let names: Vec<&str> = retained.iter().map(String::as_str).collect();
Some(format!(
"its clone retains {} — {} its lender {} would not take, and removing the slot is \
what would make {} unreachable",
guidance::listed(&names),
match retained.len() {
1 => "a branch",
_ => "branches",
},
lender.display(),
match retained.len() {
1 => "it",
_ => "them",
},
))
}
fn remove(slot: &Surveyed) -> std::result::Result<(), String> {
let Some(_exclusive) = lock::try_exclusive(&workspace::occupancy_identity(&slot.dir))
.map_err(|e| e.to_string())?
else {
return Err("a command is working in it right now".to_owned());
};
std::fs::remove_dir_all(&slot.dir).map_err(|e| format!("it could not be removed: {e}"))
}
fn shed(survey: &mut Survey, file_pool: u32, judge: &Path) {
let mut removed = Vec::new();
for slot in sheddable(survey, file_pool, judge) {
if remove(slot).is_ok() {
removed.push(slot.number);
}
}
survey.slots.retain(|slot| !removed.contains(&slot.number));
}
fn sheddable<'a>(survey: &'a Survey, file_pool: u32, judge: &Path) -> Vec<&'a Surveyed> {
let mut surplus = u32::try_from(survey.slots.len())
.unwrap_or(u32::MAX)
.saturating_sub(file_pool);
let mut shed = Vec::new();
for slot in survey.slots.iter().rev() {
if surplus == 0 {
break;
}
if !slot.takeable() || keeps(slot, judge).is_some() {
continue;
}
shed.push(slot);
surplus -= 1;
}
shed
}
#[derive(Debug)]
pub(crate) enum Placement {
Slot {
number: u32,
dir: PathBuf,
cut: Cut,
_held: lock::Guard,
_serial: lock::Guard,
},
RunRoot {
_serial: Option<lock::Guard>,
},
}
pub(crate) struct Ask<'a> {
pub resolution: &'a Resolution,
pub execution: &'a Path,
pub resolved: &'a Resolved,
pub pinned: Option<&'a Ref>,
pub identity_root: &'a Path,
}
pub(crate) fn place(ask: &Ask<'_>) -> Result<Placement> {
let pool = pool_dir(ask.identity_root);
crate::home::ensure_dir(&pool)?;
let serial = lock::exclusive(&placement_identity(&pool))?;
let mut survey = survey(&ask.resolution.key, &pool)?;
shed(&mut survey, ask.resolved.file_pool, ask.execution);
let wanted = ask.resolved.pool.value;
if wanted > 0 {
let taken = match take_idle(&survey, ask)? {
Some(taken) => Some(taken),
None => match recreate_broken(&survey, ask)? {
Some(recreated) => Some(recreated),
None if u32::try_from(survey.slots.len()).unwrap_or(u32::MAX) < wanted => {
Some(cut_new(&survey, ask)?)
}
None => None,
},
};
if let Some(taken) = taken {
return Ok(Placement::Slot {
number: taken.number,
dir: taken.dir.clone(),
cut: taken.cut,
_held: taken.held,
_serial: serial,
});
}
}
let overflow_in_use = u32::try_from(survey.overflow().len()).unwrap_or(u32::MAX);
if ask.resolved.overflow.value.admits(overflow_in_use) {
return Ok(Placement::RunRoot {
_serial: Some(serial),
});
}
Err(exhausted(ask, &survey))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Cut {
Fresh,
KeptClone,
Warm,
}
impl Cut {
pub(crate) fn created(self) -> bool {
self != Cut::Warm
}
}
struct Taken {
number: u32,
dir: PathBuf,
cut: Cut,
held: lock::Guard,
}
fn take_idle(survey: &Survey, ask: &Ask<'_>) -> Result<Option<Taken>> {
let preferred = match ask.pinned {
Some(branch) => last_slot_of(&ask.resolution.key, branch)?,
None => None,
};
let mut candidates: Vec<&Surveyed> = survey
.slots
.iter()
.filter(|slot| slot.state == SlotState::Idle && slot.lender() == Some(ask.execution))
.collect();
candidates.sort_by_key(|slot| (Some(slot.number) != preferred, slot.number));
for slot in candidates {
let Some(held) = lock::try_exclusive(&workspace::occupancy_identity(&slot.dir))? else {
continue;
};
if !returned(&slot.worktree()) && !processes::holding(&slot.dir).is_empty() {
continue;
}
return Ok(Some(Taken {
number: slot.number,
dir: slot.dir.clone(),
cut: Cut::Warm,
held,
}));
}
Ok(None)
}
fn recreate_broken(survey: &Survey, ask: &Ask<'_>) -> Result<Option<Taken>> {
for slot in &survey.slots {
if !matches!(slot.state, SlotState::Broken { .. }) {
continue;
}
let clone = slot.clone_dir();
let worktree = slot.worktree();
let lender = slot
.lender()
.map(Path::to_path_buf)
.or_else(|| lender_of(&clone));
if lender.is_some_and(|lender| lender != ask.execution) {
continue;
}
let Some(held) = lock::try_exclusive(&workspace::occupancy_identity(&slot.dir))? else {
continue;
};
let cut = if git::is_repo(&clone) {
if git::is_repo(&worktree) {
Cut::Warm
} else {
if worktree.exists() {
std::fs::remove_dir_all(&worktree)
.map_err(error::at("remove the broken worktree at", &worktree))?;
}
git::worktree_prune(&clone)?;
Cut::KeptClone
}
} else {
std::fs::remove_dir_all(&slot.dir)
.map_err(error::at("remove the broken slot at", &slot.dir))?;
std::fs::create_dir(&slot.dir).map_err(error::at("create", &slot.dir))?;
Cut::Fresh
};
write_record(&slot.dir, &fresh_record(slot.number, ask))?;
return Ok(Some(Taken {
number: slot.number,
dir: slot.dir.clone(),
cut,
held,
}));
}
Ok(None)
}
fn lender_of(clone: &Path) -> Option<PathBuf> {
let alternates = git::objects_dir(clone).ok()?.join("info/alternates");
let recorded = std::fs::read_to_string(alternates).ok()?;
let objects = PathBuf::from(recorded.lines().next()?.trim());
let git_dir = objects
.file_name()
.is_some_and(|name| name == "objects")
.then(|| objects.parent())
.flatten()?;
git_dir
.file_name()
.is_some_and(|name| name == ".git")
.then(|| git_dir.parent().map(Path::to_path_buf))
.flatten()
}
fn cut_new(survey: &Survey, ask: &Ask<'_>) -> Result<Taken> {
let mut number: u32 = 1;
loop {
if survey.slots.iter().any(|slot| slot.number == number) {
number += 1;
continue;
}
let dir = survey.pool.join(number.to_string());
match std::fs::create_dir(&dir) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
number += 1;
continue;
}
Err(e) => return Err(error::at("create", &dir)(e)),
}
let held = lock::try_exclusive(&workspace::occupancy_identity(&dir))?.ok_or_else(|| {
error::invalid(format!(
"the new slot {} is already occupied before it was cut",
dir.display()
))
})?;
write_record(&dir, &fresh_record(number, ask))?;
return Ok(Taken {
number,
dir,
cut: Cut::Fresh,
held,
});
}
}
fn fresh_record(number: u32, ask: &Ask<'_>) -> SlotRecord {
SlotRecord {
version: SLOT_VERSION,
number,
identity: ask.resolution.key.clone(),
execution_checkout: ask.execution.to_path_buf(),
created: Timestamp::now(),
maintaining: None,
last_maintained: None,
last_outcome: None,
}
}
fn last_slot_of(identity: &str, branch: &Ref) -> Result<Option<u32>> {
let mut newest: Option<(std::time::SystemTime, u32)> = None;
for record in workspace::all()? {
let Some(number) = record.slot else {
continue;
};
if record.identity != identity
|| record.branch != *branch
|| record.state != Lifecycle::Closed
{
continue;
}
let written = workspace::record_path(&record.token)
.ok()
.and_then(|path| std::fs::metadata(path).ok())
.and_then(|meta| meta.modified().ok())
.unwrap_or(std::time::UNIX_EPOCH);
if newest.is_none_or(|(when, _)| written > when) {
newest = Some((written, number));
}
}
Ok(newest.map(|(_, number)| number))
}
pub(crate) fn returned(worktree: &Path) -> bool {
git::current_branch(worktree).is_ok_and(|branch| branch == workspace::DETACHED_HEAD)
&& git::is_dirty(worktree).is_ok_and(|dirty| !dirty)
}
fn exhausted(ask: &Ask<'_>, survey: &Survey) -> Error {
let resolved = ask.resolved;
let slots = survey.slots.len();
let idle = survey.count(|state| *state == SlotState::Idle);
let in_use = survey.count(|state| matches!(state, SlotState::InUse { .. }));
let maintaining = survey.count(|state| matches!(state, SlotState::Maintaining { .. }));
let overflow = survey.overflow();
let mut holders: Vec<String> = Vec::new();
for slot in &survey.slots {
match &slot.state {
SlotState::InUse { session } => {
let held = survey
.open
.iter()
.find(|record| *record.token == *session.0);
holders.push(match held {
Some(record) => format!("slot {}: {}", slot.number, describe(record)),
None => format!("slot {}: session {}", slot.number, session.0),
});
}
SlotState::Maintaining { pid, since } => holders.push(format!(
"slot {}: a maintenance run, pid {pid}, since {since}",
slot.number
)),
SlotState::Idle => holders.push(format!(
"slot {}: idle, bound to {}",
slot.number,
slot.lender()
.map(|lender| lender.display().to_string())
.unwrap_or_else(|| "(unknown)".to_owned())
)),
SlotState::Broken { reason } => {
holders.push(format!("slot {}: broken — {reason}", slot.number));
}
}
}
for record in &overflow {
holders.push(format!("under runs/: {}", describe(record)));
}
let file = workspaces::default_path()
.map(|path| path.display().to_string())
.unwrap_or_else(|_| "$ONEVCS_HOME/workspaces.yml".to_owned());
Error::PoolExhausted {
reason: format!(
"no session of {identity} can be placed now: its pool is {pool} (from {pool_from}) — \
{slots} created, {idle} idle, {in_use} in use, {maintaining} maintaining — and its \
overflow is {overflow} (from {overflow_from}) with {overflow_in_use} in use. \
{holding}. Close a session, raise pool or overflow for this identity in {file}, \
or open this one with `--overflow unlimited`",
identity = ask.resolution.key,
pool = resolved.pool.value,
pool_from = resolved.pool.from,
overflow = resolved.overflow.value,
overflow_from = resolved.overflow.from,
overflow_in_use = overflow.len(),
holding = match holders.is_empty() {
true => "Nothing holds it".to_owned(),
false => format!("Held by: {}", holders.join("; ")),
},
),
}
}
fn describe(record: &Record) -> String {
format!(
"session {} on {:?} in {}, opened by pid {} ({})",
record.token,
record.branch,
record.worktree.display(),
record.owner_pid,
record.liveness().as_str(),
)
}
struct Asked {
resolution: Resolution,
execution: PathBuf,
resolved: Resolved,
identity_root: PathBuf,
}
fn asked(request: &SessionRequest) -> Result<Asked> {
let registry = store::load()?;
let resolution = store::resolve(®istry, &request.repo)?;
let execution = workspace::execution_checkout(
®istry,
&resolution,
request.execution_checkout.as_deref(),
)?;
let resolved = workspaces::resolve_for(
&resolution,
Overrides {
pool: request.pool,
overflow: request.overflow,
},
)?;
let identity_root = workspace::identity_dir(&resolution.key)?;
Ok(Asked {
resolution,
execution,
resolved,
identity_root,
})
}
fn capacity_of(asked: &Asked, survey: &Survey, request: &SessionRequest) -> WorkspaceCapacity {
let resolved = &asked.resolved;
let slots = u32::try_from(survey.slots.len()).unwrap_or(u32::MAX);
let idle = survey.count(|state| *state == SlotState::Idle);
let takeable =
survey.count(|state| matches!(state, SlotState::Idle | SlotState::Broken { .. }));
let in_use = survey.count(|state| matches!(state, SlotState::InUse { .. }));
let maintaining = survey.count(|state| matches!(state, SlotState::Maintaining { .. }));
let overflow_in_use = u32::try_from(survey.overflow().len()).unwrap_or(u32::MAX);
let shed = u32::try_from(sheddable(survey, resolved.file_pool, &asked.execution).len())
.unwrap_or(u32::MAX);
let takeable_after = takeable.saturating_sub(shed);
let slots_after = slots.saturating_sub(shed);
let can_create = resolved.pool.value.saturating_sub(slots_after);
let admits = match resolved.pool.value {
0 => resolved.overflow.value.headroom(overflow_in_use),
_ => match resolved.overflow.value.headroom(overflow_in_use) {
Bound::Unlimited => Bound::Unlimited,
Bound::Bounded(room) => Bound::Bounded(
takeable_after
.saturating_add(can_create)
.saturating_add(room),
),
},
};
let resumes = request.branch.as_ref().is_some_and(|pinned| {
survey.open.iter().any(|record| {
*record.branch == **pinned
&& record.execution_checkout == asked.execution
&& record.run_root.is_dir()
&& record.clone.is_dir()
})
});
let mine = survey.slots.iter().any(|slot| {
slot.takeable() && slot.lender().is_none_or(|lender| lender == asked.execution)
});
let admitted = resumes
|| (resolved.pool.value > 0 && (mine || can_create > 0))
|| resolved.overflow.value.admits(overflow_in_use);
WorkspaceCapacity {
identity: asked.resolution.key.clone(),
pool: resolved.pool.value,
slots,
idle,
in_use,
maintaining,
overflow: resolved.overflow.value,
overflow_in_use,
admits,
admitted,
}
}
pub fn workspace_capacity(request: &SessionRequest) -> Result<WorkspaceCapacity> {
let asked = asked(request)?;
let survey = survey(&asked.resolution.key, &pool_dir(&asked.identity_root))?;
Ok(capacity_of(&asked, &survey, request))
}
pub fn pool_status(repo: &str) -> Result<PoolStatus> {
let request = SessionRequest {
repo: repo.to_owned(),
branch: None,
base: None,
execution_checkout: None,
pool: None,
overflow: None,
};
let asked = asked(&request)?;
let survey = survey(&asked.resolution.key, &pool_dir(&asked.identity_root))?;
Ok(PoolStatus {
capacity: capacity_of(&asked, &survey, &request),
slots: survey.slots.iter().map(Surveyed::status).collect(),
})
}
pub fn pool_prune(repo: &str) -> Result<PruneReport> {
let request = SessionRequest {
repo: repo.to_owned(),
branch: None,
base: None,
execution_checkout: None,
pool: None,
overflow: None,
};
let asked = asked(&request)?;
let pool = pool_dir(&asked.identity_root);
let mut report = PruneReport {
removed: Vec::new(),
kept: Vec::new(),
};
if !pool.is_dir() {
return Ok(report);
}
let _serial = lock::exclusive(&placement_identity(&pool))?;
let survey = survey(&asked.resolution.key, &pool)?;
for slot in &survey.slots {
if let Some(why) = keeps(slot, &asked.resolution.publication) {
report.kept.push((slot.number, why));
continue;
}
match remove(slot) {
Ok(()) => report.removed.push(slot.number),
Err(why) => report.kept.push((slot.number, why)),
}
}
Ok(report)
}