use std::num::NonZeroUsize;
use std::path::PathBuf;
use anyhow::anyhow;
use crate::asks::Asks;
use crate::base_fix::{Advice, BaseFix};
use crate::branch::{self, Selection};
use crate::child_run::Kind;
use crate::claim;
use crate::delivery::{Delivery, Opening};
use crate::failed_run::FailedRun;
use crate::git::Git;
use crate::github::{self, Ticket};
use crate::interrupt;
use crate::issue::IssueUrl;
use crate::launch::LaunchDirectory;
use crate::logs::{self, Work};
use crate::prompt;
use crate::session::Logs;
use crate::spec_run;
use crate::worktree::Worktree;
const IMPLEMENT: &str = "implement";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Goal {
ReadyForReview,
Merged,
}
impl Goal {
pub fn outcome(self) -> &'static str {
match self {
Goal::ReadyForReview => "ready for review",
Goal::Merged => "merged",
}
}
}
pub struct Reached {
pub pr_url: String,
pub goal: Goal,
pub log: Option<PathBuf>,
pub ticket_lines: Vec<String>,
}
pub struct Ended {
pub outcome: Result<Reached, FailedRun>,
pub base_fix: Option<String>,
pub advice: Vec<Advice>,
}
#[derive(Clone, Copy)]
pub enum StartedBy<'a> {
Command,
Dispatch { base: &'a str },
Child(&'a Kind),
}
impl<'a> StartedBy<'a> {
fn child(self) -> Option<&'a Kind> {
match self {
StartedBy::Child(kind) => Some(kind),
StartedBy::Command | StartedBy::Dispatch { .. } => None,
}
}
fn makes_claim(self) -> bool {
self.child().is_none()
}
fn given_base(self) -> Option<&'a str> {
match self {
StartedBy::Command => None,
StartedBy::Dispatch { base } => Some(base),
StartedBy::Child(kind) => Some(kind.base()),
}
}
}
pub fn run_to_end(issue: &IssueUrl, asks: &Asks, started_by: StartedBy) -> Ended {
let mut base_fix = BaseFix::new(started_by.child(), asks.base_fix.clone());
let outcome = run(issue, asks, started_by, &mut base_fix);
Ended {
outcome,
base_fix: base_fix.report(),
advice: base_fix.into_advice(),
}
}
fn run(
issue: &IssueUrl,
asks: &Asks,
started_by: StartedBy,
base_fix: &mut BaseFix,
) -> Result<Reached, FailedRun> {
let directory = LaunchDirectory::open_for_run(issue)?;
let launch = directory.git();
let tickets = match started_by.child() {
Some(_) => Vec::new(),
None => github::tickets(issue)?,
};
logs::started(match tickets.is_empty() {
true => Work::Run(issue),
false => Work::SpecRun(issue),
});
if asks.parallel_asked && tickets.is_empty() {
return Err(anyhow!(
"parallel is only for a Spec, and #{} has no sub-issues",
issue.number
)
.into());
}
let selection = branch::select(launch, issue)?;
if matches!(selection, Selection::Fresh { .. })
&& spec_run::all_closed(tickets.iter().map(|ticket| ticket.is_open))
{
return Err(
anyhow!("every Ticket is closed and there is no Spec branch; nothing to do").into(),
);
}
let given = started_by.given_base();
let named = selection.pr_base(given, directory.checked_out()).or(given);
let base = directory.base_branch(named)?;
if asks.launch_pull {
directory.pull(&base);
}
if interrupt::requested() {
return Err(anyhow!("interrupted").into());
}
let claim = started_by
.makes_claim()
.then(|| claim::make(issue))
.transpose()?;
let logs = Logs::of_run(issue);
let delivery = Delivery {
issue,
base: &base,
goal: asks.goal,
base_fix,
logs: &logs,
};
let outcome = run_in_worktree(tickets, launch, &selection, delivery, asks.tickets_at_once);
if let Some(claim) = claim {
match &outcome {
Ok(reached) if reached.goal == Goal::Merged => claim.remove_if_closed(),
Ok(_) => {}
Err(_) => claim.release_if_nothing_on_origin(launch),
}
}
outcome
}
fn run_in_worktree(
tickets: Vec<Ticket>,
launch: &Git,
selection: &Selection,
delivery: Delivery,
parallel: NonZeroUsize,
) -> Result<Reached, FailedRun> {
let (issue, base) = (delivery.issue, delivery.base);
let branch = selection.branch();
let worktree = match selection {
Selection::Fresh { .. } => Worktree::create_fresh(launch, &issue.repo, branch, base)?,
Selection::Continuation { .. } => {
Worktree::continue_existing(launch, &issue.repo, branch, base)?
}
};
if !tickets.is_empty() {
return spec_run::run(tickets, worktree, delivery, parallel);
}
let prompt = match selection {
Selection::Fresh { .. } => prompt::fresh(issue, base, branch),
Selection::Continuation { pr, .. } => {
prompt::continuation(issue, base, branch, pr.as_ref().map(|pr| pr.url.as_str()))
}
};
let opening = Opening {
kind: IMPLEMENT,
prompt,
catch_up_from_origin: false,
};
delivery.deliver(worktree, opening, || Ok(()))
}