fn print_text(value: &str) {
if value.is_empty() {
return;
}
print!("{value}");
if !value.ends_with('\n') {
println!();
}
}
fn safe_terminal(value: &str) -> String {
crate::text::visible_controls(value)
}
fn relative_display(path: &Path, root: &Path) -> String {
path.strip_prefix(root)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/")
}
fn durable_report_path(repo_root: &Path) -> PathBuf {
config::latest_dir(repo_root).join("report.json")
}
fn ephemeral_report_path(repo_root: &Path) -> Option<PathBuf> {
config::git_runtime_dir(repo_root)
.ok()
.map(|path| path.join("ephemeral/latest/report.json"))
}
fn default_report_path(repo_root: &Path) -> PathBuf {
let durable = durable_report_path(repo_root);
if durable.exists() {
return durable;
}
ephemeral_report_path(repo_root)
.filter(|path| path.exists())
.unwrap_or(durable)
}
fn default_report_locations(repo_root: &Path) -> Vec<PathBuf> {
let mut locations = vec![durable_report_path(repo_root)];
if let Some(path) = ephemeral_report_path(repo_root) {
locations.push(path);
}
locations
}
fn load_report_at(path: &Path) -> Result<Option<Value>> {
if !path.exists() {
return Ok(None);
}
report::load_report(path).map(Some)
}
fn load_default_report(
repo_root: &Path,
explicit_report: Option<&Path>,
) -> Result<Option<(Value, PathBuf)>> {
let path = explicit_report
.map(Path::to_path_buf)
.unwrap_or_else(|| default_report_path(repo_root));
Ok(load_report_at(&path)?.map(|report| (report, path)))
}
fn report_or_missing(repo_root: &Path, explicit_report: Option<&Path>) -> Result<(Value, PathBuf)> {
let fallback = explicit_report
.map(Path::to_path_buf)
.unwrap_or_else(|| default_report_path(repo_root));
let loaded = load_default_report(repo_root, explicit_report).map_err(|error| {
ClassifiedError::new(ErrorKind::Contract, "report_invalid", format!("{error:#}"))
.at("/report")
.with_details(json!({"path": fallback}))
})?;
let loaded = loaded.ok_or_else(|| -> anyhow::Error {
let searched = explicit_report.map_or_else(
|| default_report_locations(repo_root),
|path| vec![path.to_path_buf()],
);
let searched_display = searched
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
ClassifiedError::new(
ErrorKind::Contract,
"report_not_found",
format!(
"Report not found. Searched: {searched_display}\nRun `git slop find` to generate a Git-private first-run report, or `git slop init` first for durable reports."
),
)
.at("/report")
.with_details(json!({"path": fallback, "searched": searched}))
.into()
})?;
if explicit_report.is_none() && !repo_root.as_os_str().is_empty() {
match crate::freshness::evaluate(repo_root, &loaded.0) {
Ok(freshness) if !freshness.current => eprintln!(
"git-slop: warning: latest report is stale ({}); run `git slop find`.",
freshness.reason_codes()
),
Err(error) => eprintln!(
"git-slop: warning: latest report currentness could not be checked: {error:#}"
),
Ok(_) => {}
}
}
Ok(loaded)
}
fn report_or_missing_with_currentness(
repo_root: &Path,
explicit_report: Option<&Path>,
require_current: bool,
) -> Result<(Value, PathBuf)> {
let loaded = report_or_missing(repo_root, explicit_report)?;
if require_current {
require_current_report(repo_root, &loaded.0)?;
}
Ok(loaded)
}