use camino::{Utf8Path, Utf8PathBuf};
use semver::Version;
use serde::Serialize;
use crate::domain::paths::{CI_VAR, SELF_DEPEND_OFF_VAR, UserEnv, variable};
use crate::self_depend::leftovers::{self, Leftover};
use crate::self_depend::manager::{self, Detected, Manager};
use crate::self_depend::pin;
use crate::self_depend::stamp;
use crate::self_depend::venue::Venue;
use crate::self_depend::{ENVRC, SYNC_LINE};
pub const SCHEMA: &str = "sdd.self-depend-status/1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Presence {
Present,
Absent,
}
impl Presence {
const fn of(present: bool) -> Self {
if present { Self::Present } else { Self::Absent }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Freshness {
Current,
Behind,
Ahead,
}
#[derive(Debug, Clone, Serialize)]
pub struct ManagerEntry {
pub manager: Manager,
pub present: Presence,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Utf8PathBuf>,
pub pinned: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub venue: Option<Venue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pin_lines: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub freshness: Option<Freshness>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock: Option<Presence>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locked_rev: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum State {
Ready,
Unwired,
LineAbsent,
Ambiguous,
Leftovers,
}
#[derive(Debug, Clone, Serialize)]
pub struct Host {
pub nix: bool,
pub direnv: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct Report {
pub schema: &'static str,
pub target: Utf8PathBuf,
pub state: State,
pub wired: Option<Manager>,
pub managers: Vec<ManagerEntry>,
pub envrc: Presence,
pub envrc_sync: bool,
pub stamp: Option<String>,
pub off: bool,
pub host: Host,
pub leftovers: Vec<Leftover>,
pub next: Vec<String>,
}
#[must_use]
pub fn switched_off() -> bool {
variable(CI_VAR).is_some() || variable(SELF_DEPEND_OFF_VAR).is_some()
}
fn on_path(program: &str) -> bool {
std::env::var_os("PATH")
.is_some_and(|path| std::env::split_paths(&path).any(|dir| dir.join(program).is_file()))
}
#[must_use]
pub fn report(target: &Utf8Path, env: &UserEnv, this: &Version) -> Report {
let detected = manager::detect(target);
let managers = detected
.iter()
.map(|held| entry(target, held, this))
.collect();
let wired = manager::wired(&detected);
let envrc_text = std::fs::read_to_string(target.join(ENVRC)).ok();
let envrc_sync = envrc_text
.as_deref()
.is_some_and(|text| text.contains(SYNC_LINE));
let leftovers = leftovers::find(target, &[]);
let stamp = env
.state_root()
.map(|root| stamp::path(&root.path, target))
.and_then(|path| stamp::read(&path))
.map(|day| day.to_string());
let state = match (wired.len(), envrc_sync, leftovers.is_empty()) {
(0, _, _) => State::Unwired,
(1, true, true) => State::Ready,
(1, true, false) => State::Leftovers,
(1, false, _) => State::LineAbsent,
(_, _, _) => State::Ambiguous,
};
let next = next_lines(state, &wired, target);
Report {
schema: SCHEMA,
target: target.to_owned(),
state,
wired: (wired.len() == 1).then(|| wired[0].manager),
managers,
envrc: Presence::of(envrc_text.is_some()),
envrc_sync,
stamp,
off: switched_off(),
host: Host {
nix: on_path("nix"),
direnv: on_path("direnv"),
},
leftovers,
next,
}
}
fn entry(target: &Utf8Path, held: &Detected, this: &Version) -> ManagerEntry {
let lock = held.manager.lock_file().map(|lock| {
let path = target.join(lock);
(
Presence::of(path.is_file()),
std::fs::read_to_string(&path)
.ok()
.and_then(|text| pin::locked_rev(&text)),
)
});
ManagerEntry {
manager: held.manager,
present: Presence::of(held.file.is_some()),
file: held.file.clone(),
pinned: held.pin.is_some(),
version: held.pin.as_ref().map(|pin| pin.spelled.clone()),
venue: held.pin.as_ref().and_then(|pin| pin.venue),
pin_lines: held.pin.as_ref().map(|pin| pin.lines),
freshness: held.pin.as_ref().map(|pin| match pin.version.cmp(this) {
std::cmp::Ordering::Less => Freshness::Behind,
std::cmp::Ordering::Equal => Freshness::Current,
std::cmp::Ordering::Greater => Freshness::Ahead,
}),
lock: lock.as_ref().map(|(presence, _)| *presence),
locked_rev: lock.and_then(|(_, rev)| rev),
}
}
fn next_lines(state: State, wired: &[&Detected], target: &Utf8Path) -> Vec<String> {
match state {
State::Ready => vec![format!(
"sdd self-depend sync --caller operator --target {target} moves the pin by hand"
)],
State::Unwired => vec![format!(
"sdd self-depend add --target {target} serves the fragments for the manager this project runs"
)],
State::LineAbsent => vec![format!(
"add `{SYNC_LINE}` to {ENVRC}; sdd self-depend add --target {target} prints it with its placement"
)],
State::Ambiguous => {
let names: Vec<&str> = wired.iter().map(|held| held.manager.as_str()).collect();
vec![format!(
"one target runs one mechanism; keep one of {} and remove the others' pins",
names.join(", ")
)]
}
State::Leftovers => vec![format!(
"sdd self-depend clean --target {target} --apply removes what the predecessor left"
)],
}
}