use std::fmt::Display;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
use anyhow::{Context, Result};
use chrono::Local;
use crate::issue::{IssueUrl, Repo};
use crate::progress;
const STAMP_FORMAT: &str = "%Y%m%dT%H%M%S%z";
static STARTED: OnceLock<String> = OnceLock::new();
static LOG: Mutex<Log> = Mutex::new(Log::NotKept);
enum Log {
NotKept,
Held(Vec<String>),
Kept { path: PathBuf, file: File },
Failed { path: Option<PathBuf> },
}
pub fn begin(starting: impl Display) {
let now = Local::now();
let _ = STARTED.set(now.format(STAMP_FORMAT).to_string());
*log() = Log::Held(Vec::new());
progress::step(format_args!("{starting}, {}", now.format("%Y-%m-%d %z")));
}
pub fn begin_child(stamp: Option<String>) {
if let Some(stamp) = stamp {
let _ = STARTED.set(stamp);
}
}
pub fn stamp() -> &'static str {
STARTED.get_or_init(|| Local::now().format(STAMP_FORMAT).to_string())
}
pub fn of_run(logs_dir: &Path, issue: &IssueUrl) -> PathBuf {
of(logs_dir, "issue", &issue_name(issue))
}
pub fn of_pickup_run(logs_dir: &Path, issue: &IssueUrl) -> PathBuf {
of(logs_dir, "pickup", &issue_name(issue))
}
pub fn of_architect_run(logs_dir: &Path, repo: &Repo) -> PathBuf {
of(
logs_dir,
"architect",
&format!("{}-{}", repo.owner, repo.name),
)
}
fn issue_name(issue: &IssueUrl) -> String {
format!("{}-{}-issue-{}", issue.owner, issue.repo, issue.number)
}
fn of(logs_dir: &Path, folder: &str, name: &str) -> PathBuf {
logs_dir
.join("commands")
.join(folder)
.join(format!("{name}-{}.log", stamp()))
}
pub fn keep(path: PathBuf) {
let created = {
let mut log = log();
let Log::Held(held) = &*log else {
return;
};
let created = create(&path, held);
*log = match &created {
Ok(file) => Log::Kept {
path: path.clone(),
file: file.try_clone().expect("a created file can be cloned"),
},
Err(_) => Log::Failed {
path: path.exists().then(|| path.clone()),
},
};
created
};
match created {
Ok(_) => progress::step(format_args!("logging this command to {}", path.display())),
Err(error) => warn(&error),
}
}
fn create(path: &Path, held: &[String]) -> Result<File> {
if let Some(dir) = path.parent() {
fs::create_dir_all(dir).with_context(|| format!("could not create {}", dir.display()))?;
}
let mut file =
File::create(path).with_context(|| format!("could not create {}", path.display()))?;
for line in held {
writeln!(file, "{line}").with_context(|| format!("could not write {}", path.display()))?;
}
Ok(file)
}
pub fn path() -> Option<PathBuf> {
match &*log() {
Log::Kept { path, .. } => Some(path.clone()),
Log::Failed { path } => path.clone(),
Log::NotKept | Log::Held(_) => None,
}
}
pub fn eprint(line: &str) {
print_to(std::io::stderr(), line);
}
pub fn print(line: &str) {
print_to(std::io::stdout(), line);
}
fn print_to(mut terminal: impl Write, line: &str) {
let failed = {
let mut log = log();
let _ = writeln!(terminal, "{line}");
let _ = terminal.flush();
match &mut *log {
Log::NotKept | Log::Failed { .. } => None,
Log::Held(held) => {
held.push(line.to_string());
None
}
Log::Kept { path, file } => match writeln!(file, "{line}") {
Ok(()) => None,
Err(error) => {
let error = anyhow::Error::new(error)
.context(format!("could not write {}", path.display()));
*log = Log::Failed {
path: Some(path.clone()),
};
Some(error)
}
},
}
};
if let Some(error) = failed {
warn(&error);
}
}
fn warn(error: &anyhow::Error) {
progress::step(format_args!(
"warning: could not keep the Command log: {error:#}"
));
}
fn log() -> MutexGuard<'static, Log> {
LOG.lock().unwrap_or_else(PoisonError::into_inner)
}