use std::fmt;
use std::path::PathBuf;
use anyhow::{Result, anyhow};
use chrono::{SecondsFormat, Utc};
use crate::github;
use crate::host;
use crate::interrupt;
use crate::issue::IssueUrl;
use crate::progress;
use crate::run_ending::Cause;
use crate::worktree::Worktree;
pub struct FailedRun {
pub error: anyhow::Error,
pub pr_url: Option<String>,
pub log: Option<PathBuf>,
pub interrupted: bool,
pub ticket_lines: Vec<String>,
}
#[derive(Debug)]
pub struct PolicyRefusal;
impl fmt::Display for PolicyRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
"the merge was refused with nothing left to fix, so the PR stays ready for review",
)
}
}
impl From<anyhow::Error> for FailedRun {
fn from(error: anyhow::Error) -> Self {
let interrupted = interrupt::requested();
FailedRun {
error: interrupted_or(error, interrupted),
pr_url: None,
log: None,
interrupted,
ticket_lines: Vec::new(),
}
}
}
fn interrupted_or(error: anyhow::Error, interrupted: bool) -> anyhow::Error {
if interrupted {
anyhow!("interrupted")
} else {
error
}
}
pub fn fail(
issue: &IssueUrl,
worktree: Worktree,
base: &str,
log: Option<PathBuf>,
error: anyhow::Error,
) -> FailedRun {
let interrupted = interrupt::requested();
let error = interrupted_or(error, interrupted);
let reason = Cause::of(&error).first_line().to_string();
let keep_ready = error.is::<PolicyRefusal>();
let pushed = if keep_ready {
Ok(())
} else {
commit_and_push(&worktree, base, &reason)
};
if let Err(problem) = &pushed {
progress::step(format_args!(
"could not push the failed run's work, so it may exist only locally: {problem:#}"
));
}
let pr_url = match open_pr_url(issue, worktree.branch(), keep_ready) {
Ok(pr_url) => pr_url,
Err(problem) => {
progress::step(format_args!(
"could not convert the PR to a draft: {problem:#}"
));
None
}
};
if pushed.is_err() {
worktree.keep();
}
FailedRun {
error,
pr_url,
log,
interrupted,
ticket_lines: Vec::new(),
}
}
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 = host::name().as_deref().unwrap_or("unknown"),
);
git.run(&[
"commit",
"-q",
"--allow-empty",
"--no-verify",
"-m",
&message,
])?;
worktree.push()
}
fn open_pr_url(issue: &IssueUrl, branch: &str, keep_ready: bool) -> 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 && !keep_ready {
github::convert_to_draft(issue, branch)?;
}
Ok(Some(pr.url))
}