]> git.proxmox.com Git - rustc.git/blob - vendor/mdbook/src/cmd/watch.rs
New upstream version 1.68.2+dfsg1
[rustc.git] / vendor / mdbook / src / cmd / watch.rs
1 use super::command_prelude::*;
2 use crate::{get_book_dir, open};
3 use mdbook::errors::Result;
4 use mdbook::utils;
5 use mdbook::MDBook;
6 use std::path::{Path, PathBuf};
7 use std::sync::mpsc::channel;
8 use std::thread::sleep;
9 use std::time::Duration;
10
11 // Create clap subcommand arguments
12 pub fn make_subcommand() -> Command {
13 Command::new("watch")
14 .about("Watches a book's files and rebuilds it on changes")
15 .arg_dest_dir()
16 .arg_root_dir()
17 .arg_open()
18 }
19
20 // Watch command implementation
21 pub fn execute(args: &ArgMatches) -> Result<()> {
22 let book_dir = get_book_dir(args);
23 let mut book = MDBook::load(&book_dir)?;
24
25 let update_config = |book: &mut MDBook| {
26 if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
27 book.config.build.build_dir = dest_dir.into();
28 }
29 };
30 update_config(&mut book);
31
32 if args.get_flag("open") {
33 book.build()?;
34 let path = book.build_dir_for("html").join("index.html");
35 if !path.exists() {
36 error!("No chapter available to open");
37 std::process::exit(1)
38 }
39 open(path);
40 }
41
42 trigger_on_change(&book, |paths, book_dir| {
43 info!("Files changed: {:?}\nBuilding book...\n", paths);
44 let result = MDBook::load(&book_dir).and_then(|mut b| {
45 update_config(&mut b);
46 b.build()
47 });
48
49 if let Err(e) = result {
50 error!("Unable to build the book");
51 utils::log_backtrace(&e);
52 }
53 });
54
55 Ok(())
56 }
57
58 fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec<PathBuf> {
59 if paths.is_empty() {
60 return vec![];
61 }
62
63 match find_gitignore(book_root) {
64 Some(gitignore_path) => {
65 match gitignore::File::new(gitignore_path.as_path()) {
66 Ok(exclusion_checker) => filter_ignored_files(exclusion_checker, paths),
67 Err(_) => {
68 // We're unable to read the .gitignore file, so we'll silently allow everything.
69 // Please see discussion: https://github.com/rust-lang/mdBook/pull/1051
70 paths.iter().map(|path| path.to_path_buf()).collect()
71 }
72 }
73 }
74 None => {
75 // There is no .gitignore file.
76 paths.iter().map(|path| path.to_path_buf()).collect()
77 }
78 }
79 }
80
81 fn find_gitignore(book_root: &Path) -> Option<PathBuf> {
82 book_root
83 .ancestors()
84 .map(|p| p.join(".gitignore"))
85 .find(|p| p.exists())
86 }
87
88 fn filter_ignored_files(exclusion_checker: gitignore::File, paths: &[PathBuf]) -> Vec<PathBuf> {
89 paths
90 .iter()
91 .filter(|path| match exclusion_checker.is_excluded(path) {
92 Ok(exclude) => !exclude,
93 Err(error) => {
94 warn!(
95 "Unable to determine if {:?} is excluded: {:?}. Including it.",
96 &path, error
97 );
98 true
99 }
100 })
101 .map(|path| path.to_path_buf())
102 .collect()
103 }
104
105 /// Calls the closure when a book source file is changed, blocking indefinitely.
106 pub fn trigger_on_change<F>(book: &MDBook, closure: F)
107 where
108 F: Fn(Vec<PathBuf>, &Path),
109 {
110 use notify::RecursiveMode::*;
111
112 // Create a channel to receive the events.
113 let (tx, rx) = channel();
114
115 let mut debouncer = match notify_debouncer_mini::new_debouncer(Duration::from_secs(1), None, tx)
116 {
117 Ok(d) => d,
118 Err(e) => {
119 error!("Error while trying to watch the files:\n\n\t{:?}", e);
120 std::process::exit(1)
121 }
122 };
123 let watcher = debouncer.watcher();
124
125 // Add the source directory to the watcher
126 if let Err(e) = watcher.watch(&book.source_dir(), Recursive) {
127 error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
128 std::process::exit(1);
129 };
130
131 let _ = watcher.watch(&book.theme_dir(), Recursive);
132
133 // Add the book.toml file to the watcher if it exists
134 let _ = watcher.watch(&book.root.join("book.toml"), NonRecursive);
135
136 for dir in &book.config.build.extra_watch_dirs {
137 let path = dir.canonicalize().unwrap();
138 if let Err(e) = watcher.watch(&path, Recursive) {
139 error!(
140 "Error while watching extra directory {:?}:\n {:?}",
141 path, e
142 );
143 std::process::exit(1);
144 }
145 }
146
147 info!("Listening for changes...");
148
149 loop {
150 let first_event = rx.recv().unwrap();
151 sleep(Duration::from_millis(50));
152 let other_events = rx.try_iter();
153
154 let all_events = std::iter::once(first_event).chain(other_events);
155
156 let paths: Vec<_> = all_events
157 .filter_map(|event| match event {
158 Ok(events) => Some(events),
159 Err(errors) => {
160 for error in errors {
161 log::warn!("error while watching for changes: {error}");
162 }
163 None
164 }
165 })
166 .flatten()
167 .map(|event| event.path)
168 .collect();
169
170 // If we are watching files outside the current repository (via extra-watch-dirs), then they are definitionally
171 // ignored by gitignore. So we handle this case by including such files into the watched paths list.
172 let any_external_paths = paths.iter().filter(|p| !p.starts_with(&book.root)).cloned();
173 let mut paths = remove_ignored_files(&book.root, &paths[..]);
174 paths.extend(any_external_paths);
175
176 if !paths.is_empty() {
177 closure(paths, &book.root);
178 }
179 }
180 }