Check for paths collisions

Closes #366
This commit is contained in:
Vincent Prouillet 2019-12-01 18:03:24 +01:00
parent 6153d20047
commit 5532f62c2d
6 changed files with 170 additions and 3 deletions

View file

@ -12,6 +12,8 @@ accessible everywhere
- Add `total_pages` to paginator - Add `total_pages` to paginator
- Do not prepend URL prefix to links that start with a scheme - Do not prepend URL prefix to links that start with a scheme
- Allow skipping anchor checking in `zola check` for some URL prefixes - Allow skipping anchor checking in `zola check` for some URL prefixes
- Allow skipping prefixes in `zola check`
- Check for path collisions when building the site
## 0.9.0 (2019-09-28) ## 0.9.0 (2019-09-28)

View file

@ -63,6 +63,18 @@ impl Error {
pub fn chain(value: impl ToString, source: impl Into<Box<dyn StdError>>) -> Self { pub fn chain(value: impl ToString, source: impl Into<Box<dyn StdError>>) -> Self {
Self { kind: ErrorKind::Msg(value.to_string()), source: Some(source.into()) } Self { kind: ErrorKind::Msg(value.to_string()), source: Some(source.into()) }
} }
/// Create an error from a list of path collisions, formatting the output
pub fn from_collisions(collisions: Vec<(&str, Vec<String>)>) -> Self {
let mut msg = String::from("Found path collisions:\n");
for (path, filepaths) in collisions {
let row = format!("- `{}` from files {:?}\n", path, filepaths);
msg.push_str(&row);
}
Self { kind: ErrorKind::Msg(msg), source: None }
}
} }
impl From<&str> for Error { impl From<&str> for Error {

View file

@ -263,7 +263,10 @@ mod tests {
&Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.md"), &Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.md"),
&PathBuf::new(), &PathBuf::new(),
); );
assert_eq!(file.canonical, Path::new("/home/vincent/code/site/content/posts/tutorials/python/index")); assert_eq!(
file.canonical,
Path::new("/home/vincent/code/site/content/posts/tutorials/python/index")
);
} }
/// Regression test for https://github.com/getzola/zola/issues/854 /// Regression test for https://github.com/getzola/zola/issues/854
@ -277,6 +280,9 @@ mod tests {
); );
let res = file.find_language(&config); let res = file.find_language(&config);
assert!(res.is_ok()); assert!(res.is_ok());
assert_eq!(file.canonical, Path::new("/home/vincent/code/site/content/posts/tutorials/python/index")); assert_eq!(
file.canonical,
Path::new("/home/vincent/code/site/content/posts/tutorials/python/index")
);
} }
} }

View file

