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.

174 lines
6.1 KiB

  1. //! This module contains structs for working with the github templates
  2. use log::{debug, trace};
  3. use reqwest::blocking::Client;
  4. use reqwest::header::{ACCEPT, CONTENT_TYPE, USER_AGENT};
  5. use serde::{Deserialize, Serialize};
  6. use std::fs::OpenOptions;
  7. use std::io::{BufWriter, Write};
  8. use std::path::PathBuf;
  9. use crate::errors::*;
  10. /// Default user agent string used for api requests
  11. pub const DEFAULT_USER_AGENT: &str = "gitig";
  12. /// Response objects from github api
  13. #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
  14. #[serde(rename_all = "camelCase")]
  15. #[allow(clippy::missing_docs_in_private_items)]
  16. pub struct Template {
  17. pub name: String,
  18. pub path: String,
  19. pub sha: String,
  20. pub size: i64,
  21. pub url: String,
  22. #[serde(rename = "html_url")]
  23. pub html_url: String,
  24. #[serde(rename = "git_url")]
  25. pub git_url: String,
  26. #[serde(rename = "download_url")]
  27. pub download_url: Option<String>,
  28. #[serde(rename = "type")]
  29. pub type_field: String,
  30. #[serde(rename = "_links")]
  31. pub links: Links,
  32. pub content: Option<String>,
  33. }
  34. /// Part of github api response
  35. #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
  36. #[serde(rename_all = "camelCase")]
  37. #[allow(clippy::missing_docs_in_private_items)]
  38. pub struct Links {
  39. #[serde(rename = "self")]
  40. pub self_field: String,
  41. pub git: String,
  42. pub html: String,
  43. }
  44. impl Template {
  45. /// Checks if this template file is a gitignore template file
  46. pub fn is_gitignore_template(&self) -> bool { self.name.ends_with(".gitignore") }
  47. /// Returns the name of the file without the .gitignore ending
  48. ///
  49. /// if `!self.is_gitignore_template()` the whole `self.name` is returned
  50. pub fn pretty_name(&self) -> &str {
  51. if let Some(dot_index) = self.name.rfind('.') {
  52. return &self.name.get(0..dot_index).unwrap_or_else(|| self.name.as_str());
  53. }
  54. self.name.as_str()
  55. }
  56. /// Loads the template content from github
  57. pub fn load_content(&mut self) -> Result<()> {
  58. let url = self
  59. .download_url
  60. .as_ref()
  61. .ok_or_else(|| ErrorKind::TemplateNoDownloadUrl(self.pretty_name().into()))?;
  62. debug!("Loading template content for {} from {}", self.pretty_name(), url);
  63. let client = Client::new();
  64. let res = client
  65. .get(url)
  66. .header(ACCEPT, "text/html")
  67. .header(USER_AGENT, format!("{} {}", DEFAULT_USER_AGENT, env!("CARGO_PKG_VERSION")))
  68. .send()
  69. .chain_err(|| {
  70. format!("Error while getting content of template {}", self.pretty_name())
  71. })?;
  72. debug!(
  73. "Got a response from the server ({} B)",
  74. res.content_length().map_or_else(|| "?".to_string(), |v| v.to_string())
  75. );
  76. let body: String =
  77. res.text().chain_err(|| "Error while parsing body from template response")?;
  78. self.content = Some(body);
  79. debug!("Set content for template {}", self.pretty_name());
  80. Ok(())
  81. }
  82. /// Writes the content of this template to the given path
  83. ///
  84. /// Creates the file if it does not exist or trunceates an already existing one.
  85. ///
  86. /// # Errors
  87. ///
  88. /// Returns `ErrorKind::TemplateNoContent` if this templates has no content (i.e.
  89. /// `load_content` has not yet been called.
  90. /// Other reasons for `Err` can be io related errors.
  91. #[allow(clippy::option_expect_used)]
  92. pub fn write_to(&self, path: &PathBuf, append: bool) -> Result<()> {
  93. debug!(
  94. "Writing contents of {} to {} (append: {})",
  95. self.pretty_name(),
  96. path.to_string_lossy(),
  97. append
  98. );
  99. if self.content.is_none() {
  100. return Err(ErrorKind::TemplateNoContent.into());
  101. }
  102. let file = OpenOptions::new()
  103. .write(true)
  104. .append(append)
  105. .create(true)
  106. .open(path)
  107. .chain_err(|| "Error while opening gitignore file to write template")?;
  108. let mut writer = BufWriter::new(file);
  109. writer.write_all(b"# template downloaded with gitig (https://git.schneider-hosting.de/schneider/gitig) from https://github.com/github/gitignore\n")?;
  110. writer.write_all(self.content.as_ref().expect("checked before to be some").as_bytes())?;
  111. trace!("Wrote all content");
  112. Ok(())
  113. }
  114. }
  115. /// This struct holds the information about the templates available at github
  116. #[derive(Serialize, Deserialize)]
  117. pub struct GithubTemplates {
  118. /// The templates
  119. templates: Vec<Template>,
  120. }
  121. impl GithubTemplates {
  122. /// Loads the templates from the github api
  123. fn from_server() -> Result<GithubTemplates> {
  124. trace!("Loading templates from github api");
  125. let client = Client::new();
  126. let res = client
  127. .get("https://api.github.com/repos/github/gitignore/contents//")
  128. .header(ACCEPT, "application/jsonapplication/vnd.github.v3+json")
  129. .header(CONTENT_TYPE, "application/json")
  130. .header(USER_AGENT, format!("{} {}", DEFAULT_USER_AGENT, env!("CARGO_PKG_VERSION")))
  131. .send()
  132. .chain_err(|| "Error while sending request to query all templates")?;
  133. let body: Vec<Template> = res.json().chain_err(|| "json")?;
  134. debug!("Received and deserialized {} templates", body.len());
  135. Ok(GithubTemplates { templates: body })
  136. }
  137. /// Creates a new struct with templates
  138. pub fn new() -> Result<GithubTemplates> { Self::from_server() }
  139. /// Returns a list of the template names
  140. pub fn list_names(&self) -> Vec<&str> {
  141. self.templates
  142. .iter()
  143. .filter(|t| t.is_gitignore_template())
  144. .map(Template::pretty_name)
  145. .collect()
  146. }
  147. /// Returns the template for the given name, if found
  148. pub fn get(&self, name: &str) -> Option<&Template> {
  149. // names have all a .gitignore suffix
  150. let name = format!("{}.gitignore", name);
  151. self.templates.iter().find(|t| t.name.eq_ignore_ascii_case(&name))
  152. }
  153. }