From 13d6588bf60f0980ffa3d178441fa707655fee95 Mon Sep 17 00:00:00 2001 From: Josip Sokcevic Date: Mon, 16 Dec 2024 22:30:07 +0000 Subject: gc: Introduce new command to remove old projects When projects are removed from manifest, they are only removed from worktree and not from .repo/projects and .repo/project-objects. Keeping data under .repo can be desired if user expects deleted projects to be restored (e.g. checking out a release branch). Android has ongoing effort to remove many stale projects and this change allows users to easily free-up their disk space. Bug: b/344018971 Bug: 40013312 Change-Id: Id23c7524a88082ee6db908f9fd69dcd5d0c4f681 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/445921 Reviewed-by: Mike Frysinger Commit-Queue: Josip Sokcevic Reviewed-by: Gavin Mak Tested-by: Josip Sokcevic --- subcmds/gc.py | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 subcmds/gc.py (limited to 'subcmds/gc.py') diff --git a/subcmds/gc.py b/subcmds/gc.py new file mode 100644 index 00000000..f12f56f1 --- /dev/null +++ b/subcmds/gc.py @@ -0,0 +1,127 @@ +# Copyright (C) 2024 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import Set + +from command import Command +import platform_utils +from progress import Progress + + +class Gc(Command): + COMMON = True + helpSummary = "Cleaning up internal repo state." + helpUsage = """ +%prog +""" + + def _Options(self, p): + p.add_option( + "-n", + "--dry-run", + dest="dryrun", + default=False, + action="store_true", + help="do everything except actually delete", + ) + p.add_option( + "-y", + "--yes", + default=False, + action="store_true", + help="answer yes to all safe prompts", + ) + + def _find_git_to_delete( + self, to_keep: Set[str], start_dir: str + ) -> Set[str]: + """Searches no longer needed ".git" directories. + + Scans the file system starting from `start_dir` and removes all + directories that end with ".git" that are not in the `to_keep` set. + """ + to_delete = set() + for root, dirs, _ in platform_utils.walk(start_dir): + for directory in dirs: + if not directory.endswith(".git"): + continue + + path = os.path.join(root, directory) + if path not in to_keep: + to_delete.add(path) + + return to_delete + + def Execute(self, opt, args): + projects = self.GetProjects( + args, all_manifests=not opt.this_manifest_only + ) + print(f"Scanning filesystem under {self.repodir}...") + + project_paths = set() + project_object_paths = set() + + for project in projects: + project_paths.add(project.gitdir) + project_object_paths.add(project.objdir) + + to_delete = self._find_git_to_delete( + project_paths, os.path.join(self.repodir, "projects") + ) + + to_delete.update( + self._find_git_to_delete( + project_object_paths, + os.path.join(self.repodir, "project-objects"), + ) + ) + + if not to_delete: + print("Nothing to clean up.") + return + + print("Identified the following projects are no longer used:") + print("\n".join(to_delete)) + print("\n") + if not opt.yes: + print( + "If you proceed, any local commits in those projects will be " + "destroyed!" + ) + ask = input("Proceed? [y/N] ") + if ask.lower() != "y": + return 1 + + pm = Progress( + "Deleting", + len(to_delete), + delay=False, + quiet=opt.quiet, + show_elapsed=True, + elide=True, + ) + + for path in to_delete: + if opt.dryrun: + print(f"\nWould have deleted ${path}") + else: + tmp_path = os.path.join( + os.path.dirname(path), + f"to_be_deleted_{os.path.basename(path)}", + ) + platform_utils.rename(path, tmp_path) + platform_utils.rmtree(tmp_path) + pm.update(msg=path) + pm.end() -- cgit v1.2.3-54-g00ecf