A cli program to easily handle .gitignore files
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
7.0 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. /*! Application-specific logic lives here */
  2. // Parts Copyright 2017-2019, Stephan Sokolow
  3. // Standard library imports
  4. use std::path::PathBuf;
  5. // 3rd-party crate imports
  6. use structopt::StructOpt;
  7. use log::{debug, info, trace};
  8. // Local Imports
  9. use crate::errors::*;
  10. use crate::gitignore::Gitignore;
  11. use crate::helpers;
  12. use crate::helpers::{git_dir, BoilerplateOpts, HELP_TEMPLATE};
  13. use crate::template::*;
  14. /// The verbosity level when no `-q` or `-v` arguments are given, with `0` being `-q`
  15. pub const DEFAULT_VERBOSITY: u64 = 1;
  16. /// Command-line argument schema
  17. ///
  18. /// ## Relevant Conventions:
  19. ///
  20. /// * Make sure that there is a blank space between the `<name>` `<version>` line and the
  21. /// description text or the `--help` output won't comply with the platform conventions that
  22. /// `help2man` depends on to generate your manpage. (Specifically, it will mistake the `<name>
  23. /// <version>` line for part of the description.)
  24. /// * `StructOpt`'s default behaviour of including the author name in the `--help` output is an
  25. /// oddity among Linux commands and, if you don't disable it, you run the risk of people
  26. /// unfamiliar with `StructOpt` assuming that you are an egotistical person who made a conscious
  27. /// choice to add it.
  28. ///
  29. /// The proper standardized location for author information is the `AUTHOR` section which you
  30. /// can read about by typing `man help2man`.
  31. ///
  32. /// ## Cautions:
  33. /// * 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))
  34. /// * Double-check that your choice of `about` or `long_about` is actually overriding this
  35. /// doc comment. The precedence is affected by things you wouldn't expect, such as the presence
  36. /// or absence of `template` and it's easy to wind up with this doc-comment as your `--help`
  37. /// ([TeXitoi/structopt#173](https://github.com/TeXitoi/structopt/issues/173))
  38. /// * Do not begin the description text for subcommands with `\n`. It will break the formatting in
  39. /// the top-level help output's list of subcommands.
  40. #[derive(StructOpt, Debug)]
  41. #[structopt(template = HELP_TEMPLATE,
  42. about = "TODO: Replace me with the description text for the command",
  43. global_setting = structopt::clap::AppSettings::ColoredHelp)]
  44. pub struct CliOpts {
  45. #[allow(clippy::missing_docs_in_private_items)] // StructOpt won't let us document this
  46. #[structopt(flatten)]
  47. pub boilerplate: BoilerplateOpts,
  48. /// Subcommands
  49. #[structopt(subcommand)]
  50. cmd: Command,
  51. }
  52. /// gitig lets you easily start with a fresh gitignore from a template and adds new lines as you
  53. /// wish
  54. #[derive(StructOpt, Debug)]
  55. pub enum Command {
  56. /// Add a line to the gitignore
  57. Add {
  58. /// The glob string that should be added
  59. glob: Vec<String>,
  60. /// Add the entry to the repo local ignore file
  61. #[structopt(short)]
  62. local: bool,
  63. },
  64. /// Download a gitignore for a language
  65. Get {
  66. /// Append template to an existing .gitignore file
  67. #[structopt(short)]
  68. append: bool,
  69. /// The language for which the gitignore should be downloaded
  70. ///
  71. /// A list with all available languages and projects can be printed with `list-templates`.
  72. lang: String,
  73. },
  74. /// List all available templates that can be downloaded
  75. ListTemplates,
  76. /// Write a completion definition for the specified shell to stdout (bash, zsh, etc.)
  77. DumpCompletions {
  78. /// Shell to generate completion for
  79. shell: Option<structopt::clap::Shell>,
  80. },
  81. /// Print current .gitignore to stdout
  82. Cat,
  83. }
  84. /// Runs the command `add`
  85. fn run_add(glob: Vec<String>, local: bool) -> Result<()> {
  86. for g in glob {
  87. add(&g, local)?;
  88. }
  89. Ok(())
  90. }
  91. fn add(glob: &str, local: bool) -> Result<()> {
  92. trace!("running command `add` with glob '{}'", &glob);
  93. let root = match git_dir()? {
  94. Some(r) => r,
  95. None => return Err(ErrorKind::NoGitRootFound.into()),
  96. };
  97. info!("Working with git root in {:?}", root);
  98. let mut file_path = PathBuf::from(&root);
  99. let override_path = std::env::var("GITIG_OVERRIDE_PATH").ok();
  100. if let Some(override_path) = override_path {
  101. file_path = PathBuf::from(override_path);
  102. } else if local {
  103. file_path.push(".git/info/exclude")
  104. } else {
  105. file_path.push(".gitignore");
  106. }
  107. let gitig = Gitignore::from_path(&file_path);
  108. gitig.add_line(glob)?;
  109. debug!("Added '{}' to {}", glob, gitig);
  110. Ok(())
  111. }
  112. /// Runs the command `get`
  113. fn run_get(lang: &str, append: bool) -> Result<()> {
  114. trace!("Run command `get` with lang {}", &lang);
  115. let mut root = match git_dir()? {
  116. Some(r) => r,
  117. None => return Err(ErrorKind::NoGitRootFound.into()),
  118. };
  119. info!("Working with git root in {:?}", root);
  120. let cache = helpers::default_cache()?;
  121. let tmpl: Template = if cache.exists(lang) {
  122. debug!("Found a template for {} in cache", lang);
  123. cache.get(lang)?
  124. } else {
  125. let tmpls = helpers::get_templates()?;
  126. let mut tmpl =
  127. tmpls.get(lang).ok_or_else(|| ErrorKind::TemplateNotFound(lang.to_string()))?.clone();
  128. tmpl.load_content()?;
  129. cache.set(lang, &tmpl)?;
  130. tmpl
  131. };
  132. root.push(".gitignore");
  133. tmpl.write_to(&root, append)?;
  134. trace!("Wrote template to file");
  135. Ok(())
  136. }
  137. /// Runs the command `list-templates`
  138. #[allow(clippy::print_stdout)]
  139. fn run_list_templates() -> Result<()> {
  140. let tmpl = helpers::get_templates()?;
  141. let names = tmpl.list_names();
  142. println!("{}", names.join("\n"));
  143. Ok(())
  144. }
  145. /// Runs the command `dump-completion` to generate a shell completion script
  146. fn run_dump_completion(shell: Option<structopt::clap::Shell>) -> Result<()> {
  147. let shell = shell.ok_or(ErrorKind::NoShellProvided)?;
  148. debug!("Request to dump completion for {}", shell);
  149. CliOpts::clap().gen_completions_to(
  150. CliOpts::clap().get_bin_name().unwrap_or_else(|| structopt::clap::crate_name!()),
  151. shell,
  152. &mut ::std::io::stdout(),
  153. );
  154. Ok(())
  155. }
  156. /// Runs the `cat` command to print the contents
  157. fn run_cat() -> Result<()> {
  158. let ignore_file = Gitignore::from_default_path()?;
  159. let mut buf = String::new();
  160. ignore_file.contents(&mut buf)?;
  161. println!("{}", buf);
  162. Ok(())
  163. }
  164. /// The actual `main()`
  165. pub fn main(opts: CliOpts) -> Result<()> {
  166. match opts.cmd {
  167. Command::Add { glob, local } => run_add(glob, local)?,
  168. Command::Get { lang, append } => run_get(&lang, append)?,
  169. Command::ListTemplates => run_list_templates()?,
  170. Command::DumpCompletions { shell } => run_dump_completion(shell)?,
  171. Command::Cat => run_cat()?,
  172. };
  173. Ok(())
  174. }
  175. // Tests go below the code where they'll be out of the way when not the target of attention
  176. #[cfg(test)]
  177. mod tests {
  178. // TODO: Unit test to verify that the doc comment on `CliOpts` isn't overriding the intended
  179. // about string.
  180. #[test]
  181. /// Test something
  182. fn test_something() {
  183. // TODO: Test something
  184. }
  185. }