gh-download 0.5.1

Download a file or directory from a GitHub repository path.
Documentation
mod config;
mod help;
mod resolve;
mod types;

use std::env;
use std::ffi::OsString;

use clap::FromArgMatches;

use crate::i18n::Language;

pub use self::help::{command, command_for_language};
pub(crate) use self::resolve::debug_token_source_label;
pub(crate) use self::resolve::token_present;
pub use self::resolve::{
    pick_token, resolve_cli, resolve_debug, resolve_local_target, resolve_prefix_mode,
    resolve_proxy_base,
};
pub(crate) use self::resolve::{resolve_cli_with_config, resolve_language};
pub use self::types::{Cli, CliInvocation, PrefixProxyMode, ResolvedOptions};

pub(crate) use self::config::{detect_language_from_args_env_and_config, load_active_config};

pub fn parse_cli_from_env() -> Cli {
    parse_cli_invocation_from_env().cli
}

pub fn parse_cli_invocation_from_env() -> CliInvocation {
    let args: Vec<OsString> = env::args_os().collect();
    let language = detect_language_from_args_env_and_config(
        &args,
        env::var("LC_ALL").ok().as_deref(),
        env::var("LC_MESSAGES").ok().as_deref(),
        env::var("LANG").ok().as_deref(),
    );
    let args = normalize_args_for_parsing(args);
    parse_cli_invocation_from_args(args, language)
}

pub fn parse_cli_from_args<I, T>(args: I, language: Language) -> Cli
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
{
    parse_cli_invocation_from_args(args, language).cli
}

pub fn parse_cli_invocation_from_args<I, T>(args: I, language: Language) -> CliInvocation
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
{
    let args_vec: Vec<OsString> = args.into_iter().map(Into::into).collect();
    let mut command = command_for_language(language);
    let matches = command
        .try_get_matches_from_mut(args_vec)
        .unwrap_or_else(|error| error.exit());
    self::types::ParsedCli::from_arg_matches(&matches)
        .unwrap_or_else(|error| error.exit())
        .into()
}

fn normalize_args_for_parsing(mut args: Vec<OsString>) -> Vec<OsString> {
    if args.len() == 1 {
        args.push(OsString::from("--help"));
    }
    args
}

#[cfg(test)]
mod tests {
    use std::ffi::OsString;

    use clap::Parser;

    use super::*;

    #[test]
    fn cli_parses_concurrency_json_overwrite_no_color_ref_prefix_mode_and_debug() {
        let invocation = parse_cli_invocation_from_args(
            [
                "gh-download",
                "owner/repo",
                "src",
                "./downloads",
                "--config",
                "./gh-download.toml",
                "--ref",
                "main",
                "--prefix-mode",
                "prefer",
                "--api-base",
                "https://ghe.example.com/api/v3",
                "-c",
                "8",
                "--lang",
                "zh",
                "--overwrite",
                "--json",
                "--debug",
                "--no-color",
            ],
            Language::En,
        );
        let cli = invocation.cli;

        assert_eq!(
            invocation.config_path.as_deref(),
            Some(std::path::Path::new("./gh-download.toml"))
        );
        assert_eq!(cli.git_ref.as_deref(), Some("main"));
        assert_eq!(cli.prefix_mode, Some(PrefixProxyMode::Prefer));
        assert_eq!(
            cli.api_base.as_deref(),
            Some("https://ghe.example.com/api/v3")
        );
        assert_eq!(cli.concurrency, 8);
        assert_eq!(invocation.explicit_concurrency, Some(8));
        assert_eq!(cli.language, Some(Language::Zh));
        assert!(cli.overwrite);
        assert!(cli.json);
        assert!(cli.debug);
        assert!(cli.no_color);
    }

    #[test]
    fn cli_rejects_zero_concurrency() {
        let error = Cli::try_parse_from([
            "gh-download",
            "owner/repo",
            "src",
            "./downloads",
            "--concurrency",
            "0",
        ])
        .expect_err("zero concurrency should be rejected");

        assert!(error.to_string().contains("at least 1"));
    }

    #[test]
    fn cli_accepts_long_concurrency_flag() {
        let cli = parse_cli_from_args(
            [
                "gh-download",
                "owner/repo",
                "src",
                "./downloads",
                "--concurrency",
                "6",
            ],
            Language::En,
        );

        assert_eq!(cli.concurrency, 6);
    }

    #[test]
    fn cli_defaults_concurrency_to_four() {
        let invocation = parse_cli_invocation_from_args(
            ["gh-download", "owner/repo", "src", "./downloads"],
            Language::En,
        );
        let cli = invocation.cli;

        assert_eq!(cli.concurrency, 4);
        assert_eq!(invocation.explicit_concurrency, None);
        assert!(!cli.overwrite);
        assert!(!cli.json);
    }

    #[test]
    fn empty_invocation_is_normalized_to_help() {
        let args = normalize_args_for_parsing(vec![OsString::from("gh-download")]);

        assert_eq!(
            args,
            vec![OsString::from("gh-download"), OsString::from("--help")]
        );
    }
}