use anyhow::Result;
use chrono::{TimeDelta, Utc};
use crate::base_fix;
use crate::branch::{self, Started};
use crate::claim;
use crate::git::Git;
use crate::github::{self, ListedIssue, Shaping};
use crate::issue::{IssueUrl, Repo};
use crate::labels::{Label, READY_FOR_AGENT};
use crate::progress;
use crate::spec_run;
pub struct ReadyIssue {
pub listed: ListedIssue,
pub is_spec: bool,
}
pub fn first(launch: &Git, repo: &Repo) -> Result<Option<ReadyIssue>> {
let candidates = github::open_issues_labelled(&repo.slug(), READY_FOR_AGENT)?;
Search::new(launch, candidates).first_ready()
}
const SETTLE: TimeDelta = TimeDelta::minutes(10);
#[derive(Clone)]
enum Standing {
Ready {
is_spec: bool,
},
PassedOver(Reason),
}
#[derive(Clone)]
enum Reason {
Unready(Label),
Claimed,
Ticket(IssueUrl),
BaseFix,
Blocked(Vec<u64>),
Started(Started),
TicketsClosed,
Unsettled(Shaping),
}
struct Search<'a> {
launch: &'a Git,
candidates: Vec<ListedIssue>,
standings: Vec<Option<Standing>>,
}
impl<'a> Search<'a> {
fn new(launch: &'a Git, mut candidates: Vec<ListedIssue>) -> Self {
candidates.sort_by_key(|candidate| candidate.issue.number);
let standings = vec![None; candidates.len()];
Search {
launch,
candidates,
standings,
}
}
fn first_ready(mut self) -> Result<Option<ReadyIssue>> {
for at in 0..self.candidates.len() {
match self.standing(at)? {
Standing::Ready { is_spec } => {
let listed = self.candidates.swap_remove(at);
return Ok(Some(ReadyIssue { listed, is_spec }));
}
Standing::PassedOver(reason) => {
if let Some(line) = self.line(at, &reason)? {
progress::step(line);
}
}
}
}
Ok(None)
}
fn standing(&mut self, at: usize) -> Result<Standing> {
if let Some(standing) = &self.standings[at] {
return Ok(standing.clone());
}
let standing = standing_of(self.launch, &self.candidates[at])?;
self.standings[at] = Some(standing.clone());
Ok(standing)
}
fn line(&mut self, at: usize, reason: &Reason) -> Result<Option<String>> {
let number = self.candidates[at].issue.number;
let why = match reason {
Reason::Unready(label) => format!("labelled {label}"),
Reason::Claimed => format!("labelled {}", claim::IN_PROGRESS),
Reason::Ticket(spec) => {
let listed = self.candidates.iter().position(|candidate| {
candidate.issue.number == spec.number && candidate.issue.in_same_repo(spec)
});
let spec_is_ready = match listed {
Some(spec) => matches!(self.standing(spec)?, Standing::Ready { .. }),
None => false,
};
if spec_is_ready {
return Ok(None);
}
let spec = if self.candidates[at].issue.in_same_repo(spec) {
format!("#{}", spec.number)
} else {
format!("{}#{}", spec.repo_slug(), spec.number)
};
format!("is a Ticket of {spec}, which is not ready")
}
Reason::BaseFix => format!("labelled {}", base_fix::BASE_FIX),
Reason::Blocked(blockers) => {
let blockers: Vec<String> = blockers
.iter()
.map(|blocker| format!("#{blocker}"))
.collect();
format!("blocked by {}", blockers.join(", "))
}
Reason::Started(Started::Branch(branch)) => {
format!("already started: {branch} is on origin")
}
Reason::Started(Started::PullRequest(url)) => format!("already started: PR {url}"),
Reason::TicketsClosed => "every Ticket is closed".to_string(),
Reason::Unsettled(shaping) => {
let shaped = match shaping {
Shaping::Labelled => format!("labelled {READY_FOR_AGENT}"),
Shaping::SubIssues => "a sub-issue added or removed".to_string(),
Shaping::Blockers => "a \"blocked by\" link added or removed".to_string(),
};
let minutes = SETTLE.num_minutes();
format!("not settled: {shaped} less than {minutes} minutes ago")
}
};
Ok(Some(format!("#{number} {why}")))
}
}
fn standing_of(launch: &Git, candidate: &ListedIssue) -> Result<Standing> {
let passed_over = |reason| Ok(Standing::PassedOver(reason));
if let Some(label) = candidate.labels.unready() {
return passed_over(Reason::Unready(label));
}
if claim::is_on(&candidate.labels) {
return passed_over(Reason::Claimed);
}
let read = github::candidate(&candidate.issue, READY_FOR_AGENT)?;
if let Some(spec) = read.parent {
return passed_over(Reason::Ticket(spec));
}
if base_fix::is_issue(&candidate.labels) {
return passed_over(Reason::BaseFix);
}
if !read.open_blockers.is_empty() {
return passed_over(Reason::Blocked(read.open_blockers));
}
if let Some(started) = branch::started(launch, &candidate.issue)? {
return passed_over(Reason::Started(started));
}
if spec_run::all_closed(read.sub_issue_is_open.iter().copied()) {
return passed_over(Reason::TicketsClosed);
}
if let Some(shaped) = read.last_shaped
&& Utc::now() - shaped.at < SETTLE
{
return passed_over(Reason::Unsettled(shaped.by));
}
Ok(Standing::Ready {
is_spec: read.has_sub_issues(),
})
}