mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
Refactor ServeCommand and enhance placeholder replacement in syncback
- Updated the `ServeCommand` to load the initial project directly, improving clarity and reducing unnecessary session handling. - Introduced a new `replace_placeholders` function in `FsSnapshot` to efficiently replace `__ROJO_REF_<hex>__` placeholders using a HashMap, optimizing the substitution process. - Enhanced the syncback logic to handle both `__ROJO_REF_` and `Rojo_Ref_` markers, ensuring comprehensive path replacements during the syncback process. These changes aim to improve the performance and maintainability of the serve and syncback functionalities.
This commit is contained in:
parent
3ccaaa63db
commit
5fab0d573f
3 changed files with 180 additions and 99 deletions
|
|
@ -12,6 +12,7 @@ use memofs::Vfs;
|
||||||
use rbx_dom_weak::{types::Ref, types::Variant, InstanceBuilder, WeakDom};
|
use rbx_dom_weak::{types::Ref, types::Variant, InstanceBuilder, WeakDom};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
project::Project,
|
||||||
serve_session::ServeSession,
|
serve_session::ServeSession,
|
||||||
syncback::syncback_loop,
|
syncback::syncback_loop,
|
||||||
web::{
|
web::{
|
||||||
|
|
@ -48,16 +49,12 @@ impl ServeCommand {
|
||||||
|
|
||||||
let (ip, port) = {
|
let (ip, port) = {
|
||||||
let vfs = Vfs::new_oneshot();
|
let vfs = Vfs::new_oneshot();
|
||||||
let session = ServeSession::new_oneshot(vfs, project_path.clone())?;
|
let project = Project::load_initial_project(&vfs, &project_path)?;
|
||||||
let ip = self
|
let ip = self
|
||||||
.address
|
.address
|
||||||
.or_else(|| session.serve_address())
|
.or(project.serve_address)
|
||||||
.unwrap_or(DEFAULT_BIND_ADDRESS.into());
|
.unwrap_or(DEFAULT_BIND_ADDRESS.into());
|
||||||
let port = self
|
let port = self.port.or(project.serve_port).unwrap_or(DEFAULT_PORT);
|
||||||
.port
|
|
||||||
.or_else(|| session.project_port())
|
|
||||||
.unwrap_or(DEFAULT_PORT);
|
|
||||||
drop(session);
|
|
||||||
(ip, port)
|
(ip, port)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -447,6 +447,38 @@ impl FsSnapshot {
|
||||||
removed_dirs
|
removed_dirs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace `__ROJO_REF_<hex>__` placeholders in a line using HashMap lookup.
|
||||||
|
/// Scans the line for `__ROJO_REF_` markers and extracts each full
|
||||||
|
/// placeholder key for O(1) lookup, avoiding O(n) iteration over all
|
||||||
|
/// substitutions.
|
||||||
|
fn replace_placeholders(line: &str, sub_map: &std::collections::HashMap<&str, &str>) -> String {
|
||||||
|
const PREFIX: &str = "__ROJO_REF_";
|
||||||
|
const SUFFIX: &str = "__";
|
||||||
|
|
||||||
|
let mut result = String::with_capacity(line.len());
|
||||||
|
let mut remaining = line;
|
||||||
|
|
||||||
|
while let Some(start) = remaining.find(PREFIX) {
|
||||||
|
result.push_str(&remaining[..start]);
|
||||||
|
let after_prefix = &remaining[start + PREFIX.len()..];
|
||||||
|
if let Some(end) = after_prefix.find(SUFFIX) {
|
||||||
|
let placeholder = &remaining[start..start + PREFIX.len() + end + SUFFIX.len()];
|
||||||
|
if let Some(replacement) = sub_map.get(placeholder) {
|
||||||
|
result.push_str(replacement);
|
||||||
|
} else {
|
||||||
|
result.push_str(placeholder);
|
||||||
|
}
|
||||||
|
remaining = &remaining[start + PREFIX.len() + end + SUFFIX.len()..];
|
||||||
|
} else {
|
||||||
|
result.push_str(&remaining[start..]);
|
||||||
|
remaining = "";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push_str(remaining);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
/// Post-process `Rojo_Ref_*` attribute paths in meta/model JSON files.
|
/// Post-process `Rojo_Ref_*` attribute paths in meta/model JSON files.
|
||||||
///
|
///
|
||||||
/// For each entry in `substitutions` (old_path → new_path), finds all
|
/// For each entry in `substitutions` (old_path → new_path), finds all
|
||||||
|
|
@ -460,6 +492,11 @@ impl FsSnapshot {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sub_map: std::collections::HashMap<&str, &str> = substitutions
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
let ref_paths: Vec<PathBuf> = self
|
let ref_paths: Vec<PathBuf> = self
|
||||||
.added_files
|
.added_files
|
||||||
.keys()
|
.keys()
|
||||||
|
|
@ -480,23 +517,25 @@ impl FsSnapshot {
|
||||||
let Ok(text) = std::str::from_utf8(contents) else {
|
let Ok(text) = std::str::from_utf8(contents) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !text.contains("Rojo_Ref_") {
|
if !text.contains("__ROJO_REF_") && !text.contains("Rojo_Ref_") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attribute-scoped replacement: only apply substitutions on
|
|
||||||
// lines that contain a Rojo_Ref_ key. This prevents accidental
|
|
||||||
// mutation of unrelated string values in the same file.
|
|
||||||
let mut modified_lines: Vec<String> = Vec::new();
|
let mut modified_lines: Vec<String> = Vec::new();
|
||||||
let mut any_changed = false;
|
let mut any_changed = false;
|
||||||
for line in text.split('\n') {
|
for line in text.split('\n') {
|
||||||
if line.contains("Rojo_Ref_") {
|
if line.contains("__ROJO_REF_") {
|
||||||
|
let new_line = Self::replace_placeholders(line, &sub_map);
|
||||||
|
if new_line != line {
|
||||||
|
any_changed = true;
|
||||||
|
}
|
||||||
|
modified_lines.push(new_line);
|
||||||
|
} else if line.contains("Rojo_Ref_") {
|
||||||
let mut new_line = line.to_string();
|
let mut new_line = line.to_string();
|
||||||
for (old_path, new_path) in substitutions {
|
for (old_path, new_path) in substitutions {
|
||||||
if old_path == new_path {
|
if old_path != new_path && new_line.contains(old_path.as_str()) {
|
||||||
continue;
|
new_line = new_line.replace(old_path.as_str(), new_path.as_str());
|
||||||
}
|
}
|
||||||
new_line = new_line.replace(old_path.as_str(), new_path.as_str());
|
|
||||||
}
|
}
|
||||||
if new_line != line {
|
if new_line != line {
|
||||||
any_changed = true;
|
any_changed = true;
|
||||||
|
|
|
||||||
|
|
@ -126,10 +126,16 @@ pub fn syncback_loop_with_stats(
|
||||||
.map(|rules| rules.compile_tree_globs())
|
.map(|rules| rules.compile_tree_globs())
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
|
let phase_timer = std::time::Instant::now();
|
||||||
|
|
||||||
// Collect all instance paths BEFORE pruning so we can track external references
|
// Collect all instance paths BEFORE pruning so we can track external references
|
||||||
// (references to instances that will be pruned, like SoundGroups in SoundService).
|
// (references to instances that will be pruned, like SoundGroups in SoundService).
|
||||||
log::debug!("Collecting instance paths before pruning...");
|
log::debug!("Collecting instance paths before pruning...");
|
||||||
let pre_prune_paths = collect_all_paths(&new_tree);
|
let pre_prune_paths = collect_all_paths(&new_tree);
|
||||||
|
log::info!(
|
||||||
|
"syncback: collect_all_paths in {:.1?}",
|
||||||
|
phase_timer.elapsed()
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: Add a better way to tell if the root of a project is a directory
|
// TODO: Add a better way to tell if the root of a project is a directory
|
||||||
let skip_pruning = if let Some(path) = &project.tree.path {
|
let skip_pruning = if let Some(path) = &project.tree.path {
|
||||||
|
|
@ -148,17 +154,11 @@ pub fn syncback_loop_with_stats(
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
let phase_timer = std::time::Instant::now();
|
||||||
if !skip_pruning {
|
if !skip_pruning {
|
||||||
// Strip out any objects from the new tree that aren't in the old tree. This
|
|
||||||
// is necessary so that hashing the roots of each tree won't always result
|
|
||||||
// in different hashes. Shout out to Roblox for serializing a bunch of
|
|
||||||
// Services nobody cares about.
|
|
||||||
log::debug!("Pruning new tree");
|
|
||||||
strip_unknown_root_children(&mut new_tree, old_tree);
|
strip_unknown_root_children(&mut new_tree, old_tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If ignoreHiddenServices is enabled (default: true), filter to only visible services
|
|
||||||
// Check root-level setting first, then fall back to syncbackRules for backward compatibility
|
|
||||||
let ignore_hidden = project
|
let ignore_hidden = project
|
||||||
.ignore_hidden_services
|
.ignore_hidden_services
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
|
|
@ -169,17 +169,19 @@ pub fn syncback_loop_with_stats(
|
||||||
})
|
})
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
if ignore_hidden {
|
if ignore_hidden {
|
||||||
log::debug!("Filtering hidden services");
|
|
||||||
strip_hidden_services(&mut new_tree);
|
strip_hidden_services(&mut new_tree);
|
||||||
}
|
}
|
||||||
|
log::info!("syncback: prune + filter in {:.1?}", phase_timer.elapsed());
|
||||||
|
|
||||||
log::debug!("Collecting referents for new DOM...");
|
let phase_timer = std::time::Instant::now();
|
||||||
let mut deferred_referents = collect_referents(&new_tree, &pre_prune_paths, None);
|
let mut deferred_referents = collect_referents(&new_tree, &pre_prune_paths, None);
|
||||||
let placeholder_map = std::mem::take(&mut deferred_referents.placeholder_to_source_and_target);
|
let placeholder_map = std::mem::take(&mut deferred_referents.placeholder_to_source_and_target);
|
||||||
|
log::info!(
|
||||||
|
"syncback: collect_referents in {:.1?}",
|
||||||
|
phase_timer.elapsed()
|
||||||
|
);
|
||||||
|
|
||||||
// Remove any properties that are manually blocked from syncback via the
|
let phase_timer = std::time::Instant::now();
|
||||||
// project file.
|
|
||||||
log::debug!("Pre-filtering properties on DOMs");
|
|
||||||
for referent in descendants(&new_tree, new_tree.root_ref()) {
|
for referent in descendants(&new_tree, new_tree.root_ref()) {
|
||||||
let new_inst = new_tree.get_by_ref_mut(referent).unwrap();
|
let new_inst = new_tree.get_by_ref_mut(referent).unwrap();
|
||||||
if let Some(filter) = get_property_filter(project, new_inst) {
|
if let Some(filter) = get_property_filter(project, new_inst) {
|
||||||
|
|
@ -231,30 +233,29 @@ pub fn syncback_loop_with_stats(
|
||||||
.and_then(|s| s.ignore_referents)
|
.and_then(|s| s.ignore_referents)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if !ignore_referents {
|
if !ignore_referents {
|
||||||
log::debug!("Linking referents for new DOM");
|
|
||||||
link_referents(deferred_referents, &mut new_tree)?;
|
link_referents(deferred_referents, &mut new_tree)?;
|
||||||
} else {
|
|
||||||
log::debug!("Skipping referent linking as per project syncback rules");
|
|
||||||
}
|
}
|
||||||
|
log::info!(
|
||||||
|
"syncback: filter props + link refs in {:.1?}",
|
||||||
|
phase_timer.elapsed()
|
||||||
|
);
|
||||||
|
|
||||||
// As with pruning the children of the new root, we need to ensure the roots
|
|
||||||
// for both DOMs have the same name otherwise their hashes will always be
|
|
||||||
// different.
|
|
||||||
new_tree.root_mut().name = old_tree.root().name().to_string();
|
new_tree.root_mut().name = old_tree.root().name().to_string();
|
||||||
|
|
||||||
log::debug!("Hashing project DOM");
|
let phase_timer = std::time::Instant::now();
|
||||||
let old_hashes = hash_tree(project, old_tree.inner(), old_tree.get_root_id());
|
let (old_hashes, new_hashes) = rayon::join(
|
||||||
log::debug!("Hashing file DOM");
|
|| hash_tree(project, old_tree.inner(), old_tree.get_root_id()),
|
||||||
let new_hashes = hash_tree(project, &new_tree, new_tree.root_ref());
|
|| hash_tree(project, &new_tree, new_tree.root_ref()),
|
||||||
|
);
|
||||||
|
log::info!(
|
||||||
|
"syncback: hash both trees (parallel) in {:.1?}",
|
||||||
|
phase_timer.elapsed()
|
||||||
|
);
|
||||||
|
|
||||||
let project_path = project.folder_location();
|
let project_path = project.folder_location();
|
||||||
|
|
||||||
// In clean mode, we scan the actual filesystem to find ALL existing files,
|
let phase_timer = std::time::Instant::now();
|
||||||
// not just what Rojo loaded into its tree. This ensures orphaned files
|
|
||||||
// (like duplicates Rojo couldn't load) get cleaned up properly.
|
|
||||||
// Clean mode is essentially: "delete everything and rebuild from Studio"
|
|
||||||
let existing_paths: HashSet<PathBuf> = if !incremental {
|
let existing_paths: HashSet<PathBuf> = if !incremental {
|
||||||
log::debug!("Clean mode: scanning filesystem for existing paths");
|
|
||||||
let mut paths = HashSet::new();
|
let mut paths = HashSet::new();
|
||||||
|
|
||||||
// Get the source directories from the project's tree structure.
|
// Get the source directories from the project's tree structure.
|
||||||
|
|
@ -433,10 +434,38 @@ pub fn syncback_loop_with_stats(
|
||||||
);
|
);
|
||||||
|
|
||||||
for dir in &dirs_to_scan {
|
for dir in &dirs_to_scan {
|
||||||
if dir.is_dir() {
|
if !dir.is_dir() {
|
||||||
scan_directory(dir, &mut paths, &ignore_patterns, project_path);
|
continue;
|
||||||
|
}
|
||||||
|
let canonical_dir = std::fs::canonicalize(dir)
|
||||||
|
.map(|p| {
|
||||||
|
let s = p.to_string_lossy();
|
||||||
|
if s.starts_with(r"\\?\") || s.starts_with("//?/") {
|
||||||
|
PathBuf::from(&s[4..])
|
||||||
|
} else {
|
||||||
|
p
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|_| dir.clone());
|
||||||
|
for entry in walkdir::WalkDir::new(&canonical_dir)
|
||||||
|
.follow_links(true)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
if entry.depth() == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path = entry.path().to_path_buf();
|
||||||
|
if !is_valid_path(&ignore_patterns, project_path, &path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if name.starts_with('.') && name != ".gitkeep" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paths.insert(path);
|
||||||
}
|
}
|
||||||
// Note: We only add directories to dirs_to_scan now, so no else branch needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also add any alternate file representations we found earlier
|
// Also add any alternate file representations we found earlier
|
||||||
|
|
@ -454,7 +483,13 @@ pub fn syncback_loop_with_stats(
|
||||||
} else {
|
} else {
|
||||||
HashSet::new()
|
HashSet::new()
|
||||||
};
|
};
|
||||||
|
log::info!(
|
||||||
|
"syncback: orphan scan in {:.1?} ({} paths)",
|
||||||
|
phase_timer.elapsed(),
|
||||||
|
existing_paths.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let phase_timer = std::time::Instant::now();
|
||||||
let ref_path_map = std::cell::RefCell::new(HashMap::new());
|
let ref_path_map = std::cell::RefCell::new(HashMap::new());
|
||||||
let syncback_data = SyncbackData {
|
let syncback_data = SyncbackData {
|
||||||
vfs,
|
vfs,
|
||||||
|
|
@ -614,12 +649,9 @@ pub fn syncback_loop_with_stats(
|
||||||
|
|
||||||
snapshots.extend(syncback.children);
|
snapshots.extend(syncback.children);
|
||||||
}
|
}
|
||||||
|
log::info!("syncback: main walk loop in {:.1?}", phase_timer.elapsed());
|
||||||
|
|
||||||
// Post-process: substitute Ref placeholders with correct filesystem paths.
|
let phase_timer = std::time::Instant::now();
|
||||||
// During collect_referents (pre-walk), each in-DOM Ref target was assigned
|
|
||||||
// a unique placeholder string keyed by its Ref. During the walk, the
|
|
||||||
// ref_path_map was populated with (Ref → final dedup'd path). Now we
|
|
||||||
// substitute placeholder → final_path in all written meta/model files.
|
|
||||||
{
|
{
|
||||||
use ref_properties::tentative_fs_path_public;
|
use ref_properties::tentative_fs_path_public;
|
||||||
|
|
||||||
|
|
@ -637,17 +669,23 @@ pub fn syncback_loop_with_stats(
|
||||||
let relative = crate::compute_relative_ref_path(&source_abs, &target_abs);
|
let relative = crate::compute_relative_ref_path(&source_abs, &target_abs);
|
||||||
substitutions.push((placeholder.clone(), relative));
|
substitutions.push((placeholder.clone(), relative));
|
||||||
}
|
}
|
||||||
|
log::info!(
|
||||||
|
"syncback: built {} ref substitutions in {:.1?}",
|
||||||
|
substitutions.len(),
|
||||||
|
phase_timer.elapsed()
|
||||||
|
);
|
||||||
|
|
||||||
|
let sub_timer = std::time::Instant::now();
|
||||||
if !substitutions.is_empty() {
|
if !substitutions.is_empty() {
|
||||||
log::debug!(
|
|
||||||
"Fixing {} Rojo_Ref_* placeholder paths",
|
|
||||||
substitutions.len()
|
|
||||||
);
|
|
||||||
fs_snapshot.fix_ref_paths(&substitutions);
|
fs_snapshot.fix_ref_paths(&substitutions);
|
||||||
}
|
}
|
||||||
|
log::info!(
|
||||||
|
"syncback: applied ref substitutions in {:.1?}",
|
||||||
|
sub_timer.elapsed()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// In clean mode, remove any existing paths that weren't overwritten by new structure.
|
let phase_timer = std::time::Instant::now();
|
||||||
// This ensures no orphaned files remain from the old project layout.
|
|
||||||
if !incremental && !existing_paths.is_empty() {
|
if !incremental && !existing_paths.is_empty() {
|
||||||
log::debug!("Clean mode: checking for orphaned files to remove");
|
log::debug!("Clean mode: checking for orphaned files to remove");
|
||||||
|
|
||||||
|
|
@ -671,11 +709,9 @@ pub fn syncback_loop_with_stats(
|
||||||
strip_unc_prefix(canonicalized)
|
strip_unc_prefix(canonicalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Canonicalize the project path base to resolve symlinks consistently
|
let canonical_project_path = strip_unc_prefix(
|
||||||
// This is important on macOS where /var -> /private/var
|
std::fs::canonicalize(project_path).unwrap_or_else(|_| project_path.to_path_buf()),
|
||||||
let canonical_project_path =
|
);
|
||||||
std::fs::canonicalize(project_path).unwrap_or_else(|_| project_path.to_path_buf());
|
|
||||||
let canonical_project_path = strip_unc_prefix(canonical_project_path);
|
|
||||||
|
|
||||||
// Helper to canonicalize a path that may not exist yet.
|
// Helper to canonicalize a path that may not exist yet.
|
||||||
// Tries to canonicalize the full path, then falls back to canonicalizing
|
// Tries to canonicalize the full path, then falls back to canonicalizing
|
||||||
|
|
@ -716,18 +752,26 @@ pub fn syncback_loop_with_stats(
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert added_paths to absolute paths by joining with canonicalized project base path.
|
|
||||||
// fs_snapshot paths are relative to the project folder.
|
|
||||||
// We must also canonicalize the result to resolve any symlinks within the project
|
|
||||||
// directory structure, ensuring consistent comparison with canonicalized existing paths.
|
|
||||||
let added_paths: HashSet<PathBuf> = fs_snapshot
|
let added_paths: HashSet<PathBuf> = fs_snapshot
|
||||||
.added_paths()
|
.added_paths()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| canonicalize_maybe_nonexistent(&canonical_project_path.join(p)))
|
.map(|p| strip_unc_prefix(canonical_project_path.join(p)))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Get the project file path to exclude it from removal
|
let mut added_dir_prefixes: HashSet<PathBuf> = HashSet::new();
|
||||||
let project_file = normalize_existing_path(&project.file_location);
|
for p in &added_paths {
|
||||||
|
let mut ancestor = p.clone();
|
||||||
|
while ancestor.pop() {
|
||||||
|
if !added_dir_prefixes.insert(ancestor.clone()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let project_file = strip_unc_prefix(
|
||||||
|
std::fs::canonicalize(&project.file_location)
|
||||||
|
.unwrap_or_else(|_| project.file_location.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
// Collect ALL paths explicitly referenced via $path in the project.
|
// Collect ALL paths explicitly referenced via $path in the project.
|
||||||
// These paths should NOT be removed during orphan cleanup because they
|
// These paths should NOT be removed during orphan cleanup because they
|
||||||
|
|
@ -749,11 +793,18 @@ pub fn syncback_loop_with_stats(
|
||||||
) {
|
) {
|
||||||
if let Some(ref path_node) = node.path {
|
if let Some(ref path_node) = node.path {
|
||||||
let resolved = base_path.join(path_node.path());
|
let resolved = base_path.join(path_node.path());
|
||||||
let canonical = std::fs::canonicalize(&resolved)
|
let canonical = match std::fs::canonicalize(&resolved) {
|
||||||
.map(strip_unc_prefix)
|
Ok(p) => {
|
||||||
.unwrap_or_else(|_| resolved.clone());
|
let s = p.to_string_lossy();
|
||||||
|
if s.starts_with(r"\\?\") || s.starts_with("//?/") {
|
||||||
|
PathBuf::from(&s[4..])
|
||||||
|
} else {
|
||||||
|
p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => resolved.clone(),
|
||||||
|
};
|
||||||
protected.insert(canonical.clone());
|
protected.insert(canonical.clone());
|
||||||
// Add mapping from filesystem path to instance path
|
|
||||||
if !instance_path.is_empty() {
|
if !instance_path.is_empty() {
|
||||||
mappings.push((canonical, instance_path.to_string()));
|
mappings.push((canonical, instance_path.to_string()));
|
||||||
}
|
}
|
||||||
|
|
@ -802,12 +853,10 @@ pub fn syncback_loop_with_stats(
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// First pass: collect normalized paths to remove (deduplicated)
|
|
||||||
let mut paths_to_remove: HashSet<PathBuf> = HashSet::new();
|
let mut paths_to_remove: HashSet<PathBuf> = HashSet::new();
|
||||||
for old_path in &existing_paths {
|
for old_path in &existing_paths {
|
||||||
let old_path_norm = normalize_existing_path(old_path);
|
let old_path_norm = old_path.clone();
|
||||||
|
|
||||||
// Never remove the ROOT project file itself
|
|
||||||
if old_path_norm == project_file {
|
if old_path_norm == project_file {
|
||||||
log::trace!("Skipping root project file: {}", old_path.display());
|
log::trace!("Skipping root project file: {}", old_path.display());
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -901,40 +950,35 @@ pub fn syncback_loop_with_stats(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if this exact path is being written to
|
|
||||||
if added_paths.contains(&old_path_norm) {
|
if added_paths.contains(&old_path_norm) {
|
||||||
log::trace!(
|
|
||||||
"Skipping {} (exact path in added_paths)",
|
|
||||||
old_path.display()
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Skip if this path is an ancestor of something being written to
|
if old_path_norm.is_dir() && added_dir_prefixes.contains(&old_path_norm) {
|
||||||
// (e.g., a directory that will contain new files)
|
|
||||||
// NOTE: We DO NOT skip files just because their parent directory is in added_paths.
|
|
||||||
// Adding a directory does not mean all orphan files inside should be preserved.
|
|
||||||
if old_path_norm.is_dir()
|
|
||||||
&& added_paths
|
|
||||||
.iter()
|
|
||||||
.any(|added| added.starts_with(&old_path_norm))
|
|
||||||
{
|
|
||||||
log::trace!("Skipping {} (ancestor of added path)", old_path.display());
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Use normalized path to avoid duplicates from UNC vs regular paths
|
|
||||||
log::trace!("Marking for removal: {}", old_path_norm.display());
|
|
||||||
paths_to_remove.insert(old_path_norm);
|
paths_to_remove.insert(old_path_norm);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: only remove top-level orphaned paths.
|
// Second pass: only remove top-level orphaned paths.
|
||||||
// Skip any path that is inside another path we're removing,
|
let mut sorted_removals: Vec<_> = paths_to_remove.iter().cloned().collect();
|
||||||
// since remove_dir_all will handle the contents.
|
sorted_removals.sort();
|
||||||
|
|
||||||
let paths_vec: Vec<_> = paths_to_remove.iter().collect();
|
let paths_vec: Vec<_> = paths_to_remove.iter().collect();
|
||||||
for old_path in &paths_vec {
|
for old_path in &paths_vec {
|
||||||
// Check if this path is inside another path we're removing
|
let is_nested = sorted_removals
|
||||||
let is_nested = paths_vec
|
.binary_search(old_path)
|
||||||
.iter()
|
.ok()
|
||||||
.any(|other| *other != *old_path && old_path.starts_with(*other));
|
.and_then(|idx| {
|
||||||
|
if idx > 0 {
|
||||||
|
Some(
|
||||||
|
old_path.starts_with(&sorted_removals[idx - 1])
|
||||||
|
&& *old_path != &sorted_removals[idx - 1],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
if is_nested {
|
if is_nested {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -955,7 +999,8 @@ pub fn syncback_loop_with_stats(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log stats summary if using internal stats (not provided externally)
|
log::info!("syncback: orphan removal in {:.1?}", phase_timer.elapsed());
|
||||||
|
|
||||||
if external_stats.is_none() {
|
if external_stats.is_none() {
|
||||||
stats.log_summary();
|
stats.log_summary();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue