You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
removeFileArtifact treats dangling symlinks and stat errors as already absent #434
SyncStrategy.removeFileArtifact checks FileManager.fileExists(atPath:) before deleting. When that returns false, the path counts as already absent: the method returns true, and every caller drops it from files / fileHashes. That covers stale reconcile, unconfigurePack, mcs pack remove and excluded components. fileExists also returns false in two cases where something is still on disk:
Dangling symlink.fileExists follows links, so a tracked symlink whose target is gone reads as absent. It is untracked and never unlinked. Nothing is logged, and because the path is no longer tracked, no later sync retries it. mcs can install such a link: ComponentExecutor.copyWithSubstitution falls back to FileManager.copyItem, which preserves symlinks. A pack that ships a relative link (e.g. ../shared/x.md) can end up with one that dangles at its destination.
Stat failure. Any error from stat also reads as absent. For example, EACCES when a parent directory has no search permission. The file is untracked with no message and left on disk.
A related case to decide together with these: a tracked symlink that sits inside the base but points outside it. PathContainment.safePath resolves the link, returns nil, and the escape branch warns "clearing from tracking" and returns true. The link itself is inside the managed directory, so unlinking it would be safe. Today it is left behind (at least with a warning).
The fileExists guard predates the single shared implementation from #433. Both per-scope copies had it.
Proposed fix
Check existence with lstat semantics: FileManager.attributesOfItem(atPath:) in a do/catch. Treat only "no such file" (ENOENT) as absent, and any other error as a failed removal (false, path stays tracked, retried).
Judge containment by where the link sits, not where it points. Resolve the parent directory, then append lastPathComponent. This removes an in-base link to an outside target without following it.
Behavior change: a stat error will keep the path tracked and warn on each sync until it resolves. Today it is dropped silently.
Tests
A tracked dangling symlink is removed from disk and untracked.
A tracked in-base symlink that points outside the base is unlinked, and its target is untouched.
A stat failure (e.g. a parent without search permission) keeps the path tracked and warns once.
Activity
bguidolim commentedon Sep 26, 2026
Note (2026-09-26): #365 (hash check before deletion) targets the same function,
SyncStrategy.removeFileArtifact. Worth fixing both together.