use anyhow::Context as _;
use trusty_mpm::core::issue_audit::IssueFacts;
use trusty_mpm::core::issue_audit_gh::view_argv;
use super::metadata::{self, ChangedPaths, PrMetadata, RefKind, RefsIssue, RefsLookup};
use super::open::{self, Preflight};
use super::phase_title::{self, TitleDecision};
use super::{GhRunner, argv};
use crate::cli::PrOpenArgs;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ApplyOutcome {
Applied,
Partial(Vec<String>),
}
impl ApplyOutcome {
pub(crate) fn missing(self) -> Vec<String> {
match self {
Self::Applied => Vec::new(),
Self::Partial(missing) => missing,
}
}
}
pub(crate) fn apply<R: GhRunner, P: Preflight>(
gh: &R,
args: &PrOpenArgs,
pre: &P,
pr: &str,
body: &str,
) -> ApplyOutcome {
let read = pre.changed_paths(&args.base, open::diff_head(args));
let changed = match &read {
Ok(paths) => ChangedPaths::Read(&paths[..]),
Err(e) => {
eprintln!(" warning: the diff could not be read: {e:#}");
ChangedPaths::Unreadable
}
};
let number = metadata::first_linked_issue(body);
let found = number.and_then(|n| lookup_ref(gh, args, n));
let refs = match (number, found.as_ref()) {
(_, Some(issue)) => RefsLookup::Found(issue),
(Some(n), None) => RefsLookup::Unreadable(n),
(None, None) => RefsLookup::Absent,
};
let title = phase_title::decide(&args.title, &refs);
let mut meta = metadata::plan(refs, changed, &pre.ownership());
let mut unknown = None;
match title {
TitleDecision::Unchanged => {}
TitleDecision::Retitle(t) => meta.title = Some(t),
TitleDecision::Warn(w) => eprintln!(" warning: {w}"),
TitleDecision::Unknown(what) => unknown = Some(what),
}
let outcome = apply_plan(gh, args, pr, &meta);
for note in &meta.notes {
println!(" {note}");
}
match unknown {
None => outcome,
Some(what) => {
let mut missing = outcome.missing();
missing.push(what);
ApplyOutcome::Partial(missing)
}
}
}
fn lookup_ref<R: GhRunner>(gh: &R, args: &PrOpenArgs, number: u64) -> Option<RefsIssue> {
let mut view = view_argv(number);
push_repo(&mut view, args);
match read_facts(gh, &view) {
Ok(facts) => return Some(RefsIssue::from_facts(number, RefKind::Issue, &facts)),
Err(e) if is_not_an_issue(&e) => println!(
" note: #{number} is a pull request, not an issue; reading its metadata with \
`gh pr view`"
),
Err(e) => {
eprintln!(" warning: issue #{number} could not be read: {e:#}");
return None;
}
}
let mut view = metadata::pr_view_argv(number);
push_repo(&mut view, args);
match read_facts(gh, &view) {
Ok(facts) => Some(RefsIssue::from_facts(number, RefKind::PullRequest, &facts)),
Err(e) => {
eprintln!(" warning: pull request #{number} could not be read either: {e:#}");
None
}
}
}
fn read_facts<R: GhRunner>(gh: &R, view: &[String]) -> anyhow::Result<IssueFacts> {
let json = gh.run(view)?.stdout_ok(view)?;
serde_json::from_str(&json).context("`gh view --json` returned unreadable JSON")
}
fn is_not_an_issue(err: &anyhow::Error) -> bool {
format!("{err:#}")
.to_ascii_lowercase()
.contains("could not resolve to an issue")
}
fn apply_plan<R: GhRunner>(gh: &R, args: &PrOpenArgs, pr: &str, meta: &PrMetadata) -> ApplyOutcome {
if meta.is_empty() {
return ApplyOutcome::Applied;
}
let edit = metadata::edit_argv(pr, args.repo.as_deref(), meta);
let reason = match gh.run(&edit) {
Ok(out) if out.success => {
report_applied(meta);
return ApplyOutcome::Applied;
}
Ok(out) => out.stderr.trim().to_string(),
Err(e) => format!("{e:#}"),
};
eprintln!(
" warning: `gh pr edit` failed applying {}: {reason}",
describe(meta).join(", ")
);
eprintln!(" retrying once, one field at a time");
let mut missing = Vec::new();
for (what, one) in steps(meta) {
let a = metadata::edit_argv(pr, args.repo.as_deref(), &one);
match gh.run(&a) {
Ok(out) if out.success => println!(" applied on retry: {what}"),
Ok(out) => {
eprintln!(" warning: {what} still failed: {}", out.stderr.trim());
missing.push(what);
}
Err(e) => {
eprintln!(" warning: {what} could not be retried: {e:#}");
missing.push(what);
}
}
}
if missing.is_empty() {
ApplyOutcome::Applied
} else {
ApplyOutcome::Partial(missing)
}
}
fn describe(meta: &PrMetadata) -> Vec<String> {
steps(meta).into_iter().map(|(what, _)| what).collect()
}
fn steps(meta: &PrMetadata) -> Vec<(String, PrMetadata)> {
let source = meta
.inherited_from
.map(|n| format!(" (inherited from #{n})"))
.unwrap_or_default();
let mut out = Vec::new();
if let Some(title) = &meta.title {
out.push((
format!(
"title \"{title}\"{}",
meta.inherited_from
.map(|n| format!(" (tag from #{n})"))
.unwrap_or_default()
),
PrMetadata {
title: Some(title.clone()),
..PrMetadata::default()
},
));
}
if !meta.labels.is_empty() {
out.push((
format!("component labels {}", meta.labels.join(", ")),
PrMetadata {
labels: meta.labels.clone(),
..PrMetadata::default()
},
));
}
if let Some(milestone) = &meta.milestone {
out.push((
format!("milestone \"{milestone}\"{source}"),
PrMetadata {
milestone: Some(milestone.clone()),
..PrMetadata::default()
},
));
}
for project in &meta.projects {
out.push((
format!("project \"{project}\"{source}"),
PrMetadata {
projects: vec![project.clone()],
..PrMetadata::default()
},
));
}
out
}
fn report_applied(meta: &PrMetadata) {
if let Some(title) = &meta.title {
println!(" title: {title}");
}
if !meta.labels.is_empty() {
println!(" component labels: {}", meta.labels.join(", "));
}
if let Some(milestone) = &meta.milestone {
println!(" milestone: {milestone}");
}
if !meta.projects.is_empty() {
println!(" projects: {}", meta.projects.join(", "));
}
}
pub(crate) fn retry_create_defaults<R: GhRunner>(
gh: &R,
args: &PrOpenArgs,
pr: &str,
plan: &open::OpenPlan,
) -> ApplyOutcome {
let mut a = argv(&["pr", "edit", pr]);
push_repo(&mut a, args);
a.push("--add-assignee".to_string());
a.push(plan.assignee.clone());
for label in plan.create_labels() {
a.push("--add-label".to_string());
a.push(label);
}
let what = format!(
"assignee {} and labels {}",
plan.assignee,
plan.create_labels().join(", ")
);
match gh.run(&a) {
Ok(out) if out.success => {
println!(" re-applied after the failed create: {what}");
ApplyOutcome::Applied
}
Ok(out) => {
eprintln!(
" warning: {what} could not be re-applied: {}",
out.stderr.trim()
);
ApplyOutcome::Partial(vec![what])
}
Err(e) => {
eprintln!(" warning: {what} could not be re-applied: {e:#}");
ApplyOutcome::Partial(vec![what])
}
}
}
fn push_repo(a: &mut Vec<String>, args: &PrOpenArgs) {
if let Some(repo) = args
.repo
.as_deref()
.map(str::trim)
.filter(|r| !r.is_empty())
{
a.push("--repo".to_string());
a.push(repo.to_string());
}
}