diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..7530651 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 120 diff --git a/src/generate.rs b/src/generate.rs index e412fe2..966e3a5 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -1,30 +1,15 @@ use crate::{ - error, normal, normal_noln, loud, louder, loudest, - git::{ - dir_listing, - GitFile, - GitRepo, - GitsyMetadata, - parse_repo, - }, - settings::{ - GitsyCli, - GitsyRepoDescriptions, - GitsySettings, - GitsySettingsRepo, - }, - template::{ - DirFilter, - FileFilter, - TsDateFn, - TsTimestampFn, - }, + error, + git::{dir_listing, parse_repo, GitFile, GitRepo, GitsyMetadata}, + loud, louder, loudest, normal, normal_noln, + settings::{GitsyCli, GitsyRepoDescriptions, GitsySettings, GitsySettingsRepo}, + template::{DirFilter, FileFilter, TsDateFn, TsTimestampFn}, util::GitsyError, }; -use git2::{Repository, Error}; +use git2::{Error, Repository}; use rayon::prelude::*; use std::cmp; -use std::fs::{File, create_dir}; +use std::fs::{create_dir, File}; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -36,9 +21,9 @@ use pulldown_cmark::{html, Options, Parser as MdParser}; #[cfg(any(feature = "highlight", feature = "highlight_fast"))] use syntect::{ - html::{ClassedHTMLGenerator, ClassStyle, css_for_theme_with_class_style}, - parsing::SyntaxSet, highlighting::ThemeSet, + html::{css_for_theme_with_class_style, ClassStyle, ClassedHTMLGenerator}, + parsing::SyntaxSet, util::LinesWithEndings, }; @@ -52,7 +37,7 @@ macro_rules! size_check { if total.saturating_add($cur) > $settings.limit_total_size.unwrap_or(usize::MAX) { $action; } - } + }; } pub struct GitsyGenerator { @@ -62,8 +47,7 @@ pub struct GitsyGenerator { } impl GitsyGenerator { - pub fn new(cli: GitsyCli, settings: GitsySettings, - repo_descriptions: GitsyRepoDescriptions) -> GitsyGenerator { + pub fn new(cli: GitsyCli, settings: GitsySettings, repo_descriptions: GitsyRepoDescriptions) -> GitsyGenerator { GitsyGenerator { cli, settings, @@ -86,16 +70,18 @@ impl GitsyGenerator { let syntax_set = SyntaxSet::load_defaults_newlines(); let syntax = match syntax_set.find_syntax_by_extension(extension) { Some(s) => s, - _ => { return contents.to_string(); }, + _ => { + return contents.to_string(); + } }; let mut html_generator = ClassedHTMLGenerator::new_with_class_style(syntax, &syntax_set, ClassStyle::Spaced); for line in LinesWithEndings::from(contents) { match html_generator.parse_html_for_line_which_includes_newline(line) { - Ok(_) => {}, + Ok(_) => {} Err(_) => { error!("Warning: failed to apply syntax highlighting."); return contents.to_string(); - }, + } } } html_generator.finalize() @@ -115,18 +101,22 @@ impl GitsyGenerator { loudest!(" - rendering Markdown in {}", path.display()); let (cstr, rendered, pre) = (GitsyGenerator::parse_markdown(&cstr), true, false); (cstr, rendered, pre) - }, + } #[cfg(any(feature = "highlight", feature = "highlight_fast"))] Some(x) if settings.syntax_highlight.unwrap_or(false) => { loudest!(" - syntax highlighting {}", path.display()); - (GitsyGenerator::syntax_highlight(&cstr, x.to_string_lossy().to_string().as_str()), true, true) - }, + ( + GitsyGenerator::syntax_highlight(&cstr, x.to_string_lossy().to_string().as_str()), + true, + true, + ) + } _ => (cstr, false, true), }; file.contents_safe = rendered; file.contents_preformatted = pre; Some(content) - }, + } true => Some(format!("[Binary data ({} bytes)]", blob.content().len())), }; } @@ -134,8 +124,7 @@ impl GitsyGenerator { } fn write_rendered(path: &str, rendered: &str) -> usize { - let mut file = File::create(path) - .expect(&format!("Unable to write to output path: {}", path)); + let mut file = File::create(path).expect(&format!("Unable to write to output path: {}", path)); file.write(rendered.as_bytes()) .expect(&format!("Failed to save rendered html to path: {}", path)); louder!(" - wrote file: {}", path); @@ -147,10 +136,10 @@ impl GitsyGenerator { template_path.push("**"); template_path.push("*.html"); let mut tera = Tera::new(&template_path.to_string_lossy().to_string())?; - tera.register_filter("only_files", FileFilter{}); - tera.register_filter("only_dirs", DirFilter{}); - tera.register_function("ts_to_date", TsDateFn{}); - tera.register_function("ts_to_git_timestamp", TsTimestampFn{}); + tera.register_filter("only_files", FileFilter {}); + tera.register_filter("only_dirs", DirFilter {}); + tera.register_function("ts_to_date", TsDateFn {}); + tera.register_function("ts_to_git_timestamp", TsTimestampFn {}); Ok(tera) } @@ -164,22 +153,31 @@ impl GitsyGenerator { let generated_dt = chrono::offset::Local::now(); let mut global_bytes = 0; let mut total_bytes = 0; - let mut repos: Vec = vec!(); + let mut repos: Vec = vec![]; if self.repo_descriptions.len() == 0 { - panic!("No Git repositories defined! Please check your configuration file ({})", - self.cli.path.display()); + panic!( + "No Git repositories defined! Please check your configuration file ({})", + self.cli.path.display() + ); } // Sort the repositories by name let mut repo_vec: Vec = self.repo_descriptions.iter().cloned().collect(); - repo_vec.sort_by(|x,y| x.name.as_deref().map(|n| n.cmp(&y.name.as_deref().unwrap_or_default())) - .unwrap_or(cmp::Ordering::Equal)); + repo_vec.sort_by(|x, y| { + x.name + .as_deref() + .map(|n| n.cmp(&y.name.as_deref().unwrap_or_default())) + .unwrap_or(cmp::Ordering::Equal) + }); // Find the one with the longest name, for pretty printing let global_name = "repo list"; - let longest_repo_name = repo_vec.iter().fold(0, |acc, x| { - cmp::max(acc, x.name.as_deref().map(|n| n.len()).unwrap_or(0)) - }).max(global_name.len()); + let longest_repo_name = repo_vec + .iter() + .fold(0, |acc, x| { + cmp::max(acc, x.name.as_deref().map(|n| n.len()).unwrap_or(0)) + }) + .max(global_name.len()); loudest!("Global settings:\n{:#?}", &self.settings); @@ -191,78 +189,89 @@ impl GitsyGenerator { let name = repo_desc.name.as_deref().expect("A configured repository has no name!"); let repo_path = match &repo_desc.path { - url if url.starts_with("https://") || - url.to_str().unwrap_or_default().contains("@") => { - if self.settings.outputs.cloned_repos.is_none() { - error!("ERROR: Found remote repo [{}], but `cloned_repos` directory not configured.", name); - continue; - }; - let clone_path: PathBuf = [self.settings.outputs.cloned_repos.as_deref().unwrap(), - name].iter().collect(); - match Repository::open(&clone_path) { - Ok(r) => { - // Repo already cloned, so update all refs - let refs: Vec = r.references() - .expect(&format!("Unable to enumerate references for repo [{}]", name)) - .map(|x| x.expect(&format!("Found invalid reference in repo [{}]", name)) - .name() - .expect(&format!("Found unnamed reference in repo: [{}]", name)) - .to_string()).collect(); - r.find_remote("origin") - .expect(&format!("Clone of repo [{}] missing `origin` remote.", name)) - .fetch(&refs, None, None) - .expect(&format!("Failed to fetch updates from remote repo [{}]", name)); - clone_path.to_string_lossy().to_string() - }, - Err(_) => { - let mut builder = git2::build::RepoBuilder::new(); + url if url.starts_with("https://") || url.to_str().unwrap_or_default().contains("@") => { + if self.settings.outputs.cloned_repos.is_none() { + error!( + "ERROR: Found remote repo [{}], but `cloned_repos` directory not configured.", + name + ); + continue; + }; + let clone_path: PathBuf = [self.settings.outputs.cloned_repos.as_deref().unwrap(), name] + .iter() + .collect(); + match Repository::open(&clone_path) { + Ok(r) => { + // Repo already cloned, so update all refs + let refs: Vec = r + .references() + .expect(&format!("Unable to enumerate references for repo [{}]", name)) + .map(|x| { + x.expect(&format!("Found invalid reference in repo [{}]", name)) + .name() + .expect(&format!("Found unnamed reference in repo: [{}]", name)) + .to_string() + }) + .collect(); + r.find_remote("origin") + .expect(&format!("Clone of repo [{}] missing `origin` remote.", name)) + .fetch(&refs, None, None) + .expect(&format!("Failed to fetch updates from remote repo [{}]", name)); + clone_path.to_string_lossy().to_string() + } + Err(_) => { + let mut builder = git2::build::RepoBuilder::new(); - // TODO: git2-rs's ssh support just doesn't seem to - // work. It finds the repo, but fails to either - // decrypt or use the private key. - // - //if !url.starts_with("https://") { - // use secrecy::ExposeSecret; - // // this must be SSH, which needs credentials. - // let mut callbacks = git2::RemoteCallbacks::new(); - // callbacks.credentials(|_url, username_from_url, _allowed_types| { - // //git2::Cred::ssh_key_from_agent(username_from_url.unwrap()) - // - // let keyfile = format!("{}/.ssh/id_rsa", std::env::var("HOME").unwrap()); - // let passphrase = pinentry::PassphraseInput::with_default_binary().unwrap() - // .with_description(&format!("Enter passphrase for SSH key {} (repo: {})", - // keyfile, url.display())) - // .with_prompt("Passphrase:") - // .interact().unwrap(); - // git2::Cred::ssh_key( - // username_from_url.unwrap(), - // None, - // Path::new(&keyfile), - // Some(passphrase.expose_secret()), - // ) - // }); - // let mut options = git2::FetchOptions::new(); - // options.remote_callbacks(callbacks); - // builder.fetch_options(options); - //} - builder - .bare(true) - .clone(&url.to_string_lossy().to_string(), &clone_path) - .expect(&format!("Failed to clone remote repo [{}]", name)); - clone_path.to_string_lossy().to_string() - } + // TODO: git2-rs's ssh support just doesn't seem to + // work. It finds the repo, but fails to either + // decrypt or use the private key. + // + //if !url.starts_with("https://") { + // use secrecy::ExposeSecret; + // // this must be SSH, which needs credentials. + // let mut callbacks = git2::RemoteCallbacks::new(); + // callbacks.credentials(|_url, username_from_url, _allowed_types| { + // //git2::Cred::ssh_key_from_agent(username_from_url.unwrap()) + // + // let keyfile = format!("{}/.ssh/id_rsa", std::env::var("HOME").unwrap()); + // let passphrase = pinentry::PassphraseInput::with_default_binary().unwrap() + // .with_description(&format!("Enter passphrase for SSH key {} (repo: {})", + // keyfile, url.display())) + // .with_prompt("Passphrase:") + // .interact().unwrap(); + // git2::Cred::ssh_key( + // username_from_url.unwrap(), + // None, + // Path::new(&keyfile), + // Some(passphrase.expose_secret()), + // ) + // }); + // let mut options = git2::FetchOptions::new(); + // options.remote_callbacks(callbacks); + // builder.fetch_options(options); + //} + builder + .bare(true) + .clone(&url.to_string_lossy().to_string(), &clone_path) + .expect(&format!("Failed to clone remote repo [{}]", name)); + clone_path.to_string_lossy().to_string() } } + } dir => { match dir.metadata() { - Ok(m) if m.is_dir() => {}, + Ok(m) if m.is_dir() => {} _ => { - error!("ERROR: local repository [{}]: directory not found: {}", name, dir.display()); + error!( + "ERROR: local repository [{}]: directory not found: {}", + name, + dir.display() + ); continue; - }, + } } dir.to_string_lossy().to_string() - }, + } }; let repo = Repository::open(&repo_path).expect("Unable to find git repository."); let metadata = GitsyMetadata { @@ -277,7 +286,9 @@ impl GitsyGenerator { let mut local_ctx = Context::from_serialize(&summary).unwrap(); if let Some(extra) = &self.settings.extra { - local_ctx.try_insert("extra", extra).expect("Failed to add extra settings to template engine."); + local_ctx + .try_insert("extra", extra) + .expect("Failed to add extra settings to template engine."); } if let Some(site_name) = &self.settings.site_name { local_ctx.insert("site_name", site_name); @@ -294,8 +305,11 @@ impl GitsyGenerator { if let Some(templ_file) = self.settings.templates.repo_summary.as_deref() { match tera.render(templ_file, &local_ctx) { Ok(rendered) => { - repo_bytes += GitsyGenerator::write_rendered(&self.settings.outputs.repo_summary(Some(&summary), None), &rendered); - }, + repo_bytes += GitsyGenerator::write_rendered( + &self.settings.outputs.repo_summary(Some(&summary), None), + &rendered, + ); + } Err(x) => match x.kind { _ => error!("ERROR: {:?}", x), }, @@ -308,8 +322,11 @@ impl GitsyGenerator { if let Some(templ_file) = self.settings.templates.branch.as_deref() { match tera.render(templ_file, &local_ctx) { Ok(rendered) => { - repo_bytes += GitsyGenerator::write_rendered(&self.settings.outputs.branch(Some(&summary), Some(branch)), &rendered); - }, + repo_bytes += GitsyGenerator::write_rendered( + &self.settings.outputs.branch(Some(&summary), Some(branch)), + &rendered, + ); + } Err(x) => match x.kind { _ => error!("ERROR: {:?}", x), }, @@ -329,8 +346,11 @@ impl GitsyGenerator { if let Some(templ_file) = self.settings.templates.tag.as_deref() { match tera.render(templ_file, &local_ctx) { Ok(rendered) => { - repo_bytes += GitsyGenerator::write_rendered(&self.settings.outputs.tag(Some(&summary), Some(tag)), &rendered); - }, + repo_bytes += GitsyGenerator::write_rendered( + &self.settings.outputs.tag(Some(&summary), Some(tag)), + &rendered, + ); + } Err(x) => match x.kind { _ => error!("ERROR: {:?}", x), }, @@ -342,12 +362,17 @@ impl GitsyGenerator { for (_id, commit) in &summary.commits { size_check!(repo_desc, repo_bytes, total_bytes, break); - local_ctx.try_insert("commit", &commit).expect("Failed to add commit to template engine."); + local_ctx + .try_insert("commit", &commit) + .expect("Failed to add commit to template engine."); if let Some(templ_file) = self.settings.templates.commit.as_deref() { match tera.render(templ_file, &local_ctx) { Ok(rendered) => { - repo_bytes += GitsyGenerator::write_rendered(&self.settings.outputs.commit(Some(&summary), Some(commit)), &rendered); - }, + repo_bytes += GitsyGenerator::write_rendered( + &self.settings.outputs.commit(Some(&summary), Some(commit)), + &rendered, + ); + } Err(x) => match x.kind { _ => error!("ERROR: {:?}", x), }, @@ -359,44 +384,65 @@ impl GitsyGenerator { #[cfg(any(feature = "highlight", feature = "highlight_fast"))] if self.settings.templates.file.is_some() { let ts = ThemeSet::load_defaults(); - let theme = ts.themes.get(repo_desc.syntax_highlight_theme.as_deref() - .unwrap_or("base16-ocean.light")).expect("Invalid syntax highlighting theme specified."); + let theme = ts + .themes + .get( + repo_desc + .syntax_highlight_theme + .as_deref() + .unwrap_or("base16-ocean.light"), + ) + .expect("Invalid syntax highlighting theme specified."); let css: String = css_for_theme_with_class_style(theme, syntect::html::ClassStyle::Spaced) .expect("Invalid syntax highlighting theme specified."); - repo_bytes += GitsyGenerator::write_rendered(&self.settings.outputs.syntax_css(Some(&summary), None), css.as_str()); + repo_bytes += GitsyGenerator::write_rendered( + &self.settings.outputs.syntax_css(Some(&summary), None), + css.as_str(), + ); } - // TODO: parallelize the rest of the processing steps. This one is // done first because syntax highlighting is very slow. let files: Vec<&GitFile> = summary.all_files.iter().filter(|x| x.kind == "file").collect(); let atomic_bytes: AtomicUsize = AtomicUsize::new(repo_bytes); - let _ = files.par_iter().fold(|| Some(0), |acc, file| { - // These two have to be recreated. Cloning the Tera context is expensive. - let repo = Repository::open(&repo_path).expect("Unable to find git repository."); - let mut local_ctx = local_ctx.clone(); + let _ = files + .par_iter() + .fold( + || Some(0), + |acc, file| { + // These two have to be recreated. Cloning the Tera context is expensive. + let repo = Repository::open(&repo_path).expect("Unable to find git repository."); + let mut local_ctx = local_ctx.clone(); - let mut local_bytes = 0; - let cur_repo_bytes = atomic_bytes.load(Ordering::Relaxed); - size_check!(repo_desc, cur_repo_bytes, total_bytes, return None); - let file = match file.size < repo_desc.limit_file_size.unwrap_or(usize::MAX) { - true => GitsyGenerator::fill_file_contents(&repo, &file, &repo_desc).expect("Failed to parse file."), - false => (*file).clone(), - }; - local_ctx.try_insert("file", &file).expect("Failed to add file to template engine."); - if let Some(templ_file) = self.settings.templates.file.as_deref() { - match tera.render(templ_file, &local_ctx) { - Ok(rendered) => { - local_bytes = GitsyGenerator::write_rendered(&self.settings.outputs.file(Some(&summary), Some(&file)), &rendered); - atomic_bytes.fetch_add(local_bytes, Ordering::Relaxed); - }, - Err(x) => match x.kind { - _ => error!("ERROR: {:?}", x), - }, - } - } - local_ctx.remove("file"); - Some(acc.unwrap() + local_bytes)}) + let mut local_bytes = 0; + let cur_repo_bytes = atomic_bytes.load(Ordering::Relaxed); + size_check!(repo_desc, cur_repo_bytes, total_bytes, return None); + let file = match file.size < repo_desc.limit_file_size.unwrap_or(usize::MAX) { + true => GitsyGenerator::fill_file_contents(&repo, &file, &repo_desc) + .expect("Failed to parse file."), + false => (*file).clone(), + }; + local_ctx + .try_insert("file", &file) + .expect("Failed to add file to template engine."); + if let Some(templ_file) = self.settings.templates.file.as_deref() { + match tera.render(templ_file, &local_ctx) { + Ok(rendered) => { + local_bytes = GitsyGenerator::write_rendered( + &self.settings.outputs.file(Some(&summary), Some(&file)), + &rendered, + ); + atomic_bytes.fetch_add(local_bytes, Ordering::Relaxed); + } + Err(x) => match x.kind { + _ => error!("ERROR: {:?}", x), + }, + } + } + local_ctx.remove("file"); + Some(acc.unwrap() + local_bytes) + }, + ) .while_some() // allow short-circuiting if size limit is reached .sum::(); repo_bytes = atomic_bytes.load(Ordering::Relaxed); @@ -407,12 +453,17 @@ impl GitsyGenerator { continue; } let listing = dir_listing(&repo, &dir).expect("Failed to parse file."); - local_ctx.try_insert("files", &listing).expect("Failed to add dir to template engine."); + local_ctx + .try_insert("files", &listing) + .expect("Failed to add dir to template engine."); if let Some(templ_file) = self.settings.templates.dir.as_deref() { match tera.render(templ_file, &local_ctx) { Ok(rendered) => { - repo_bytes += GitsyGenerator::write_rendered(&self.settings.outputs.dir(Some(&summary), Some(dir)), &rendered); - }, + repo_bytes += GitsyGenerator::write_rendered( + &self.settings.outputs.dir(Some(&summary), Some(dir)), + &rendered, + ); + } Err(x) => match x.kind { _ => error!("ERROR: {:?}", x), }, @@ -426,12 +477,16 @@ impl GitsyGenerator { for src_file in repo_desc.asset_files.as_ref().unwrap() { let src_file = PathBuf::from(repo_path.to_owned() + "/" + src_file); let mut dst_file = PathBuf::from(&target_dir); - dst_file.push(src_file.file_name() - .expect(&format!("Failed to copy repo asset file: {} ({})", - src_file.display(), repo_desc.name.as_deref().unwrap_or_default()))); - std::fs::copy(&src_file, &dst_file) - .expect(&format!("Failed to copy repo asset file: {} ({})", - src_file.display(), repo_desc.name.as_deref().unwrap_or_default())); + dst_file.push(src_file.file_name().expect(&format!( + "Failed to copy repo asset file: {} ({})", + src_file.display(), + repo_desc.name.as_deref().unwrap_or_default() + ))); + std::fs::copy(&src_file, &dst_file).expect(&format!( + "Failed to copy repo asset file: {} ({})", + src_file.display(), + repo_desc.name.as_deref().unwrap_or_default() + )); if let Ok(meta) = std::fs::metadata(dst_file) { repo_bytes += meta.len() as usize; } @@ -439,22 +494,33 @@ impl GitsyGenerator { } repos.push(summary); - normal!("{}done in {:.2}s ({} bytes)", - match crate::util::VERBOSITY.load(Ordering::Relaxed) > 1 { - true => " - ", - _ => "", - }, - start_repo.elapsed().as_secs_f32(), repo_bytes); + normal!( + "{}done in {:.2}s ({} bytes)", + match crate::util::VERBOSITY.load(Ordering::Relaxed) > 1 { + true => " - ", + _ => "", + }, + start_repo.elapsed().as_secs_f32(), + repo_bytes + ); total_bytes += repo_bytes; size_check!(repo_desc, 0, total_bytes, break); // break if total is exceeded } let start_global = Instant::now(); - normal_noln!("[{}{}]... ", global_name, " ".repeat(longest_repo_name - global_name.len())); + normal_noln!( + "[{}{}]... ", + global_name, + " ".repeat(longest_repo_name - global_name.len()) + ); let mut global_ctx = Context::new(); - global_ctx.try_insert("repos", &repos).expect("Failed to add repo to template engine."); + global_ctx + .try_insert("repos", &repos) + .expect("Failed to add repo to template engine."); if let Some(extra) = &self.settings.extra { - global_ctx.try_insert("extra", extra).expect("Failed to add extra settings to template engine."); + global_ctx + .try_insert("extra", extra) + .expect("Failed to add extra settings to template engine."); } if let Some(site_name) = &self.settings.site_name { global_ctx.insert("site_name", site_name); @@ -471,8 +537,9 @@ impl GitsyGenerator { if let Some(templ_file) = self.settings.templates.repo_list.as_deref() { match tera.render(templ_file, &global_ctx) { Ok(rendered) => { - global_bytes += GitsyGenerator::write_rendered(&self.settings.outputs.repo_list(None, None), &rendered); - }, + global_bytes += + GitsyGenerator::write_rendered(&self.settings.outputs.repo_list(None, None), &rendered); + } Err(x) => match x.kind { _ => error!("ERROR: {:?}", x), }, @@ -483,7 +550,7 @@ impl GitsyGenerator { match tera.render(templ_file, &global_ctx) { Ok(rendered) => { global_bytes += GitsyGenerator::write_rendered(&self.settings.outputs.error(None, None), &rendered); - }, + } Err(x) => match x.kind { _ => error!("ERROR: {:?}", x), }, @@ -495,8 +562,11 @@ impl GitsyGenerator { for src_file in self.settings.asset_files.as_ref().unwrap() { let src_file = PathBuf::from(src_file); let mut dst_file = PathBuf::from(&target_dir); - dst_file.push(src_file.file_name() - .expect(&format!("Failed to copy asset file: {}", src_file.display()))); + dst_file.push( + src_file + .file_name() + .expect(&format!("Failed to copy asset file: {}", src_file.display())), + ); std::fs::copy(&src_file, &dst_file) .expect(&format!("Failed to copy asset file: {}", src_file.display())); if let Ok(meta) = std::fs::metadata(dst_file) { @@ -506,8 +576,16 @@ impl GitsyGenerator { } total_bytes += global_bytes; - normal!("done in {:.2}s ({} bytes)", start_global.elapsed().as_secs_f32(), global_bytes); - loud!("Wrote {} bytes in {:.2}s", total_bytes, start_all.elapsed().as_secs_f32()); + normal!( + "done in {:.2}s ({} bytes)", + start_global.elapsed().as_secs_f32(), + global_bytes + ); + loud!( + "Wrote {} bytes in {:.2}s", + total_bytes, + start_all.elapsed().as_secs_f32() + ); Ok(()) } } diff --git a/src/git.rs b/src/git.rs index a9f73f1..fd0ad3f 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,7 +1,7 @@ -use crate::{error, loud, loudest}; use crate::settings::GitsySettingsRepo; -use git2::{DiffOptions, Repository, Error}; -use serde::{Serialize, Deserialize}; +use crate::{error, loud, loudest}; +use git2::{DiffOptions, Error, Repository}; +use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::BTreeMap; use std::rc::Rc; @@ -98,7 +98,7 @@ pub struct GitDiffFile { pub extra: String, pub additions: usize, pub deletions: usize, - pub hunks: Vec + pub hunks: Vec, } #[derive(Serialize, Default)] @@ -114,8 +114,15 @@ pub struct GitDiffLine { pub text: String, } -fn walk_file_tree(repo: &git2::Repository, rev: &str, files: &mut Vec, - depth: usize, max_depth: usize, recurse: bool, prefix: &str) -> Result<(), Error> { +fn walk_file_tree( + repo: &git2::Repository, + rev: &str, + files: &mut Vec, + depth: usize, + max_depth: usize, + recurse: bool, + prefix: &str, +) -> Result<(), Error> { let obj = repo.revparse_single(rev)?; let tree = obj.peel_to_tree()?; for entry in tree.iter() { @@ -151,23 +158,35 @@ fn walk_file_tree(repo: &git2::Repository, rev: &str, files: &mut Vec, }); if recurse && depth < (max_depth - 1) && entry.kind() == Some(git2::ObjectType::Tree) { let prefix = path + "/"; - walk_file_tree(repo, &entry.id().to_string(), files, - depth+1, max_depth, true, &prefix)?; + walk_file_tree( + repo, + &entry.id().to_string(), + files, + depth + 1, + max_depth, + true, + &prefix, + )?; } } Ok(()) } pub fn dir_listing(repo: &Repository, file: &GitFile) -> Result, Error> { - let mut files: Vec = vec!(); + let mut files: Vec = vec![]; walk_file_tree(&repo, &file.id, &mut files, 0, usize::MAX, false, "")?; Ok(files) } -pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, metadata: GitsyMetadata) -> Result { - let mut history: Vec = vec!(); - let mut branches: Vec = vec!(); - let mut tags: Vec = vec!(); +pub fn parse_repo( + repo: &Repository, + name: &str, + settings: &GitsySettingsRepo, + metadata: GitsyMetadata, +) -> Result { + let mut history: Vec = vec![]; + let mut branches: Vec = vec![]; + let mut tags: Vec = vec![]; let mut commits: BTreeMap = BTreeMap::new(); let mut commit_count = 0; let mut history_count = 0; @@ -181,8 +200,12 @@ pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, m if let (Some(target), Some(name)) = (refr.target(), refr.shorthand()) { let id = target.to_string(); match references.contains_key(&id) { - false => { references.insert(target.to_string(), vec!(name.to_string())); }, - true => { references.get_mut(&id).unwrap().push(name.to_string()); }, + false => { + references.insert(target.to_string(), vec![name.to_string()]); + } + true => { + references.get_mut(&id).unwrap().push(name.to_string()); + } } } } @@ -194,9 +217,10 @@ pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, m loudest!(" - Parsing history:"); for oid in revwalk { let oid = oid?; - if commit_count >= settings.limit_commits.unwrap_or(usize::MAX) || - history_count >= settings.limit_history.unwrap_or(usize::MAX) { - break; + if commit_count >= settings.limit_commits.unwrap_or(usize::MAX) + || history_count >= settings.limit_history.unwrap_or(usize::MAX) + { + break; } commits.insert(oid.to_string(), parse_commit(repo, &oid.to_string())?); commit_count += 1; @@ -205,7 +229,7 @@ pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, m let full_hash = commit.id().to_string(); let short_hash = obj.short_id()?.as_str().unwrap_or_default().to_string(); - let mut parents: Vec = vec!(); + let mut parents: Vec = vec![]; let a = if commit.parents().len() == 1 { let parent = commit.parent(0)?; parents.push(parent.id().to_string()); @@ -223,8 +247,10 @@ pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, m deletions: stats.deletions(), }; - let alt_refs: Vec = references.get(&commit.id().to_string()) - .map(|x| x.to_owned()).unwrap_or_default(); + let alt_refs: Vec = references + .get(&commit.id().to_string()) + .map(|x| x.to_owned()) + .unwrap_or_default(); if history_count < settings.limit_history.unwrap_or(usize::MAX) { loudest!(" + {} {}", full_hash, first_line(commit.message_bytes())); @@ -241,7 +267,7 @@ pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, m ref_name: None, alt_refs, author: GitAuthor { - name: commit.author().name().map(|x| x.to_owned()), + name: commit.author().name().map(|x| x.to_owned()), email: commit.author().email().map(|x| x.to_owned()), }, summary: Some(first_line(commit.message_bytes())), @@ -267,7 +293,7 @@ pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, m // this is a bad idea? match refr.kind() { Some(k) if k == git2::ReferenceType::Symbolic => continue, - _ => {}, + _ => {} } let commit = repo.find_commit(obj.id())?; let full_hash = obj.id().to_string(); @@ -278,7 +304,7 @@ pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, m short_hash, ts_utc: commit.author().when().seconds(), ts_offset: (commit.author().when().offset_minutes() as i64) * 60, - parents: vec!(), + parents: vec![], ref_name: Some(name.to_string()), author: GitAuthor { name: commit.author().name().map(|x| x.to_owned()), @@ -323,9 +349,8 @@ pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, m Some(t) => (t.when().seconds(), (t.when().offset_minutes() as i64) * 60), _ => (0, 0), }; - let (author,email) = match commit.tagger() { - Some(t) => (t.name().map(|x| x.to_owned()), - t.email().map(|x| x.to_owned())), + let (author, email) = match commit.tagger() { + Some(t) => (t.name().map(|x| x.to_owned()), t.email().map(|x| x.to_owned())), _ => (None, None), }; let summary = match commit.message_bytes() { @@ -339,10 +364,7 @@ pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, m ts_utc: ts, ts_offset: tz, ref_name: Some(tag.to_string()), - author: GitAuthor { - name: author, - email, - }, + author: GitAuthor { name: author, email }, tagged_id: Some(commit.target_id().to_string()), message: commit.message().map(|x| x.to_string()), summary, @@ -352,8 +374,8 @@ pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, m } loud!(" - parsed {} tags", tag_count); - let mut root_files: Vec = vec!(); - let mut all_files: Vec = vec!(); + let mut root_files: Vec = vec![]; + let mut all_files: Vec = vec![]; let max_depth = settings.limit_tree_depth.unwrap_or(usize::MAX); if max_depth > 0 { loudest!(" - Walking root files"); @@ -380,23 +402,21 @@ pub fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, m pub fn parse_commit(repo: &Repository, refr: &str) -> Result { let obj = repo.revparse_single(refr)?; let commit = repo.find_commit(obj.id())?; - let mut parents: Vec = vec!(); + let mut parents: Vec = vec![]; let a = match commit.parents().len() { x if x == 1 => { let parent = commit.parent(0).unwrap(); parents.push(parent.id().to_string()); Some(parent.tree()?) - }, + } x if x > 1 => { for parent in commit.parents() { parents.push(parent.id().to_string()); } None - }, - _ => { - None - }, + } + _ => None, }; let b = commit.tree()?; let mut diffopts = DiffOptions::new(); @@ -409,22 +429,38 @@ pub fn parse_commit(repo: &Repository, refr: &str) -> Result { deletions: stats.deletions(), ..Default::default() }; - let files: Rc>> = Rc::new(RefCell::new(vec!())); + let files: Rc>> = Rc::new(RefCell::new(vec![])); diff.foreach( &mut |file, _progress| { let mut file_diff: GitDiffFile = Default::default(); file_diff.newfile = match file.status() { git2::Delta::Deleted => "/dev/null".to_owned(), - _ => file.new_file().path().map(|x| "b/".to_string() + &x.to_string_lossy()).unwrap_or("/dev/null".to_string()), + _ => file + .new_file() + .path() + .map(|x| "b/".to_string() + &x.to_string_lossy()) + .unwrap_or("/dev/null".to_string()), }; file_diff.oldfile = match file.status() { git2::Delta::Added => "/dev/null".to_owned(), - _ => file.old_file().path().map(|x| "a/".to_string() + &x.to_string_lossy()).unwrap_or("/dev/null".to_string()), + _ => file + .old_file() + .path() + .map(|x| "a/".to_string() + &x.to_string_lossy()) + .unwrap_or("/dev/null".to_string()), }; file_diff.basefile = match file.status() { - git2::Delta::Added => file.new_file().path().map(|x| x.to_string_lossy().to_string()).unwrap_or("/dev/null".to_string()), - _ => file.old_file().path().map(|x| x.to_string_lossy().to_string()).unwrap_or("/dev/null".to_string()), + git2::Delta::Added => file + .new_file() + .path() + .map(|x| x.to_string_lossy().to_string()) + .unwrap_or("/dev/null".to_string()), + _ => file + .old_file() + .path() + .map(|x| x.to_string_lossy().to_string()) + .unwrap_or("/dev/null".to_string()), }; file_diff.oldid = file.old_file().id().to_string(); file_diff.newid = file.new_file().id().to_string(); @@ -443,7 +479,10 @@ pub fn parse_commit(repo: &Repository, refr: &str) -> Result { Some(&mut |_file, _hunk, line| { let mut files = files.borrow_mut(); let file_diff: &mut GitDiffFile = files.last_mut().expect("Diff hunk not associated with a file!"); - let hunk_diff: &mut GitDiffHunk = file_diff.hunks.last_mut().expect("Diff line not associated with a hunk!"); + let hunk_diff: &mut GitDiffHunk = file_diff + .hunks + .last_mut() + .expect("Diff line not associated with a hunk!"); let (kind, prefix) = match line.origin() { ' ' => ("ctx", " "), '-' => ("del", "-"), @@ -453,7 +492,7 @@ pub fn parse_commit(repo: &Repository, refr: &str) -> Result { match line.origin() { '-' => file_diff.deletions += 1, '+' => file_diff.additions += 1, - _ => {}, + _ => {} } let line_diff = GitDiffLine { text: String::from_utf8_lossy(line.content()).to_string(), @@ -462,15 +501,15 @@ pub fn parse_commit(repo: &Repository, refr: &str) -> Result { }; hunk_diff.lines.push(line_diff); true - }) + }), )?; match Rc::try_unwrap(files) { Ok(files) => { let files: Vec = files.into_inner(); commit_diff.files = files; - }, - Err(_) => {}, + } + Err(_) => {} } let tree = obj.peel_to_tree()?; @@ -483,7 +522,7 @@ pub fn parse_commit(repo: &Repository, refr: &str) -> Result { tree_id: Some(tree.id().to_string()), parents, ref_name: None, - alt_refs: vec!(), + alt_refs: vec![], author: GitAuthor { name: commit.author().name().map(|x| x.to_string()), email: commit.author().email().map(|x| x.to_string()), diff --git a/src/settings.rs b/src/settings.rs index 6210fe5..09a34c9 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,10 +1,6 @@ -use clap::Parser; use crate::error; -use crate::git::{ - GitFile, - GitObject, - GitRepo, -}; +use crate::git::{GitFile, GitObject, GitRepo}; +use clap::Parser; use serde::Deserialize; use std::collections::{BTreeMap, HashMap, HashSet}; use std::fs::{create_dir_all, read_dir, read_to_string}; @@ -39,7 +35,10 @@ impl GitsyCli { pub fn new() -> Self { let cli = CliArgs::parse(); let config_path = cli.config.as_deref().unwrap_or(Path::new("config.toml")).to_owned(); - let config_dir = config_path.parent().expect("Config file not in valid directory.").to_owned(); + let config_dir = config_path + .parent() + .expect("Config file not in valid directory.") + .to_owned(); let config_dir = match config_dir.to_str().unwrap_or_default().len() > 0 { true => config_dir, false => PathBuf::from("."), @@ -52,10 +51,13 @@ impl GitsyCli { Ok(d) => d, _ => config_dir.clone(), }; - crate::util::VERBOSITY.store(match cli.quiet { - true => 0, - false => (cli.verbose + 1).into(), - }, Ordering::Relaxed); + crate::util::VERBOSITY.store( + match cli.quiet { + true => 0, + false => (cli.verbose + 1).into(), + }, + Ordering::Relaxed, + ); GitsyCli { path: config_path, dir: config_dir, @@ -63,7 +65,6 @@ impl GitsyCli { } } - #[derive(Deserialize, Debug)] pub struct GitsySettingsTemplates { pub path: PathBuf, @@ -102,7 +103,7 @@ macro_rules! output_path_fn { (true, true) => { let name = repo.map(|x| &x.name).unwrap(); tmpl_str.replace("%REPO%", name) - }, + } (true, false) => { panic!("%REPO% variable not available for output path: {}", tmpl_str); } @@ -112,7 +113,7 @@ macro_rules! output_path_fn { (true, true) => { let name = obj.map(|x| &x.$id).unwrap(); tmpl_str.replace("%ID%", name) - }, + } (true, false) => { panic!("%ID% variable not available for output path: {}", tmpl_str); } @@ -124,12 +125,12 @@ macro_rules! output_path_fn { match $is_dir { true => { let _ = create_dir_all(&path); - }, + } false => { if let Some(dir) = path.parent() { let _ = create_dir_all(dir); } - }, + } } path.to_str() .expect(&format!("Output is not a valid path: {}", path.display())) @@ -140,17 +141,17 @@ macro_rules! output_path_fn { //step_map_first!(boil_in_wort, Boil, Wort, |b: &Boil| { b.wort_start() }); impl GitsySettingsOutputs { - output_path_fn!(repo_list, GitObject, full_hash, false, "repos.html"); - output_path_fn!(repo_summary, GitObject, full_hash, false, "%REPO%/summary.html"); - output_path_fn!(commit, GitObject, full_hash, false, "%REPO%/commit/%ID%.html"); - output_path_fn!(branch, GitObject, full_hash, false, "%REPO%/branch/%ID%.html"); - output_path_fn!(tag, GitObject, full_hash, false, "%REPO%/tag/%ID%.html"); - output_path_fn!(file, GitFile, id, false, "%REPO%/file/%ID%.html"); - output_path_fn!(syntax_css, GitObject, full_hash, false, "%REPO%/file/syntax.css"); - output_path_fn!(dir, GitFile, id, false, "%REPO%/dir/%ID%.html"); - output_path_fn!(error, GitObject, full_hash, false, "404.html"); - output_path_fn!(global_assets, GitObject, full_hash, true, "assets/"); - output_path_fn!(repo_assets , GitObject, full_hash, true, "%REPO%/assets/"); + output_path_fn!(repo_list, GitObject, full_hash, false, "repos.html"); + output_path_fn!(repo_summary, GitObject, full_hash, false, "%REPO%/summary.html"); + output_path_fn!(commit, GitObject, full_hash, false, "%REPO%/commit/%ID%.html"); + output_path_fn!(branch, GitObject, full_hash, false, "%REPO%/branch/%ID%.html"); + output_path_fn!(tag, GitObject, full_hash, false, "%REPO%/tag/%ID%.html"); + output_path_fn!(file, GitFile, id, false, "%REPO%/file/%ID%.html"); + output_path_fn!(syntax_css, GitObject, full_hash, false, "%REPO%/file/syntax.css"); + output_path_fn!(dir, GitFile, id, false, "%REPO%/dir/%ID%.html"); + output_path_fn!(error, GitObject, full_hash, false, "404.html"); + output_path_fn!(global_assets, GitObject, full_hash, true, "assets/"); + output_path_fn!(repo_assets, GitObject, full_hash, true, "%REPO%/assets/"); } #[derive(Clone, Deserialize, Default, Debug)] @@ -222,22 +223,26 @@ impl GitsySettings { let settings: GitsySettings = toml::from_str(&toml).expect("Configuration file is invalid."); // Settings are valid, so let's move into the directory with the config file - if cli.dir.to_str().unwrap_or_default().len() > 0 { // empty string means current directory + if cli.dir.to_str().unwrap_or_default().len() > 0 { + // empty string means current directory std::env::set_current_dir(&cli.dir) .expect(&format!("Unable to set working directory to: {}", cli.dir.display())); } // Get a list of all remaining TOML "tables" in the file. // These are the user-supplied individual repositories. - let reserved_keys = vec!("gitsy_templates", "gitsy_outputs", "gitsy_extra"); + let reserved_keys = vec!["gitsy_templates", "gitsy_outputs", "gitsy_extra"]; let settings_raw: HashMap = toml::from_str(&toml).expect("blah"); - let table_keys: Vec = settings_raw.iter().filter_map(|x| match x.1.is_table() { - true => match reserved_keys.contains(&x.0.as_str()) { - false => Some(x.0.clone()), - true => None, - }, - false => None - }).collect(); + let table_keys: Vec = settings_raw + .iter() + .filter_map(|x| match x.1.is_table() { + true => match reserved_keys.contains(&x.0.as_str()) { + false => Some(x.0.clone()), + true => None, + }, + false => None, + }) + .collect(); // Try to convert each unknown "table" into a repo struct, and // save the ones that are successful. If no repo name is @@ -245,14 +250,18 @@ impl GitsySettings { let mut repo_descriptions: HashSet = HashSet::new(); macro_rules! global_to_repo { ($settings:ident, $repo:ident, $field:ident) => { - if $repo.$field.is_none() { $repo.$field = $settings.$field.clone() } - } + if $repo.$field.is_none() { + $repo.$field = $settings.$field.clone() + } + }; } for k in &table_keys { let v = settings_raw.get(k).unwrap(); match toml::from_str::(&v.to_string()) { Ok(mut repo) => { - if repo.name.is_none() { repo.name = Some(k.clone()); } + if repo.name.is_none() { + repo.name = Some(k.clone()); + } global_to_repo!(settings, repo, render_markdown); global_to_repo!(settings, repo, syntax_highlight); global_to_repo!(settings, repo, syntax_highlight_theme); @@ -266,10 +275,10 @@ impl GitsySettings { global_to_repo!(settings, repo, limit_total_size); repo_descriptions.insert(repo); - }, + } Err(e) => { error!("Failed to parse repo [{}]: {:?}", k, e); - }, + } } } @@ -297,8 +306,8 @@ impl GitsySettings { }); } } - }, - _ => {}, + } + _ => {} } (settings, repo_descriptions) } diff --git a/src/template.rs b/src/template.rs index 611358e..48efbb4 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,16 +1,13 @@ -use chrono::{ - DateTime, - offset::FixedOffset, - naive::NaiveDateTime, -}; use crate::git::GitFile; +use chrono::{naive::NaiveDateTime, offset::FixedOffset, DateTime}; use std::collections::HashMap; -use tera::{Filter, Function, Value, to_value, try_get_value}; +use tera::{to_value, try_get_value, Filter, Function, Value}; fn ts_to_date(ts: i64, offset: Option, format: Option) -> String { let offset = offset.unwrap_or(0); let dt = NaiveDateTime::from_timestamp_opt(ts + offset, 0).expect("Invalid timestamp"); - let dt_tz: DateTime = DateTime::from_local(dt, FixedOffset::east_opt(offset as i32).expect("Invalid timezone")); + let dt_tz: DateTime = + DateTime::from_local(dt, FixedOffset::east_opt(offset as i32).expect("Invalid timezone")); match format { Some(f) => dt_tz.format(&f).to_string(), None => dt_tz.format("%Y-%m-%d").to_string(), @@ -20,32 +17,37 @@ fn ts_to_date(ts: i64, offset: Option, format: Option) -> String { fn ts_to_git_timestamp(ts: i64, offset: Option) -> String { let offset = offset.unwrap_or(0); let dt = chrono::naive::NaiveDateTime::from_timestamp_opt(ts + offset, 0).expect("invalid timestamp"); - let dt_tz: DateTime = DateTime::from_local(dt, FixedOffset::east_opt(offset as i32).expect("Invalid timezone")); + let dt_tz: DateTime = + DateTime::from_local(dt, FixedOffset::east_opt(offset as i32).expect("Invalid timezone")); dt_tz.format("%a %b %e %T %Y %z").to_string() } pub struct FileFilter; impl Filter for FileFilter { - fn filter(&self, value: &Value, _args: &HashMap - ) -> Result { + fn filter(&self, value: &Value, _args: &HashMap) -> Result { let file_list: Vec = try_get_value!("only_files", "value", Vec, value); - let file_list: Vec = file_list.iter().filter_map(|x| match x.kind.as_str() { - "file" => Some(x.clone()), - _ => None, - }).collect(); + let file_list: Vec = file_list + .iter() + .filter_map(|x| match x.kind.as_str() { + "file" => Some(x.clone()), + _ => None, + }) + .collect(); Ok(to_value(file_list).unwrap()) } } pub struct DirFilter; impl Filter for DirFilter { - fn filter(&self, value: &Value, _args: &HashMap - ) -> Result { + fn filter(&self, value: &Value, _args: &HashMap) -> Result { let file_list: Vec = try_get_value!("only_dirs", "value", Vec, value); - let file_list: Vec = file_list.iter().filter_map(|x| match x.kind.as_str() { - "dir" => Some(x.clone()), - _ => None, - }).collect(); + let file_list: Vec = file_list + .iter() + .filter_map(|x| match x.kind.as_str() { + "dir" => Some(x.clone()), + _ => None, + }) + .collect(); Ok(to_value(file_list).unwrap()) } }