commit-lsp 0.2.0

Language Server for commit messages
use std::{fs::File, io::Read, process::ExitCode, sync::Mutex};

use clap::Parser as _;
use cli::Cli;
use git::guess_repo_url;
use healthcheck::{ComponentState, HealthReport, OptionExt, ResultExt};
use issue_tracker::IssueTracker;
use tracing::{info, trace};

pub mod analysis;
mod cli;
pub mod issue_tracker;
mod lsp;

pub mod healthcheck;

pub mod config;

pub mod git;

pub mod text_util;

#[tokio::main]
async fn main() -> ExitCode {
    let cli = Cli::parse();

    if cfg!(debug_assertions) {
        let log_file = File::create("commit-lsp.log").expect("Failed to create log file");
        let subscriber = tracing_subscriber::fmt()
            .without_time()
            .pretty()
            .with_writer(Mutex::new(log_file))
            .finish();
        tracing::subscriber::set_global_default(subscriber).unwrap();
    }

    match cli.action {
        cli::Action::Run => {
            let mut health = HealthReport::silent();
            let user_config = config::User::load_default_file(&mut health);
            let repo_config = config::Repository::load_default_file(&mut health);
            trace!("Using config: {:?}", repo_config);
            let remote = initialize_issue_tracker(&user_config, &mut health);
            let analysis = analysis::State::new(repo_config);
            lsp::run_stdio(analysis, remote).await;
        }
        cli::Action::Lint { file } => {
            let mut health = HealthReport::silent();
            let mut text = String::new();
            File::open(&file)
                .unwrap()
                .read_to_string(&mut text)
                .unwrap();
            let repo_config = config::Repository::load_default_file(&mut health);
            return analyse_commit(repo_config, &text);
        }
        cli::Action::Checkhealth => {
            let mut health = HealthReport::new("commit-lsp");
            let user_config = config::User::load_default_file(&mut health);
            let _repo_config = config::Repository::load_default_file(&mut health);
            let remote = initialize_issue_tracker(&user_config, &mut health);

            if let Some(remote) = remote.report(&mut health, "Issue tracker initialized") {
                let check = health.start("request tickets");
                match remote.request_ticket_information().await {
                    Ok(tickets) if !tickets.is_empty() => {
                        let example = tickets.first().unwrap();
                        check.ok_with(format!(
                            "Example ticket: #{} '{}'",
                            example.id(),
                            example.title()
                        ));
                    }
                    Ok(_) => {
                        check.warn("Got empty list of tickets");
                    }
                    Err(e) => {
                        check.error(e.to_string());
                    }
                }
            }
        }
    }

    ExitCode::SUCCESS
}

fn analyse_commit(config: config::Repository, text: &str) -> ExitCode {
    let mut state = analysis::State::new(config);
    state.update_text(text);
    let diagnostics = state.all_diagnostics();

    for diag in &diagnostics {
        println!("{}", diag);
    }

    if diagnostics.is_empty() {
        ExitCode::SUCCESS
    } else {
        ExitCode::FAILURE
    }
}

fn initialize_issue_tracker(
    config: &config::User,
    health: &mut HealthReport,
) -> Option<IssueTracker> {
    health.set_context("Issue Tracker");

    let check = health.start("retrieve repo url");
    let remote_url = guess_repo_url();
    match &remote_url {
        Ok(url) => check.ok_with(url.to_string()),
        Err(_) => check.error("Failed to get remote url"),
    }
    let remote_url = remote_url.ok()?;

    info!("Using git url '{remote_url}'");
    let mut builder = issue_tracker::Builder::new(remote_url.clone());

    let remote_config = config.remote_specific_configuration(&remote_url.to_string());
    health.report(
        "Check for remote specific user config",
        if remote_config.is_some() {
            ComponentState::Ok(None)
        } else {
            ComponentState::Info("None found".into())
        },
    );

    let mut do_guess = true;

    if let Some(remote_config) = remote_config {
        let report = health.start("Check for configured issue tracker");

        if let Some(tracker) = remote_config.issue_tracker_type {
            report.ok_with(tracker.to_string());
            builder.tracker_type = Some(tracker);
            do_guess = false;
        } else {
            report.info("None configured, continue to guess");
        }
    }

    if do_guess {
        builder
            .tracker_type
            .report_with_some(health, "Guess used issue tracker");
    }

    if let Some(remote_config) = remote_config {
        builder.add_remote_config(health, remote_config.clone());
    }

    builder.build(health)
}