use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use chrono::{SecondsFormat, Utc};
use crate::github;
use crate::interrupt;
use crate::issue::IssueUrl;
use crate::progress;
use crate::worktree::Worktree;
pub struct FailedRun {
pub error: anyhow::Error,
pub pr_url: Option<String>,
pub log: Option<PathBuf>,
}
impl From<anyhow::Error> for FailedRun {
fn from(error: anyhow::Error) -> Self {
FailedRun {
error,
pr_url: None,
log: None,
}
}
}
pub fn fail(
issue: &IssueUrl,
worktree: &Worktree,
base: &str,
log: &Path,
error: anyhow::Error,
) -> FailedRun {
let error = if interrupt::requested() {
anyhow!("interrupted")
} else {
error
};
let reason = error
.to_string()
.lines()
.next()
.unwrap_or_default()
.to_string();
if let Err(problem) = commit_and_push(worktree, base, &reason) {
progress::step(format_args!(
"could not push the failed run's work: {problem:#}"
));
}
let pr_url = match open_pr_as_draft(issue, worktree.branch()) {
Ok(pr_url) => pr_url,
Err(problem) => {
progress::step(format_args!(
"could not convert the PR to a draft: {problem:#}"
));
None
}
};
FailedRun {
error,
pr_url,
log: log.exists().then(|| log.to_path_buf()),
}
}
fn commit_and_push(worktree: &Worktree, base: &str, reason: &str) -> Result<()> {
let git = worktree.git();
if worktree.merge_in_progress()? {
git.run(&["merge", "--abort"])?;
}
git.run(&["add", "-A"])?;
if git.succeeds(&["diff", "--cached", "--quiet", &format!("origin/{base}")])? {
return Ok(());
}
let message = format!(
"thirdshift: failed run ({reason})\n\n\
{timestamp}, host {host}. Uncommitted work at the time of failure is included in this commit.",
timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
host = hostname(),
);
git.run(&[
"commit",
"-q",
"--allow-empty",
"--no-verify",
"-m",
&message,
])?;
git.run(&["push", "origin", worktree.branch()])?;
Ok(())
}
fn open_pr_as_draft(issue: &IssueUrl, branch: &str) -> Result<Option<String>> {
let Some(pr) = github::pull_request_for(issue, branch)?.filter(|pr| pr.is_open()) else {
return Ok(None);
};
if !pr.is_draft {
github::convert_to_draft(issue, branch)?;
}
Ok(Some(pr.url))
}
fn hostname() -> String {
let mut buffer = [0u8; 256];
let result = unsafe { libc::gethostname(buffer.as_mut_ptr().cast(), buffer.len()) };
if result != 0 {
return "unknown".to_string();
}
let end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
String::from_utf8_lossy(&buffer[..end]).into_owned()
}