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
174 lines
6.1 KiB
//! This module contains structs for working with the github templates
|
|
|
|
use log::{debug, trace};
|
|
use reqwest::blocking::Client;
|
|
use reqwest::header::{ACCEPT, CONTENT_TYPE, USER_AGENT};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fs::OpenOptions;
|
|
use std::io::{BufWriter, Write};
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use crate::errors::*;
|
|
|
|
/// Default user agent string used for api requests
|
|
pub const DEFAULT_USER_AGENT: &str = "gitig";
|
|
|
|
/// Response objects from github api
|
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[allow(clippy::missing_docs_in_private_items)]
|
|
pub struct Template {
|
|
pub name: String,
|
|
pub path: String,
|
|
pub sha: String,
|
|
pub size: i64,
|
|
pub url: String,
|
|
#[serde(rename = "html_url")]
|
|
pub html_url: String,
|
|
#[serde(rename = "git_url")]
|
|
pub git_url: String,
|
|
#[serde(rename = "download_url")]
|
|
pub download_url: Option<String>,
|
|
#[serde(rename = "type")]
|
|
pub type_field: String,
|
|
#[serde(rename = "_links")]
|
|
pub links: Links,
|
|
pub content: Option<String>,
|
|
}
|
|
|
|
/// Part of github api response
|
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[allow(clippy::missing_docs_in_private_items)]
|
|
pub struct Links {
|
|
#[serde(rename = "self")]
|
|
pub self_field: String,
|
|
pub git: String,
|
|
pub html: String,
|
|
}
|
|
|
|
impl Template {
|
|
/// Checks if this template file is a gitignore template file
|
|
pub fn is_gitignore_template(&self) -> bool { self.name.ends_with(".gitignore") }
|
|
|
|
/// Returns the name of the file without the .gitignore ending
|
|
///
|
|
/// if `!self.is_gitignore_template()` the whole `self.name` is returned
|
|
pub fn pretty_name(&self) -> &str {
|
|
if let Some(dot_index) = self.name.rfind('.') {
|
|
return &self.name.get(0..dot_index).unwrap_or_else(|| self.name.as_str());
|
|
}
|
|
self.name.as_str()
|
|
}
|
|
|
|
/// Loads the template content from github
|
|
pub fn load_content(&mut self) -> Result<()> {
|
|
let url = self
|
|
.download_url
|
|
.as_ref()
|
|
.ok_or_else(|| ErrorKind::TemplateNoDownloadUrl(self.pretty_name().into()))?;
|
|
debug!("Loading template content for {} from {}", self.pretty_name(), url);
|
|
let client = Client::new();
|
|
let res = client
|
|
.get(url)
|
|
.header(ACCEPT, "text/html")
|
|
.header(USER_AGENT, format!("{} {}", DEFAULT_USER_AGENT, env!("CARGO_PKG_VERSION")))
|
|
.send()
|
|
.chain_err(|| {
|
|
format!("Error while getting content of template {}", self.pretty_name())
|
|
})?;
|
|
|
|
debug!(
|
|
"Got a response from the server ({} B)",
|
|
res.content_length().map_or_else(|| "?".to_string(), |v| v.to_string())
|
|
);
|
|
|
|
let body: String =
|
|
res.text().chain_err(|| "Error while parsing body from template response")?;
|
|
self.content = Some(body);
|
|
debug!("Set content for template {}", self.pretty_name());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Writes the content of this template to the given path
|
|
///
|
|
/// Creates the file if it does not exist or trunceates an already existing one.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `ErrorKind::TemplateNoContent` if this templates has no content (i.e.
|
|
/// `load_content` has not yet been called.
|
|
/// Other reasons for `Err` can be io related errors.
|
|
#[allow(clippy::option_expect_used)]
|
|
pub fn write_to(&self, path: &PathBuf, append: bool) -> Result<()> {
|
|
debug!(
|
|
"Writing contents of {} to {} (append: {})",
|
|
self.pretty_name(),
|
|
path.to_string_lossy(),
|
|
append
|
|
);
|
|
if self.content.is_none() {
|
|
return Err(ErrorKind::TemplateNoContent.into());
|
|
}
|
|
let file = OpenOptions::new()
|
|
.write(true)
|
|
.append(append)
|
|
.create(true)
|
|
.open(path)
|
|
.chain_err(|| "Error while opening gitignore file to write template")?;
|
|
let mut writer = BufWriter::new(file);
|
|
|
|
writer.write_all(b"# template downloaded with gitig (https://git.schneider-hosting.de/schneider/gitig) from https://github.com/github/gitignore\n")?;
|
|
writer.write_all(self.content.as_ref().expect("checked before to be some").as_bytes())?;
|
|
trace!("Wrote all content");
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// This struct holds the information about the templates available at github
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct GithubTemplates {
|
|
/// The templates
|
|
templates: Vec<Template>,
|
|
}
|
|
|
|
impl GithubTemplates {
|
|
/// Loads the templates from the github api
|
|
fn from_server() -> Result<GithubTemplates> {
|
|
trace!("Loading templates from github api");
|
|
let client = Client::new();
|
|
let res = client
|
|
.get("https://api.github.com/repos/github/gitignore/contents//")
|
|
.header(ACCEPT, "application/jsonapplication/vnd.github.v3+json")
|
|
.header(CONTENT_TYPE, "application/json")
|
|
.header(USER_AGENT, format!("{} {}", DEFAULT_USER_AGENT, env!("CARGO_PKG_VERSION")))
|
|
.send()
|
|
.chain_err(|| "Error while sending request to query all templates")?;
|
|
|
|
let body: Vec<Template> = res.json().chain_err(|| "json")?;
|
|
debug!("Received and deserialized {} templates", body.len());
|
|
|
|
Ok(GithubTemplates { templates: body })
|
|
}
|
|
|
|
/// Creates a new struct with templates
|
|
pub fn new() -> Result<GithubTemplates> { Self::from_server() }
|
|
|
|
/// Returns a list of the template names
|
|
pub fn list_names(&self) -> Vec<&str> {
|
|
self.templates
|
|
.iter()
|
|
.filter(|t| t.is_gitignore_template())
|
|
.map(Template::pretty_name)
|
|
.collect()
|
|
}
|
|
|
|
/// Returns the template for the given name, if found
|
|
pub fn get(&self, name: &str) -> Option<&Template> {
|
|
// names have all a .gitignore suffix
|
|
let name = format!("{}.gitignore", name);
|
|
self.templates.iter().find(|t| t.name.eq_ignore_ascii_case(&name))
|
|
}
|
|
}
|