mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
Implement fixes and enhancements from audit pass 2
- Introduced a new plan addressing critical bugs, correctness issues, and refactors identified during the audit, including improvements to metadata handling and slugification logic. - Replaced inline removal logic in `handle_api_write` with a new `remove_instance_at_path` method to ensure correct deletion of instances. - Updated pre-seeding logic in `dir.rs` and `project.rs` to derive deduplication keys from actual filesystem paths instead of slugified names. - Enhanced rename handling in `change_processor.rs` to prevent collisions by deduplicating names against existing siblings. - Added tests for various scenarios, including handling of forbidden characters and ensuring correct deduplication behavior. - Refactored child iteration in `dir.rs` and `project.rs` to sort children alphabetically for deterministic deduplication ordering.
This commit is contained in:
parent
baf05dcb3d
commit
2ff5c3936c
30 changed files with 871 additions and 229 deletions
|
|
@ -77,10 +77,16 @@ pub fn snapshot_csv_init(
|
|||
|
||||
let mut init_snapshot = snapshot_csv(context, vfs, init_path, &dir_snapshot.name)?.unwrap();
|
||||
|
||||
// Preserve the init script's instigating_source (the actual file path)
|
||||
// before copying the directory's metadata (which has the folder path)
|
||||
let script_instigating_source = init_snapshot.metadata.instigating_source.take();
|
||||
|
||||
init_snapshot.children = dir_snapshot.children;
|
||||
init_snapshot.metadata = dir_snapshot.metadata;
|
||||
// The directory snapshot middleware includes all possible init paths
|
||||
// so we don't need to add it here.
|
||||
|
||||
// Restore the init script's instigating_source so two-way sync writes
|
||||
// to the actual file (e.g., init.csv) instead of the directory
|
||||
init_snapshot.metadata.instigating_source = script_instigating_source;
|
||||
|
||||
DirectoryMetadata::read_and_apply_all(vfs, folder_path, &mut init_snapshot)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ use memofs::{DirEntry, Vfs};
|
|||
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource},
|
||||
snapshot_middleware::Middleware,
|
||||
syncback::{
|
||||
hash_instance, name_needs_slugify, slugify_name, FsSnapshot, SyncbackReturn,
|
||||
SyncbackSnapshot,
|
||||
hash_instance, name_needs_slugify, slugify_name, strip_middleware_extension, FsSnapshot,
|
||||
SyncbackReturn, SyncbackSnapshot,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -186,25 +187,49 @@ pub fn syncback_dir_no_meta<'sync>(
|
|||
old_child_map.insert(inst.name(), inst);
|
||||
}
|
||||
|
||||
// Pre-seed taken_names from old children's slugified instance names
|
||||
// so that new-only children correctly dedup against existing siblings
|
||||
// (e.g., new "A/B" → slug "A_B" won't collide with existing "A_B").
|
||||
// Pre-seed taken_names from old children's actual filesystem dedup keys
|
||||
// so that new-only children correctly dedup against existing siblings.
|
||||
// We derive keys from relevant_paths (the real filename on disk) rather
|
||||
// than slugifying instance names, because an old instance may already
|
||||
// have a tilde suffix (e.g. A_B~1.luau from prior dedup) that the bare
|
||||
// slug wouldn't capture.
|
||||
// Only in incremental mode: in clean mode, old_ref is forced to None
|
||||
// for all children, so every child is treated as new and pre-seeding
|
||||
// would cause false collisions (spurious ~1 suffixes).
|
||||
if snapshot.data.is_incremental() {
|
||||
for inst in old_child_map.values() {
|
||||
let name = inst.name();
|
||||
let slug = if name_needs_slugify(name) {
|
||||
slugify_name(name).to_lowercase()
|
||||
if let Some(path) = inst.metadata().relevant_paths.first() {
|
||||
if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
|
||||
let middleware = inst.metadata().middleware.unwrap_or(Middleware::Dir);
|
||||
let dedup_key =
|
||||
strip_middleware_extension(filename, middleware).to_lowercase();
|
||||
taken_names.insert(dedup_key);
|
||||
}
|
||||
} else {
|
||||
name.to_lowercase()
|
||||
};
|
||||
taken_names.insert(slug);
|
||||
// Fallback (shouldn't happen for old instances with disk presence)
|
||||
let name = inst.name();
|
||||
let slug = if name_needs_slugify(name) {
|
||||
slugify_name(name).to_lowercase()
|
||||
} else {
|
||||
name.to_lowercase()
|
||||
};
|
||||
taken_names.insert(slug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for new_child_ref in new_inst.children() {
|
||||
// Sort children alphabetically for deterministic dedup ordering.
|
||||
// Without this, DOM iteration order determines which sibling gets
|
||||
// the base slug vs ~1, causing non-deterministic output across
|
||||
// different serializations of the same place.
|
||||
let mut sorted_children: Vec<_> = new_inst
|
||||
.children()
|
||||
.iter()
|
||||
.map(|r| (*r, snapshot.get_new_instance(*r).unwrap().name.clone()))
|
||||
.collect();
|
||||
sorted_children.sort_by(|(_, a), (_, b)| a.cmp(b));
|
||||
|
||||
for (new_child_ref, _) in &sorted_children {
|
||||
let new_child = snapshot.get_new_instance(*new_child_ref).unwrap();
|
||||
|
||||
// Skip children with duplicate names - cannot reliably sync them
|
||||
|
|
@ -282,7 +307,15 @@ pub fn syncback_dir_no_meta<'sync>(
|
|||
}));
|
||||
} else {
|
||||
// There is no old instance. Just add every child.
|
||||
for new_child_ref in new_inst.children() {
|
||||
// Sort alphabetically for deterministic dedup ordering.
|
||||
let mut sorted_children: Vec<_> = new_inst
|
||||
.children()
|
||||
.iter()
|
||||
.map(|r| (*r, snapshot.get_new_instance(*r).unwrap().name.clone()))
|
||||
.collect();
|
||||
sorted_children.sort_by(|(_, a), (_, b)| a.cmp(b));
|
||||
|
||||
for (new_child_ref, _) in &sorted_children {
|
||||
let new_child = snapshot.get_new_instance(*new_child_ref).unwrap();
|
||||
|
||||
// Skip children with duplicate names - cannot reliably sync them
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ use crate::{
|
|||
},
|
||||
snapshot_middleware::Middleware,
|
||||
syncback::{
|
||||
filter_properties, inst_path, name_needs_slugify, slugify_name, FsSnapshot, SyncbackReturn,
|
||||
SyncbackSnapshot,
|
||||
filter_properties, inst_path, name_needs_slugify, slugify_name,
|
||||
strip_middleware_extension, FsSnapshot, SyncbackReturn, SyncbackSnapshot,
|
||||
},
|
||||
variant_eq::variant_eq,
|
||||
RojoRef,
|
||||
|
|
@ -658,9 +658,12 @@ pub fn syncback_project<'sync>(
|
|||
std::collections::HashSet<String>,
|
||||
> = std::collections::HashMap::new();
|
||||
|
||||
// Pre-seed taken_names from old children's slugified instance names
|
||||
// so that new-only children correctly dedup against existing siblings
|
||||
// (e.g., new "utils" won't collide with existing "Utils.luau").
|
||||
// Pre-seed taken_names from old children's actual filesystem dedup keys
|
||||
// so that new-only children correctly dedup against existing siblings.
|
||||
// We derive keys from relevant_paths (the real filename on disk) rather
|
||||
// than slugifying instance names, because an old instance may already
|
||||
// have a tilde suffix (e.g. A_B~1.luau from prior dedup) that the bare
|
||||
// slug wouldn't capture.
|
||||
// Only in incremental mode: in clean mode, old_ref is forced to None
|
||||
// for all children, so every child is treated as new and pre-seeding
|
||||
// would cause false collisions (spurious ~1 suffixes).
|
||||
|
|
@ -668,13 +671,24 @@ pub fn syncback_project<'sync>(
|
|||
if let Some(parent_path) = ref_to_path_map.get(&new_inst.referent()) {
|
||||
let taken = taken_names_per_dir.entry(parent_path.clone()).or_default();
|
||||
for old_child in old_child_map.values() {
|
||||
let name = old_child.name();
|
||||
let slug = if name_needs_slugify(name) {
|
||||
slugify_name(name).to_lowercase()
|
||||
if let Some(path) = old_child.metadata().relevant_paths.first() {
|
||||
if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
|
||||
let middleware =
|
||||
old_child.metadata().middleware.unwrap_or(Middleware::Dir);
|
||||
let dedup_key =
|
||||
strip_middleware_extension(filename, middleware).to_lowercase();
|
||||
taken.insert(dedup_key);
|
||||
}
|
||||
} else {
|
||||
name.to_lowercase()
|
||||
};
|
||||
taken.insert(slug);
|
||||
// Fallback (shouldn't happen for old instances with disk presence)
|
||||
let name = old_child.name();
|
||||
let slug = if name_needs_slugify(name) {
|
||||
slugify_name(name).to_lowercase()
|
||||
} else {
|
||||
name.to_lowercase()
|
||||
};
|
||||
taken.insert(slug);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ snapshot_id: "00000000000000000000000000000000"
|
|||
metadata:
|
||||
ignore_unknown_instances: false
|
||||
instigating_source:
|
||||
Path: /root
|
||||
Path: /root/init.csv
|
||||
relevant_paths:
|
||||
- /root
|
||||
- /root/init.luau
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ snapshot_id: "00000000000000000000000000000000"
|
|||
metadata:
|
||||
ignore_unknown_instances: false
|
||||
instigating_source:
|
||||
Path: /root
|
||||
Path: /root/init.csv
|
||||
relevant_paths:
|
||||
- /root
|
||||
- /root/init.luau
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue