diff --git a/plugin/src/Reconciler/matching.lua b/plugin/src/Reconciler/matching.lua index b66c9c26..8dc0bbd6 100644 --- a/plugin/src/Reconciler/matching.lua +++ b/plugin/src/Reconciler/matching.lua @@ -59,6 +59,7 @@ type VCache = { attrs: { [string]: any }, refs: { [string]: RefIdentity }, childCount: number, + validChildren: { string }, } type SCache = { @@ -120,7 +121,11 @@ end -- Pre-computation helpers (called once per instance per group) -- ================================================================ -local function cacheVirtual(vInst: VirtualInstance, classKeys: ClassComparisonKeys, virtualInstances: VirtualInstances): VCache +local function cacheVirtual( + vInst: VirtualInstance, + classKeys: ClassComparisonKeys, + virtualInstances: VirtualInstances +): VCache local decoded: { [string]: any } = {} local extraProps: { string } = {} local refs: { [string]: RefIdentity } = {} @@ -172,6 +177,15 @@ local function cacheVirtual(vInst: VirtualInstance, classKeys: ClassComparisonKe end end + local validChildren: { string } = {} + if vInst.Children then + for _, childId in ipairs(vInst.Children) do + if virtualInstances[childId] then + table.insert(validChildren, childId) + end + end + end + return { props = decoded, extraProps = extraProps, @@ -179,10 +193,16 @@ local function cacheVirtual(vInst: VirtualInstance, classKeys: ClassComparisonKe attrs = decodedAttrs, refs = refs, childCount = if vInst.Children then #vInst.Children else 0, + validChildren = validChildren, } end -local function cacheStudio(studioInstance: Instance, classKeys: ClassComparisonKeys, extraPropNames: { string }, refPropNames: { string }): SCache +local function cacheStudio( + studioInstance: Instance, + classKeys: ClassComparisonKeys, + extraPropNames: { string }, + refPropNames: { string } +): SCache local props: { [string]: any } = {} for _, propName in ipairs(classKeys.propNames) do @@ -244,13 +264,16 @@ local function countOwnDiffs(vCache: VCache, sCache: SCache, classKeys: ClassCom vVal = defaults[propName] end - if not trueEquals(vVal, sProps[propName]) then + local sVal = sProps[propName] + if vVal ~= sVal and not trueEquals(vVal, sVal) then cost += 1 end end for _, propName in ipairs(vCache.extraProps) do - if not trueEquals(vProps[propName], sProps[propName]) then + local vVal = vProps[propName] + local sVal = sProps[propName] + if vVal ~= sVal and not trueEquals(vVal, sVal) then cost += 1 end end @@ -271,7 +294,8 @@ local function countOwnDiffs(vCache: VCache, sCache: SCache, classKeys: ClassCom local vAttrs = vCache.attrs local sAttrs = sCache.attrs for key, vVal in pairs(vAttrs) do - if not trueEquals(vVal, sAttrs[key]) then + local sVal = sAttrs[key] + if vVal ~= sVal and not trueEquals(vVal, sVal) then cost += 1 end end @@ -306,23 +330,6 @@ local function countOwnDiffs(vCache: VCache, sCache: SCache, classKeys: ClassCom return cost end --- ================================================================ --- Utilities --- ================================================================ - -local function removeMatched(arr: { any }, matchedIndices: { [number]: boolean }): () - local write = 1 - for read = 1, #arr do - if not matchedIndices[read] then - arr[write] = arr[read] - write += 1 - end - end - for i = write, #arr do - arr[i] = nil - end -end - -- ================================================================ -- Core: 2-function mutual recursion -- ================================================================ @@ -362,32 +369,18 @@ local function computePairCost( return cost end - local vChildren = vInst.Children + local validVChildren = vCache.validChildren local studioKids = sCache.children - if (vChildren and #vChildren > 0) or #studioKids > 0 then - if not vChildren or #vChildren == 0 then + if #validVChildren > 0 or #studioKids > 0 then + if #validVChildren == 0 then cost += #studioKids * UNMATCHED_PENALTY elseif #studioKids == 0 then - for _, childId in ipairs(vChildren) do - if virtualInstances[childId] then - cost += UNMATCHED_PENALTY - end - end + cost += #validVChildren * UNMATCHED_PENALTY else - local validVChildren: { string } = {} - for _, childId in ipairs(vChildren) do - if virtualInstances[childId] then - table.insert(validVChildren, childId) - end - end - if #validVChildren > 0 then - local childResult = - matchChildren(session, validVChildren, studioKids, virtualInstances, virtualId, studioInstance) - cost += childResult.totalCost - else - cost += #studioKids * UNMATCHED_PENALTY - end + local childResult = + matchChildren(session, validVChildren, studioKids, virtualInstances, virtualId, studioInstance) + cost += childResult.totalCost end end @@ -409,7 +402,6 @@ matchChildren = function( parentVirtualId: string?, parentStudioInstance: Instance? ): MatchResult - -- Cache lookup if parentVirtualId and parentStudioInstance then local pc = session.matchCache[parentVirtualId] if pc then @@ -421,14 +413,14 @@ matchChildren = function( end local matched: { MatchPair } = {} - local remainingVirtual: { string } = table.clone(virtualChildren) - local remainingStudio: { Instance } = table.clone(studioChildren) + local matchedV: { [number]: boolean } = {} + local matchedS: { [number]: boolean } = {} -- ============================================================ - -- Group by (Name, ClassName) -- direct property access, no pcall + -- Group by (Name, ClassName) once -- ============================================================ local vByKey: { [string]: { number } } = {} - for i, id in ipairs(remainingVirtual) do + for i, id in ipairs(virtualChildren) do local vInst = virtualInstances[id] if vInst then local key = vInst.Name .. "\0" .. vInst.ClassName @@ -442,7 +434,7 @@ matchChildren = function( end local sByKey: { [string]: { number } } = {} - for i, inst in ipairs(remainingStudio) do + for i, inst in ipairs(studioChildren) do local key = inst.Name .. "\0" .. inst.ClassName local group = sByKey[key] if not group then @@ -453,222 +445,160 @@ matchChildren = function( end -- ============================================================ - -- 1:1 instant match + -- 1:1 instant match + ambiguous scoring in single pass -- ============================================================ - local matchedV: { [number]: boolean } = {} - local matchedS: { [number]: boolean } = {} - for key, vIndices in pairs(vByKey) do local sIndices = sByKey[key] - if sIndices and #vIndices == 1 and #sIndices == 1 then + if not sIndices then + continue + end + + if #vIndices == 1 and #sIndices == 1 then local vi, si = vIndices[1], sIndices[1] - if not matchedV[vi] and not matchedS[si] then - table.insert(matched, { - virtualId = remainingVirtual[vi], - studioInstance = remainingStudio[si], - }) - matchedV[vi] = true - matchedS[si] = true - end + table.insert(matched, { + virtualId = virtualChildren[vi], + studioInstance = studioChildren[si], + }) + matchedV[vi] = true + matchedS[si] = true + continue end - end - removeMatched(remainingVirtual, matchedV) - removeMatched(remainingStudio, matchedS) + local firstVInst = virtualInstances[virtualChildren[vIndices[1]]] + if not firstVInst then + continue + end + local classKeys = RbxDom.getClassComparisonKeys(firstVInst.ClassName) - -- ============================================================ - -- Ambiguous groups: change-count scoring + greedy assignment - -- ============================================================ - if #remainingVirtual > 0 and #remainingStudio > 0 then - local vByKey2: { [string]: { number } } = {} - for i, id in ipairs(remainingVirtual) do - local vInst = virtualInstances[id] - if vInst then - local key = vInst.Name .. "\0" .. vInst.ClassName - local group = vByKey2[key] - if not group then - group = {} - vByKey2[key] = group - end - table.insert(group, i) + local vCaches: { [number]: VCache } = {} + local allExtraProps: { [string]: boolean } = {} + local allRefProps: { [string]: boolean } = {} + for _, vi in ipairs(vIndices) do + local vInst = virtualInstances[virtualChildren[vi]] + if not vInst then + continue + end + local vCache = cacheVirtual(vInst, classKeys, virtualInstances) + vCaches[vi] = vCache + for _, propName in ipairs(vCache.extraProps) do + allExtraProps[propName] = true + end + for propName, _ in pairs(vCache.refs) do + allRefProps[propName] = true end end - local sByKey2: { [string]: { number } } = {} - for i, inst in ipairs(remainingStudio) do - local key = inst.Name .. "\0" .. inst.ClassName - local group = sByKey2[key] - if not group then - group = {} - sByKey2[key] = group - end - table.insert(group, i) + local extraPropNamesArray: { string } = {} + for propName, _ in pairs(allExtraProps) do + table.insert(extraPropNamesArray, propName) end - local matchedV2: { [number]: boolean } = {} - local matchedS2: { [number]: boolean } = {} + local refPropNamesArray: { string } = {} + for propName, _ in pairs(allRefProps) do + table.insert(refPropNamesArray, propName) + end - for key, vIndices in pairs(vByKey2) do - local sIndices = sByKey2[key] - if not sIndices then + local sCaches: { [number]: SCache } = {} + for _, si in ipairs(sIndices) do + sCaches[si] = cacheStudio(studioChildren[si], classKeys, extraPropNamesArray, refPropNamesArray) + end + + local scoredPairs: { ScoredPair } = {} + local pairIdx = 0 + local bestSoFar = math.huge + + for _, vi in ipairs(vIndices) do + local vCache = vCaches[vi] + if not vCache then continue end - local firstVInst = virtualInstances[remainingVirtual[vIndices[1]]] - if not firstVInst then - continue - end - local classKeys = RbxDom.getClassComparisonKeys(firstVInst.ClassName) - - local vCaches: { [number]: VCache } = {} - local allExtraProps: { [string]: boolean } = {} - local allRefProps: { [string]: boolean } = {} - for _, vi in ipairs(vIndices) do - if matchedV2[vi] then - continue - end - local vInst = virtualInstances[remainingVirtual[vi]] - if not vInst then - continue - end - local vCache = cacheVirtual(vInst, classKeys, virtualInstances) - vCaches[vi] = vCache - for _, propName in ipairs(vCache.extraProps) do - allExtraProps[propName] = true - end - for propName, _ in pairs(vCache.refs) do - allRefProps[propName] = true - end - end - - local extraPropNamesArray: { string } = {} - for propName, _ in pairs(allExtraProps) do - table.insert(extraPropNamesArray, propName) - end - - local refPropNamesArray: { string } = {} - for propName, _ in pairs(allRefProps) do - table.insert(refPropNamesArray, propName) - end - - local sCaches: { [number]: SCache } = {} for _, si in ipairs(sIndices) do - if not matchedS2[si] then - sCaches[si] = cacheStudio(remainingStudio[si], classKeys, extraPropNamesArray, refPropNamesArray) - end - end - - -- Score all (A, B) pairs - local scoredPairs: { ScoredPair } = {} - local pairIdx = 0 - local bestSoFar = math.huge - - for _, vi in ipairs(vIndices) do - if matchedV2[vi] then - continue - end - local vCache = vCaches[vi] - if not vCache then + local sCache = sCaches[si] + if not sCache then continue end - for _, si in ipairs(sIndices) do - if matchedS2[si] then - continue - end - local sCache = sCaches[si] - if not sCache then - continue - end + pairIdx += 1 + local cost = countOwnDiffs(vCache, sCache, classKeys) - pairIdx += 1 + if cost < bestSoFar then + local validVChildren = vCache.validChildren + local studioKids = sCache.children - local cost = countOwnDiffs(vCache, sCache, classKeys) - - if cost < bestSoFar then - local vInst = virtualInstances[remainingVirtual[vi]] - if vInst then - local vChildren = vInst.Children - local studioKids = sCache.children - - if (vChildren and #vChildren > 0) or #studioKids > 0 then - if not vChildren or #vChildren == 0 then - cost += #studioKids * UNMATCHED_PENALTY - elseif #studioKids == 0 then - for _, childId in ipairs(vChildren) do - if virtualInstances[childId] then - cost += UNMATCHED_PENALTY - end - end - else - local validVChildren: { string } = {} - for _, childId in ipairs(vChildren) do - if virtualInstances[childId] then - table.insert(validVChildren, childId) - end - end - if #validVChildren > 0 then - local childResult = matchChildren( - session, - validVChildren, - studioKids, - virtualInstances, - remainingVirtual[vi], - sCache.instance - ) - cost += childResult.totalCost - else - cost += #studioKids * UNMATCHED_PENALTY - end - end - end + if #validVChildren > 0 or #studioKids > 0 then + if #validVChildren == 0 then + cost += #studioKids * UNMATCHED_PENALTY + elseif #studioKids == 0 then + cost += #validVChildren * UNMATCHED_PENALTY + else + local childResult = matchChildren( + session, + validVChildren, + studioKids, + virtualInstances, + virtualChildren[vi], + sCache.instance + ) + cost += childResult.totalCost end end - - table.insert(scoredPairs, { vi = vi, si = si, cost = cost, idx = pairIdx }) - if cost < bestSoFar then - bestSoFar = cost - end end - end - table.sort(scoredPairs, function(a: ScoredPair, b: ScoredPair): boolean - if a.cost ~= b.cost then - return a.cost < b.cost + table.insert(scoredPairs, { vi = vi, si = si, cost = cost, idx = pairIdx }) + if cost < bestSoFar then + bestSoFar = cost end - return a.idx < b.idx - end) - - for _, pair in ipairs(scoredPairs) do - if matchedV2[pair.vi] or matchedS2[pair.si] then - continue - end - table.insert(matched, { - virtualId = remainingVirtual[pair.vi], - studioInstance = remainingStudio[pair.si], - }) - matchedV2[pair.vi] = true - matchedS2[pair.si] = true end end - removeMatched(remainingVirtual, matchedV2) - removeMatched(remainingStudio, matchedS2) + table.sort(scoredPairs, function(a: ScoredPair, b: ScoredPair): boolean + if a.cost ~= b.cost then + return a.cost < b.cost + end + return a.idx < b.idx + end) + + for _, pair in ipairs(scoredPairs) do + if matchedV[pair.vi] or matchedS[pair.si] then + continue + end + table.insert(matched, { + virtualId = virtualChildren[pair.vi], + studioInstance = studioChildren[pair.si], + }) + matchedV[pair.vi] = true + matchedS[pair.si] = true + end end -- ============================================================ - -- Compute totalCost for ALL matched pairs (session cache helps) + -- Build unmatched lists + compute totalCost -- ============================================================ + local unmatchedVirtual: { string } = {} + for i, id in ipairs(virtualChildren) do + if not matchedV[i] then + table.insert(unmatchedVirtual, id) + end + end + + local unmatchedStudio: { Instance } = {} + for i, inst in ipairs(studioChildren) do + if not matchedS[i] then + table.insert(unmatchedStudio, inst) + end + end + local totalCost = 0 for _, pair in ipairs(matched) do totalCost += computePairCost(session, pair.virtualId, pair.studioInstance, virtualInstances, math.huge) end - totalCost += (#remainingVirtual + #remainingStudio) * UNMATCHED_PENALTY + totalCost += (#unmatchedVirtual + #unmatchedStudio) * UNMATCHED_PENALTY local result = { matched = matched, - unmatchedVirtual = remainingVirtual, - unmatchedStudio = remainingStudio, + unmatchedVirtual = unmatchedVirtual, + unmatchedStudio = unmatchedStudio, totalCost = totalCost, } diff --git a/plugin/src/Reconciler/matching.spec.lua b/plugin/src/Reconciler/matching.spec.lua index 7f323ee0..2e5612e5 100644 --- a/plugin/src/Reconciler/matching.spec.lua +++ b/plugin/src/Reconciler/matching.spec.lua @@ -64,10 +64,20 @@ return function() local parentInst = container local result1 = Matching.matchChildren( - session, { "A" }, { studioChild }, virtualInstances, "PARENT", parentInst + session, + { "A" }, + { studioChild }, + virtualInstances, + "PARENT", + parentInst ) local result2 = Matching.matchChildren( - session, { "A" }, { studioChild }, virtualInstances, "PARENT", parentInst + session, + { "A" }, + { studioChild }, + virtualInstances, + "PARENT", + parentInst ) expect(result1).to.equal(result2) end) @@ -82,10 +92,20 @@ return function() local studioY = makeStudioFolder("Y", container) local result1 = Matching.matchChildren( - session, { "A" }, { studioX }, virtualInstances, "PARENT_1", container + session, + { "A" }, + { studioX }, + virtualInstances, + "PARENT_1", + container ) local result2 = Matching.matchChildren( - session, { "B" }, { studioY }, virtualInstances, "PARENT_2", container + session, + { "B" }, + { studioY }, + virtualInstances, + "PARENT_2", + container ) expect(result1).never.to.equal(result2) expect(result1.matched[1].virtualId).to.equal("A") @@ -100,12 +120,8 @@ return function() } local studioChild = makeStudioFolder("Child", container) - local result1 = Matching.matchChildren( - session1, { "A" }, { studioChild }, virtualInstances, "P", container - ) - local result2 = Matching.matchChildren( - session2, { "A" }, { studioChild }, virtualInstances, "P", container - ) + local result1 = Matching.matchChildren(session1, { "A" }, { studioChild }, virtualInstances, "P", container) + local result2 = Matching.matchChildren(session2, { "A" }, { studioChild }, virtualInstances, "P", container) expect(result1).never.to.equal(result2) expect(result1.totalCost).to.equal(result2.totalCost) end) @@ -120,7 +136,12 @@ return function() local studioAlpha = makeStudioFolder("Alpha", container) local result = Matching.matchChildren( - session, { "A" }, { studioAlpha }, virtualInstances, "ROOT", container + session, + { "A" }, + { studioAlpha }, + virtualInstances, + "ROOT", + container ) expect(#result.matched).to.equal(1) expect(result.totalCost).to.equal(0) @@ -136,7 +157,12 @@ return function() local studioBeta = makeStudioFolder("Beta", container) local result = Matching.matchChildren( - session, { "A", "B" }, { studioAlpha, studioBeta }, virtualInstances, "ROOT", container + session, + { "A", "B" }, + { studioAlpha, studioBeta }, + virtualInstances, + "ROOT", + container ) expect(#result.matched).to.equal(2) expect(result.totalCost).to.equal(0) @@ -149,9 +175,7 @@ return function() } local studioPart = makeStudioPart("P", container, 0) - local result = Matching.matchChildren( - session, { "A" }, { studioPart }, virtualInstances, "ROOT", container - ) + local result = Matching.matchChildren(session, { "A" }, { studioPart }, virtualInstances, "ROOT", container) expect(#result.matched).to.equal(1) expect(result.totalCost > 0).to.equal(true) end) @@ -165,7 +189,12 @@ return function() local studioAlpha = makeStudioFolder("Alpha", container) local result = Matching.matchChildren( - session, { "A", "B" }, { studioAlpha }, virtualInstances, "ROOT", container + session, + { "A", "B" }, + { studioAlpha }, + virtualInstances, + "ROOT", + container ) expect(#result.matched).to.equal(1) expect(#result.unmatchedVirtual).to.equal(1) @@ -181,7 +210,12 @@ return function() local studioBeta = makeStudioFolder("Beta", container) local result = Matching.matchChildren( - session, { "A" }, { studioAlpha, studioBeta }, virtualInstances, "ROOT", container + session, + { "A" }, + { studioAlpha, studioBeta }, + virtualInstances, + "ROOT", + container ) expect(#result.matched).to.equal(1) expect(#result.unmatchedStudio).to.equal(1) @@ -361,7 +395,12 @@ return function() local s2 = makeStudioFolder("Data", container) local result = Matching.matchChildren( - session, { "A", "B" }, { s1, s2 }, virtualInstances, "ROOT", container + session, + { "A", "B" }, + { s1, s2 }, + virtualInstances, + "ROOT", + container ) expect(#result.matched).to.equal(2) expect(result.totalCost).to.equal(0) @@ -378,7 +417,12 @@ return function() local s2 = makeStudioFolder("Data", container) local result = Matching.matchChildren( - session, { "A", "B", "C" }, { s1, s2 }, virtualInstances, "ROOT", container + session, + { "A", "B", "C" }, + { s1, s2 }, + virtualInstances, + "ROOT", + container ) expect(#result.matched).to.equal(2) expect(#result.unmatchedVirtual).to.equal(1) @@ -442,9 +486,7 @@ return function() A = makeVirtualInstance("Alpha", "Folder", {}, {}), B = makeVirtualInstance("Beta", "Folder", {}, {}), } - local result = Matching.matchChildren( - session, { "A", "B" }, {}, virtualInstances, "ROOT", container - ) + local result = Matching.matchChildren(session, { "A", "B" }, {}, virtualInstances, "ROOT", container) expect(#result.matched).to.equal(0) expect(#result.unmatchedVirtual).to.equal(2) expect(result.totalCost).to.equal(20000) @@ -454,9 +496,7 @@ return function() local session = Matching.newSession() local s1 = makeStudioFolder("Alpha", container) local s2 = makeStudioFolder("Beta", container) - local result = Matching.matchChildren( - session, {}, { s1, s2 }, {}, "ROOT", container - ) + local result = Matching.matchChildren(session, {}, { s1, s2 }, {}, "ROOT", container) expect(#result.matched).to.equal(0) expect(#result.unmatchedStudio).to.equal(2) expect(result.totalCost).to.equal(20000) @@ -497,9 +537,7 @@ return function() } local studioOnly = makeStudioFolder("Only", container) - local result = Matching.matchChildren( - session, { "A" }, { studioOnly }, virtualInstances, "ROOT", container - ) + local result = Matching.matchChildren(session, { "A" }, { studioOnly }, virtualInstances, "ROOT", container) expect(#result.matched).to.equal(1) expect(result.matched[1].virtualId).to.equal("A") expect(result.matched[1].studioInstance).to.equal(studioOnly) diff --git a/src/change_processor.rs b/src/change_processor.rs index e0d4f180..efe90f47 100644 --- a/src/change_processor.rs +++ b/src/change_processor.rs @@ -937,14 +937,14 @@ impl JobThreadContext { let start = Instant::now(); let instance_context = InstanceContext::new(); - let snapshot = match snapshot_from_vfs(&instance_context, &self.vfs, &self.project_file_path) - { - Ok(s) => s, - Err(e) => { - log::error!("Tree reconciliation snapshot error: {:?}", e); - return; - } - }; + let snapshot = + match snapshot_from_vfs(&instance_context, &self.vfs, &self.project_file_path) { + Ok(s) => s, + Err(e) => { + log::error!("Tree reconciliation snapshot error: {:?}", e); + return; + } + }; let mut tree = self.tree.lock().unwrap(); let root_id = tree.get_root_id(); diff --git a/src/cli/serve.rs b/src/cli/serve.rs index 2b11375a..01a6d365 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -4,7 +4,6 @@ use std::{ mem::forget, net::{IpAddr, Ipv4Addr, SocketAddr}, path::{Path, PathBuf}, - process::{Command, Stdio}, sync::Arc, }; @@ -131,7 +130,7 @@ fn run_live_syncback(project_path: &Path, payload: SyncbackPayload) -> anyhow::R result.fs_snapshot.removed_paths().len() ); - refresh_git_index(base_path); + crate::git::refresh_git_index(base_path); forget(session_old); @@ -231,30 +230,3 @@ fn fixup_ref_properties(dom: &mut WeakDom, ref_map: &HashMap) { } } } - -fn refresh_git_index(project_dir: &Path) { - let mut check_dir = Some(project_dir); - let mut is_git_repo = false; - while let Some(dir) = check_dir { - if dir.join(".git").exists() { - is_git_repo = true; - break; - } - check_dir = dir.parent(); - } - - if is_git_repo { - log::info!("Refreshing git index..."); - match Command::new("git") - .args(["update-index", "--refresh", "-q"]) - .current_dir(project_dir) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - { - Ok(_) => log::info!("Git index refreshed."), - Err(e) => log::warn!("Failed to run git update-index --refresh: {}", e), - } - } -} diff --git a/src/cli/syncback.rs b/src/cli/syncback.rs index 557e3788..c33c9628 100644 --- a/src/cli/syncback.rs +++ b/src/cli/syncback.rs @@ -2,7 +2,6 @@ use std::{ io::{self, BufReader, Write as _}, mem::forget, path::{Path, PathBuf}, - process::{Command, Stdio}, time::Instant, }; @@ -280,8 +279,7 @@ impl SyncbackCommand { result.fs_snapshot.removed_paths().len() ); - // Refresh git index if in a git repository - refresh_git_index(base_path); + crate::git::refresh_git_index(base_path); // Delete input file if using default Project.rbxl location if let Some(input_path) = &delete_input_after_syncback { @@ -386,42 +384,6 @@ fn download_place(place_id: u64) -> anyhow::Result { Ok(temp_file) } -/// Refreshes the git index if the project is in a git repository. -/// -/// This is useful because syncback may rewrite files with identical content, -/// which can cause git to report them as modified due to timestamp changes. -fn refresh_git_index(project_dir: &Path) { - // Check if .git exists in project dir or any parent - let mut check_dir = Some(project_dir); - let mut is_git_repo = false; - while let Some(dir) = check_dir { - if dir.join(".git").exists() { - is_git_repo = true; - break; - } - check_dir = dir.parent(); - } - - if is_git_repo { - log::info!("Refreshing git index..."); - // Exit code 1 is normal - it just means files were refreshed. - // We only warn if git itself fails to run. - match Command::new("git") - .args(["update-index", "--refresh", "-q"]) - .current_dir(project_dir) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - { - Ok(_) => log::info!("Git index refreshed."), - Err(e) => log::warn!("Failed to run git update-index --refresh: {}", e), - } - } else { - log::debug!("Not a git repository, skipping index refresh."); - } -} - fn read_dom(path: &Path, file_kind: FileKind) -> anyhow::Result { let content = BufReader::new(File::open(path)?); match file_kind { diff --git a/src/git.rs b/src/git.rs index efe4834b..6996144c 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,7 +1,7 @@ use std::{ collections::{HashMap, HashSet}, path::{Path, PathBuf}, - process::Command, + process::{Command, Stdio}, sync::{Arc, Mutex}, }; @@ -244,6 +244,39 @@ pub fn compute_git_metadata(tree_handle: &Arc>, repo_root: &Path } } +/// Refreshes the git index if the project is in a git repository. +/// +/// This is useful because syncback may rewrite files with identical content, +/// which can cause git to report them as modified due to timestamp changes. +pub fn refresh_git_index(project_dir: &Path) { + let mut check_dir = Some(project_dir); + let mut is_git_repo = false; + while let Some(dir) = check_dir { + if dir.join(".git").exists() { + is_git_repo = true; + break; + } + check_dir = dir.parent(); + } + + if is_git_repo { + log::info!("Refreshing git index..."); + match Command::new("git") + .args(["update-index", "--refresh", "-q"]) + .current_dir(project_dir) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + { + Ok(_) => log::info!("Git index refreshed."), + Err(e) => log::warn!("Failed to run git update-index --refresh: {}", e), + } + } else { + log::debug!("Not a git repository, skipping index refresh."); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/snapshot/matching.rs b/src/snapshot/matching.rs index 8f4e1b06..001c4f5b 100644 --- a/src/snapshot/matching.rs +++ b/src/snapshot/matching.rs @@ -1318,7 +1318,10 @@ mod tests { let (tree, children) = make_tree_with_children(&[("Alpha", "Folder"), ("Beta", "Folder")]); let result = match_forward(snaps, &children, &tree, &MatchingSession::new()); assert_eq!(result.matched.len(), 2); - assert_eq!(result.total_cost, 0, "Identical instances should have zero cost"); + assert_eq!( + result.total_cost, 0, + "Identical instances should have zero cost" + ); } #[test] @@ -1390,14 +1393,8 @@ mod tests { #[test] fn session_cache_consistent() { - let snaps1 = vec![ - make_snapshot("X", "Folder"), - make_snapshot("Y", "Folder"), - ]; - let snaps2 = vec![ - make_snapshot("X", "Folder"), - make_snapshot("Y", "Folder"), - ]; + let snaps1 = vec![make_snapshot("X", "Folder"), make_snapshot("Y", "Folder")]; + let snaps2 = vec![make_snapshot("X", "Folder"), make_snapshot("Y", "Folder")]; let (tree, children) = make_tree_with_children(&[("X", "Folder"), ("Y", "Folder")]); let session = MatchingSession::new(); @@ -1406,10 +1403,7 @@ mod tests { assert_eq!(r1.matched.len(), r2.matched.len()); assert_eq!(r1.total_cost, r2.total_cost); - assert_eq!( - r1.unmatched_snapshot.len(), - r2.unmatched_snapshot.len() - ); + assert_eq!(r1.unmatched_snapshot.len(), r2.unmatched_snapshot.len()); } #[test] diff --git a/src/syncback/matching.rs b/src/syncback/matching.rs index 47ba37e4..5841550d 100644 --- a/src/syncback/matching.rs +++ b/src/syncback/matching.rs @@ -829,29 +829,14 @@ mod tests { fn total_cost_includes_unmatched() { let mut new_dom = WeakDom::new(InstanceBuilder::new("DataModel")); let new_root = new_dom.root_ref(); - new_dom.insert( - new_root, - InstanceBuilder::new("Folder").with_name("A"), - ); - new_dom.insert( - new_root, - InstanceBuilder::new("Folder").with_name("B"), - ); - new_dom.insert( - new_root, - InstanceBuilder::new("Folder").with_name("C"), - ); + new_dom.insert(new_root, InstanceBuilder::new("Folder").with_name("A")); + new_dom.insert(new_root, InstanceBuilder::new("Folder").with_name("B")); + new_dom.insert(new_root, InstanceBuilder::new("Folder").with_name("C")); let mut old_dom = WeakDom::new(InstanceBuilder::new("DataModel")); let old_root = old_dom.root_ref(); - old_dom.insert( - old_root, - InstanceBuilder::new("Folder").with_name("A"), - ); - old_dom.insert( - old_root, - InstanceBuilder::new("Folder").with_name("B"), - ); + old_dom.insert(old_root, InstanceBuilder::new("Folder").with_name("A")); + old_dom.insert(old_root, InstanceBuilder::new("Folder").with_name("B")); let new_children: Vec = new_dom.get_by_ref(new_root).unwrap().children().to_vec(); let old_children: Vec = old_dom.get_by_ref(old_root).unwrap().children().to_vec(); diff --git a/src/syncback/property_filter.rs b/src/syncback/property_filter.rs index 0575291d..ddb1fad5 100644 --- a/src/syncback/property_filter.rs +++ b/src/syncback/property_filter.rs @@ -61,12 +61,22 @@ pub fn filter_properties_with_stats<'inst>( return true; } if !sync_unscriptable { - if let Some(data) = class_data { + let mut current = class_data; + loop { + let data = match current { + Some(d) => d, + None => break, + }; if let Some(prop_data) = data.properties.get(prop_name.as_str()) { if matches!(prop_data.scriptability, Scriptability::None) { return true; } + break; } + current = data + .superclass + .as_ref() + .and_then(|s| database.classes.get(&**s)); } } false