oyo 0.1.67

CLI TUI for oyo - Step-through diff viewer
use super::{http, loading, run_command};
use crate::ReviewProviderKind;
use anyhow::Result;
use serde_json::Value;
use std::collections::HashMap;
use std::process::Command;
use std::sync::{Mutex, OnceLock};

#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReadTransport {
    AuthenticatedCli,
    PublicHttps,
}

fn gh_command() -> Command {
    let mut command = Command::new("gh");
    command
        .env_remove("GH_DEBUG")
        .env("GH_NO_UPDATE_NOTIFIER", "1");
    command
}

fn config_absent() -> bool {
    let paths = if let Some(directory) =
        std::env::var_os("GH_CONFIG_DIR").filter(|value| !value.is_empty())
    {
        vec![std::path::PathBuf::from(directory).join("hosts.yml")]
    } else {
        let (Some(home), Some(config)) = (dirs::home_dir(), dirs::config_dir()) else {
            return false;
        };
        let mut paths = vec![
            home.join(".config/gh/hosts.yml"),
            config.join("gh/hosts.yml"),
        ];
        if let Some(directory) =
            std::env::var_os("XDG_CONFIG_HOME").filter(|value| !value.is_empty())
        {
            paths.push(std::path::PathBuf::from(directory).join("gh/hosts.yml"));
        }
        if cfg!(windows) {
            if let Some(directory) = std::env::var_os("AppData").filter(|value| !value.is_empty()) {
                paths.push(std::path::PathBuf::from(directory).join("GitHub CLI/hosts.yml"));
            }
        }
        paths
    };
    paths.iter().all(|path| {
        std::fs::symlink_metadata(path)
            .is_err_and(|error| error.kind() == std::io::ErrorKind::NotFound)
    })
}

impl ReadTransport {
    pub(crate) fn for_host(host: &str) -> Self {
        if host != "github.com"
            || ["GH_TOKEN", "GITHUB_TOKEN"]
                .iter()
                .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
        {
            return Self::AuthenticatedCli;
        }
        static AUTH: OnceLock<Mutex<HashMap<String, bool>>> = OnceLock::new();
        let mut hosts = AUTH
            .get_or_init(Default::default)
            .lock()
            .unwrap_or_else(|e| e.into_inner());
        let authenticated = hosts.get(host).copied().unwrap_or_else(|| {
            let mut command = gh_command();
            command.args([
                "auth",
                "status",
                "--hostname",
                host,
                "--json",
                "hosts",
                "--jq",
                ".hosts | length > 0",
            ]);
            let authenticated = match run_command(command, None) {
                Ok(output) => output.trim().parse::<bool>().unwrap_or(true),
                Err(error)
                    if error.chain().any(|e| {
                        e.downcast_ref::<std::io::Error>()
                            .is_some_and(|e| e.kind() == std::io::ErrorKind::NotFound)
                    }) =>
                {
                    !config_absent()
                }
                Err(_) => {
                    let mut command = gh_command();
                    command.args(["auth", "status", "--hostname", host]);
                    match run_command(command, None) {
                        Ok(_) => true,
                        Err(error) => !error
                            .to_string()
                            .contains("You are not logged into any GitHub hosts."),
                    }
                }
            };
            if !loading::cancelled() {
                hosts.insert(host.to_owned(), authenticated);
            }
            authenticated
        });
        if authenticated {
            Self::AuthenticatedCli
        } else {
            Self::PublicHttps
        }
    }
}

pub(crate) fn json(host: &str, endpoint: &str) -> Result<Value> {
    let transport = ReadTransport::for_host(host);
    let command = match transport {
        ReadTransport::AuthenticatedCli => {
            let mut command = gh_command();
            command.args(["api", "--hostname", host, endpoint]);
            command
        }
        ReadTransport::PublicHttps => http::request(
            &format!("https://api.github.com/{endpoint}"),
            "Accept: application/vnd.github+json",
        ),
    };
    let data = match transport {
        ReadTransport::AuthenticatedCli => run_command(command, None)?,
        ReadTransport::PublicHttps => super::run_public_http(command, ReviewProviderKind::GitHub)?,
    };
    Ok(serde_json::from_str(&data)?)
}

pub(crate) fn patch(
    host: &str,
    repo: &str,
    base_tip: &str,
    head: &str,
) -> (Command, http::ResponseFormat) {
    let transport = ReadTransport::for_host(host);
    let command = match transport {
        ReadTransport::AuthenticatedCli => {
            let mut command = gh_command();
            command.args(["api", "--hostname", host]);
            if streaming_flag() {
                command.arg("--allow-escape-sequences");
            }
            command.args([
                "-H",
                "Accept: application/vnd.github.diff",
                &format!("repos/{repo}/compare/{base_tip}...{head}"),
            ]);
            command
        }
        ReadTransport::PublicHttps => http::request(
            &format!("https://github.com/{repo}/compare/{base_tip}...{head}.diff"),
            "Accept: text/plain",
        ),
    };
    let format = match transport {
        ReadTransport::AuthenticatedCli => http::ResponseFormat::Body,
        ReadTransport::PublicHttps => http::ResponseFormat::Headers(ReviewProviderKind::GitHub),
    };
    (command, format)
}

fn streaming_flag() -> bool {
    static FLAG: OnceLock<bool> = OnceLock::new();
    *FLAG.get_or_init(|| {
        let mut command = gh_command();
        command.args(["api", "--help"]);
        run_command(command, None).is_ok_and(|help| help.contains("--allow-escape-sequences"))
    })
}