diff --git a/plugin/src/Reconciler/reify.lua b/plugin/src/Reconciler/reify.lua index de7725ce..e28dd903 100644 --- a/plugin/src/Reconciler/reify.lua +++ b/plugin/src/Reconciler/reify.lua @@ -41,14 +41,41 @@ function reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualIn invariant("Cannot reify an instance not present in virtualInstances\nID: {}", id) end - -- Instance.new can fail if we're passing in something that can't be - -- created, like a service, something enabled with a feature flag, or - -- something that requires higher security than we have. - local createSuccess, instance = pcall(Instance.new, virtualInstance.ClassName) + -- Before creating a new instance, check if the parent already has an + -- untracked child with the same Name and ClassName. This enables "late + -- adoption" of instances that exist in Studio but weren't in the initial + -- Rojo tree (e.g., when using --git-since filtering). Without this, + -- newly acknowledged files would create duplicate instances. + local adoptedExisting = false + local instance = nil - if not createSuccess then - addAllToPatch(unappliedPatch, virtualInstances, id) - return + for _, child in ipairs(parentInstance:GetChildren()) do + local accessSuccess, name, className = pcall(function() + return child.Name, child.ClassName + end) + + if accessSuccess + and name == virtualInstance.Name + and className == virtualInstance.ClassName + and instanceMap.fromInstances[child] == nil + then + instance = child + adoptedExisting = true + break + end + end + + if not adoptedExisting then + -- Instance.new can fail if we're passing in something that can't be + -- created, like a service, something enabled with a feature flag, or + -- something that requires higher security than we have. + local createSuccess + createSuccess, instance = pcall(Instance.new, virtualInstance.ClassName) + + if not createSuccess then + addAllToPatch(unappliedPatch, virtualInstances, id) + return + end end -- TODO: Can this fail? Previous versions of Rojo guarded against this, but @@ -96,7 +123,9 @@ function reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualIn reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, childId, instance) end - instance.Parent = parentInstance + if not adoptedExisting then + instance.Parent = parentInstance + end instanceMap:insert(id, instance) end diff --git a/src/snapshot_middleware/project.rs b/src/snapshot_middleware/project.rs index 3d5b2475..f59a5c90 100644 --- a/src/snapshot_middleware/project.rs +++ b/src/snapshot_middleware/project.rs @@ -83,6 +83,19 @@ pub fn snapshot_project( // file being updated. snapshot.metadata.relevant_paths.push(path.to_path_buf()); + // When git filter is active, also register the project folder as a + // relevant path. This serves as a catch-all so that file changes + // not under any specific $path node can still walk up the directory + // tree and trigger a re-snapshot of the entire project. + if context.has_git_filter() { + if let Some(folder) = path.parent() { + let normalized = vfs + .canonicalize(folder) + .unwrap_or_else(|_| folder.to_path_buf()); + snapshot.metadata.relevant_paths.push(normalized); + } + } + Ok(Some(snapshot)) } None => Ok(None), @@ -137,6 +150,16 @@ pub fn snapshot_project_node( // Take the snapshot's metadata as-is, which will be mutated later // on. metadata = snapshot.metadata; + } else if context.has_git_filter() { + // When the git filter is active and the $path was filtered out + // (no acknowledged files yet), we still need to register the path + // in relevant_paths. This allows the change processor to map file + // changes in this directory back to this project node instance, + // triggering a re-snapshot that will pick up newly modified files. + let normalized = vfs + .canonicalize(full_path.as_ref()) + .unwrap_or_else(|_| full_path.to_path_buf()); + metadata.relevant_paths.push(normalized); } }