@ -121,6 +121,7 @@ impl Section {
} else { } else {
section.path = format!("{}/", path); section.path = format!("{}/", path);
} }
section.components = section section.components = section
.path .path
.split('/') .split('/')
@ -131,7 +132,7 @@ impl Section {
Ok(section) Ok(section)
} }
/// Read and parse a .md file into a Page struct /// Read and parse a .md file into a Section struct
pub fn from_file<P: AsRef<Path>>( pub fn from_file<P: AsRef<Path>>(
path: P, path: P,
config: &Config, config: &Config,

View file

@ -9,6 +9,19 @@ use config::Config;
use content::{Page, Section}; use content::{Page, Section};
use sorting::{find_siblings, sort_pages_by_date, sort_pages_by_weight}; use sorting::{find_siblings, sort_pages_by_date, sort_pages_by_weight};
// Like vec! but for HashSet
macro_rules! set {
( $( $x:expr ),* ) => {
{
let mut s = HashSet::new();
$(
s.insert($x);
)*
s
}
};
}
/// Houses everything about pages and sections /// Houses everything about pages and sections
/// Think of it as a database where each page and section has an id (Key here) /// Think of it as a database where each page and section has an id (Key here)
/// that can be used to find the actual value /// that can be used to find the actual value
@ -398,4 +411,128 @@ impl Library {
pub fn contains_page<P: AsRef<Path>>(&self, path: P) -> bool { pub fn contains_page<P: AsRef<Path>>(&self, path: P) -> bool {
self.paths_to_pages.contains_key(path.as_ref()) self.paths_to_pages.contains_key(path.as_ref())
} }
/// This will check every section/page paths + the aliases and ensure none of them
/// are colliding.
/// Returns (path colliding, [list of files causing that collision])
pub fn check_for_path_collisions(&self) -> Vec<(&str, Vec<String>)> {
let mut paths: HashMap<&str, HashSet<DefaultKey>> = HashMap::new();
for (key, page) in &self.pages {
paths
.entry(&page.path)
.and_modify(|s| {
s.insert(key);
})
.or_insert_with(|| set!(key));
for alias in &page.meta.aliases {
paths
.entry(&alias)
.and_modify(|s| {
s.insert(key);
})
.or_insert_with(|| set!(key));
}
}
for (key, section) in &self.sections {
if !section.meta.render {
continue;
}
paths
.entry(&section.path)
.and_modify(|s| {
s.insert(key);
})
.or_insert_with(|| set!(key));
}
let mut collisions = vec![];
for (p, keys) in paths {
if keys.len() > 1 {
let file_paths: Vec<String> = keys
.iter()
.map(|k| {
self.pages.get(*k).map(|p| p.file.relative.clone()).unwrap_or_else(|| {
self.sections.get(*k).map(|s| s.file.relative.clone()).unwrap()
})
})
.collect();
collisions.push((p, file_paths));
}
}
collisions
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_find_no_collisions() {
let mut library = Library::new(10, 10, false);
let mut page = Page::default();
page.path = "hello".to_string();
let mut page2 = Page::default();
page2.path = "hello-world".to_string();
let mut section = Section::default();
section.path = "blog".to_string();
library.insert_page(page);
library.insert_page(page2);
library.insert_section(section);
let collisions = library.check_for_path_collisions();
assert_eq!(collisions.len(), 0);
}
#[test]
fn can_find_collisions_between_pages() {
let mut library = Library::new(10, 10, false);
let mut page = Page::default();
page.path = "hello".to_string();
page.file.relative = "hello".to_string();
let mut page2 = Page::default();
page2.path = "hello".to_string();
page2.file.relative = "hello-world".to_string();
let mut section = Section::default();
section.path = "blog".to_string();
section.file.relative = "hello-world".to_string();
library.insert_page(page.clone());
library.insert_page(page2.clone());
library.insert_section(section);
let collisions = library.check_for_path_collisions();
assert_eq!(collisions.len(), 1);
assert_eq!(collisions[0].0, page.path);
assert!(collisions[0].1.contains(&page.file.relative));
assert!(collisions[0].1.contains(&page2.file.relative));
}
#[test]
fn can_find_collisions_with_an_alias() {
let mut library = Library::new(10, 10, false);
let mut page = Page::default();
page.path = "hello".to_string();
page.file.relative = "hello".to_string();
let mut page2 = Page::default();
page2.path = "hello-world".to_string();
page2.file.relative = "hello-world".to_string();
page2.meta.aliases = vec!["hello".to_string()];
let mut section = Section::default();
section.path = "blog".to_string();
section.file.relative = "hello-world".to_string();
library.insert_page(page.clone());
library.insert_page(page2.clone());
library.insert_section(section);
let collisions = library.check_for_path_collisions();
assert_eq!(collisions.len(), 1);
assert_eq!(collisions[0].0, page.path);
assert!(collisions[0].1.contains(&page.file.relative));
assert!(collisions[0].1.contains(&page2.file.relative));
}
} }

View file

@ -253,6 +253,14 @@ impl Site {
self.add_page(p, false)?; self.add_page(p, false)?;
} }
{
let library = self.library.read().unwrap();
let collisions = library.check_for_path_collisions();
if !collisions.is_empty() {
return Err(Error::from_collisions(collisions));
}
}
// taxonomy Tera fns are loaded in `register_early_global_fns` // taxonomy Tera fns are loaded in `register_early_global_fns`
// so we do need to populate it first. // so we do need to populate it first.
self.populate_taxonomies()?; self.populate_taxonomies()?;
@ -465,6 +473,7 @@ impl Site {
index_path.file_name().unwrap().to_string_lossy().to_string(); index_path.file_name().unwrap().to_string_lossy().to_string();
if let Some(ref l) = lang { if let Some(ref l) = lang {
index_section.file.name = format!("_index.{}", l); index_section.file.name = format!("_index.{}", l);
index_section.path = format!("{}/", l);
index_section.permalink = self.config.make_permalink(l); index_section.permalink = self.config.make_permalink(l);
let filename = format!("_index.{}.md", l); let filename = format!("_index.{}.md", l);
index_section.file.path = self.content_path.join(&filename); index_section.file.path = self.content_path.join(&filename);