use std::io::{BufRead, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Args, ValueEnum};
use dre_core::runs::BindingRuns;
use serde_json::{Value, json};
use crate::exit;
#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum HistoryOutput {
Text,
Json,
}
#[derive(Args)]
pub struct HistoryArgs {
report: String,
#[arg(long)]
binding: Option<String>,
#[arg(long)]
latest: bool,
#[arg(long)]
path: bool,
#[arg(long, value_enum, default_value = "text")]
output: HistoryOutput,
#[arg(long, default_value = ".")]
project_dir: PathBuf,
#[arg(long, value_name = "PATH")]
target_path: Option<String>,
}
#[derive(Args)]
pub struct UnlockArgs {
report: String,
#[arg(long, default_value = "default")]
binding: String,
#[arg(long)]
yes: bool,
#[arg(long, default_value = ".")]
project_dir: PathBuf,
#[arg(long, value_name = "PATH")]
target_path: Option<String>,
}
fn report_dir(project_dir: &Path, target_path: Option<&str>, report: &str) -> Result<PathBuf, String> {
let from_file = dre_core::target::project_value(project_dir);
let t = dre_core::target::resolve(project_dir, target_path, from_file.as_deref())?;
Ok(t.dir.join("run").join(report))
}
fn shown(project_dir: &Path, p: &Path) -> String {
let p = p.strip_prefix(project_dir).unwrap_or(p);
dre_core::slash(p).display().to_string()
}
pub fn history(a: HistoryArgs) -> ExitCode {
let mask_secrets = std::fs::read_to_string(a.project_dir.join(dre_core::project::PROJECT_FILE))
.ok()
.and_then(|text| dre_core::config::node::parse(&text).ok())
.and_then(|node| node.get("mask_secrets").map(|v| v.to_json()))
.and_then(|value| value.as_bool())
.unwrap_or(true);
dre_core::secrets::set_enabled(mask_secrets);
let dir = match report_dir(&a.project_dir, a.target_path.as_deref(), &a.report) {
Ok(d) => d,
Err(e) => {
eprintln!("error: {e}");
return exit::not_started();
}
};
let mut bindings: Vec<String> = match &a.binding {
Some(b) => vec![b.clone()],
None => std::fs::read_dir(&dir)
.into_iter()
.flatten()
.flatten()
.filter(|e| e.path().is_dir())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect(),
};
bindings.sort();
let mut rows: Vec<Value> = Vec::new();
for b in &bindings {
let runs = BindingRuns::new(&dir.join(b));
let current = runs.current();
let mut ids = runs.list();
ids.reverse();
if a.latest {
ids.retain(|id| Some(id) == current.as_ref());
}
for id in ids {
let folder = runs.run_dir(&id);
let results: Value = std::fs::read_to_string(folder.join("run_results.json"))
.ok()
.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or(Value::Null);
rows.push(json!({
"report": a.report,
"binding": b,
"run_id": id,
"current": Some(&id) == current.as_ref(),
"status": results.get("status").cloned().unwrap_or(json!("unfinished")),
"started_at": results.get("started_at").cloned().unwrap_or(Value::Null),
"scheduled_at": results.get("scheduled_at").cloned().unwrap_or(Value::Null),
"duration_ms": results.get("duration_ms").cloned().unwrap_or(Value::Null),
"error": results.get("error").cloned().unwrap_or(Value::Null),
"retry_of": results.get("retry_of").cloned().unwrap_or(Value::Null),
"release_of": results.get("release_of").cloned().unwrap_or(Value::Null),
"path": shown(&a.project_dir, &folder),
}));
}
if let Some(h) = runs.holder() {
eprintln!("note: {} {b} is locked by {}", a.report, h.describe());
}
}
if rows.is_empty() {
eprintln!(
"error: no runs of `{}`{} in {}",
a.report,
a.binding
.as_ref()
.map(|b| format!(" (Binding `{b}`)"))
.unwrap_or_default(),
shown(&a.project_dir, &dir)
);
return exit::failed();
}
if a.path {
for r in &rows {
println!("{}", r["path"].as_str().unwrap_or_default());
}
} else if a.output == HistoryOutput::Json {
println!("{}", dre_core::secrets::to_json_pretty(&rows).unwrap());
} else {
for r in &rows {
let mark = if r["current"] == true { "*" } else { " " };
let of = match (r["retry_of"].as_str(), r["release_of"].as_str()) {
(Some(id), _) => format!(" (retry of {id})"),
(_, Some(id)) => format!(" (release of {id})"),
_ => String::new(),
};
let line = format!(
"{mark} {:<10} {:<24} {:<12} {}{of}",
r["binding"].as_str().unwrap_or_default(),
r["run_id"].as_str().unwrap_or_default(),
r["status"].as_str().unwrap_or_default(),
r["path"].as_str().unwrap_or_default()
);
println!("{}", dre_core::secrets::mask(&line));
}
println!("(* the current run)");
}
exit::ok()
}
pub fn unlock(a: UnlockArgs) -> ExitCode {
let dir = match report_dir(&a.project_dir, a.target_path.as_deref(), &a.report) {
Ok(d) => d.join(&a.binding),
Err(e) => {
eprintln!("error: {e}");
return exit::not_started();
}
};
let runs = BindingRuns::new(&dir);
let lock = dir.join(dre_core::runs::LOCK);
if !lock.exists() {
eprintln!("{} {} isn't locked", a.report, a.binding);
return exit::ok();
}
match runs.holder() {
Some(h) => eprintln!("{} {} is locked by {}", a.report, a.binding, h.describe()),
None => eprintln!(
"{} {} has an unreadable lock at {}",
a.report,
a.binding,
lock.display()
),
}
if !a.yes {
if !std::io::stdin().is_terminal() {
eprintln!("error: not removing the lock without confirmation; pass --yes");
return exit::not_started();
}
eprint!("Remove it? Only do this if that run is no longer going on. [y/N] ");
let _ = std::io::stderr().flush();
let mut answer = String::new();
let _ = std::io::stdin().lock().read_line(&mut answer);
if !matches!(answer.trim(), "y" | "Y" | "yes") {
eprintln!("Left it in place.");
return exit::failed();
}
}
match runs.unlock() {
Ok(_) => {
eprintln!("Removed the lock.");
exit::ok()
}
Err(e) => {
eprintln!("error: can't remove {}: {e}", lock.display());
exit::failed()
}
}
}