loud, louder, loudest, normal, normal_noln,
settings::{GitsyCli, GitsyRepoDescriptions, GitsySettings, GitsySettingsRepo},
template::{DirFilter, FileFilter, HexFilter, MaskFilter, OctFilter, Pagination, TsDateFn, TsTimestampFn},
- util::GitsyError,
+ util::{GitsyError, GitsyErrorKind, VERBOSITY},
};
use git2::{Error, Repository};
use rayon::prelude::*;
+use chrono::{DateTime, Local};
use std::cmp;
use std::fs::File;
use std::io::Write;
cli: GitsyCli,
settings: GitsySettings,
repo_descriptions: GitsyRepoDescriptions,
+ tera: Option<Tera>,
+ total_bytes: AtomicUsize,
+ generated_dt: DateTime<Local>,
}
impl GitsyGenerator {
cli,
settings,
repo_descriptions,
+ tera: None,
+ total_bytes: AtomicUsize::new(0),
+ generated_dt: chrono::offset::Local::now(),
}
}
+ fn new_context(&self, repo: Option<&GitRepo>) -> Result<Context, GitsyError> {
+ let mut ctx = match repo {
+ Some(repo) => Context::from_serialize(repo)?,
+ _ => Context::new(),
+ };
+ if let Some(extra) = &self.settings.extra {
+ ctx
+ .try_insert("extra", extra)
+ .expect("Failed to add extra settings to template engine.");
+ }
+ if let Some(site_name) = &self.settings.site_name {
+ ctx.insert("site_name", site_name);
+ }
+ if let Some(site_url) = &self.settings.site_url {
+ ctx.insert("site_url", site_url);
+ }
+ if let Some(site_description) = &self.settings.site_description {
+ ctx.insert("site_description", site_description);
+ }
+ ctx.insert("site_dir", &self.settings.outputs.output_dir());
+ if self.settings.outputs.global_assets.is_some() {
+ ctx.insert("site_assets", &self.settings.outputs.to_relative(&self.settings.outputs.global_assets::<GitFile>(None, None)));
+ }
+ ctx.insert("site_generated_ts", &self.generated_dt.timestamp());
+ ctx.insert("site_generated_offset", &self.generated_dt.offset().local_minus_utc());
+ Ok(ctx)
+ }
+
+ fn find_repo(&self, name: &str, repo_desc: &GitsySettingsRepo) -> Result<String, GitsyError> {
+ 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() {
+ return Err(GitsyError::kind(GitsyErrorKind::Settings,
+ Some(&format!("ERROR: Found remote repo [{}], but `cloned_repos` directory not configured.", name))));
+ };
+ 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<String> = 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()
+ }
+ }
+ }
+ dir => {
+ match dir.metadata() {
+ Ok(m) if m.is_dir() => {}
+ _ => {
+ error!(
+ "ERROR: local repository [{}]: directory not found: {}",
+ name,
+ dir.display()
+ );
+ return Err(GitsyError::kind(GitsyErrorKind::Settings,
+ Some(&format!("ERROR: Local repository not found: {}", name))));
+ }
+ }
+ dir.to_string_lossy().to_string()
+ }
+ };
+ Ok(repo_path)
+ }
+
#[cfg(feature = "markdown")]
fn parse_markdown(contents: &str) -> String {
let mut options = Options::empty();
}
fn write_rendered<P: AsRef<Path>>(&self, path: &P, rendered: &str) -> usize {
- let path = path.as_ref().to_str()
- .expect(&format!("ERROR: attempted to write unrecognizeable path: {}", path.as_ref().display()));
- // Ensure that the requested output path is actually a child
- // of the output directory, as a sanity check to ensure we
- // aren't writing out of bounds.
- let canonical_root = self.settings.outputs.path.canonicalize().expect(&format!(
- "Cannot find canonical version of output path: {}",
- self.settings.outputs.path.display()
- ));
- let canonical_path = PathBuf::from(path);
- let has_relative_dirs = canonical_path
- .ancestors()
- .any(|x| x.file_name().is_none() && x != Path::new("/"));
- assert!(
- canonical_path.is_absolute(),
- "ERROR: write_rendered called with a relative path: {}",
- path
- );
- assert!(
- !has_relative_dirs,
- "ERROR: write_rendered called with a relative path: {}",
- path
- );
- let _ = canonical_path
- .ancestors()
- .find(|x| x == &canonical_root)
- .expect(&format!(
- "Output file {} not contained in output path: {}",
- canonical_path.display(),
- canonical_root.display()
- ));
-
+ let path: &Path = path.as_ref();
+ assert!(self.settings.outputs.assert_valid(&path),
+ "ERROR: attempted to write invalid path: {}", path.display());
// Write the file to disk
- 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.display()));
file.write(rendered.as_bytes())
- .expect(&format!("Failed to save rendered html to path: {}", path));
- louder!(" - wrote file: {}", path);
+ .expect(&format!("Failed to save rendered html to path: {}", path.display()));
+ louder!(" - wrote file: {}", path.display());
rendered.as_bytes().len()
}
fn tera_init(&self) -> Result<Tera, GitsyError> {
- let mut template_path = self.settings.templates.path.clone();
+ let mut template_path = self.settings.outputs.template_dir();
template_path.push("**");
template_path.push("*.html");
let mut tera = Tera::new(&template_path.to_string_lossy().to_string())?;
Ok(tera)
}
- pub fn generate(&self) -> Result<(), GitsyError> {
- let start_all = Instant::now();
- let tera = self.tera_init()?;
-
- // Create output directory
- if self.cli.should_clean {
- louder!("Cleaning output directory: {}", self.settings.outputs.path.display());
- self.settings.outputs.clean();
+ pub fn gen_repo_list(&self, ctx: &Context) -> Result<usize, GitsyError> {
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
+ let mut global_bytes = 0;
+ for (templ_path, out_path) in self.settings.outputs.repo_list::<GitRepo>(None, None) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ match tera.render(templ_path, &ctx) {
+ Ok(rendered) => {
+ global_bytes += self.write_rendered(&out_path, &rendered);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
}
- louder!("Creating output directory: {}", self.settings.outputs.path.display());
- self.settings.outputs.create();
+ Ok(global_bytes)
+ }
- let generated_dt = chrono::offset::Local::now();
+ pub fn gen_error(&self, ctx: &Context) -> Result<usize, GitsyError> {
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
let mut global_bytes = 0;
+ for (templ_path, out_path) in self.settings.outputs.error::<GitRepo>(None, None) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ match tera.render(templ_path, &ctx) {
+ Ok(rendered) => {
+ global_bytes += self.write_rendered(&out_path, &rendered);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
+ }
+ Ok(global_bytes)
+ }
+
+ pub fn gen_summary(&self, ctx: &Context, parsed_repo: &GitRepo, _repo_desc: &GitsySettingsRepo, _repo: &Repository) -> Result<usize, GitsyError> {
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
+ let mut repo_bytes = 0;
+ for (templ_path, out_path) in self.settings.outputs.summary::<GitRepo>(Some(parsed_repo), None) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ match tera.render(templ_path, &ctx) {
+ Ok(rendered) => {
+ repo_bytes +=
+ self.write_rendered(&out_path, &rendered);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
+ }
+ Ok(repo_bytes)
+ }
+
+ pub fn gen_history(&self, ctx: &Context, parsed_repo: &GitRepo, _repo_desc: &GitsySettingsRepo, _repo: &Repository) -> Result<usize, GitsyError> {
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
+ let mut repo_bytes = 0;
+ for (templ_path, out_path) in self.settings.outputs.history::<GitRepo>(Some(parsed_repo), None) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ let mut paged_ctx = ctx.clone();
+ paged_ctx.remove("history");
+ let pages = parsed_repo.history.chunks(self.settings.paginate_history());
+ let page_count = pages.len();
+ for (idx, page) in pages.enumerate() {
+ let pagination = Pagination::new(
+ idx + 1,
+ page_count,
+ &out_path,
+ );
+ paged_ctx.insert("page", &pagination.with_relative_paths());
+ paged_ctx.insert("history", &page);
+ match tera.render(templ_path, &paged_ctx) {
+ Ok(rendered) => {
+ repo_bytes += self.write_rendered(&pagination.cur_page, &rendered);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
+ paged_ctx.remove("page");
+ paged_ctx.remove("history");
+ }
+ }
+ Ok(repo_bytes)
+ }
+
+ pub fn gen_commit(&self, ctx: &Context, parsed_repo: &GitRepo, repo_desc: &GitsySettingsRepo, _repo: &Repository) -> Result<usize, GitsyError> {
+ let mut ctx = ctx.clone();
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
+ let mut repo_bytes = 0;
+ for (_id, commit) in &parsed_repo.commits {
+ size_check!(repo_desc, repo_bytes, self.total_bytes.load(Ordering::Relaxed), break);
+ ctx
+ .try_insert("commit", &commit)
+ .expect("Failed to add commit to template engine.");
+ for (templ_path, out_path) in self.settings.outputs.commit(Some(parsed_repo), Some(commit)) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ match tera.render(templ_path, &ctx) {
+ Ok(rendered) => {
+ repo_bytes += self
+ .write_rendered(&out_path, &rendered);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
+ }
+ ctx.remove("commit");
+ }
+ Ok(repo_bytes)
+ }
+
+ pub fn gen_branches(&self, ctx: &Context, parsed_repo: &GitRepo, _repo_desc: &GitsySettingsRepo, _repo: &Repository) -> Result<usize, GitsyError> {
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
+ let mut repo_bytes = 0;
+ for (templ_path, out_path) in self.settings.outputs.branches::<GitRepo>(Some(parsed_repo), None) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ let mut paged_ctx = ctx.clone();
+ paged_ctx.remove("branches");
+ let pages = parsed_repo.branches.chunks(self.settings.paginate_branches());
+ let page_count = pages.len();
+ for (idx, page) in pages.enumerate() {
+ let pagination = Pagination::new(
+ idx + 1,
+ page_count,
+ &out_path,
+ );
+ paged_ctx.insert("page", &pagination.with_relative_paths());
+ paged_ctx.insert("branches", &page);
+ match tera.render(templ_path, &paged_ctx) {
+ Ok(rendered) => {
+ repo_bytes += self.write_rendered(&pagination.cur_page, &rendered);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
+ paged_ctx.remove("page");
+ paged_ctx.remove("branches");
+ }
+ }
+ Ok(repo_bytes)
+ }
+
+ pub fn gen_branch(&self, ctx: &Context, parsed_repo: &GitRepo, repo_desc: &GitsySettingsRepo, _repo: &Repository) -> Result<usize, GitsyError> {
+ let mut ctx = ctx.clone();
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
+ let mut repo_bytes = 0;
+ for branch in &parsed_repo.branches {
+ size_check!(repo_desc, repo_bytes, self.total_bytes.load(Ordering::Relaxed), break);
+ ctx.insert("branch", branch);
+ for (templ_path, out_path) in self.settings.outputs.branch(Some(parsed_repo), Some(branch)) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ match tera.render(templ_path, &ctx) {
+ Ok(rendered) => {
+ repo_bytes += self
+ .write_rendered(&out_path, &rendered);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
+ }
+ ctx.remove("branch");
+ }
+ Ok(repo_bytes)
+ }
+
+ pub fn gen_tags(&self, ctx: &Context, parsed_repo: &GitRepo, _repo_desc: &GitsySettingsRepo, _repo: &Repository) -> Result<usize, GitsyError> {
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
+ let mut repo_bytes = 0;
+ for (templ_path, out_path) in self.settings.outputs.tags::<GitRepo>(Some(parsed_repo), None) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ let mut paged_ctx = ctx.clone();
+ paged_ctx.remove("tags");
+ let pages = parsed_repo.tags.chunks(self.settings.paginate_tags());
+ let page_count = pages.len();
+ for (idx, page) in pages.enumerate() {
+ let pagination =
+ Pagination::new(idx + 1, page_count, &out_path);
+ paged_ctx.insert("page", &pagination.with_relative_paths());
+ paged_ctx.insert("tags", &page);
+ match tera.render(templ_path, &paged_ctx) {
+ Ok(rendered) => {
+ repo_bytes += self.write_rendered(&pagination.cur_page, &rendered);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
+ paged_ctx.remove("page");
+ paged_ctx.remove("tags");
+ }
+ }
+ Ok(repo_bytes)
+ }
+
+ pub fn gen_tag(&self, ctx: &Context, parsed_repo: &GitRepo, repo_desc: &GitsySettingsRepo, _repo: &Repository) -> Result<usize, GitsyError> {
+ let mut ctx = ctx.clone();
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
+ let mut repo_bytes = 0;
+ for tag in &parsed_repo.tags {
+ size_check!(repo_desc, repo_bytes, self.total_bytes.load(Ordering::Relaxed), break);
+ ctx.insert("tag", tag);
+ if let Some(tagged_id) = tag.tagged_id.as_ref() {
+ if let Some(commit) = parsed_repo.commits.get(tagged_id) {
+ ctx.insert("commit", &commit);
+ }
+ }
+ for (templ_path, out_path) in self.settings.outputs.tag(Some(parsed_repo), Some(tag)) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ match tera.render(templ_path, &ctx) {
+ Ok(rendered) => {
+ repo_bytes +=
+ self.write_rendered(&out_path, &rendered);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
+ }
+ ctx.remove("tag");
+ ctx.remove("commit");
+ }
+ Ok(repo_bytes)
+ }
+
+ pub fn gen_files(&self, ctx: &Context, parsed_repo: &GitRepo, _repo_desc: &GitsySettingsRepo, _repo: &Repository) -> Result<usize, GitsyError> {
+ let mut ctx = ctx.clone();
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
+ let mut repo_bytes = 0;
+ for (templ_path, out_path) in self.settings.outputs.files::<GitRepo>(Some(parsed_repo), None) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ ctx.insert("root_files", &parsed_repo.root_files);
+ ctx.insert("all_files", &parsed_repo.all_files);
+ match tera.render(templ_path, &ctx) {
+ Ok(rendered) => {
+ repo_bytes +=
+ self.write_rendered(&out_path, &rendered);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
+ }
+ Ok(repo_bytes)
+ }
+
+ pub fn gen_file(&self, ctx: &Context, parsed_repo: &GitRepo, repo_desc: &GitsySettingsRepo, repo: &Repository) -> Result<usize, GitsyError> {
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
+ let mut repo_bytes = 0;
+
+ #[cfg(any(feature = "highlight", feature = "highlight_fast"))]
+ if self.settings.outputs.has_files() {
+ let ts = ThemeSet::load_defaults();
+ let theme = ts
+ .themes
+ .get(
+ repo_desc
+ .syntax_highlight_theme
+ .as_deref()
+ .unwrap_or("base16-ocean.dark"),
+ )
+ .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 +=
+ self.write_rendered(&self.settings.outputs.syntax_css::<GitFile>(Some(&parsed_repo), 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> = parsed_repo.all_files.iter().filter(|x| x.kind == "file").collect();
+ let atomic_bytes: AtomicUsize = AtomicUsize::new(repo_bytes);
+ let repo_path = repo.path().to_str().expect("ERROR: unable to determine path to local repository");
+ 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 ctx = ctx.clone();
+
+ let mut local_bytes = 0;
+ let cur_repo_bytes = atomic_bytes.load(Ordering::Relaxed);
+ size_check!(repo_desc, cur_repo_bytes, self.total_bytes.load(Ordering::Relaxed), 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(),
+ };
+ ctx
+ .try_insert("file", &file)
+ .expect("Failed to add file to template engine.");
+ for (templ_path, out_path) in self.settings.outputs.file(Some(parsed_repo), Some(&file)) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ match tera.render(templ_path, &ctx) {
+ Ok(rendered) => {
+ local_bytes = self.write_rendered(&out_path, &rendered,);
+ atomic_bytes.fetch_add(local_bytes, Ordering::Relaxed);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
+ }
+ ctx.remove("file");
+ Some(acc.unwrap() + local_bytes)
+ },
+ )
+ .while_some() // allow short-circuiting if size limit is reached
+ .sum::<usize>();
+ repo_bytes = atomic_bytes.load(Ordering::Relaxed);
+ Ok(repo_bytes)
+ }
+
+ pub fn gen_dir(&self, ctx: &Context, parsed_repo: &GitRepo, repo_desc: &GitsySettingsRepo, repo: &Repository) -> Result<usize, GitsyError> {
+ let mut ctx = ctx.clone();
+ let tera = self.tera.as_ref().expect("ERROR: generate called without a context!?");
+ let mut repo_bytes = 0;
+ for dir in parsed_repo.all_files.iter().filter(|x| x.kind == "dir") {
+ size_check!(repo_desc, repo_bytes, self.total_bytes.load(Ordering::Relaxed), break);
+ let listing = dir_listing(&repo, &dir).expect("Failed to parse file.");
+ ctx.insert("dir", dir);
+ ctx
+ .try_insert("files", &listing)
+ .expect("Failed to add dir to template engine.");
+ for (templ_path, out_path) in self.settings.outputs.dir(Some(parsed_repo), Some(dir)) {
+ let templ_path = templ_path.to_str().expect(&format!("ERROR: a summary template path is invalid: {}", templ_path.display()));
+ let out_path = out_path.to_str().expect(&format!("ERROR: a summary output path is invalid: {}", out_path.display()));
+ match tera.render(templ_path, &ctx) {
+ Ok(rendered) => {
+ repo_bytes +=
+ self.write_rendered(&out_path, &rendered);
+ }
+ Err(x) => match x.kind {
+ _ => error!("ERROR: {:?}", x),
+ },
+ }
+ }
+ ctx.remove("files");
+ ctx.remove("dir");
+ }
+ Ok(repo_bytes)
+ }
+
+ fn copy_assets(&self, repo_desc: Option<&GitsySettingsRepo>, parsed_repo: Option<&GitRepo>, repo: Option<&Repository>) -> Result<usize, GitsyError> {
+ let mut bytes = 0;
+ match repo_desc {
+ Some(repo_desc) => {
+ let parsed_repo = parsed_repo.expect("ERROR: attempted to fill repo assets without a repository");
+ let repo = repo.expect("ERROR: attempted to fill repo assets without a repository");
+ //let repo_path = repo.path().to_str().expect("ERROR: repository has no path!");
+ if repo_desc.asset_files.is_some() {
+ let target_dir = self.settings.outputs.repo_assets::<GitFile>(Some(&parsed_repo), None);
+ for src_file in repo_desc.asset_files.as_ref().unwrap() {
+ let src_file = self.settings.outputs.asset(src_file, Some(parsed_repo), Some(repo));
+ 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()
+ ));
+ if let Ok(meta) = std::fs::metadata(dst_file) {
+ bytes += meta.len() as usize;
+ }
+ loud!(" - copied asset: {}", src_file.display());
+ }
+ }
+ },
+ _ => {
+ if self.settings.asset_files.is_some() {
+ let target_dir = self.settings.outputs.global_assets::<GitFile>(None, None);
+ for src_file in self.settings.asset_files.as_ref().unwrap() {
+ let src_file = self.settings.outputs.asset(src_file, None, None);
+ 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())),
+ );
+ 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) {
+ bytes += meta.len() as usize;
+ }
+ loud!(" - copied asset: {}", src_file.display());
+ }
+ }
+ },
+ }
+ Ok(bytes)
+ }
+
+ pub fn generate(&mut self) -> Result<(), GitsyError> {
+ let start_all = Instant::now();
+ self.tera = Some(self.tera_init()?);
+ self.generated_dt = chrono::offset::Local::now();
let mut total_bytes = 0;
let mut repos: Vec<GitRepo> = vec![];
+ if self.cli.should_clean {
+ self.settings.outputs.clean();
+ }
+
if self.repo_descriptions.len() == 0 {
panic!(
"No Git repositories defined! Please check your configuration file ({})",
);
}
+ self.settings.outputs.create();
+
// Sort the repositories by name
let mut repo_vec: Vec<GitsySettingsRepo> = self.repo_descriptions.iter().cloned().collect();
repo_vec.sort_by(|x, y| {
.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
loudest!("Repo settings:\n{:#?}", &repo_desc);
let start_repo = Instant::now();
let mut repo_bytes = 0;
+
let name = repo_desc.name.as_deref().expect("A configured repository has no name!");
normal_noln!("[{}{}]... ", name, " ".repeat(longest_repo_name - name.len()));
-
- 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<String> = 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()
- }
- }
- }
- dir => {
- match dir.metadata() {
- Ok(m) if m.is_dir() => {}
- _ => {
- error!(
- "ERROR: local repository [{}]: directory not found: {}",
- name,
- dir.display()
- );
- continue;
- }
- }
- dir.to_string_lossy().to_string()
- }
- };
-
+ let repo_path = self.find_repo(&name, &repo_desc)?;
let repo = Repository::open(&repo_path).expect("Unable to find git repository.");
+
let metadata = GitsyMetadata {
full_name: repo_desc.name.clone(),
description: repo_desc.description.clone(),
let parsed_repo = parse_repo(&repo, &name, &repo_desc, metadata).expect("Failed to analyze repo HEAD.");
let minimized_repo = parsed_repo.minimal_clone(self.settings.limit_context.unwrap_or(usize::MAX));
- let mut local_ctx = Context::from_serialize(&minimized_repo).unwrap();
- if let Some(extra) = &self.settings.extra {
- 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);
- }
- if let Some(site_url) = &self.settings.site_url {
- local_ctx.insert("site_url", site_url);
- }
- if let Some(site_description) = &self.settings.site_description {
- local_ctx.insert("site_description", site_description);
- }
- local_ctx.insert("site_dir", &self.settings.outputs.output_dir());
- if self.settings.outputs.global_assets.is_some() {
- local_ctx.insert("site_assets", &self.settings.outputs.to_relative(&self.settings.outputs.global_assets::<GitFile>(None, None)));
- }
- local_ctx.insert("site_generated_ts", &generated_dt.timestamp());
- local_ctx.insert("site_generated_offset", &generated_dt.offset().local_minus_utc());
+ let mut local_ctx = self.new_context(Some(&minimized_repo))?;
+ // Add README file to context, if specified and found
if let Some(readmes) = &repo_desc.readme_files {
for readme in readmes {
if let Some(file) = parsed_repo.root_files.iter().filter(|x| &x.name == readme).next() {
}
};
- if let Some(templ_file) = self.settings.templates.summary.as_deref() {
- match tera.render(templ_file, &local_ctx) {
- Ok(rendered) => {
- repo_bytes +=
- self.write_rendered(&self.settings.outputs.summary::<GitFile>(Some(&parsed_repo), None), &rendered);
- }
- Err(x) => match x.kind {
- _ => error!("ERROR: {:?}", x),
- },
- }
- }
-
- if let Some(templ_file) = self.settings.templates.branches.as_deref() {
- let mut paged_ctx = local_ctx.clone();
- paged_ctx.remove("branches");
- let pages = parsed_repo.branches.chunks(self.settings.paginate_branches());
- let page_count = pages.len();
- for (idx, page) in pages.enumerate() {
- let pagination = Pagination::new(
- idx + 1,
- page_count,
- &self.settings.outputs.branches::<GitFile>(Some(&parsed_repo), None),
- );
- paged_ctx.insert("page", &pagination.with_relative_paths());
- paged_ctx.insert("branches", &page);
- match tera.render(templ_file, &paged_ctx) {
- Ok(rendered) => {
- repo_bytes += self.write_rendered(&pagination.cur_page, &rendered);
- }
- Err(x) => match x.kind {
- _ => error!("ERROR: {:?}", x),
- },
- }
- paged_ctx.remove("page");
- paged_ctx.remove("branches");
- }
- }
-
- for branch in &parsed_repo.branches {
- size_check!(repo_desc, repo_bytes, total_bytes, break);
- local_ctx.insert("branch", branch);
- if let Some(templ_file) = self.settings.templates.branch.as_deref() {
- match tera.render(templ_file, &local_ctx) {
- Ok(rendered) => {
- repo_bytes += self
- .write_rendered(&self.settings.outputs.branch(Some(&parsed_repo), Some(branch)), &rendered);
- }
- Err(x) => match x.kind {
- _ => error!("ERROR: {:?}", x),
- },
- }
- }
- local_ctx.remove("branch");
- }
-
- if let Some(templ_file) = self.settings.templates.tags.as_deref() {
- let mut paged_ctx = local_ctx.clone();
- paged_ctx.remove("tags");
- let pages = parsed_repo.tags.chunks(self.settings.paginate_tags());
- let page_count = pages.len();
- for (idx, page) in pages.enumerate() {
- let pagination =
- Pagination::new(idx + 1, page_count, &self.settings.outputs.tags::<GitFile>(Some(&parsed_repo), None));
- paged_ctx.insert("page", &pagination.with_relative_paths());
- paged_ctx.insert("tags", &page);
- match tera.render(templ_file, &paged_ctx) {
- Ok(rendered) => {
- repo_bytes += self.write_rendered(&pagination.cur_page, &rendered);
- }
- Err(x) => match x.kind {
- _ => error!("ERROR: {:?}", x),
- },
- }
- paged_ctx.remove("page");
- paged_ctx.remove("tags");
- }
- }
-
- for tag in &parsed_repo.tags {
- size_check!(repo_desc, repo_bytes, total_bytes, break);
- local_ctx.insert("tag", tag);
- if let Some(tagged_id) = tag.tagged_id.as_ref() {
- if let Some(commit) = parsed_repo.commits.get(tagged_id) {
- local_ctx.insert("commit", &commit);
- }
- }
- if let Some(templ_file) = self.settings.templates.tag.as_deref() {
- match tera.render(templ_file, &local_ctx) {
- Ok(rendered) => {
- repo_bytes +=
- self.write_rendered(&self.settings.outputs.tag(Some(&parsed_repo), Some(tag)), &rendered);
- }
- Err(x) => match x.kind {
- _ => error!("ERROR: {:?}", x),
- },
- }
- }
- local_ctx.remove("tag");
- local_ctx.remove("commit");
- }
-
- if let Some(templ_file) = self.settings.templates.history.as_deref() {
- let mut paged_ctx = local_ctx.clone();
- paged_ctx.remove("history");
- let pages = parsed_repo.history.chunks(self.settings.paginate_history());
- let page_count = pages.len();
- for (idx, page) in pages.enumerate() {
- let pagination = Pagination::new(
- idx + 1,
- page_count,
- &self.settings.outputs.history::<GitFile>(Some(&parsed_repo), None),
- );
- paged_ctx.insert("page", &pagination.with_relative_paths());
- paged_ctx.insert("history", &page);
- match tera.render(templ_file, &paged_ctx) {
- Ok(rendered) => {
- repo_bytes += self.write_rendered(&pagination.cur_page, &rendered);
- }
- Err(x) => match x.kind {
- _ => error!("ERROR: {:?}", x),
- },
- }
- paged_ctx.remove("page");
- paged_ctx.remove("history");
- }
- }
-
- for (_id, commit) in &parsed_repo.commits {
- size_check!(repo_desc, repo_bytes, total_bytes, break);
- 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 += self
- .write_rendered(&self.settings.outputs.commit(Some(&parsed_repo), Some(commit)), &rendered);
- }
- Err(x) => match x.kind {
- _ => error!("ERROR: {:?}", x),
- },
- }
- }
- local_ctx.remove("commit");
- }
-
- #[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.dark"),
- )
- .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 +=
- self.write_rendered(&self.settings.outputs.syntax_css::<GitFile>(Some(&parsed_repo), 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> = parsed_repo.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 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 = self.write_rendered(
- &self.settings.outputs.file(Some(&parsed_repo), 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::<usize>();
- repo_bytes = atomic_bytes.load(Ordering::Relaxed);
-
- for dir in parsed_repo.all_files.iter().filter(|x| x.kind == "dir") {
- size_check!(repo_desc, repo_bytes, total_bytes, break);
- let listing = dir_listing(&repo, &dir).expect("Failed to parse file.");
- local_ctx.insert("dir", dir);
- 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 +=
- self.write_rendered(&self.settings.outputs.dir(Some(&parsed_repo), Some(dir)), &rendered);
- }
- Err(x) => match x.kind {
- _ => error!("ERROR: {:?}", x),
- },
- }
- }
- local_ctx.remove("files");
- local_ctx.remove("dir");
- }
-
- if let Some(templ_file) = self.settings.templates.files.as_deref() {
- let mut local_ctx = local_ctx.clone();
- local_ctx.insert("root_files", &parsed_repo.root_files);
- local_ctx.insert("all_files", &parsed_repo.all_files);
- match tera.render(templ_file, &local_ctx) {
- Ok(rendered) => {
- repo_bytes +=
- self.write_rendered(&self.settings.outputs.files::<GitFile>(Some(&parsed_repo), None), &rendered);
- }
- Err(x) => match x.kind {
- _ => error!("ERROR: {:?}", x),
- },
- }
- }
+ repo_bytes += self.gen_summary( &local_ctx, &parsed_repo, repo_desc, &repo)?;
+ repo_bytes += self.gen_branches(&local_ctx, &parsed_repo, repo_desc, &repo)?;
+ repo_bytes += self.gen_branch( &local_ctx, &parsed_repo, repo_desc, &repo)?;
+ repo_bytes += self.gen_tags( &local_ctx, &parsed_repo, repo_desc, &repo)?;
+ repo_bytes += self.gen_tag( &local_ctx, &parsed_repo, repo_desc, &repo)?;
+ repo_bytes += self.gen_history( &local_ctx, &parsed_repo, repo_desc, &repo)?;
+ repo_bytes += self.gen_commit( &local_ctx, &parsed_repo, repo_desc, &repo)?;
+ repo_bytes += self.gen_file( &local_ctx, &parsed_repo, repo_desc, &repo)?;
+ repo_bytes += self.gen_dir( &local_ctx, &parsed_repo, repo_desc, &repo)?;
+ repo_bytes += self.gen_files( &local_ctx, &parsed_repo, repo_desc, &repo)?;
- if repo_desc.asset_files.is_some() {
- let target_dir = self.settings.outputs.repo_assets::<GitFile>(Some(&parsed_repo), None);
- for src_file in repo_desc.asset_files.as_ref().unwrap() {
- let src_file = src_file.replace("%TEMPLATE%", &self.settings.templates.template_dir());
- let src_file = src_file.replace("%REPO%", &repo_path);
- 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()
- ));
- if let Ok(meta) = std::fs::metadata(dst_file) {
- repo_bytes += meta.len() as usize;
- }
- loud!(" - copied asset: {}", src_file.display());
- }
- }
+ self.copy_assets(Some(&repo_desc), Some(&parsed_repo), Some(&repo))?;
repos.push(minimized_repo);
normal!(
"{}done in {:.2}s ({} bytes)",
- match crate::util::VERBOSITY.load(Ordering::Relaxed) > 1 {
+ match VERBOSITY.load(Ordering::Relaxed) > 1 {
true => " - ",
_ => "",
},
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.");
- if let Some(extra) = &self.settings.extra {
- 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);
- }
- if let Some(site_url) = &self.settings.site_url {
- global_ctx.insert("site_url", site_url);
- }
- if let Some(site_description) = &self.settings.site_description {
- global_ctx.insert("site_description", site_description);
- }
- global_ctx.insert("site_dir", &self.settings.outputs.output_dir());
- if self.settings.outputs.global_assets.is_some() {
- global_ctx.insert("site_assets", &self.settings.outputs.to_relative(&self.settings.outputs.global_assets::<GitFile>(None, None)));
- }
- global_ctx.insert("site_generated_ts", &generated_dt.timestamp());
- global_ctx.insert("site_generated_offset", &generated_dt.offset().local_minus_utc());
+ let mut global_ctx = self.new_context(None)?;
+ global_ctx.try_insert("repos", &repos)?;
- if let Some(templ_file) = self.settings.templates.repo_list.as_deref() {
- match tera.render(templ_file, &global_ctx) {
- Ok(rendered) => {
- global_bytes += self.write_rendered(&self.settings.outputs.repo_list::<GitFile>(None, None), &rendered);
- }
- Err(x) => match x.kind {
- _ => error!("ERROR: {:?}", x),
- },
- }
- }
-
- if let Some(templ_file) = self.settings.templates.error.as_deref() {
- match tera.render(templ_file, &global_ctx) {
- Ok(rendered) => {
- global_bytes += self.write_rendered(&self.settings.outputs.error::<GitFile>(None, None), &rendered);
- }
- Err(x) => match x.kind {
- _ => error!("ERROR: {:?}", x),
- },
- }
- }
+ let mut global_bytes = 0;
+ global_bytes += self.gen_repo_list(&global_ctx)?;
+ global_bytes += self.gen_error(&global_ctx)?;
- if self.settings.asset_files.is_some() {
- let target_dir = self.settings.outputs.global_assets::<GitFile>(None, None);
- for src_file in self.settings.asset_files.as_ref().unwrap() {
- let src_file = src_file.replace("%TEMPLATE%", &self.settings.templates.template_dir());
- 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())),
- );
- 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) {
- global_bytes += meta.len() as usize;
- }
- loud!(" - copied asset: {}", src_file.display());
- }
- }
+ self.copy_assets(None, None, None)?;
total_bytes += global_bytes;
normal!(
);
if self.cli.should_open {
- let _ = open::that(&format!("file://{}", self.settings.outputs.repo_list::<GitFile>(None, None).display()));
+ if let Some((_templ, out)) = self.settings.outputs.repo_list::<GitFile>(None, None).first() {
+ let _ = open::that(&format!("file://{}", out.display()));
+ }
}
Ok(())
fn main() {
let cli = GitsyCli::new();
let (settings, repo_descriptions) = GitsySettings::new(&cli);
- let generator = GitsyGenerator::new(cli, settings, repo_descriptions);
+ let mut generator = GitsyGenerator::new(cli, settings, repo_descriptions);
generator.generate().expect("Itsy-Gitsy generation failed!");
}
* You should have received a copy of the GNU General Public License
* along with Itsy-Gitsy. If not, see <http://www.gnu.org/licenses/>.
*/
-use crate::error;
+use crate::{louder, error};
use crate::git::GitRepo;
use crate::util::SafePathVar;
use clap::Parser;
+use git2::Repository;
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fs::{create_dir, create_dir_all, read_dir, remove_dir_all, read_to_string};
}
#[derive(Deserialize, Debug)]
-pub struct GitsySettingsTemplates {
- pub path: PathBuf,
- pub repo_list: Option<String>,
- pub summary: Option<String>,
- pub history: Option<String>,
- pub commit: Option<String>,
- pub branches: Option<String>,
- pub branch: Option<String>,
- pub tags: Option<String>,
- pub tag: Option<String>,
- pub files: Option<String>,
- pub file: Option<String>,
- pub dir: Option<String>,
- pub error: Option<String>,
-}
-
-impl GitsySettingsTemplates {
- pub fn template_dir(&self) -> String {
- self.path.clone().canonicalize()
- .expect(&format!("ERROR: unable to canonicalize template path: {}", self.path.display()))
- .to_str().expect(&format!("ERROR: unable to parse template path: {}", self.path.display()))
- .to_string()
- }
-}
-
-#[derive(Deserialize, Debug)]
pub struct GitsySettingsOutputs {
- pub path: PathBuf,
+ pub output_root: PathBuf,
+ pub template_root: PathBuf,
+ pub templates: Option<Vec<GitsySettingsTemplate>>,
pub cloned_repos: Option<String>,
- pub repo_list: Option<String>,
- pub summary: Option<String>,
- pub history: Option<String>,
- pub commit: Option<String>,
- pub branches: Option<String>,
- pub branch: Option<String>,
- pub tags: Option<String>,
- pub tag: Option<String>,
- pub files: Option<String>,
- pub file: Option<String>,
- pub dir: Option<String>,
- pub error: Option<String>,
pub syntax_css: Option<String>,
pub global_assets: Option<String>,
pub repo_assets: Option<String>,
}
-#[derive(Deserialize, Debug)]
+#[derive(Deserialize, Debug, PartialEq)]
#[allow(non_camel_case_types)]
-pub enum GitsySettingsExtraType {
+pub enum GitsyTemplateType {
repo_list,
summary,
history,
tag,
files,
file,
- dir
+ dir,
+ error,
}
pub fn substitute_path_vars<P,S>(path: &P, repo: Option<&GitRepo>, obj: Option<&S>) -> PathBuf
}
#[derive(Deserialize, Debug)]
-pub struct GitsySettingsExtraOutput {
+pub struct GitsySettingsTemplate {
pub template: String,
pub output: String,
- pub kind: GitsySettingsExtraType,
+ pub kind: GitsyTemplateType,
}
-macro_rules! output_path_fn {
+macro_rules! template_fn {
($var:ident, $is_dir:expr, $default:expr) => {
pub fn $var<S: SafePathVar>(&self, repo: Option<&GitRepo>, obj: Option<&S>) -> PathBuf {
let tmpl_path = PathBuf::from(self.$var.as_deref().unwrap_or($default));
}
}
+macro_rules! templates_fn {
+ ($var:ident, $is_dir:expr) => {
+ pub fn $var<S: SafePathVar>(&self, repo: Option<&GitRepo>, obj: Option<&S>) -> Vec<(PathBuf, PathBuf)> {
+ match &self.templates {
+ Some(template) => {
+ template.iter()
+ .filter(|x| x.kind == GitsyTemplateType::$var)
+ .map(|x| {
+ let tmpl_path = PathBuf::from(&x.output);
+ let new_path = substitute_path_vars(&tmpl_path, repo, obj);
+ (PathBuf::from(&x.template),
+ self.canonicalize_and_create(&new_path, $is_dir))
+ }).collect()
+ },
+ None => {
+ vec!()
+ },
+ }
+ }
+ }
+}
+
+impl SafePathVar for GitsySettingsOutputs {
+ fn safe_substitute(&self, path: &impl AsRef<Path>) -> PathBuf {
+ let src: &Path = path.as_ref();
+ let mut dst = PathBuf::new();
+ let root = self.template_root.to_str()
+ .expect(&format!("ERROR: couldn't parse template root: {}", self.template_root.display()));
+ for cmp in src.components() {
+ // NOTE: this variable is not sanitized, since it's
+ // allowed to create new directory structure.
+ let cmp = cmp.as_os_str().to_string_lossy()
+ .replace("%TEMPLATE%", &root);
+ dst.push(cmp);
+ }
+ dst
+ }
+}
+
#[rustfmt::skip]
impl GitsySettingsOutputs {
- output_path_fn!(repo_list, false, "index.html");
- output_path_fn!(summary, false, "%REPO%/index.html");
- output_path_fn!(history, false, "%REPO%/history%PAGE%.html");
- output_path_fn!(commit, false, "%REPO%/commit/%ID%.html");
- output_path_fn!(branches, false, "%REPO%/branches%PAGE%.html");
- output_path_fn!(branch, false, "%REPO%/branch/%ID%.html");
- output_path_fn!(tags, false, "%REPO%/tags%PAGE%.html");
- output_path_fn!(tag, false, "%REPO%/tag/%ID%.html");
- output_path_fn!(files, false, "%REPO%/files.html");
- output_path_fn!(file, false, "%REPO%/file/%ID%.html");
- output_path_fn!(syntax_css, false, "%REPO%/file/syntax.css");
- output_path_fn!(dir, false, "%REPO%/dir/%ID%.html");
- output_path_fn!(error, false, "404.html");
- output_path_fn!(global_assets, true, "assets/");
- output_path_fn!(repo_assets, true, "%REPO%/assets/");
+ // Single entries:
+ template_fn!(syntax_css, false, "%REPO%/file/syntax.css");
+ template_fn!(global_assets, true, "assets/");
+ template_fn!(repo_assets, true, "%REPO%/assets/");
+ // Zero or more entries (Vec):
+ templates_fn!(repo_list, false);
+ templates_fn!(summary, false);
+ templates_fn!(history, false);
+ templates_fn!(commit, false);
+ templates_fn!(branches, false);
+ templates_fn!(branch, false);
+ templates_fn!(tags, false);
+ templates_fn!(tag, false);
+ templates_fn!(files, false);
+ templates_fn!(file, false);
+ templates_fn!(dir, false);
+ templates_fn!(error, false);
fn canonicalize_and_create(&self, path: &Path, is_dir: bool) -> PathBuf {
- let mut canonical_path = self.path.clone()
+ let mut canonical_path = self.output_root.clone()
.canonicalize().expect(&format!(
"ERROR: unable to canonicalize output path: {}",
- self.path.display()));
+ self.output_root.display()));
canonical_path.push(path);
match is_dir {
true => {
canonical_path
}
- pub fn output_dir(&self) -> String {
- self.path.clone().canonicalize()
- .expect(&format!("ERROR: unable to canonicalize output path: {}", self.path.display()))
- .to_str().expect(&format!("ERROR: unable to parse output path: {}", self.path.display()))
- .to_string()
+ pub fn output_dir(&self) -> PathBuf {
+ self.output_root.clone().canonicalize()
+ .expect(&format!("ERROR: unable to canonicalize output path: {}", self.output_root.display()))
+ }
+
+ pub fn template_dir(&self) -> PathBuf {
+ self.template_root.clone().canonicalize()
+ .expect(&format!("ERROR: unable to canonicalize template path: {}", self.template_root.display()))
+ }
+
+ pub fn has_files(&self) -> bool {
+ match &self.templates {
+ Some(template) => template.iter().filter(|x| x.kind == GitsyTemplateType::file).count() > 0,
+ _ => false,
+ }
+ }
+
+ pub fn asset<P: AsRef<Path>>(&self, asset: &P, parsed_repo: Option<&GitRepo>, repo: Option<&Repository>) -> PathBuf {
+ let tmpl_path = asset.as_ref().to_path_buf();
+ let asset_path = substitute_path_vars(&tmpl_path, parsed_repo, Some(self));
+ let full_path = match repo {
+ Some(repo) => {
+ let mut full_path = repo.path().to_owned();
+ full_path.push(asset_path);
+ full_path
+ },
+ _ => {
+ asset_path
+ }
+ };
+ full_path
}
pub fn create(&self) {
- let _ = create_dir(self.path.to_str().expect(&format!("ERROR: output path invalid: {}", self.path.display())));
+ louder!("Creating output directory: {}", self.output_root.display());
+ let _ = create_dir(self.output_root.to_str().expect(&format!("ERROR: output path invalid: {}", self.output_root.display())));
}
pub fn clean(&self) {
- if !self.path.exists() {
+ if !self.output_root.exists() {
return;
}
+ louder!("Cleaning output directory: {}", self.output_root.display());
let dir: PathBuf = PathBuf::from(&self.output_dir());
assert!(dir.is_dir(), "ERROR: Output directory is... not a directory? {}", dir.display());
remove_dir_all(&dir)
.expect(&format!("ERROR: Unable to make path relative: {}", path))
.to_string()
}
+
+ pub fn assert_valid<P: AsRef<Path>>(&self, path: &P) -> bool {
+ let path = path.as_ref().to_str()
+ .expect(&format!("ERROR: attempted to write unrecognizeable path: {}", path.as_ref().display()));
+ // Ensure that the requested output path is actually a child
+ // of the output directory, as a sanity check to ensure we
+ // aren't writing out of bounds.
+ let canonical_root = self.output_root.canonicalize().expect(&format!(
+ "Cannot find canonical version of output path: {}",
+ self.output_root.display()
+ ));
+ let canonical_path = PathBuf::from(path);
+ let has_relative_dirs = canonical_path
+ .ancestors()
+ .any(|x| x.file_name().is_none() && x != Path::new("/"));
+ assert!(
+ canonical_path.is_absolute(),
+ "ERROR: write_rendered called with a relative path: {}",
+ path
+ );
+ assert!(
+ !has_relative_dirs,
+ "ERROR: write_rendered called with a relative path: {}",
+ path
+ );
+ let _ = canonical_path
+ .ancestors()
+ .find(|x| x == &canonical_root)
+ .expect(&format!(
+ "Output file {} not contained in output path: {}",
+ canonical_path.display(),
+ canonical_root.display()
+ ));
+ true
+ }
}
#[derive(Clone, Deserialize, Default, Debug)]
pub readme_files: Option<Vec<String>>,
pub asset_files: Option<Vec<String>>,
pub branch: Option<String>,
- #[serde(rename(deserialize = "gitsy_templates"))]
- pub templates: GitsySettingsTemplates,
- #[serde(rename(deserialize = "gitsy_outputs"))]
- pub outputs: GitsySettingsOutputs,
pub paginate_history: Option<usize>,
pub paginate_branches: Option<usize>,
pub paginate_tags: Option<usize>,
pub render_markdown: Option<bool>,
pub syntax_highlight: Option<bool>,
pub syntax_highlight_theme: Option<String>,
+ #[serde(rename(deserialize = "gitsy_outputs"))]
+ pub outputs: GitsySettingsOutputs,
#[serde(rename(deserialize = "gitsy_extra"))]
pub extra: Option<BTreeMap<String, toml::Value>>,
}