diff --git a/Cargo.lock b/Cargo.lock index 90b8073f8e..1055800c4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "ctor" version = "0.1.20" @@ -483,20 +492,18 @@ dependencies = [ ] [[package]] -name = "gitignore" -version = "1.0.7" +name = "globset" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78aa90e4620c1498ac434c06ba6e521b525794bbdacf085d490cc794b4a2f9a4" +checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" dependencies = [ - "glob", + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", ] -[[package]] -name = "glob" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - [[package]] name = "h2" version = "0.3.4" @@ -659,6 +666,24 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.7.0" @@ -807,8 +832,8 @@ dependencies = [ "elasticlunr-rs", "env_logger", "futures-util", - "gitignore", "handlebars", + "ignore", "log", "memchr", "notify", @@ -1631,6 +1656,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + [[package]] name = "time" version = "0.1.43" diff --git a/Cargo.toml b/Cargo.toml index 5b564ec900..1ce783c2ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,11 +20,12 @@ anyhow = "1.0.28" chrono = "0.4" clap = { version = "3.0", features = ["cargo"] } clap_complete = "3.0" -once_cell = "1" env_logger = "0.9.0" +ignore = "0.4" handlebars = "4.0" log = "0.4" memchr = "2.0" +once_cell = "1" opener = "0.5" pulldown-cmark = { version = "0.9.1", default-features = false } regex = "1.5.5" @@ -37,7 +38,6 @@ topological-sort = "0.1.0" # Watch feature notify = { version = "4.0", optional = true } -gitignore = { version = "1.0", optional = true } # Serve feature futures-util = { version = "0.3.4", optional = true } @@ -58,7 +58,7 @@ walkdir = "2.0" [features] default = ["watch", "serve", "search"] -watch = ["notify", "gitignore"] +watch = ["notify"] serve = ["futures-util", "tokio", "warp"] search = ["elasticlunr-rs", "ammonia"] diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md index b9c3086114..c53da08366 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -279,6 +279,18 @@ The value can be any valid URI the browser should navigate to (e.g. `https://rus This will generate an HTML page which will automatically redirect to the given location. Note that the source location does not support `#` anchor redirects. +### `.mdbookignore` + +You can use a `.mdbookignore` file to exclude files from the build process. +The file is placed in the `src` directory of your book and has the same format as +[`.gitignore`](https://git-scm.com/docs/gitignore) files. + +For example: +``` +*.rs +/target/ +``` + ## Markdown Renderer The Markdown renderer will run preprocessors and then output the resulting diff --git a/src/book/mod.rs b/src/book/mod.rs index 5ec64d7c3a..e41f0c0af6 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -587,6 +587,7 @@ fn preprocessor_should_run( mod tests { use super::*; use std::str::FromStr; + use tempfile::Builder as TempFileBuilder; use toml::value::{Table, Value}; #[test] @@ -856,4 +857,21 @@ mod tests { let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg); assert_eq!(got, should_be); } + + #[test] + fn mdbookignore_ignores_file() { + let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap(); + let test_book_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_book"); + + utils::fs::copy_files_except_ext(&test_book_dir, temp_dir.path(), true, None, None) + .expect("Error while copying test book to temp dir"); + + let book = MDBook::load(temp_dir.path()).expect("Unable to load book"); + book.build().expect("Error while building book"); + + let book_dir = temp_dir.path().join("book"); + assert!(book_dir.join("index.html").exists()); + assert!(book_dir.join(".mdbookignore").exists()); + assert!(!book_dir.join("ignored_file").exists()); + } } diff --git a/src/cmd/watch.rs b/src/cmd/watch.rs index 9336af779a..4021afbc29 100644 --- a/src/cmd/watch.rs +++ b/src/cmd/watch.rs @@ -1,5 +1,6 @@ use crate::{get_book_dir, open}; use clap::{arg, App, Arg, ArgMatches}; +use ignore::gitignore::Gitignore; use mdbook::errors::Result; use mdbook::utils; use mdbook::MDBook; @@ -75,16 +76,7 @@ fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec { } match find_gitignore(book_root) { - Some(gitignore_path) => { - match gitignore::File::new(gitignore_path.as_path()) { - Ok(exclusion_checker) => filter_ignored_files(exclusion_checker, paths), - Err(_) => { - // We're unable to read the .gitignore file, so we'll silently allow everything. - // Please see discussion: https://github.com/rust-lang/mdBook/pull/1051 - paths.iter().map(|path| path.to_path_buf()).collect() - } - } - } + Some(gitignore_path) => filter_ignored_files(&Gitignore::new(gitignore_path).0, paths), None => { // There is no .gitignore file. paths.iter().map(|path| path.to_path_buf()).collect() @@ -99,18 +91,16 @@ fn find_gitignore(book_root: &Path) -> Option { .find(|p| p.exists()) } -fn filter_ignored_files(exclusion_checker: gitignore::File, paths: &[PathBuf]) -> Vec { +fn filter_ignored_files(ignore: &Gitignore, paths: &[PathBuf]) -> Vec { paths .iter() - .filter(|path| match exclusion_checker.is_excluded(path) { - Ok(exclude) => !exclude, - Err(error) => { - warn!( - "Unable to determine if {:?} is excluded: {:?}. Including it.", - &path, error - ); - true - } + .filter(|path| { + let canonicalized = path.canonicalize(); + let p = canonicalized.as_ref().unwrap_or(path); + + !ignore + .matched_path_or_any_parents(p, p.is_dir()) + .is_ignore() }) .map(|path| path.to_path_buf()) .collect() diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 710449af0d..47ca002b63 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -14,6 +14,7 @@ use std::path::{Path, PathBuf}; use crate::utils::fs::get_404_output_file; use handlebars::Handlebars; +use ignore::gitignore::GitignoreBuilder; use log::{debug, trace, warn}; use once_cell::sync::Lazy; use regex::{Captures, Regex}; @@ -589,7 +590,23 @@ impl Renderer for HtmlHandlebars { .context("Unable to emit redirects")?; // Copy all remaining files, avoid a recursive copy from/to the book build dir - utils::fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?; + let mut builder = GitignoreBuilder::new(&src_dir); + let mdbook_ignore = src_dir.join(".mdbookignore"); + if mdbook_ignore.exists() { + if let Some(err) = builder.add(mdbook_ignore) { + warn!("Unable to load '.mdbookignore' file: {}", err); + } + } + builder.add_line(None, "*.md")?; + let ignore = builder.build()?; + + utils::fs::copy_files_except_ext( + &src_dir, + destination, + true, + Some(&build_dir), + Some(&ignore), + )?; Ok(()) } diff --git a/src/utils/fs.rs b/src/utils/fs.rs index 0d6f383746..8c3a50b7a0 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -1,4 +1,5 @@ use crate::errors::*; +use ignore::gitignore::Gitignore; use log::{debug, trace}; use std::convert::Into; use std::fs::{self, File}; @@ -94,13 +95,13 @@ pub fn copy_files_except_ext( to: &Path, recursive: bool, avoid_dir: Option<&PathBuf>, - ext_blacklist: &[&str], + ignore: Option<&Gitignore>, ) -> Result<()> { debug!( "Copying all files from {} to {} (blacklist: {:?}), avoiding {:?}", from.display(), to.display(), - ext_blacklist, + ignore, avoid_dir ); @@ -116,6 +117,14 @@ pub fn copy_files_except_ext( .metadata() .with_context(|| format!("Failed to read {:?}", entry.path()))?; + // Check if it is in the blacklist + if let Some(ignore) = ignore { + let path = entry.path(); + if ignore.matched(&path, path.is_dir()).is_ignore() { + continue; + } + } + // If the entry is a dir and the recursive option is enabled, call itself if metadata.is_dir() && recursive { if entry.path() == to.to_path_buf() { @@ -138,15 +147,9 @@ pub fn copy_files_except_ext( &to.join(entry.file_name()), true, avoid_dir, - ext_blacklist, + ignore, )?; } else if metadata.is_file() { - // Check if it is in the blacklist - if let Some(ext) = entry.path().extension() { - if ext_blacklist.contains(&ext.to_str().unwrap()) { - continue; - } - } debug!( "creating path for file: {:?}", &to.join( @@ -191,6 +194,7 @@ pub fn get_404_output_file(input_404: &Option) -> String { #[cfg(test)] mod tests { use super::copy_files_except_ext; + use ignore::gitignore::GitignoreBuilder; use std::{fs, io::Result, path::Path}; #[cfg(target_os = "windows")] @@ -247,9 +251,19 @@ mod tests { panic!("Could not create output/sub_dir_exists: {}", err); } - if let Err(e) = - copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"]) - { + let ignore = GitignoreBuilder::new(tmp.path()) + .add_line(None, "*.md") + .expect("Unable to add '*.md' to gitignore builder") + .build() + .expect("Unable to build gitignore"); + + if let Err(e) = copy_files_except_ext( + tmp.path(), + &tmp.path().join("output"), + true, + None, + Some(&ignore), + ) { panic!("Error while executing the function:\n{:?}", e); } diff --git a/test_book/src/.mdbookignore b/test_book/src/.mdbookignore new file mode 100644 index 0000000000..6a79f808a9 --- /dev/null +++ b/test_book/src/.mdbookignore @@ -0,0 +1 @@ +ignored_file diff --git a/test_book/src/ignored_file b/test_book/src/ignored_file new file mode 100644 index 0000000000..1ce1f71700 --- /dev/null +++ b/test_book/src/ignored_file @@ -0,0 +1 @@ +This will not be copied to the book directory.