Implement fix for stem-level deduplication in file handling

- Introduced a migration plan to address the deduplication issue where bare slugs were compared against full filenames, leading to undetected file-format collisions.
- Updated `name_for_inst` to return a deduplication key alongside the filename and metadata flag, ensuring callers use the bare slug for collision detection.
- Replaced disk-based seeding of `taken_names` with tree-based slugified names to maintain consistency and avoid extension mismatches.
- Refactored relevant modules including `dir.rs`, `project.rs`, and `api.rs` to utilize the new deduplication logic.
- Updated tests to validate the new deduplication behavior and ensure correct handling of file-format instances.
This commit is contained in:
Joe 2026-02-10 03:32:17 -06:00
parent 126c8e66a4
commit 560fce1352
9 changed files with 333 additions and 208 deletions

View file

@ -648,8 +648,8 @@ pub fn syncback_project<'sync>(
// All of the children in this loop are by their nature not in the
// project, so we just need to run syncback on them.
//
// Track taken names per parent directory so siblings under the same
// parent deduplicate against each other (and against existing files).
// Track taken names (bare slugs) per parent directory so siblings
// under the same parent deduplicate against each other.
let mut taken_names_per_dir: std::collections::HashMap<
std::path::PathBuf,
std::collections::HashSet<String>,
@ -690,35 +690,14 @@ pub fn syncback_project<'sync>(
if parent_middleware != Middleware::Project {
let taken_names = taken_names_per_dir
.entry(parent_path.clone())
.or_insert_with(|| {
// Seed with existing files on disk so new children
// don't collide with files already present.
vfs.read_dir(&parent_path)
.ok()
.map(|entries| {
entries
.filter_map(|e| e.ok())
.filter_map(|e| {
e.path()
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.to_lowercase())
})
.collect()
})
.unwrap_or_default()
});
let (child_snap, _needs_meta) = snapshot.with_base_path(
.or_insert_with(std::collections::HashSet::new);
let (child_snap, _needs_meta, dedup_key) = snapshot.with_base_path(
&parent_path,
new_child.referent(),
None,
taken_names,
)?;
if let Some(file_name) =
child_snap.path.file_name().and_then(|n| n.to_str())
{
taken_names.insert(file_name.to_lowercase());
}
taken_names.insert(dedup_key.to_lowercase());
descendant_snapshots.push(child_snap);
}
}