use std::path::PathBuf;
use anyhow::{Context, Result, anyhow, bail};
use crate::branch::{self, Selection};
use crate::ci::{self, Ci};
use crate::failed_run::{self, FailedRun};
use crate::git::Git;
use crate::github::{self, Mergeable, PullRequest};
use crate::interrupt;
use crate::issue::IssueUrl;
use crate::plugin::Plugin;
use crate::poll;
use crate::preflight;
use crate::progress;
use crate::prompt;
use crate::session::{self, Sessions};
use crate::worktree::{Merge, Worktree};
pub fn run(issue: &IssueUrl) -> Result<String, FailedRun> {
let timestamp = chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
let launch = Git::new(std::env::current_dir().context("no current directory")?);
preflight::check(&launch, issue)?;
let checked_out = launch
.run(&["symbolic-ref", "--quiet", "--short", "HEAD"])
.ok();
let selection = branch::select(&launch, issue)?;
let branch = selection.branch().to_string();
let base = selection.base_branch(checked_out.as_deref())?;
preflight::check_base_branch(&launch, &base)?;
if interrupt::requested() {
return Err(anyhow!("interrupted").into());
}
let (worktree, prompt) = match &selection {
Selection::Fresh { .. } => (
Worktree::create_fresh(&launch, &issue.repo, &branch, &base)?,
prompt::fresh(issue, &base, &branch),
),
Selection::Continuation { pr, .. } => (
Worktree::continue_existing(&launch, &issue.repo, &branch, &base)?,
prompt::continuation(issue, &base, &branch, pr.as_ref().map(|pr| pr.url.as_str())),
),
};
let mut log = session::log_path(issue, ×tamp, "implement")?;
implement(issue, &worktree, &base, &prompt, ×tamp, &mut log)
.map_err(|error| failed_run::fail(issue, worktree, &base, &log, error))
}
fn implement(
issue: &IssueUrl,
worktree: &Worktree,
base: &str,
prompt: &str,
timestamp: &str,
log: &mut PathBuf,
) -> Result<String> {
let branch = worktree.branch();
let plugin = Plugin::write()?;
let sessions = Sessions {
issue,
timestamp,
worktree: worktree.path(),
plugin_dir: plugin.path(),
};
let mut run_session = |kind: &str, prompt: &str| sessions.run(kind, prompt, log);
run_session("implement", prompt)?;
worktree.push()?;
progress::step("checking the PR");
let pr = open_pr(issue, branch)?;
if pr.base != base {
bail!("PR targets {}, not {base}", pr.base);
}
if pr.is_draft {
github::mark_ready(issue, branch)?;
}
repair_loop(issue, worktree, base, &pr.url, &mut run_session)?;
ensure_pr_ready_and_mergeable(issue, branch)?;
if interrupt::requested() {
bail!("interrupted");
}
Ok(pr.url)
}
const MAX_REPAIRS: usize = 3;
const MAX_BASE_MOVES: usize = 3;
fn repair_loop(
issue: &IssueUrl,
worktree: &Worktree,
base: &str,
pr_url: &str,
run_session: &mut impl FnMut(&str, &str) -> Result<()>,
) -> Result<()> {
let branch = worktree.branch();
let mut repairs = 0;
let mut base_moves = 0;
let mut next_repair = |cause: &str| -> Result<String> {
if repairs == MAX_REPAIRS {
bail!("repairs exhausted: {cause}");
}
repairs += 1;
progress::step(format_args!(
"{cause}; starting Repair {repairs} of {MAX_REPAIRS}"
));
Ok(format!("repair-{repairs}"))
};
loop {
if worktree.merge_base_branch(base)? == Merge::Conflicted {
let kind = next_repair("conflict")?;
run_session(&kind, &prompt::conflict_repair(issue, base, branch, pr_url))?;
worktree.ensure_base_branch_merged(base)?;
worktree.push()?;
continue;
}
worktree.push()?;
match ci::watch(issue, &worktree.head()?)? {
Ci::Absent | Ci::Passed => {
if !worktree.base_branch_moved(base)? {
return Ok(());
}
if base_moves == MAX_BASE_MOVES {
bail!(
"origin/{base} kept moving while CI ran: merged it again {MAX_BASE_MOVES} times"
);
}
base_moves += 1;
progress::step(format_args!(
"origin/{base} moved while CI ran; merging it again"
));
}
Ci::Failed(failed) => {
let kind = next_repair("CI red")?;
run_session(
&kind,
&prompt::ci_fix_repair(issue, base, branch, pr_url, &failed),
)?;
worktree.push()?;
}
}
}
}
fn open_pr(issue: &IssueUrl, branch: &str) -> Result<PullRequest> {
let pr = github::pull_request_for(issue, branch)?.context("no PR found")?;
if !pr.is_open() {
bail!("PR {} is {}, not open", pr.url, pr.state);
}
Ok(pr)
}
fn ensure_pr_ready_and_mergeable(issue: &IssueUrl, branch: &str) -> Result<()> {
progress::step("checking the PR is open, ready and mergeable");
let pr = open_pr(issue, branch)?;
if pr.is_draft {
bail!("PR {} is a draft", pr.url);
}
let mergeable = poll::within(poll::grace_period(), || {
Ok(Some(github::mergeable(issue, branch)?).filter(|m| *m != Mergeable::Unknown))
})?;
match mergeable {
Some(Mergeable::Yes) => Ok(()),
Some(Mergeable::No) => bail!("PR {} is not mergeable", pr.url),
_ => bail!(
"GitHub has not worked out whether PR {} is mergeable",
pr.url
),
}
}