diff --git a/man/repo-gc.1 b/man/repo-gc.1 new file mode 100644 index 00000000..e465a253 --- /dev/null +++ b/man/repo-gc.1 @@ -0,0 +1,43 @@ +.\" DO NOT MODIFY THIS FILE! It was generated by help2man. +.TH REPO "1" "December 2024" "repo gc" "Repo Manual" +.SH NAME +repo \- repo gc - manual page for repo gc +.SH SYNOPSIS +.B repo +\fI\,gc\/\fR +.SH DESCRIPTION +Summary +.PP +Cleaning up internal repo state. +.SH OPTIONS +.TP +\fB\-h\fR, \fB\-\-help\fR +show this help message and exit +.TP +\fB\-n\fR, \fB\-\-dry\-run\fR +do everything except actually delete +.TP +\fB\-y\fR, \fB\-\-yes\fR +answer yes to all safe prompts +.SS Logging options: +.TP +\fB\-v\fR, \fB\-\-verbose\fR +show all output +.TP +\fB\-q\fR, \fB\-\-quiet\fR +only show errors +.SS Multi\-manifest options: +.TP +\fB\-\-outer\-manifest\fR +operate starting at the outermost manifest +.TP +\fB\-\-no\-outer\-manifest\fR +do not operate on outer manifests +.TP +\fB\-\-this\-manifest\-only\fR +only operate on this (sub)manifest +.TP +\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR +operate on this manifest and its submanifests +.PP +Run `repo help gc` to view the detailed manual. diff --git a/man/repo-manifest.1 b/man/repo-manifest.1 index 10ec2e75..2ee23e64 100644 --- a/man/repo-manifest.1 +++ b/man/repo-manifest.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man. -.TH REPO "1" "April 2024" "repo manifest" "Repo Manual" +.TH REPO "1" "December 2024" "repo manifest" "Repo Manual" .SH NAME repo \- repo manifest - manual page for repo manifest .SH SYNOPSIS @@ -192,11 +192,13 @@ CDATA #IMPLIED> + .IP + .IP @@ -495,6 +497,14 @@ project. Same syntax as the corresponding element of `project`. Attribute `upstream`: If specified, overrides the upstream of the original project. Same syntax as the corresponding element of `project`. .PP +Attribute `base\-rev`: If specified, adds a check against the revision to be +extended. Manifest parse will fail and give a list of mismatch extends if the +revisions being extended have changed since base\-rev was set. Intended for use +with layered manifests using hash revisions to prevent patch branches hiding +newer upstream revisions. Also compares named refs like branches or tags but is +misleading if branches are used as base\-rev. Same syntax as the corresponding +element of `project`. +.PP Element annotation .PP Zero or more annotation elements may be specified as children of a project or @@ -556,6 +566,14 @@ Logic otherwise behaves like both are specified. Attribute `optional`: Set to true to ignore remove\-project elements with no matching `project` element. .PP +Attribute `base\-rev`: If specified, adds a check against the revision to be +removed. Manifest parse will fail and give a list of mismatch removes if the +revisions being removed have changed since base\-rev was set. Intended for use +with layered manifests using hash revisions to prevent patch branches hiding +newer upstream revisions. Also compares named refs like branches or tags but is +misleading if branches are used as base\-rev. Same syntax as the corresponding +element of `project`. +.PP Element repo\-hooks .PP NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks. diff --git a/man/repo.1 b/man/repo.1 index bda68c39..1c05dcfc 100644 --- a/man/repo.1 +++ b/man/repo.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man. -.TH REPO "1" "April 2024" "repo" "Repo Manual" +.TH REPO "1" "December 2024" "repo" "Repo Manual" .SH NAME repo \- repository management tool built on top of git .SH SYNOPSIS @@ -79,6 +79,9 @@ Download and checkout a change forall Run a shell command in each project .TP +gc +Cleaning up internal repo state. +.TP grep Print lines matching a pattern .TP 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()