From b825057512dacb46768328227e450f1956cc1197 Mon Sep 17 00:00:00 2001 From: MeiWagatsuma Date: Tue, 29 Jul 2025 14:14:08 +0900 Subject: [PATCH] project: Use git ls-files for DeleteWorktree file removal Bug: 434825203 Replace filesystem walking approach with git ls-files to only delete tracked files during worktree deletion. This is more efficient and ensures only git-tracked content is removed. Changes: - Use `git ls-files -z` to get tracked files list - Remove tracked files first, then clean up empty directories - Throw DeleteWorktreeError immediately on any removal failure - Remove fallback rename logic for failed deletions Change-Id: I8d56367cd02359f23b0625788c1b9f441349af76 --- project.py | 110 ++++++++++++++++++++++++----------------------------- 1 file changed, 50 insertions(+), 60 deletions(-) diff --git a/project.py b/project.py index 2d802f249..019695ab2 100644 --- a/project.py +++ b/project.py @@ -1907,69 +1907,59 @@ class Project: ) raise DeleteWorktreeError(aggregate_errors=[e]) - # Delete everything under the worktree, except for directories that - # contain another git project. - dirs_to_remove = [] - failed = False - errors = [] - for root, dirs, files in platform_utils.walk(self.worktree): - for f in files: - path = os.path.join(root, f) + # Delete tracked files using git ls-files, then clean up empty dirs + # Use git ls-files to get all tracked files in the worktree + try: + files_output = self.bare_git.ls_files( + "-z", _cwd=self.worktree + ).split("\0") + tracked_files = [ + f for f in files_output if f + ] # Filter out empty strings + + # Remove all tracked files + for rel_path in tracked_files: + abs_path = os.path.join(self.worktree, rel_path) try: - platform_utils.remove(path) + platform_utils.remove(abs_path) except OSError as e: if e.errno != errno.ENOENT: - logger.warning("%s: Failed to remove: %s", path, e) - failed = True - errors.append(e) - dirs[:] = [ - d - for d in dirs - if not os.path.lexists(os.path.join(root, d, ".git")) - ] - dirs_to_remove += [ - os.path.join(root, d) - for d in dirs - if os.path.join(root, d) not in dirs_to_remove - ] - for d in reversed(dirs_to_remove): - if platform_utils.islink(d): - try: - platform_utils.remove(d) - except OSError as e: - if e.errno != errno.ENOENT: - logger.warning("%s: Failed to remove: %s", d, e) - failed = True - errors.append(e) - elif not platform_utils.listdir(d): - try: - platform_utils.rmdir(d) - except OSError as e: - if e.errno != errno.ENOENT: - logger.warning("%s: Failed to remove: %s", d, e) - failed = True - errors.append(e) - if failed: - rename_path = ( - f"{self.worktree}_repo_to_be_deleted_{int(time.time())}" - ) - try: - platform_utils.rename(self.worktree, rename_path) - logger.warning( - "warning: renamed %s to %s. You can delete it, but you " - "might need elevated permissions (e.g. root)", - self.worktree, - rename_path, - ) - # Rename successful! Clear the errors. - errors = [] - except OSError: - logger.error( - "%s: Failed to delete obsolete checkout.\n", - " Remove manually, then run `repo sync -l`.", - self.RelPath(local=False), - ) - raise DeleteWorktreeError(aggregate_errors=errors) + logger.error("%s: Failed to remove: %s", abs_path, e) + raise DeleteWorktreeError(aggregate_errors=[e]) + + # Collect and remove empty directories (excluding those with .git) + dirs_to_remove = set() + for rel_path in tracked_files: + # Add all parent directories of this file + dir_path = os.path.dirname(rel_path) + while dir_path and dir_path != ".": + dirs_to_remove.add(os.path.join(self.worktree, dir_path)) + dir_path = os.path.dirname(dir_path) + + # Remove empty directories in reverse order (deepest first) + for d in sorted(dirs_to_remove, key=len, reverse=True): + # Skip directories that contain another git project + if os.path.lexists(os.path.join(d, ".git")): + continue + + if platform_utils.islink(d): + try: + platform_utils.remove(d) + except OSError as e: + if e.errno != errno.ENOENT: + logger.error("%s: Failed to remove: %s", d, e) + raise DeleteWorktreeError(aggregate_errors=[e]) + elif not platform_utils.listdir(d): + try: + platform_utils.rmdir(d) + except OSError as e: + if e.errno != errno.ENOENT: + logger.error("%s: Failed to remove: %s", d, e) + raise DeleteWorktreeError(aggregate_errors=[e]) + + except GitError as e: + logger.error("Failed to get tracked files with git ls-files: %s", e) + raise DeleteWorktreeError(aggregate_errors=[e]) # Try deleting parent dirs if they are empty. path = self.worktree