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 {
lines: Vec<(Stream, String)>,
shown: bool,
},
Kept { path: PathBuf, file: File },
Failed { path: Option<PathBuf> },
}
pub fn begin(starting: impl Display) {
begin_held(starting, false);
}
pub fn begin_pass(starting: impl Display) {
begin_held(starting, true);
}
fn begin_held(starting: impl Display, from_terminal: bool) {
let now = Local::now();
let _ = STARTED.set(now.format(STAMP_FORMAT).to_string());
*log() = Log::Held {
lines: Vec::new(),
shown: !from_terminal,
};
progress::step(format_args!("{starting}, {}", now.format("%Y-%m-%d %z")));
}
pub fn show_held() {
if let Log::Held { lines, shown } = &mut *log()
&& !*shown
{
for (stream, line) in lines.iter() {
stream.print(line);
}
*shown = true;
}
}
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 root(logs_dir: &Path, repo: &Repo) -> PathBuf {
logs_dir.join(&repo.owner).join(&repo.name)
}
pub fn of_run(logs_dir: &Path, issue: &IssueUrl) -> PathBuf {
of(
logs_dir,
&issue.repo(),
"issue",
&format!("{}-", issue.number),
)
}
pub fn of_pickup_run(logs_dir: &Path, issue: &IssueUrl) -> PathBuf {
of(
logs_dir,
&issue.repo(),
"pickup",
&format!("{}-", issue.number),
)
}
pub fn of_architect_run(logs_dir: &Path, repo: &Repo) -> PathBuf {
of(logs_dir, repo, "architect", "")
}
fn of(logs_dir: &Path, repo: &Repo, folder: &str, prefix: &str) -> PathBuf {
root(logs_dir, repo)
.join("commands")
.join(folder)
.join(format!("{prefix}{}.log", stamp()))
}
pub fn keep(path: PathBuf) {
show_held();
let created = {
let mut log = log();
let Log::Held { lines, .. } = &*log else {
return;
};
let created = create(&path, lines.iter().map(|(_, line)| line));
*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<'a>(path: &Path, held: impl Iterator<Item = &'a 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(Stream::Stderr, line);
}
pub fn print(line: &str) {
print_to(Stream::Stdout, line);
}
#[derive(Clone, Copy)]
enum Stream {
Stdout,
Stderr,
}
impl Stream {
fn print(self, line: &str) {
fn to(mut terminal: impl Write, line: &str) {
let _ = writeln!(terminal, "{line}");
let _ = terminal.flush();
}
match self {
Stream::Stdout => to(std::io::stdout(), line),
Stream::Stderr => to(std::io::stderr(), line),
}
}
}
fn print_to(stream: Stream, line: &str) {
let failed = {
let mut log = log();
if !matches!(&*log, Log::Held { shown: false, .. }) {
stream.print(line);
}
match &mut *log {
Log::NotKept | Log::Failed { .. } => None,
Log::Held { lines, .. } => {
lines.push((stream, 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)
}