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 <vapier@google.com>
Commit-Queue: Josip Sokcevic <sokcevic@chromium.org>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Josip Sokcevic <sokcevic@chromium.org>
This commit is contained in:
Josip Sokcevic 2024-12-16 22:30:07 +00:00 committed by LUCI
parent 9500aca754
commit 13d6588bf6
4 changed files with 193 additions and 2 deletions

43
man/repo-gc.1 Normal file
View File

@ -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.

View File

@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man. .\" 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 .SH NAME
repo \- repo manifest - manual page for repo manifest repo \- repo manifest - manual page for repo manifest
.SH SYNOPSIS .SH SYNOPSIS
@ -192,11 +192,13 @@ CDATA #IMPLIED>
<!ATTLIST extend\-project remote CDATA #IMPLIED> <!ATTLIST extend\-project remote CDATA #IMPLIED>
<!ATTLIST extend\-project dest\-branch CDATA #IMPLIED> <!ATTLIST extend\-project dest\-branch CDATA #IMPLIED>
<!ATTLIST extend\-project upstream CDATA #IMPLIED> <!ATTLIST extend\-project upstream CDATA #IMPLIED>
<!ATTLIST extend\-project base\-rev CDATA #IMPLIED>
.IP .IP
<!ELEMENT remove\-project EMPTY> <!ELEMENT remove\-project EMPTY>
<!ATTLIST remove\-project name CDATA #IMPLIED> <!ATTLIST remove\-project name CDATA #IMPLIED>
<!ATTLIST remove\-project path CDATA #IMPLIED> <!ATTLIST remove\-project path CDATA #IMPLIED>
<!ATTLIST remove\-project optional CDATA #IMPLIED> <!ATTLIST remove\-project optional CDATA #IMPLIED>
<!ATTLIST remove\-project base\-rev CDATA #IMPLIED>
.IP .IP
<!ELEMENT repo\-hooks EMPTY> <!ELEMENT repo\-hooks EMPTY>
<!ATTLIST repo\-hooks in\-project CDATA #REQUIRED> <!ATTLIST repo\-hooks in\-project CDATA #REQUIRED>
@ -495,6 +497,14 @@ project. Same syntax as the corresponding element of `project`.
Attribute `upstream`: If specified, overrides the upstream of the original Attribute `upstream`: If specified, overrides the upstream of the original
project. Same syntax as the corresponding element of `project`. project. Same syntax as the corresponding element of `project`.
.PP .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 Element annotation
.PP .PP
Zero or more annotation elements may be specified as children of a project or 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 Attribute `optional`: Set to true to ignore remove\-project elements with no
matching `project` element. matching `project` element.
.PP .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 Element repo\-hooks
.PP .PP
NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks. NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks.

View File

@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man. .\" 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 .SH NAME
repo \- repository management tool built on top of git repo \- repository management tool built on top of git
.SH SYNOPSIS .SH SYNOPSIS
@ -79,6 +79,9 @@ Download and checkout a change
forall forall
Run a shell command in each project Run a shell command in each project
.TP .TP
gc
Cleaning up internal repo state.
.TP
grep grep
Print lines matching a pattern Print lines matching a pattern
.TP .TP

127
subcmds/gc.py Normal file
View File

@ -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()