linkfile: Handle directory-to-symlink transitions safely

When a manifest changes from individual linkfiles inside a directory
(e.g. dest=".llms/rules", dest=".llms/skills") to a single linkfile
for the whole directory (e.g. dest=".llms", src="dot-llms"), two
things need to happen:

1. __linkIt must replace a real directory with a symlink.  Use
   os.rmdir() instead of platform_utils.remove() for real directories.
   rmdir only removes empty directories, so user-created content is
   never deleted.

2. UpdateCopyLinkfileList must handle the cleanup correctly:
   - Use os.rmdir() for directories (safe for non-empty)
   - Remove empty parent directories after cleaning old dests
   - Retry _CopyAndLinkFiles for all projects, since in interleaved
     sync mode _CopyAndLinkFiles runs before cleanup and may have
     failed because the directory was not yet empty

Change-Id: I0437b80beab98bce064cea81c11c47d699be91aa
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/569243
Tested-by: Carlos Fernandez <carlosfsanz@meta.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Carlos Fernandez <carlosfsanz@meta.com>
This commit is contained in:
Carlos Fernandez
2026-05-07 09:00:33 -07:00
committed by gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 67e52a120b
commit 5534f164d6
6 changed files with 320 additions and 9 deletions
+6 -2
View File
@@ -453,9 +453,13 @@ class _LinkFile(NamedTuple):
platform_utils.readlink(absDest) != relSrc
):
try:
# Remove existing file first, since it might be read-only.
# Remove existing path first, since it might be read-only.
if os.path.lexists(absDest):
platform_utils.remove(absDest)
# removedirs handles symlinks, empty dirs, and nested
# trees of stale linkfile dests without deleting user
# content. Falls through to symlink() if the path was
# fully removed, or raises OSError if not.
platform_utils.removedirs(absDest)
else:
dest_dir = os.path.dirname(absDest)
if not platform_utils.isdir(dest_dir):