3.1 KiB · text History b868907
//! `gg` — a command-line client for a GitGud forge.
//!
//! Speaks the same `/api/mobile/v1` surface as the Android app, so the two
//! stay in step: a capability that exists on the phone exists here, and a
//! field that changes shape breaks both at once rather than drifting.

mod api;
mod cli;
mod commands;
mod config;
mod output;
mod tui;

use anyhow::Result;
use clap::Parser;

use cli::{Cli, Command};
use commands::Ctx;

#[tokio::main]
async fn main() {
    restore_sigpipe();

    if let Err(e) = run().await {
        // `{:#}` so the cause chain lands on one line: "couldn't reach
        // https://forge…: dns error" says more than either half alone.
        eprintln!("{}: {:#}", output::paint("error", output::Tone::Bad), e);
        std::process::exit(1);
    }
}

/// Die quietly when a pager or `head` closes the pipe.
///
/// Rust masks SIGPIPE at startup, which turns `gg ci logs | head` into
/// a panic on a broken pipe rather than the silent exit every other
/// command-line tool manages.
#[cfg(unix)]
fn restore_sigpipe() {
    const SIGPIPE: i32 = 13;
    const SIG_DFL: usize = 0;

    unsafe extern "C" {
        fn signal(signum: i32, handler: usize) -> usize;
    }

    unsafe {
        signal(SIGPIPE, SIG_DFL);
    }
}

#[cfg(not(unix))]
fn restore_sigpipe() {}

async fn run() -> Result<()> {
    let args = Cli::parse();

    // Login and logout are the two commands that can't require credentials
    // to already exist.
    match &args.command {
        Command::Login(login) => {
            return commands::auth::login(
                login,
                args.url.as_deref(),
                args.token.as_deref(),
                args.json,
            )
            .await;
        }
        Command::Logout => return commands::auth::logout(args.json),
        _ => {}
    }

    let cfg = config::Config::load()?;
    let creds = config::resolve(args.url.as_deref(), args.token.as_deref(), &cfg)?;
    let ctx = Ctx {
        api: api::Client::new(&creds)?,
        base_url: creds.url.clone(),
        cfg,
        repo_flag: args.repo.clone(),
        json: args.json,
    };

    match &args.command {
        Command::Login(_) | Command::Logout => unreachable!("handled above"),
        Command::Whoami => commands::auth::whoami(&ctx).await,
        Command::RepoList(list) => commands::repo::list(&ctx, list).await,
        Command::Repo(repo) => commands::repo::show(&ctx, repo).await,
        Command::Ci(ci) => commands::ci::run(&ctx, &ci.command).await,
        Command::Issue(issue) => commands::issue::run(&ctx, &issue.command).await,
        Command::Pull(pull) => commands::pull::run(&ctx, &pull.command).await,
        Command::Labels(labels) => commands::label::run(&ctx, labels.command.as_ref()).await,
        Command::Wiki(wiki) => commands::wiki::run(&ctx, &wiki.command).await,
        Command::Package(package) => commands::package::run(&ctx, &package.command).await,
        Command::Badges(badges) => commands::badges::run(&ctx, badges.command.as_ref()).await,
        Command::Dashboard(dashboard) => tui::run(&ctx, dashboard).await,
        Command::Audit(audit) => commands::audit::run(&ctx, audit).await,
    }
}