gitig 0.3.0

A cli utility to manage gitignore files easily
/*! Application-specific logic lives here */
// Parts Copyright 2017-2019, Stephan Sokolow

// Standard library imports
use std::path::PathBuf;

// 3rd-party crate imports
use structopt::StructOpt;

use log::{debug, info, trace};

// Local Imports

use crate::errors::*;
use crate::gitignore::Gitignore;
use crate::helpers;
use crate::helpers::{git_dir, BoilerplateOpts, HELP_TEMPLATE};
use crate::template::*;

/// The verbosity level when no `-q` or `-v` arguments are given, with `0` being `-q`
pub const DEFAULT_VERBOSITY: u64 = 1;

/// Command-line argument schema
///
/// ## Relevant Conventions:
///
///  * Make sure that there is a blank space between the `<name>` `<version>` line and the
///    description text or the `--help` output won't comply with the platform conventions that
///    `help2man` depends on to generate your manpage. (Specifically, it will mistake the `<name>
///    <version>` line for part of the description.)
///  * `StructOpt`'s default behaviour of including the author name in the `--help` output is an
///    oddity among Linux commands and, if you don't disable it, you run the risk of people
///    unfamiliar with `StructOpt` assuming that you are an egotistical person who made a conscious
///    choice to add it.
///
///    The proper standardized location for author information is the `AUTHOR` section which you
///    can read about by typing `man help2man`.
///
/// ## Cautions:
///  * Subcommands do not inherit `template` and it must be re-specified for each one. ([clap-rs/clap#1184](https://github.com/clap-rs/clap/issues/1184))
///  * Double-check that your choice of `about` or `long_about` is actually overriding this
///    doc comment. The precedence is affected by things you wouldn't expect, such as the presence
///    or absence of `template` and it's easy to wind up with this doc-comment as your `--help`
///    ([TeXitoi/structopt#173](https://github.com/TeXitoi/structopt/issues/173))
///  * Do not begin the description text for subcommands with `\n`. It will break the formatting in
///    the top-level help output's list of subcommands.
#[derive(StructOpt, Debug)]
#[structopt(template = HELP_TEMPLATE,
            about = "TODO: Replace me with the description text for the command",
            global_setting = structopt::clap::AppSettings::ColoredHelp)]
pub struct CliOpts {
    #[allow(clippy::missing_docs_in_private_items)] // StructOpt won't let us document this
    #[structopt(flatten)]
    pub boilerplate: BoilerplateOpts,

    /// Subcommands
    #[structopt(subcommand)]
    cmd: Command,
}

/// gitig lets you easily start with a fresh gitignore from a template and adds new lines as you
/// wish
#[derive(StructOpt, Debug)]
pub enum Command {
    /// Add a line to the gitignore
    Add {
        /// The glob string that should be added
        glob: Vec<String>,
        /// Add the entry to the repo local ignore file
        #[structopt(short)]
        local: bool,
    },
    /// Download a gitignore for a language
    Get {
        /// Append template to an existing .gitignore file
        #[structopt(short)]
        append: bool,
        /// The language for which the gitignore should be downloaded
        ///
        /// A list with all available languages and projects can be printed with `list-templates`.
        lang: String,
    },
    /// List all available templates that can be downloaded
    ListTemplates,
    /// Write a completion definition for the specified shell to stdout (bash, zsh, etc.)
    DumpCompletions {
        /// Shell to generate completion for
        shell: Option<structopt::clap::Shell>,
    },
    /// Print current .gitignore to stdout
    Cat,
}

/// Runs the command `add`
fn run_add(glob: Vec<String>, local: bool) -> Result<()> {
    for g in glob {
        add(&g, local)?;
    }
    Ok(())
}
fn add(glob: &str, local: bool) -> Result<()> {
    trace!("running command `add` with glob '{}'", &glob);
    let root = match git_dir()? {
        Some(r) => r,
        None => return Err(ErrorKind::NoGitRootFound.into()),
    };
    info!("Working with git root in {:?}", root);

    let mut file_path = PathBuf::from(&root);
    if local {
        file_path.push(".git/info/exclude")
    } else {
        file_path.push(".gitignore");
    }
    let gitig = Gitignore::from_path(&file_path);
    gitig.add_line(glob)?;
    debug!("Added '{}' to {}", glob, gitig);

    Ok(())
}

/// Runs the command `get`
fn run_get(lang: &str, append: bool) -> Result<()> {
    trace!("Run command `get` with lang {}", &lang);
    let mut root = match git_dir()? {
        Some(r) => r,
        None => return Err(ErrorKind::NoGitRootFound.into()),
    };
    info!("Working with git root in {:?}", root);

    let cache = helpers::default_cache()?;
    let tmpl: Template = if cache.exists(lang) {
        debug!("Found a template for {} in cache", lang);
        cache.get(lang)?
    } else {
        let tmpls = helpers::get_templates()?;
        let mut tmpl =
            tmpls.get(lang).ok_or_else(|| ErrorKind::TemplateNotFound(lang.to_string()))?.clone();
        tmpl.load_content()?;
        cache.set(lang, &tmpl)?;
        tmpl
    };

    root.push(".gitignore");
    tmpl.write_to(&root, append)?;
    trace!("Wrote template to file");

    Ok(())
}

/// Runs the command `list-templates`
#[allow(clippy::print_stdout)]
fn run_list_templates() -> Result<()> {
    let tmpl = helpers::get_templates()?;
    let names = tmpl.list_names();
    println!("{}", names.join("\n"));
    Ok(())
}

/// Runs the command `dump-completion` to generate a shell completion script
fn run_dump_completion(shell: Option<structopt::clap::Shell>) -> Result<()> {
    let shell = shell.ok_or(ErrorKind::NoShellProvided)?;
    debug!("Request to dump completion for {}", shell);
    CliOpts::clap().gen_completions_to(
        CliOpts::clap().get_bin_name().unwrap_or_else(|| structopt::clap::crate_name!()),
        shell,
        &mut ::std::io::stdout(),
    );

    Ok(())
}

/// Runs the `cat` command to print the contents
fn run_cat() -> Result<()> {
    let ignore_file = Gitignore::from_default_path()?;
    let mut buf = String::new();
    ignore_file.contents(&mut buf)?;
    println!("{}", buf);
    Ok(())
}

/// The actual `main()`
pub fn main(opts: CliOpts) -> Result<()> {
    match opts.cmd {
        Command::Add { glob, local } => run_add(glob, local)?,
        Command::Get { lang, append } => run_get(&lang, append)?,
        Command::ListTemplates => run_list_templates()?,
        Command::DumpCompletions { shell } => run_dump_completion(shell)?,
        Command::Cat => run_cat()?,
    };

    Ok(())
}

// Tests go below the code where they'll be out of the way when not the target of attention
#[cfg(test)]
mod tests {

    // TODO: Unit test to verify that the doc comment on `CliOpts` isn't overriding the intended
    // about string.

    #[test]
    /// Test something
    fn test_something() {
        // TODO: Test something
    }
}