ghtool 0.4.0

A command-line tool for interacting with Github API with some specialized features oriented around Checks
use std::collections::HashSet;

use eyre::Result;
use futures::stream::FuturesUnordered;
use futures::stream::StreamExt;
use tracing::info;

use crate::github::get_log_futures;
use crate::github::CheckConclusionState;
use crate::github::GithubClient;
use crate::repo_config::TypecheckConfig;
use crate::term::print_check_run_header;
use crate::{git::Repository, github, repo_config::RepoConfig, term::green};

use self::tsc::TscLogParser;

mod tsc;

pub async fn typecheck(
    client: &GithubClient,
    repo: &Repository,
    branch: &str,
    repo_config: &RepoConfig,
    show_files_only: bool,
) -> Result<()> {
    let typecheck_config = repo_config
        .typecheck
        .as_ref()
        .ok_or_else(|| eyre::eyre!("No typecheck config found in .ghtool.toml"))?;

    let pr = client
        .get_pr_for_branch_memoized(&repo.owner, &repo.name, branch)
        .await?;

    let check_runs = client.get_pr_status_checks(&pr.id).await?;
    let (typecheck_check_runs, any_in_progress) =
        filter_typecheck_runs(check_runs, typecheck_config);
    info!(?typecheck_check_runs, "got typecheck check runs");

    if typecheck_check_runs.is_empty() {
        eprintln!(
            "No typechecking jobs found matching the pattern /{}/",
            typecheck_config.job_pattern
        );
    } else {
        process_failing_checks(
            client,
            repo,
            typecheck_check_runs,
            any_in_progress,
            show_files_only,
        )
        .await?;
    }

    Ok(())
}

async fn process_failing_checks(
    client: &GithubClient,
    repo: &Repository,
    typecheck_check_runs: Vec<github::SimpleCheckRun>,
    any_in_progress: bool,
    show_files_only: bool,
) -> Result<()> {
    let failing_typecheck_check_runs: Vec<_> = typecheck_check_runs
        .into_iter()
        .filter(|cr| cr.conclusion == Some(CheckConclusionState::Failure))
        .collect();

    if failing_typecheck_check_runs.is_empty() {
        if any_in_progress {
            eprintln!("⏳  Some typecheck jobs are in progress");
        } else {
            eprintln!("{}  All typecheck jobs are green", green(""));
        }
        return Ok(());
    }

    if show_files_only {
        get_failing_typechecks_files(client, repo, failing_typecheck_check_runs).await?;
    } else {
        get_failing_typechecks(client, repo, failing_typecheck_check_runs).await?;
    }

    Ok(())
}

fn filter_typecheck_runs(
    check_runs: Vec<github::SimpleCheckRun>,
    typecheck_config: &TypecheckConfig,
) -> (Vec<github::SimpleCheckRun>, bool) {
    let mut typecheck_check_runs = Vec::new();
    let mut any_in_progress = false;

    for cr in check_runs {
        if typecheck_config.job_pattern.is_match(&cr.name) {
            if cr.conclusion.is_none() {
                any_in_progress = true;
            }
            typecheck_check_runs.push(cr);
        }
    }

    (typecheck_check_runs, any_in_progress)
}

async fn get_failing_typechecks(
    client: &GithubClient,
    repo: &Repository,
    failing_typecheck_check_runs: Vec<github::SimpleCheckRun>,
) -> Result<()> {
    let mut log_futures: FuturesUnordered<_> =
        get_log_futures(client, repo, &failing_typecheck_check_runs);

    let mut failing_typechecks = Vec::new();
    while let Some(result) = log_futures.next().await {
        let bytes = result.map_err(|_| eyre::eyre!("Error when getting job logs"))?;
        let logs = String::from_utf8_lossy(&bytes);
        let failing_typechecks_inner = TscLogParser::parse(&logs)?;
        failing_typechecks.push(failing_typechecks_inner);
    }

    if failing_typechecks.iter().all(|s| s.is_empty()) {
        eprintln!("No type errors found in log output");
        return Ok(());
    }

    for (check_run, failing_typecheck) in
        failing_typecheck_check_runs.iter().zip(failing_typechecks)
    {
        print_check_run_header(check_run);

        for tsc_error in failing_typecheck {
            for line in tsc_error.lines {
                println!("{}", line);
            }
        }
    }

    Ok(())
}

async fn get_failing_typechecks_files(
    client: &GithubClient,
    repo: &Repository,
    failing_typecheck_check_runs: Vec<github::SimpleCheckRun>,
) -> Result<()> {
    let mut log_futures: FuturesUnordered<_> =
        get_log_futures(client, repo, &failing_typecheck_check_runs);

    let mut failing_typecheck_files: HashSet<String> = HashSet::new();

    while let Some(result) = log_futures.next().await {
        let bytes = result.map_err(|_| eyre::eyre!("Error when getting job logs"))?;
        let logs = String::from_utf8_lossy(&bytes);
        let parsed = TscLogParser::parse(&logs)?;
        for tsc_error in parsed {
            failing_typecheck_files.insert(tsc_error.path);
        }
    }

    if failing_typecheck_files.is_empty() {
        eprintln!("No type errors found in log output");
        return Ok(());
    }

    for file in failing_typecheck_files {
        println!("{}", file);
    }

    Ok(())
}