use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::identity::Id;
use super::model::*;
pub(super) fn slash_path(path: &Path) -> String {
path.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/")
}
pub(super) fn path_sort_key(path: &Path) -> String {
slash_path(path)
}
pub(super) fn manifest_of(files: &[FileEntry]) -> BTreeMap<&Path, (&Option<Id>, &str)> {
files
.iter()
.map(|f| (f.path.as_path(), (&f.id, f.hash.as_str())))
.collect()
}
pub(super) fn under(path: &Path, dir: &Path) -> bool {
dir.as_os_str().is_empty() || path == dir || path.starts_with(dir)
}
pub(super) fn case_fold_collision<'a>(
paths: impl Iterator<Item = &'a Path>,
) -> Option<(PathBuf, PathBuf)> {
let mut seen: BTreeMap<String, &Path> = BTreeMap::new();
for path in paths {
let key = path.to_string_lossy().to_ascii_lowercase();
match seen.get(&key) {
Some(&other) if other != path => {
return Some((other.to_path_buf(), path.to_path_buf()));
}
_ => {
seen.insert(key, path);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::super::support::entry;
use super::*;
#[test]
fn manifest_order_is_byte_wise_on_the_joined_string_not_path_component_order() {
let notes_file = Path::new("notes.md");
let notes_dir_file = Path::new("notes/x.md");
assert!(
notes_dir_file < notes_file,
"Path::cmp really does get this backwards"
);
assert!(
path_sort_key(notes_file) < path_sort_key(notes_dir_file),
"the manifest's own key must get it the other way round"
);
let mut paths = [
Path::new("deep/notes/x.md"),
Path::new("deep/notes.md"),
Path::new("index.md"),
Path::new("notes/x.md"),
Path::new("notes.md"),
];
paths.sort_by_key(|p| path_sort_key(p));
assert_eq!(
paths,
[
Path::new("deep/notes.md"),
Path::new("deep/notes/x.md"),
Path::new("index.md"),
Path::new("notes.md"),
Path::new("notes/x.md"),
]
);
}
#[test]
fn manifest_equality_for_the_unchanged_check_ignores_row_order() {
let sorted = vec![entry("notes.md", b"n"), entry("notes/x.md", b"x")];
let mut component_order = sorted.clone();
component_order.reverse();
assert_ne!(
sorted, component_order,
"the derived Vec equality this replaces really is row-order-sensitive"
);
assert_eq!(manifest_of(&sorted), manifest_of(&component_order));
}
#[test]
fn the_capture_set_exclusion_is_by_directory_prefix() {
let store = Path::new("history");
assert!(under(Path::new("history"), store));
assert!(under(Path::new("history/index.md"), store));
assert!(under(Path::new("history/events/2026/07/x.md"), store));
assert!(!under(Path::new("historybook.md"), store));
assert!(!under(Path::new("notes/a.md"), store));
}
}