use crate::template::{GitRunner, descriptions};
use crate::workspace::{self, SKILLS_DIR};
use std::collections::BTreeSet;
use std::path::Path;
const FORMAT: &str = "--format=%ct %cr";
const ARCHIVED: &str = descriptions::ARCHIVED_SUBDIR;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Owner {
Pool,
Workspace,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum State {
Active,
Claimed,
Unused,
Archived,
}
#[derive(Debug)]
pub(crate) struct Stamp {
secs: i64,
age: String,
}
#[derive(Debug)]
pub(crate) struct Row {
pub(crate) name: String,
pub(crate) owner: Owner,
pub(crate) state: State,
last_use: Option<Stamp>,
last_patch: Option<Stamp>,
}
pub(crate) fn census(ws: &Path, commit: &str, data_root: &Path, git: &dyn GitRunner) -> Vec<Row> {
let archived = tree_names(ws, commit, &format!("{SKILLS_DIR}/{ARCHIVED}"), git);
let mut committed = tree_names(ws, commit, SKILLS_DIR, git);
committed.remove(ARCHIVED);
let pooled = pool_names(&data_root.join(descriptions::SKILLS_SUBDIR));
let mut rows: Vec<Row> = archived
.iter()
.map(|n| (n, Owner::Workspace))
.chain(committed.iter().map(|n| (n, Owner::Workspace)))
.chain(pooled.iter().map(|n| (n, Owner::Pool)))
.map(|(name, owner)| row(ws, commit, name, owner, &archived, git))
.collect();
order(&mut rows);
rows
}
fn order(rows: &mut [Row]) {
rows.sort_by(|a, b| key(a).cmp(&key(b)));
}
fn key(r: &Row) -> (Option<i64>, &str) {
(r.last_use.as_ref().map(|s| s.secs), r.name.as_str())
}
fn row(
ws: &Path,
commit: &str,
name: &str,
owner: Owner,
archived: &BTreeSet<String>,
git: &dyn GitRunner,
) -> Row {
let body = format!("{SKILLS_DIR}/{name}");
let stored = format!("{SKILLS_DIR}/{ARCHIVED}/{name}");
let last_use = stamp(
ws,
git,
&[
"log",
"--diff-filter=A",
"--max-count=1",
FORMAT,
"--branches=agents/*",
"--not",
"--branches=config/*",
"--",
&body,
],
);
let last_patch = stamp(
ws,
git,
&[
"log",
"--max-count=1",
FORMAT,
"--branches=config/*",
"--",
&body,
&stored,
],
);
let state = if archived.contains(name) {
State::Archived
} else if last_use.is_some() {
State::Active
} else if tool_claimed(ws, commit, name, git) {
State::Claimed
} else {
State::Unused
};
Row {
name: name.to_owned(),
owner,
state,
last_use,
last_patch,
}
}
fn tool_claimed(ws: &Path, commit: &str, name: &str, git: &dyn GitRunner) -> bool {
let path = format!(
"{}/{}/{name}.json",
descriptions::DESCRIPTIONS_DIR,
descriptions::TOOLS_SUBDIR
);
workspace::control_exists(ws, commit, &path, git)
}
fn stamp(ws: &Path, git: &dyn GitRunner, args: &[&str]) -> Option<Stamp> {
let out = git
.run_capture(&workspace::repo_git(ws), args)
.unwrap_or_default();
let (secs, age) = out.lines().next()?.trim().split_once(' ')?;
Some(Stamp {
secs: secs.parse().ok()?,
age: age.to_owned(),
})
}
fn tree_names(ws: &Path, commit: &str, path: &str, git: &dyn GitRunner) -> BTreeSet<String> {
let spec = format!("{commit}:{path}");
git.run_capture(&workspace::repo_git(ws), &["ls-tree", "--name-only", &spec])
.unwrap_or_default()
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(str::to_owned)
.collect()
}
fn pool_names(pool: &Path) -> BTreeSet<String> {
match std::fs::read_dir(pool) {
Ok(rd) => rd
.flatten()
.filter(|e| e.path().is_dir())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n != ARCHIVED)
.collect(),
Err(_) => BTreeSet::new(),
}
}
mod render;
pub(crate) use render::render;
#[cfg(test)]
mod tests;