use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
mod repair_loop;
mod stage;
pub(crate) use repair_loop::MAX_REPAIRS;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Limits {
pub repair_cap: usize,
pub protected_paths: Vec<String>,
}
impl Default for Limits {
fn default() -> Self {
Self {
repair_cap: MAX_REPAIRS,
protected_paths: Vec::new(),
}
}
}
use crate::pull_request::{AlreadyMerged, Identified, MergeAttempt, PullRequest};
use crate::base_fix::{self, BaseFix};
use crate::child_run::ChildAsks;
use crate::ci::{self, Ci, FailedChecks};
use crate::failed_run::{FailedRun, MergeHold, PolicyRefusal, interrupted_or};
use crate::git::Git;
use crate::github::GitHub;
use crate::harness::Choice;
use crate::interrupt;
use crate::issue::IssueUrl;
use crate::progress;
use crate::prompt;
use crate::recovery::{self, Preservation, Recovery};
use crate::run::{Goal, Reached, Resolution};
use crate::run_ending::Cause;
use crate::security::review::{Hold as SecurityHold, Outcome as SecurityOutcome, step};
use crate::session::{Kind, Logs, Sessions};
use crate::skills;
use crate::trust::{self, IssueText};
use crate::worktree::{ForeignCommits, Merge, PendingMerge, Preserved, Worktree};
use repair_loop::{Repair, Upstream};
use stage::{CommandLog, CommandRecord, Stage, Tracker};
pub struct Delivery<'a> {
pub security_review: bool,
pub child_asks: &'a ChildAsks,
pub issue: &'a IssueUrl,
pub base: &'a str,
pub goal: Goal,
pub base_fix: &'a mut BaseFix,
pub logs: &'a Logs,
pub started: DateTime<Utc>,
pub repair_cap: usize,
pub protected_paths: &'a [String],
}
pub struct Opening {
pub session: OpeningSession,
pub prompt: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpeningSession {
Implementation,
BugDiagnosis,
SpecReview,
}
impl OpeningSession {
pub fn kind(self) -> Kind {
match self {
Self::Implementation | Self::BugDiagnosis => Kind::Implementation,
Self::SpecReview => Kind::SpecReview,
}
}
fn stage(self) -> Stage {
match self {
Self::Implementation | Self::BugDiagnosis => Stage::Implementation,
Self::SpecReview => Stage::SpecReview,
}
}
}
impl Delivery<'_> {
pub fn spec_review(
self,
worktree: Worktree,
prompt: String,
pull_request: &mut PullRequest,
checklist: &str,
) -> Result<Reached, FailedRun> {
self.deliver_steps(
worktree,
Opening {
session: OpeningSession::SpecReview,
prompt,
},
pull_request,
Some(checklist),
None,
)
}
pub fn ticket_run(
self,
worktree: Worktree,
opening: Opening,
pull_request: &mut PullRequest,
) -> Result<Reached, FailedRun> {
let steps = crate::processes::ticket::steps()?;
self.deliver_steps(worktree, opening, pull_request, None, Some(steps))
}
fn deliver_steps(
self,
worktree: Worktree,
opening: Opening,
pull_request: &mut PullRequest,
checklist: Option<&str>,
steps: Option<Vec<crate::processes::ticket::Step>>,
) -> Result<Reached, FailedRun> {
let tracker = Tracker::new(opening.session.stage(), CommandLog);
let logs = self.logs;
let issue = self.issue;
let started = self.started;
let base = self.base;
let goal = self.goal;
let security_review = self.security_review;
let repair_cap = self.repair_cap;
let protected_paths = self.protected_paths;
let child_asks = self.child_asks;
let base_fix = self.base_fix;
run_checked(
CheckedRun {
logs,
issue,
started,
harness: &child_asks.harness,
security_review: child_asks.security_review_harness.as_ref(),
security_review_default: child_asks.security_review_default.as_ref(),
base,
},
worktree,
pull_request,
&tracker,
|resolution, log| Reached {
resolution,
log,
ticket_lines: Vec::new(),
},
|merged, log| Reached {
resolution: Resolution::PullRequest {
url: merged.pr.url.clone(),
goal: Goal::Merged,
},
log,
ticket_lines: Vec::new(),
},
|tracker, worktree, pull_request, sessions| {
let route = Route {
security_review,
issue,
base,
branch: worktree.branch(),
goal,
harness: &child_asks.harness,
};
let mut outside = InWorktree {
tracker,
issue,
worktree,
base,
base_fix,
child_asks,
sessions,
pull_request,
repair_cap,
protected_paths,
};
match steps {
Some(steps) => route.process_steps(&mut outside, &opening, steps),
None => route.spec_steps(&mut outside, &opening.prompt, checklist),
}
},
)
}
pub fn publish(
self,
worktree: Worktree,
pull_request: &mut PullRequest,
reviews: &[SecurityOutcome],
) -> Result<String, FailedRun> {
let tracker = Tracker::new(Stage::Push, CommandLog);
let logs = self.logs;
let issue = self.issue;
let started = self.started;
let base = self.base;
let goal = self.goal;
let repair_cap = self.repair_cap;
let protected_paths = self.protected_paths;
let child_asks = self.child_asks;
let base_fix = self.base_fix;
run_checked(
CheckedRun {
logs,
issue,
started,
harness: &child_asks.harness,
security_review: child_asks.security_review_harness.as_ref(),
security_review_default: child_asks.security_review_default.as_ref(),
base,
},
worktree,
pull_request,
&tracker,
|url, _log| url,
|merged, _log| merged.pr.url.clone(),
|tracker, worktree, pull_request, sessions| {
publish_steps(
&mut InWorktree {
tracker,
issue,
worktree,
base,
base_fix,
child_asks,
sessions,
pull_request,
repair_cap,
protected_paths,
},
goal,
reviews,
)
},
)
}
}
struct CheckedRun<'a> {
logs: &'a Logs,
issue: &'a IssueUrl,
started: DateTime<Utc>,
harness: &'a Choice,
security_review: Option<&'a Choice>,
security_review_default: Option<&'a Choice>,
base: &'a str,
}
fn run_checked<T, S>(
checked: CheckedRun<'_>,
worktree: Worktree,
pull_request: &mut PullRequest,
tracker: &Tracker,
on_ok: impl FnOnce(T, Option<PathBuf>) -> S,
on_merged: impl FnOnce(&AlreadyMerged, Option<PathBuf>) -> S,
steps: impl FnOnce(&Tracker, &Worktree, &mut PullRequest, &Sessions) -> Result<T>,
) -> Result<S, FailedRun> {
let CheckedRun {
logs,
issue,
started,
harness,
security_review,
security_review_default,
base,
} = checked;
pull_request.begin_delivery();
let path = worktree.path().to_path_buf();
let before_each = move || {
no_untrusted_edit_since(issue, started, "Ticket run")?;
hand_over_issue(issue, &path)
};
let (delivered, log) = Sessions::checked_within(
logs,
worktree.path(),
harness,
security_review,
security_review_default,
&before_each,
|sessions| steps(tracker, &worktree, pull_request, sessions),
);
let result = match delivered {
Ok(value) => Ok(on_ok(value, log)),
Err(error) => {
if let Some(merged) = error.downcast_ref::<AlreadyMerged>()
&& worktree.fully_landed_at(&merged.head).unwrap_or(false)
{
progress::step("the pull request was merged while Delivery was running");
Ok(on_merged(merged, log))
} else {
let stopped_at = stopped_at(tracker, &error);
let path = worktree.path().to_path_buf();
let mut outside = OnFailure {
base,
worktree,
pull_request,
};
Err(fail(&mut outside, stopped_at, path, log, error))
}
}
};
crate::logs::unaddressed_findings(pull_request.unaddressed_findings());
result
}
fn publish_steps(
outside: &mut InWorktree<'_>,
goal: Goal,
reviews: &[SecurityOutcome],
) -> Result<String> {
outside.tracker.enter(Stage::Push);
outside.pull_request.ensure_actionable(false)?;
outside
.worktree
.push_if_actionable(&mut || outside.pull_request.ensure_actionable(false))?;
let title = GitHub::new().issue_title(outside.issue)?;
let body = format!("Closes #{}", outside.issue.number);
outside.pull_request.open_if_absent(&title, &body)?;
if let Err(error) = outside
.pull_request
.write_built_with(&outside.child_asks.harness)
{
progress::warn(
&error,
format_args!("could not write the Built-with line in the pull request's body"),
);
}
outside.tracker.enter(Stage::PullRequestReadiness);
let pr = outside.pull_request.mark_ready(None)?;
take_reviewed_to_goal(outside, &pr.url, goal, reviews)?;
if goal == Goal::Merged {
let branch = outside.worktree.branch().to_string();
Route {
security_review: false,
issue: outside.issue,
base: outside.base,
branch: &branch,
goal,
harness: &outside.child_asks.harness,
}
.after_merge(outside, &pr);
}
Ok(pr.url)
}
struct Route<'a> {
security_review: bool,
issue: &'a IssueUrl,
base: &'a str,
branch: &'a str,
goal: Goal,
harness: &'a Choice,
}
impl Route<'_> {
fn spec_steps<O: Outside>(
&self,
outside: &mut O,
prompt: &str,
checklist: Option<&str>,
) -> Result<Resolution> {
outside.catch_up()?;
outside.stage(Stage::SpecReview);
outside.session(Kind::SpecReview, prompt)?;
let review = self.review(outside)?;
let pr = self.publish(outside, checklist)?;
self.finish(outside, pr, self.goal, review.as_slice())
}
fn process_steps(
&self,
outside: &mut impl Outside,
opening: &Opening,
steps: Vec<crate::processes::ticket::Step>,
) -> Result<Resolution> {
use crate::processes::ticket::Step;
let mut steps = steps.into_iter().peekable();
let mut review = None;
let mut resolution = None;
while let Some(step) = steps.next() {
match step {
Step::Implement => {
if let Some(early) = self.implement(outside, opening)? {
return Ok(early);
}
}
Step::SecurityReview => review = self.review(outside)?,
Step::Delivery => {
let goal = if matches!(steps.peek(), Some(Step::SelfMerge)) {
steps.next();
self.goal
} else {
Goal::ReadyForReview
};
let pr = self.publish(outside, None)?;
resolution = Some(self.finish(outside, pr, goal, review.as_slice())?);
}
Step::SelfMerge => anyhow::bail!("self-merge must directly follow delivery"),
}
}
resolution.context("ticket-run ended without delivery")
}
fn implement(
&self,
outside: &mut impl Outside,
opening: &Opening,
) -> Result<Option<Resolution>> {
let session = opening.session;
outside.stage(session.stage());
if let crate::implement::Completed::NotReproduced(comment_url) =
crate::implement::run(self.issue, opening, |prompt| {
outside.session(session.kind(), prompt)
})?
{
outside.confirm_needs_info()?;
return Ok(Some(Resolution::NotReproduced { comment_url }));
}
Ok(None)
}
fn review(&self, outside: &mut impl Outside) -> Result<Option<SecurityOutcome>> {
self.security_review
.then(|| {
outside.stage(Stage::SecurityReview);
outside.security_review(&prompt::security_review(
self.issue,
self.base,
self.branch,
))
})
.transpose()
}
fn publish(&self, outside: &mut impl Outside, checklist: Option<&str>) -> Result<Identified> {
outside.stage(Stage::Push);
outside.push()?;
self.write_built_with(outside);
outside.stage(Stage::PullRequestReadiness);
outside.mark_pr_ready(checklist)
}
fn finish(
&self,
outside: &mut impl Outside,
pr: Identified,
goal: Goal,
reviews: &[SecurityOutcome],
) -> Result<Resolution> {
take_reviewed_to_goal(outside, &pr.url, goal, reviews)?;
if goal == Goal::Merged {
self.after_merge(outside, &pr);
}
Ok(Resolution::PullRequest { url: pr.url, goal })
}
fn write_built_with(&self, outside: &mut impl Outside) {
if let Err(error) = outside.write_built_with(self.harness) {
outside.warn(
&error,
"could not write the Built-with line in the pull request's body".into(),
);
}
}
fn after_merge(&self, outside: &mut impl Outside, pr: &Identified) {
let branch = self.branch;
if let Err(error) = retry_if_interrupted(outside, |outside| outside.delete_branch()) {
outside.warn(
&error,
format!(
"could not delete {branch} on origin, so delete it by hand: \
git push origin --delete {branch}"
),
);
}
let comment = format!(
"Closed by #{}, merged into {} by a thirdshift Ticket run with Self-merge.",
pr.number, self.base
);
if let Err(error) = retry_if_interrupted(outside, |outside| {
self.close_unless_closed(outside, &comment)
}) {
let issue = self.issue;
outside.warn(
&error,
format!(
"could not close issue #{number} ({url}), so if it is still open, close it by hand: \
gh issue close {number} --repo {repo} --comment '{quoted}'",
number = issue.number,
url = issue.url,
repo = issue.repo_slug(),
quoted = comment.replace('\'', r"'\''")
),
);
}
}
fn close_unless_closed(&self, outside: &mut impl Outside, comment: &str) -> Result<()> {
if !outside.issue_is_open()? {
return Ok(());
}
outside.step(format!("closing issue #{}", self.issue.number));
outside.close_issue(comment)
}
}
fn retry_if_interrupted<O: Outside>(
outside: &mut O,
step: impl Fn(&mut O) -> Result<()>,
) -> Result<()> {
match step(outside) {
Err(_) if outside.interrupted() => step(outside),
done => done,
}
}
fn fail(
outside: &mut impl FailedOutside,
stopped_at: recovery::Stage,
worktree: PathBuf,
log: Option<PathBuf>,
error: anyhow::Error,
) -> FailedRun {
let interrupted = outside.interrupted();
let error = interrupted_or(error, interrupted);
let reason = Cause::of(&error).first_line().to_string();
let keep_ready =
error.is::<PolicyRefusal>() || error.is::<SecurityHold>() || error.is::<MergeHold>();
let log = error
.downcast_ref::<SecurityHold>()
.and_then(SecurityHold::log)
.map(Path::to_path_buf)
.or(log);
let preservation = if keep_ready {
Preservation::Remote
} else {
let preserved = outside.preserve_failed_run(&reason);
recovery::preservation_of(preserved, |line| outside.step(line))
};
let pr_url = match outside.finish_pr(keep_ready) {
Ok(pr_url) => pr_url,
Err(problem) => {
outside.step(format!("could not convert the PR to a draft: {problem:#}"));
None
}
};
let mut recovery = Recovery::new(stopped_at, preservation);
recovery.record_worktree(worktree);
FailedRun {
recovery: Some(Box::new(recovery)),
error,
pr_url,
log,
interrupted,
ticket_lines: Vec::new(),
}
}
fn stopped_at(tracker: &Tracker<impl CommandRecord>, error: &anyhow::Error) -> recovery::Stage {
if error.is::<SecurityHold>() {
tracker.stop_at_security_review();
}
tracker.stopped_at()
}
fn take_reviewed_to_goal(
outside: &mut impl Outside,
pr_url: &str,
goal: Goal,
reviews: &[SecurityOutcome],
) -> Result<()> {
let entry = reviews
.iter()
.map(SecurityOutcome::entry)
.collect::<Vec<_>>()
.join("\n");
if !reviews.is_empty()
&& let Err(error) = outside.record_security_review(&entry)
{
outside.warn(
&error,
"could not write the Security review outcome in the pull request's body".to_string(),
);
}
let hold = reviews.iter().find_map(SecurityOutcome::hold);
outside.take_to_goal(
pr_url,
if hold.is_some() {
Goal::ReadyForReview
} else {
goal
},
)?;
if let Some(hold) = hold {
outside.step(hold.to_string());
if goal == Goal::Merged {
return Err(hold.into());
}
}
Ok(())
}
trait Outside {
fn stage(&mut self, stage: Stage);
fn catch_up(&mut self) -> Result<()>;
fn session(&mut self, kind: Kind, prompt: &str) -> Result<Option<String>>;
fn confirm_needs_info(&mut self) -> Result<()>;
fn security_review(&mut self, prompt: &str) -> Result<SecurityOutcome>;
fn record_security_review(&mut self, entry: &str) -> Result<()>;
fn push(&mut self) -> Result<()>;
fn write_built_with(&mut self, choice: &Choice) -> Result<()>;
fn mark_pr_ready(&mut self, checklist: Option<&str>) -> Result<Identified>;
fn take_to_goal(&mut self, pr_url: &str, goal: Goal) -> Result<()>;
fn delete_branch(&mut self) -> Result<()>;
fn issue_is_open(&mut self) -> Result<bool>;
fn close_issue(&mut self, comment: &str) -> Result<()>;
fn interrupted(&mut self) -> bool;
fn step(&mut self, line: String);
fn warn(&mut self, error: &anyhow::Error, warning: String);
}
trait FailedOutside {
fn preserve_failed_run(&mut self, reason: &str) -> Result<Preserved>;
fn finish_pr(&mut self, keep_ready: bool) -> Result<Option<String>>;
fn interrupted(&mut self) -> bool;
fn step(&mut self, line: String);
}
pub(crate) fn no_untrusted_edit_since(
issue: &IssueUrl,
started: DateTime<Utc>,
run_name: &'static str,
) -> Result<()> {
match trust::untrusted_edit(&GitHub::new(), issue, Some(started))? {
Some(text) => Err(UntrustedEdit {
number: issue.number,
text,
run_name,
}
.into()),
None => Ok(()),
}
}
pub(crate) fn hand_over_issue(issue: &IssueUrl, worktree: &Path) -> Result<()> {
skills::exclude(&Git::new(worktree), &format!("/{}", prompt::ISSUE_FILE))?;
let text = GitHub::new().issue_for_session(issue)?;
let path = worktree.join(prompt::ISSUE_FILE);
fs::write(&path, text).with_context(|| format!("could not write {}", path.display()))
}
#[derive(Debug)]
pub(crate) struct UntrustedEdit {
number: u64,
text: IssueText,
run_name: &'static str,
}
impl std::fmt::Display for UntrustedEdit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"#{} {} edited since the {} started by an account that is not a Trusted account; starting no further session",
self.number, self.text, self.run_name
)
}
}
impl std::error::Error for UntrustedEdit {}
struct InWorktree<'a> {
tracker: &'a Tracker,
issue: &'a IssueUrl,
worktree: &'a Worktree,
base: &'a str,
base_fix: &'a mut BaseFix,
child_asks: &'a ChildAsks,
sessions: &'a Sessions<'a>,
pull_request: &'a mut PullRequest,
repair_cap: usize,
protected_paths: &'a [String],
}
impl Outside for InWorktree<'_> {
fn stage(&mut self, stage: Stage) {
self.tracker.enter(stage);
}
fn catch_up(&mut self) -> Result<()> {
self.worktree.fast_forward_to_origin()
}
fn session(&mut self, kind: Kind, prompt: &str) -> Result<Option<String>> {
let final_message = self.sessions.run(kind, prompt)?;
self.pull_request.ensure_actionable(false)?;
Ok(final_message)
}
fn confirm_needs_info(&mut self) -> Result<()> {
crate::implement::confirm_needs_info(&GitHub::new().issue_labels(self.issue)?)
}
fn security_review(&mut self, prompt: &str) -> Result<SecurityOutcome> {
step::Step {
sessions: self.sessions,
worktree: self.worktree,
issue: self.issue,
base: self.base,
}
.run(self.pull_request, prompt)
}
fn record_security_review(&mut self, entry: &str) -> Result<()> {
self.pull_request.record_security_review(entry)
}
fn push(&mut self) -> Result<()> {
self.pull_request.ensure_actionable(false)?;
self.worktree
.push_if_actionable(&mut || self.pull_request.ensure_actionable(false))
}
fn write_built_with(&mut self, choice: &Choice) -> Result<()> {
self.pull_request.write_built_with(choice)
}
fn mark_pr_ready(&mut self, checklist: Option<&str>) -> Result<Identified> {
self.pull_request.mark_ready(checklist)
}
fn take_to_goal(&mut self, pr_url: &str, goal: Goal) -> Result<()> {
let mut outside = RunOutside {
tracker: self.tracker,
issue: self.issue,
worktree: self.worktree,
base: self.base,
pr_url,
base_fix: self.base_fix,
child_asks: self.child_asks,
sessions: self.sessions,
pull_request: self.pull_request,
ci_watch: ci::Watch::new(self.issue),
protected_paths: self.protected_paths,
};
repair_loop::take_to_goal(&mut outside, self.base, goal, self.repair_cap)
}
fn delete_branch(&mut self) -> Result<()> {
self.worktree.delete_from_origin()
}
fn issue_is_open(&mut self) -> Result<bool> {
GitHub::new().completion().issue_is_open(self.issue)
}
fn close_issue(&mut self, comment: &str) -> Result<()> {
GitHub::new().completion().close_issue(self.issue, comment)
}
fn interrupted(&mut self) -> bool {
interrupt::requested()
}
fn step(&mut self, line: String) {
progress::step(line);
}
fn warn(&mut self, error: &anyhow::Error, warning: String) {
progress::warn(error, format_args!("{warning}"));
}
}
impl InWorktree<'_> {}
struct OnFailure<'a> {
base: &'a str,
worktree: Worktree,
pull_request: &'a mut PullRequest,
}
impl FailedOutside for OnFailure<'_> {
fn preserve_failed_run(&mut self, reason: &str) -> Result<Preserved> {
if let Err(error) = self.pull_request.ensure_preservable() {
self.worktree.retain_local();
return Err(error);
}
self.worktree
.preserve_failed_run(self.base, reason, &mut || {
self.pull_request.ensure_preservable()
})
}
fn finish_pr(&mut self, keep_ready: bool) -> Result<Option<String>> {
self.pull_request.finish_failed_run(keep_ready)
}
fn interrupted(&mut self) -> bool {
interrupt::requested()
}
fn step(&mut self, line: String) {
progress::step(line);
}
}
struct RunOutside<'a> {
tracker: &'a Tracker,
issue: &'a IssueUrl,
worktree: &'a Worktree,
base: &'a str,
pr_url: &'a str,
base_fix: &'a mut BaseFix,
child_asks: &'a ChildAsks,
sessions: &'a Sessions<'a>,
pull_request: &'a mut PullRequest,
ci_watch: ci::Watch<'a>,
protected_paths: &'a [String],
}
impl RunOutside<'_> {
fn refresh_built_with(&mut self) -> Result<()> {
self.pull_request.ensure_actionable(true)?;
if let Err(error) = self.pull_request.write_built_with(&self.child_asks.harness) {
progress::warn(
&error,
format_args!("could not write the Built-with line in the pull request's body"),
);
}
Ok(())
}
}
impl repair_loop::Outside for RunOutside<'_> {
type Pending = PendingMerge;
fn stage(&mut self, stage: Stage) {
self.tracker.enter(stage);
}
fn merge_base_branch(&mut self) -> Result<Merge> {
self.pull_request.ensure_actionable(true)?;
self.worktree.merge_base_branch(self.base)
}
fn merge_new_commits(&mut self, observed: ForeignCommits) -> Result<Merge> {
self.worktree.merge_new_commits(observed)
}
fn ensure_merged(&mut self, pending: &PendingMerge) -> Result<()> {
self.worktree.ensure_merged(pending)
}
fn push(&mut self) -> Result<()> {
self.pull_request.ensure_actionable(true)?;
self.worktree
.push_if_actionable(&mut || self.pull_request.ensure_actionable(true))
}
fn head(&mut self) -> Result<String> {
self.worktree.head()
}
fn same_tree_as(&mut self, commit: &str) -> Result<bool> {
self.worktree.same_tree_as(commit)
}
fn base_branch_moved(&mut self) -> Result<bool> {
self.worktree.base_branch_moved(self.base)
}
fn new_commits_on_origin(&mut self) -> Result<ForeignCommits> {
self.pull_request.ensure_actionable(true)?;
self.worktree.new_commits_on_origin()
}
fn assess_ci(&mut self, head: &str, base_commit: Option<&str>) -> Result<Ci> {
self.ci_watch.assess(
head,
base_commit,
&mut || self.pull_request.ensure_actionable(true),
&mut |watched| Ok(self.worktree.new_commits_on_origin()?.origin_head() != watched),
)
}
fn ci_fix_completed(&mut self, head: String, failed: FailedChecks) {
self.ci_watch.after_repair(head, failed);
}
fn repair(&mut self, kind: Kind, repair: Repair) -> Result<()> {
let (issue, base, pr_url) = (self.issue, self.base, self.pr_url);
let branch = self.worktree.branch();
let prompt = match repair {
Repair::Conflict(Upstream::BaseBranch) => {
prompt::conflict_repair(issue, base, branch, pr_url)
}
Repair::Conflict(Upstream::IssueBranch) => {
prompt::conflict_repair(issue, branch, branch, pr_url)
}
Repair::CiFix(failed) => prompt::ci_fix_repair(issue, base, branch, pr_url, failed),
Repair::Review { from } => prompt::review_repair(issue, branch, pr_url, from),
};
self.pull_request.ensure_actionable(true)?;
let result = self.sessions.run(kind, &prompt);
let refreshed = self.refresh_built_with();
result.and(refreshed)
}
fn sees_inherited_failures(&mut self) -> bool {
self.base_fix.sees_inherited_failures()
}
fn base_fix(&mut self, base_commit: &str, failed: &FailedChecks) -> Result<()> {
let outside = base_fix::LaunchAndGitHub {
launch: self.worktree.launch(),
issue: self.issue,
asks: self.child_asks,
spec_pr: self.pull_request.spec_pr().cloned(),
};
let result = self
.base_fix
.fix(outside, self.pr_url, self.base, base_commit, failed);
let refreshed = self.refresh_built_with();
result.and(refreshed)
}
fn pr_mergeable(&mut self) -> Result<bool> {
self.pull_request.is_mergeable()
}
fn ensure_pr_ready_and_mergeable(&mut self) -> Result<()> {
self.pull_request.ensure_ready_and_mergeable()
}
fn merge(&mut self, head: &str) -> Result<MergeAttempt> {
self.pull_request.merge(head)
}
fn merge_hold(&mut self) -> Result<Option<String>> {
if self.protected_paths.is_empty() {
return Ok(None);
}
let spec = format!("origin/{}...HEAD", self.base);
let listed = Git::new(self.worktree.path()).run_bytes(&[
"diff",
"--no-renames",
"--name-only",
"-z",
&spec,
])?;
Ok(protected_hold(
self.protected_paths,
listed
.split(|byte| *byte == 0)
.filter(|name| !name.is_empty()),
))
}
fn interrupt_requested(&mut self) -> bool {
interrupt::requested()
}
fn progress(&mut self, line: String) {
progress::step(line);
}
}
fn protected_hold<'a>(
patterns: &[String],
paths: impl Iterator<Item = &'a [u8]>,
) -> Option<String> {
let mut matched: Vec<&[u8]> = paths
.filter(|path| {
patterns
.iter()
.any(|pattern| glob_match(pattern.as_bytes(), path))
})
.collect();
if matched.is_empty() {
return None;
}
matched.sort_unstable();
matched.dedup();
let listed = matched
.iter()
.map(|path| String::from_utf8_lossy(path))
.collect::<Vec<_>>()
.join(", ");
Some(if matched.len() == 1 {
format!(
"protected path {listed} holds the Self-merge; the pull request stays ready for review"
)
} else {
format!(
"protected paths {listed} hold the Self-merge; the pull request stays ready for review"
)
})
}
fn glob_match(pattern: &[u8], path: &[u8]) -> bool {
let mut pattern_at = 0;
let mut path_at = 0;
while pattern_at < pattern.len() {
if pattern[pattern_at] == b'*' && pattern.get(pattern_at + 1) == Some(&b'*') {
pattern_at += 2;
if pattern.get(pattern_at) == Some(&b'/') {
pattern_at += 1;
}
if pattern_at == pattern.len() {
return true;
}
let mut rest = path_at;
loop {
if glob_match(&pattern[pattern_at..], &path[rest..]) {
return true;
}
if rest >= path.len() {
return false;
}
while rest < path.len() && path[rest] != b'/' {
rest += 1;
}
if rest == path.len() {
return false;
}
rest += 1;
}
} else if pattern[pattern_at] == b'*' {
pattern_at += 1;
loop {
if glob_match(&pattern[pattern_at..], &path[path_at..]) {
return true;
}
if path_at == path.len() || path[path_at] == b'/' {
return false;
}
path_at += 1;
}
} else if pattern[pattern_at] == b'?' {
if path_at == path.len() || path[path_at] == b'/' {
return false;
}
pattern_at += 1;
path_at += utf8_char_len(&path[path_at..]);
} else {
if path_at == path.len() || pattern[pattern_at] != path[path_at] {
return false;
}
pattern_at += 1;
path_at += 1;
}
}
path_at == path.len()
}
fn utf8_char_len(bytes: &[u8]) -> usize {
let width = match bytes[0] {
0x00..=0x7F => 1,
0xC0..=0xDF => 2,
0xE0..=0xEF => 3,
0xF0..=0xF7 => 4,
_ => return 1,
};
if bytes.len() >= width && bytes[1..width].iter().all(|byte| byte & 0xC0 == 0x80) {
width
} else {
1
}
}
#[cfg(test)]
pub(crate) mod test_support;
#[cfg(test)]
mod tests;