diff options
| author | Josip Sokcevic <sokcevic@chromium.org> | 2024-12-16 22:30:07 +0000 |
|---|---|---|
| committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2024-12-18 09:23:49 -0800 |
| commit | 13d6588bf60f0980ffa3d178441fa707655fee95 (patch) | |
| tree | 38d1bd9e2ad3988739576d82d4d6a62ffd69364b /subcmds | |
| parent | 9500aca754058bff18ddf35db62852ca4f722c63 (diff) | |
| download | git-repo-13d6588bf60f0980ffa3d178441fa707655fee95.tar.gz | |
gc: Introduce new command to remove old projectsv2.50.1
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 <vapier@google.com>
Commit-Queue: Josip Sokcevic <sokcevic@chromium.org>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Josip Sokcevic <sokcevic@chromium.org>
Diffstat (limited to 'subcmds')
| -rw-r--r-- | subcmds/gc.py | 127 |
1 files changed, 127 insertions, 0 deletions
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 @@ | |||
| 1 | # Copyright (C) 2024 The Android Open Source Project | ||
| 2 | # | ||
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | # you may not use this file except in compliance with the License. | ||
| 5 | # You may obtain a copy of the License at | ||
| 6 | # | ||
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | # | ||
| 9 | # Unless required by applicable law or agreed to in writing, software | ||
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | # See the License for the specific language governing permissions and | ||
| 13 | # limitations under the License. | ||
| 14 | |||
| 15 | import os | ||
| 16 | from typing import Set | ||
| 17 | |||
| 18 | from command import Command | ||
| 19 | import platform_utils | ||
| 20 | from progress import Progress | ||
| 21 | |||
| 22 | |||
| 23 | class Gc(Command): | ||
| 24 | COMMON = True | ||
| 25 | helpSummary = "Cleaning up internal repo state." | ||
| 26 | helpUsage = """ | ||
| 27 | %prog | ||
| 28 | """ | ||
| 29 | |||
| 30 | def _Options(self, p): | ||
| 31 | p.add_option( | ||
| 32 | "-n", | ||
| 33 | "--dry-run", | ||
| 34 | dest="dryrun", | ||
| 35 | default=False, | ||
| 36 | action="store_true", | ||
| 37 | help="do everything except actually delete", | ||
| 38 | ) | ||
| 39 | p.add_option( | ||
| 40 | "-y", | ||
| 41 | "--yes", | ||
| 42 | default=False, | ||
| 43 | action="store_true", | ||
| 44 | help="answer yes to all safe prompts", | ||
| 45 | ) | ||
| 46 | |||
| 47 | def _find_git_to_delete( | ||
| 48 | self, to_keep: Set[str], start_dir: str | ||
| 49 | ) -> Set[str]: | ||
| 50 | """Searches no longer needed ".git" directories. | ||
| 51 | |||
| 52 | Scans the file system starting from `start_dir` and removes all | ||
| 53 | directories that end with ".git" that are not in the `to_keep` set. | ||
| 54 | """ | ||
| 55 | to_delete = set() | ||
| 56 | for root, dirs, _ in platform_utils.walk(start_dir): | ||
| 57 | for directory in dirs: | ||
| 58 | if not directory.endswith(".git"): | ||
| 59 | continue | ||
| 60 | |||
| 61 | path = os.path.join(root, directory) | ||
| 62 | if path not in to_keep: | ||
| 63 | to_delete.add(path) | ||
| 64 | |||
| 65 | return to_delete | ||
| 66 | |||
| 67 | def Execute(self, opt, args): | ||
| 68 | projects = self.GetProjects( | ||
| 69 | args, all_manifests=not opt.this_manifest_only | ||
| 70 | ) | ||
| 71 | print(f"Scanning filesystem under {self.repodir}...") | ||
| 72 | |||
| 73 | project_paths = set() | ||
| 74 | project_object_paths = set() | ||
| 75 | |||
| 76 | for project in projects: | ||
| 77 | project_paths.add(project.gitdir) | ||
| 78 | project_object_paths.add(project.objdir) | ||
| 79 | |||
| 80 | to_delete = self._find_git_to_delete( | ||
| 81 | project_paths, os.path.join(self.repodir, "projects") | ||
| 82 | ) | ||
| 83 | |||
| 84 | to_delete.update( | ||
| 85 | self._find_git_to_delete( | ||
| 86 | project_object_paths, | ||
| 87 | os.path.join(self.repodir, "project-objects"), | ||
| 88 | ) | ||
| 89 | ) | ||
| 90 | |||
| 91 | if not to_delete: | ||
| 92 | print("Nothing to clean up.") | ||
| 93 | return | ||
| 94 | |||
| 95 | print("Identified the following projects are no longer used:") | ||
| 96 | print("\n".join(to_delete)) | ||
| 97 | print("\n") | ||
| 98 | if not opt.yes: | ||
| 99 | print( | ||
| 100 | "If you proceed, any local commits in those projects will be " | ||
| 101 | "destroyed!" | ||
| 102 | ) | ||
| 103 | ask = input("Proceed? [y/N] ") | ||
| 104 | if ask.lower() != "y": | ||
| 105 | return 1 | ||
| 106 | |||
| 107 | pm = Progress( | ||
| 108 | "Deleting", | ||
| 109 | len(to_delete), | ||
| 110 | delay=False, | ||
| 111 | quiet=opt.quiet, | ||
| 112 | show_elapsed=True, | ||
| 113 | elide=True, | ||
| 114 | ) | ||
| 115 | |||
| 116 | for path in to_delete: | ||
| 117 | if opt.dryrun: | ||
| 118 | print(f"\nWould have deleted ${path}") | ||
| 119 | else: | ||
| 120 | tmp_path = os.path.join( | ||
| 121 | os.path.dirname(path), | ||
| 122 | f"to_be_deleted_{os.path.basename(path)}", | ||
| 123 | ) | ||
| 124 | platform_utils.rename(path, tmp_path) | ||
| 125 | platform_utils.rmtree(tmp_path) | ||
| 126 | pm.update(msg=path) | ||
| 127 | pm.end() | ||
