commit-lsp 0.2.0

Language Server for commit messages
use std::{ffi::OsStr, fmt::Display, process::Command};

use git_url_parse::GitUrl;
use secure_string::SecureString;
use serde::Deserialize;
use tracing::warn;

use crate::{
    config::Remote,
    healthcheck::{HealthReport, ResultExt},
};

use super::{
    IssueTracker, IssueTrackerAdapter, azure::AzureDevops, demo::DemoAdapter, github::Github,
};

#[derive(Copy, Clone, Debug, Deserialize)]
pub enum IssueTrackerType {
    Demo,
    Gitlab,
    Github,
    AzureDevOps,
}

impl Display for IssueTrackerType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                IssueTrackerType::Demo => "<DEMO>",
                IssueTrackerType::Gitlab => "Gitlab",
                IssueTrackerType::Github => "Github",
                IssueTrackerType::AzureDevOps => "Azure DevOps",
            }
        )
    }
}

impl IssueTrackerType {
    pub fn guess_from_url(url: GitUrl) -> Option<Self> {
        if cfg!(debug_assertions) && std::env::var("COMMIT_LSP_DEMO_FOLDER").is_ok() {
            return Some(Self::Demo);
        }

        match url.host.as_ref()?.as_str() {
            "ssh.dev.azure.com" | "dev.azure.com" => Some(Self::AzureDevOps),
            "github.com" => Some(Self::Github),
            host if host.contains("gitlab") => Some(Self::Gitlab),
            _ => None,
        }
    }
}

pub struct Builder {
    pub tracker_type: Option<IssueTrackerType>,
    url: GitUrl,
    credential_command: Option<Vec<String>>,
}

pub(super) struct TrackerConfig {
    pub url: GitUrl,
    pub secret: Option<SecureString>,
}

impl Builder {
    pub fn new(url: GitUrl) -> Self {
        Self {
            tracker_type: IssueTrackerType::guess_from_url(url.clone()),
            url,
            credential_command: None,
        }
    }

    pub fn add_remote_config(&mut self, health: &mut HealthReport, config: Remote) {
        self.credential_command = config.credentials_command;
        if let Some(url_override) = config.issue_tracker_url {
            let report = health.start("Apply url overwrite");
            let parsed = GitUrl::parse(&url_override);
            match parsed {
                Ok(url) => {
                    self.url = url.clone();
                    report.ok_with(url.trim_auth().to_string());
                }
                Err(err) => {
                    report.error(err.to_string());
                }
            }
            self.url = GitUrl::parse(&url_override).expect("URL override is not a valid url!");
        }
    }

    pub fn build(self, health: &mut HealthReport) -> Option<IssueTracker> {
        let mut secret = None;
        let report = health.start("Check for credentials command");
        if let Some(cmd) = self.credential_command {
            report.ok_with(cmd.join(" "));
            secret = get_credentials(&cmd).report(health, "Get credentials");
        } else {
            report.info("None configured")
        }

        let cfg = TrackerConfig {
            url: self.url,
            secret,
        };

        let adapter: Box<dyn IssueTrackerAdapter> = match self.tracker_type? {
            IssueTrackerType::Demo => Box::new(DemoAdapter::new(
                std::env::var("COMMIT_LSP_DEMO_FOLDER").unwrap().into(),
            )),
            IssueTrackerType::Gitlab => Box::new(super::gitlab::Gitlab::new(cfg)?),
            IssueTrackerType::Github => Box::new(Github::new(cfg)?),
            IssueTrackerType::AzureDevOps => Box::new(AzureDevops::new(cfg)?),
        };

        Some(IssueTracker::new(adapter))
    }
}

fn get_credentials(cmdline: &[impl AsRef<OsStr>]) -> Option<SecureString> {
    let pat = {
        let (cmd, args) = cmdline.split_first()?;

        let out = Command::new(cmd).args(args).output().unwrap();
        if !out.status.success() {
            let stderr = String::from_utf8_lossy(&out.stderr).to_string();
            let code = out.status.code();
            warn!(stderr, ?code, "Failed to execute credentials command!");
            return None;
        }
        String::from_utf8(out.stdout).unwrap().trim().into()
    };
    Some(pat)
}