diff --git a/Cargo.lock b/Cargo.lock index b5723ae..92e5251 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,6 +364,8 @@ dependencies = [ "fehler", "git2", "globset", + "ignore", + "lexiclean", "libc", "log", "pretty_env_logger", @@ -376,6 +378,7 @@ dependencies = [ "strum_macros", "tempfile", "url", + "walkdir", ] [[package]] diff --git a/bin/gen/Cargo.toml b/bin/gen/Cargo.toml index b8f8647..b4acaa2 100644 --- a/bin/gen/Cargo.toml +++ b/bin/gen/Cargo.toml @@ -12,6 +12,7 @@ chrono = "0.4.11" fehler = "1.0.0" git2 = "0.13.1" globset = "0.4.5" +ignore = "0.4.14" libc = "0.2.69" log = "0.4.8" pretty_env_logger = "0.4.0" @@ -22,6 +23,8 @@ structopt = "0.3.12" strum = "0.18.0" strum_macros = "0.18.0" tempfile = "3.1.0" +walkdir = "2.3.1" +lexiclean = "0.0.1" [dependencies.serde] version = "1.0.106" diff --git a/bin/gen/src/common.rs b/bin/gen/src/common.rs index 7d5a11e..1031cc5 100644 --- a/bin/gen/src/common.rs +++ b/bin/gen/src/common.rs @@ -6,7 +6,7 @@ pub(crate) use std::{ fs::{self, File}, io, ops::Deref, - path::{Path, PathBuf}, + path::{Path, PathBuf, StripPrefixError}, process::{self, Command, ExitStatus, Stdio}, str, }; @@ -16,6 +16,8 @@ pub(crate) use cargo_toml::Manifest; pub(crate) use chrono::{DateTime, NaiveDateTime, Utc}; pub(crate) use fehler::{throw, throws}; pub(crate) use git2::{Commit, Oid, Repository}; +pub(crate) use ignore::overrides::OverrideBuilder; +pub(crate) use lexiclean::Lexiclean; pub(crate) use libc::EXIT_FAILURE; pub(crate) use log::info; pub(crate) use regex::Regex; @@ -26,6 +28,7 @@ pub(crate) use structopt::StructOpt; pub(crate) use strum::VariantNames; pub(crate) use strum_macros::{EnumVariantNames, IntoStaticStr}; pub(crate) use url::Url; +pub(crate) use walkdir::WalkDir; // modules pub(crate) use crate::error; diff --git a/bin/gen/src/error.rs b/bin/gen/src/error.rs index 62dba36..6937def 100644 --- a/bin/gen/src/error.rs +++ b/bin/gen/src/error.rs @@ -52,6 +52,12 @@ pub(crate) enum Error { }, #[snafu(display("I/O error at `{}`: {}", path.display(), source))] Filesystem { path: PathBuf, source: io::Error }, + #[snafu(display("I/O error copying `{}` to `{}`: {}", src.display(), dst.display(), source))] + FilesystemCopy { + src: PathBuf, + dst: PathBuf, + source: io::Error, + }, #[snafu(display("Git error: {}", source))] Git { source: git2::Error }, #[snafu(display("Regex compilation error: {}", source))] @@ -67,6 +73,12 @@ pub(crate) enum Error { TemplateRender { source: askama::Error }, #[snafu(display("Failed to get workdir for repo at `{}`", repo.display()))] Workdir { repo: PathBuf }, + #[snafu(display("Failed to build overrides: {}", source))] + Ignore { source: ignore::Error }, + #[snafu(display("Failed to traverse worktree: {}", source))] + Walkdir { source: walkdir::Error }, + #[snafu(display("Failed to strip path prefix: {}", source))] + StripPrefix { source: StripPrefixError }, } impl From for Error { @@ -86,3 +98,15 @@ impl From for Error { Self::CargoToml { source } } } + +impl From for Error { + fn from(source: ignore::Error) -> Self { + Self::Ignore { source } + } +} + +impl From for Error { + fn from(source: walkdir::Error) -> Self { + Self::Walkdir { source } + } +} diff --git a/bin/gen/src/opt.rs b/bin/gen/src/opt.rs index 76d7bd1..55426cd 100644 --- a/bin/gen/src/opt.rs +++ b/bin/gen/src/opt.rs @@ -4,6 +4,8 @@ use crate::common::*; pub(crate) enum Opt { #[structopt(about("Update all generated docs"))] All, + #[structopt(about("Generate book"))] + Book, #[structopt(about("Generate the changelog"))] Changelog, #[structopt(about("Print a commit template to standard output"))] @@ -12,10 +14,10 @@ pub(crate) enum Opt { CommitTypes, #[structopt(about("Generate completion scripts"))] CompletionScripts, + #[structopt(about("Diff generated content between commits"))] + Diff, #[structopt(about("Generate readme"))] Readme, - #[structopt(about("Generate book"))] - Book, #[structopt(about("Generate man pages"))] Man, } @@ -70,16 +72,20 @@ impl Opt { Self::Readme => Self::readme(&project)?, Self::Book => Self::book(&project)?, Self::Man => Self::man(&project)?, - Self::All => { - Self::changelog(&project)?; - Self::completion_scripts(&project)?; - Self::readme(&project)?; - Self::book(&project)?; - Self::man(&project)?; - } + Self::Diff => Self::diff(&project)?, + Self::All => Self::all(&project)?, } } + #[throws] + pub(crate) fn all(project: &Project) { + Self::changelog(&project)?; + Self::completion_scripts(&project)?; + Self::readme(&project)?; + Self::book(&project)?; + Self::man(&project)?; + } + #[throws] pub(crate) fn changelog(project: &Project) { info!("Generating changelogā€¦"); @@ -109,6 +115,91 @@ impl Opt { .status_into_result()? } + #[throws] + pub(crate) fn diff(project: &Project) { + let tmp = tempfile::tempdir().context(error::Tempdir)?; + + let generated = &[ + "/CHANGELOG.md", + "/README.md", + "/book/src/SUMMARY.md", + "/book/src/bittorrent.md", + "/book/src/commands.md", + "/book/src/commands/*", + "/book/src/faq.md", + "/book/src/introduction.md", + "/book/src/references.md", + "/book/src/references/*", + "/completions/*", + "/man/*", + ]; + + let gen = |name: &str| -> Result<(), Error> { + cmd!("cargo", "run", "--package", "gen", "all").status_into_result()?; + + let dir = tmp.path().join(name); + + fs::create_dir(&dir).context(error::Filesystem { path: &dir })?; + + let mut builder = OverrideBuilder::new(&project.root); + + for pattern in generated { + builder.add(pattern)?; + } + + let overrides = builder.build()?; + + for result in WalkDir::new(&project.root) { + let entry = result?; + + let src = entry.path(); + + if src.is_dir() { + continue; + } + + if !overrides.matched(&src, false).is_whitelist() { + continue; + } + + let relative = src + .strip_prefix(&project.root) + .context(error::StripPrefix)?; + + let dst = dir.join(relative); + + let dst_dir = dst.join("..").lexiclean(); + + fs::create_dir_all(&dst_dir).context(error::Filesystem { path: dst_dir })?; + + fs::copy(&src, &dst).context(error::FilesystemCopy { src, dst })?; + } + + Ok(()) + }; + + const HEAD: &str = "HEAD"; + + gen(HEAD)?; + + let head = project.repo.head()?.peel_to_commit()?; + + let parent = head.parent(0)?; + + let parent_hash = parent.id().to_string(); + + cmd!("git", "checkout", &parent_hash).status_into_result()?; + + gen(&parent_hash)?; + + cmd!("diff", "-r", parent_hash, HEAD) + .current_dir(tmp.path()) + .status_into_result() + .ok(); + + cmd!("git", "checkout", &head.id().to_string()).status_into_result()?; + } + #[throws] pub(crate) fn readme(project: &Project) { info!("Generating readmeā€¦");