thirdshift 0.16.3

Turn a GitHub issue into a ready-for-review pull request, unattended
//! Keep the PR head separate from the later merge tree and bug fix.
use anyhow::{Context, Result, ensure};
use chrono::{DateTime, Utc};
use serde::Deserialize;

use super::super::{Connection, GitHub, LinkedIssue, issue_branch};
use crate::issue::IssueUrl;
use crate::security::fixing::SECURITY_FIX;

pub(crate) struct ReviewedPullRequest {
    pub body: String,
    pub base: String,
    pub head: String,
    pub input: String,
    pub trial: crate::trial::Inputs,
    pub merged_at: DateTime<Utc>,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Metadata {
    body: String,
    head_ref_oid: String,
    base_ref_name: String,
    head_ref_name: String,
    merged_at: DateTime<Utc>,
    commits: Commits,
    merge_commit: MergeCommit,
    closing_issues_references: Connection<Linked>,
}

#[derive(Deserialize)]
struct Linked {
    url: String,
    #[serde(flatten)]
    labels: LinkedIssue,
}

#[derive(Deserialize)]
struct Commits {
    nodes: Vec<CommitNode>,
}
#[derive(Deserialize)]
struct CommitNode {
    commit: Parents,
}
#[derive(Deserialize)]
struct Parents {
    parents: Oids,
}
#[derive(Deserialize)]
struct MergeCommit {
    oid: String,
    parents: Oids,
}
#[derive(Deserialize)]
struct Oids {
    nodes: Vec<Oid>,
}
#[derive(Deserialize)]
struct Oid {
    oid: String,
}

impl GitHub {
    pub(crate) fn bug_origin_review(&self, pr: &IssueUrl) -> Result<ReviewedPullRequest> {
        let json = self.issue_query(pr, QUERY, None)?;
        let metadata: Metadata =
            serde_json::from_value(json["data"]["repository"]["pullRequest"].clone())
                .context("invalid Bug-origin reviewed pull request")?;
        ensure!(
            !metadata.closing_issues_references.page_info.has_next_page,
            "Bug-origin linked issues are incomplete"
        );
        for issue in &metadata.closing_issues_references.nodes {
            ensure!(
                !issue.labels.labels.page_info.has_next_page,
                "Bug-origin issue labels are incomplete"
            );
            ensure!(
                !issue
                    .labels
                    .labels
                    .nodes
                    .iter()
                    .any(|label| SECURITY_FIX.is_named(&label.name)),
                "Security fixes cannot become Bug-origin Eval tasks"
            );
        }
        let inferred = issue_branch(&pr.repo_slug(), &metadata.head_ref_name)?;
        if let Some(issue) = &inferred {
            ensure!(
                !self.issue_labels(issue)?.has(SECURITY_FIX),
                "Security fixes cannot become Bug-origin Eval tasks"
            );
        }
        let issue = metadata
            .closing_issues_references
            .nodes
            .first()
            .map(|issue| IssueUrl::parse(&issue.url))
            .transpose()?
            .or(inferred);
        let (issue_url, input) = if let Some(issue) = issue {
            let text = self.scoring_issue_at(&issue, metadata.merged_at)?;
            (issue.url, format!("# {}\n\n{}\n", text.title, text.body))
        } else {
            // GitHub's /issues/<PR number> redirects to that PR. The Trial
            // needs an identity, but must not be given the later bug report.
            (
                pr.url.clone(),
                format!(
                    "# Historical pull request\n\n{}\n\nNo originating issue was linked; no original issue specification is available.\n",
                    metadata.body
                ),
            )
        };
        // A squash or merge commit records the target at merge time. GitHub
        // rebases rewrite commit IDs; merge-base with the original PR head
        // therefore also discards the rebased PR commits on that parent.
        // Keep the first-commit parent for a preserved, linear PR head.
        let base = if metadata.merge_commit.oid == metadata.head_ref_oid {
            metadata
                .commits
                .nodes
                .first()
                .and_then(|node| node.commit.parents.nodes.first())
        } else {
            metadata.merge_commit.parents.nodes.first()
        }
        .context("Bug-origin pull request has no base commit")?
        .oid
        .clone();
        Ok(ReviewedPullRequest {
            body: metadata.body,
            base,
            head: metadata.head_ref_oid,
            input,
            trial: crate::trial::Inputs {
                issue_url,
                base: metadata.base_ref_name,
                branch: metadata.head_ref_name,
                pr_url: format!("https://github.com/{}/pull/{}", pr.repo_slug(), pr.number),
            },
            merged_at: metadata.merged_at,
        })
    }
}

const QUERY: &str = r#"query BugOriginReview($owner: String!, $repo: String!, $number: Int!) {
  repository(owner: $owner, name: $repo) { pullRequest(number: $number) {
    body headRefOid baseRefName headRefName mergedAt
    mergeCommit { oid parents(first: 1) { nodes { oid } } }
    commits(first: 1) { nodes { commit { parents(first: 1) { nodes { oid } } } } }
    closingIssuesReferences(first: 100) { pageInfo { hasNextPage } nodes {
      url labels(first: 100) { pageInfo { hasNextPage } nodes { name } }
    } }
  } }
}"#;