Browse Source

Check for paths collisions

Closes #366
index-subcmd
Vincent Prouillet 4 years ago
parent
commit
5532f62c2d
6 changed files with 170 additions and 3 deletions
  1. +2
    -0
      CHANGELOG.md
  2. +12
    -0
      components/errors/src/lib.rs
  3. +8
    -2
      components/library/src/content/file_info.rs
  4. +2
    -1
      components/library/src/content/section.rs
  5. +137
    -0
      components/library/src/library.rs
  6. +9
    -0
      components/site/src/lib.rs

+ 2
- 0
CHANGELOG.md View File

@@ -12,6 +12,8 @@ accessible everywhere
- Add `total_pages` to paginator
- 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 prefixes in `zola check`
- Check for path collisions when building the site

## 0.9.0 (2019-09-28)



+ 12
- 0
components/errors/src/lib.rs View File

@@ -63,6 +63,18 @@ impl Error {
pub fn chain(value: impl ToString, source: impl Into<Box<dyn StdError>>) -> Self {
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 {


+ 8
- 2
components/library/src/content/file_info.rs View File

@@ -263,7 +263,10 @@ mod tests {
&Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.md"),
&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
@@ -277,6 +280,9 @@ mod tests {
);
let res = file.find_language(&config);
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")
);
}
}

+ 2
- 1
components/library/src/content/section.rs View File

@@ -121,6 +121,7 @@ impl Section {
} else {
section.path = format!("{}/", path);
}

section.components = section
.path
.split('/')
@@ -131,7 +132,7 @@ impl 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>>(
path: P,
config: &Config,


+ 137
- 0
components/library/src/library.rs View File

@@ -9,6 +9,19 @@ use config::Config;
use content::{Page, Section};
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
/// 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
@@ -398,4 +411,128 @@ impl Library {
pub fn contains_page<P: AsRef<Path>>(&self, path: P) -> bool {
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));
}
}

+ 9
- 0
components/site/src/lib.rs View File

@@ -253,6 +253,14 @@ impl Site {
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`
// so we do need to populate it first.
self.populate_taxonomies()?;
@@ -465,6 +473,7 @@ impl Site {
index_path.file_name().unwrap().to_string_lossy().to_string();
if let Some(ref l) = lang {
index_section.file.name = format!("_index.{}", l);
index_section.path = format!("{}/", l);
index_section.permalink = self.config.make_permalink(l);
let filename = format!("_index.{}.md", l);
index_section.file.path = self.content_path.join(&filename);


Loading…
Cancel
Save