use std::collections::HashMap;
use super::path::path_is_under;
use super::source::Source;
#[derive(Debug, Default)]
pub struct ExcludableDuplicatesResult {
pub to_exclude: Vec<i64>,
pub skipped_no_hash: usize,
pub skipped_in_prefer: usize,
pub skipped_not_covered: usize,
pub skipped_multiple: usize,
}
pub fn find_excludable_duplicates(
scope_sources: &[Source],
all_sources_by_object: &HashMap<i64, Vec<Source>>,
prefer_prefix: &str,
) -> ExcludableDuplicatesResult {
let mut result = ExcludableDuplicatesResult::default();
for source in scope_sources {
let Some(object_id) = source.object_id else {
result.skipped_no_hash += 1;
continue;
};
let source_path = source.path();
if path_is_under(&source_path, prefer_prefix) {
result.skipped_in_prefer += 1;
continue;
}
let prefer_copies = all_sources_by_object
.get(&object_id)
.map(|sources| {
sources
.iter()
.filter(|s| s.id != source.id) .filter(|s| !s.is_excluded()) .filter(|s| path_is_under(&s.path(), prefer_prefix)) .count()
})
.unwrap_or(0);
match prefer_copies {
0 => {
result.skipped_not_covered += 1;
}
1 => {
result.to_exclude.push(source.id);
}
_ => {
result.skipped_multiple += 1;
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
fn make_source(
id: i64,
root_path: &str,
rel_path: &str,
object_id: Option<i64>,
excluded: bool,
object_excluded: bool,
) -> Source {
Source {
id,
root_id: 1,
root_path: root_path.to_string(),
rel_path: rel_path.to_string(),
object_id,
size: 1000,
mtime: 1704067200,
excluded,
object_excluded: Some(object_excluded),
device: 0,
inode: 0,
partial_hash: "hash".to_string(),
basis_rev: 1,
root_role: "source".to_string(),
root_suspended: false,
}
}
#[test]
fn find_excludable_empty_scope() {
let result = find_excludable_duplicates(&[], &HashMap::new(), "/archive");
assert!(result.to_exclude.is_empty());
assert_eq!(result.skipped_no_hash, 0);
assert_eq!(result.skipped_in_prefer, 0);
assert_eq!(result.skipped_not_covered, 0);
assert_eq!(result.skipped_multiple, 0);
}
#[test]
fn find_excludable_skips_unhashed() {
let scope = vec![make_source(1, "/source", "file.txt", None, false, false)];
let result = find_excludable_duplicates(&scope, &HashMap::new(), "/archive");
assert!(result.to_exclude.is_empty());
assert_eq!(result.skipped_no_hash, 1);
}
#[test]
fn find_excludable_skips_already_in_prefer() {
let scope = vec![make_source(
1,
"/archive",
"file.txt",
Some(100),
false,
false,
)];
let result = find_excludable_duplicates(&scope, &HashMap::new(), "/archive");
assert!(result.to_exclude.is_empty());
assert_eq!(result.skipped_in_prefer, 1);
}
#[test]
fn find_excludable_one_copy_excludes() {
let source = make_source(1, "/source", "photo.jpg", Some(100), false, false);
let archive_copy = make_source(2, "/archive", "photo.jpg", Some(100), false, false);
let scope = vec![source];
let mut by_object = HashMap::new();
by_object.insert(
100,
vec![
make_source(1, "/source", "photo.jpg", Some(100), false, false),
archive_copy,
],
);
let result = find_excludable_duplicates(&scope, &by_object, "/archive");
assert_eq!(result.to_exclude, vec![1]);
assert_eq!(result.skipped_not_covered, 0);
assert_eq!(result.skipped_multiple, 0);
}
#[test]
fn find_excludable_no_copy_skips() {
let source = make_source(1, "/source", "unique.jpg", Some(100), false, false);
let scope = vec![source.clone()];
let mut by_object = HashMap::new();
by_object.insert(100, vec![source]);
let result = find_excludable_duplicates(&scope, &by_object, "/archive");
assert!(result.to_exclude.is_empty());
assert_eq!(result.skipped_not_covered, 1);
}
#[test]
fn find_excludable_multiple_copies_skips() {
let source = make_source(1, "/source", "photo.jpg", Some(100), false, false);
let archive_copy1 = make_source(2, "/archive", "copy1.jpg", Some(100), false, false);
let archive_copy2 = make_source(3, "/archive", "copy2.jpg", Some(100), false, false);
let scope = vec![source];
let mut by_object = HashMap::new();
by_object.insert(
100,
vec![
make_source(1, "/source", "photo.jpg", Some(100), false, false),
archive_copy1,
archive_copy2,
],
);
let result = find_excludable_duplicates(&scope, &by_object, "/archive");
assert!(result.to_exclude.is_empty());
assert_eq!(result.skipped_multiple, 1);
}
#[test]
fn find_excludable_ignores_excluded_copies() {
let source = make_source(1, "/source", "photo.jpg", Some(100), false, false);
let archive_copy = make_source(2, "/archive", "photo.jpg", Some(100), false, false);
let excluded_copy = make_source(3, "/archive", "excluded.jpg", Some(100), true, false);
let scope = vec![source];
let mut by_object = HashMap::new();
by_object.insert(
100,
vec![
make_source(1, "/source", "photo.jpg", Some(100), false, false),
archive_copy,
excluded_copy,
],
);
let result = find_excludable_duplicates(&scope, &by_object, "/archive");
assert_eq!(result.to_exclude, vec![1]);
}
#[test]
fn find_excludable_ignores_object_excluded_copies() {
let source = make_source(1, "/source", "photo.jpg", Some(100), false, false);
let object_excluded_copy = make_source(2, "/archive", "photo.jpg", Some(100), false, true);
let scope = vec![source];
let mut by_object = HashMap::new();
by_object.insert(
100,
vec![
make_source(1, "/source", "photo.jpg", Some(100), false, false),
object_excluded_copy,
],
);
let result = find_excludable_duplicates(&scope, &by_object, "/archive");
assert!(result.to_exclude.is_empty());
assert_eq!(result.skipped_not_covered, 1);
}
#[test]
fn find_excludable_uses_source_path() {
let source = make_source(1, "/source", "photo.jpg", Some(100), false, false);
let archive_file = make_source(2, "/archive/photo.jpg", "", Some(100), false, false);
let scope = vec![source];
let mut by_object = HashMap::new();
by_object.insert(
100,
vec![
make_source(1, "/source", "photo.jpg", Some(100), false, false),
archive_file,
],
);
let result = find_excludable_duplicates(&scope, &by_object, "/archive/photo.jpg");
assert_eq!(result.to_exclude, vec![1]);
}
#[test]
fn find_excludable_empty_rel_path_in_scope() {
let source = make_source(1, "/archive/photo.jpg", "", Some(100), false, false);
let scope = vec![source];
let result = find_excludable_duplicates(&scope, &HashMap::new(), "/archive/photo.jpg");
assert!(result.to_exclude.is_empty());
assert_eq!(result.skipped_in_prefer, 1);
}
#[test]
fn find_excludable_mixed_scenarios() {
let src_unhashed = make_source(1, "/source", "unhashed.txt", None, false, false);
let src_in_prefer = make_source(2, "/archive", "already.txt", Some(200), false, false);
let src_no_backup = make_source(3, "/source", "no_backup.txt", Some(300), false, false);
let src_to_exclude = make_source(4, "/source", "has_backup.txt", Some(400), false, false);
let src_ambiguous = make_source(5, "/source", "ambiguous.txt", Some(500), false, false);
let scope = vec![
src_unhashed,
src_in_prefer,
src_no_backup.clone(),
src_to_exclude.clone(),
src_ambiguous.clone(),
];
let mut by_object = HashMap::new();
by_object.insert(300, vec![src_no_backup]);
by_object.insert(
400,
vec![
src_to_exclude,
make_source(10, "/archive", "backup.txt", Some(400), false, false),
],
);
by_object.insert(
500,
vec![
src_ambiguous,
make_source(11, "/archive", "backup1.txt", Some(500), false, false),
make_source(12, "/archive", "backup2.txt", Some(500), false, false),
],
);
let result = find_excludable_duplicates(&scope, &by_object, "/archive");
assert_eq!(result.to_exclude, vec![4]); assert_eq!(result.skipped_no_hash, 1); assert_eq!(result.skipped_in_prefer, 1); assert_eq!(result.skipped_not_covered, 1); assert_eq!(result.skipped_multiple, 1); }
}