thirdshift 0.6.0

Turn a GitHub issue into a ready-for-review pull request, unattended
//! Pre-flight checks: everything that must hold before a Run or an Architect
//! run creates anything.

use anyhow::{Result, bail};

use crate::git::Git;
use crate::github;
use crate::issue::IssueUrl;

/// Check that a Run on `issue` from the `launch` repository makes sense. The
/// Base branch is checked separately, by [`check_base_branch`], once Issue
/// branch selection has picked it.
pub fn check(launch: &Git, issue: &IssueUrl) -> Result<()> {
    let origin = launch.run(&["config", "remote.origin.url"])?;
    if !issue.matches_origin(&origin) {
        bail!(
            "origin mismatch: {} is not in the repository at origin {origin}",
            issue.url
        );
    }
    if !github::issue_is_open(issue)? {
        bail!("issue #{} is closed", issue.number);
    }
    check_identity(launch)
}

/// Check that git has the name and email an agent commits with.
pub fn check_identity(launch: &Git) -> Result<()> {
    for (key, example_value) in [
        ("user.name", r#""Your Name""#),
        ("user.email", "you@example.com"),
    ] {
        if launch.run(&["config", "--default", "", key])?.is_empty() {
            bail!(
                "git {key} is not set; the agent needs it to commit. \
                 Set it with: git config --global {key} {example_value}"
            );
        }
    }
    Ok(())
}

/// Check that the Base branch `base` exists on origin and that the local
/// `base`, if any, has no commits origin lacks.
pub fn check_base_branch(launch: &Git, base: &str) -> Result<()> {
    let on_origin = launch.run(&[
        "ls-remote",
        "--heads",
        "origin",
        &format!("refs/heads/{base}"),
    ])?;
    if on_origin.is_empty() {
        bail!("base branch {base} does not exist on origin; push it first");
    }
    launch.run(&["fetch", "origin", base])?;
    let local = format!("refs/heads/{base}");
    if launch
        .run(&["rev-parse", "--verify", "--quiet", &local])
        .is_err()
    {
        return Ok(());
    }
    let ahead = launch.run(&["rev-list", "--count", &format!("origin/{base}..{base}")])?;
    if ahead != "0" {
        bail!("local {base} is {ahead} commit(s) ahead of origin/{base}; push them first");
    }
    Ok(())
}