gc: add --keep-manifest-projects

By default, `repo gc` treats projects excluded by the configured
manifest groups as unused. This matches the active project set, but it
can remove cached Git data for projects that might be used again after
changing groups.

Add `--keep-manifest-projects` to keep Git data for projects that are
still in the manifests, even if the current groups exclude them or their
checkout is missing. Git data for projects no longer in the manifests
can still be removed.

Change-Id: I63ae18ce1c6acc2463be86f81921476b12fb27d7
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/629741
Reviewed-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Victor Pushkarev <corvinus.v@gmail.com>
Commit-Queue: Victor Pushkarev <corvinus.v@gmail.com>
This commit is contained in:
Victor Pushkarev authored and gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com committed 2026-09-30 01:11:40 -07:00
1 parent cf499e312a
commit 5d4149f32e
3 files changed
+144 -7

No files matched your search

+12 -1
View File
@@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
.TH REPO "1" "June 2026" "repo gc" "Repo Manual"
.TH REPO "1" "September 2026" "repo gc" "Repo Manual"
.SH NAME
repo \- repo gc - manual page for repo gc
.SH SYNOPSIS
@@ -22,6 +22,9 @@ answer yes to all safe prompts
.TP
\fB\-\-repack\fR
repack all projects that use partial clone with filter=blob:none
.TP
\fB\-\-keep\-manifest\-projects\fR
keep Git data for projects still in the manifests
.SS Logging options:
.TP
\fB\-v\fR, \fB\-\-verbose\fR
@@ -44,3 +47,11 @@ only operate on this (sub)manifest
operate on this manifest and its submanifests
.PP
Run `repo help gc` to view the detailed manual.
.SH DETAILS
.PP
The 'repo gc' command removes internal Git data for projects that are no longer
active in the current configuration.
.PP
Projects excluded by the configured manifest groups are inactive by default. Use
\fB\-\-keep\-manifest\-projects\fR to keep Git data for projects that are still in the
manifests.
+25 -6
View File
@@ -28,6 +28,14 @@ class Gc(Command):
helpSummary = "Cleaning up internal repo and Git state."
helpUsage = """
%prog
"""
helpDescription = """
The '%prog' command removes internal Git data for projects that are no
longer active in the current configuration.
Projects excluded by the configured manifest groups are inactive by
default. Use --keep-manifest-projects to keep Git data for projects that
are still in the manifests.
"""
def _Options(self, p):
@@ -53,6 +61,12 @@ class Gc(Command):
help="repack all projects that use partial clone with "
"filter=blob:none",
)
p.add_option(
"--keep-manifest-projects",
default=False,
action="store_true",
help="keep Git data for projects still in the manifests",
)
def _find_git_to_delete(
self, to_keep: Set[str], start_dir: str
@@ -292,16 +306,21 @@ class Gc(Command):
args, all_manifests=not opt.this_manifest_only
)
# If the user specified projects, fetch the global list separately
# to avoid deleting untargeted projects.
if args:
all_projects = self.GetProjects(
if opt.keep_manifest_projects:
projects_to_keep = self.GetProjects(
[],
groups="all",
missing_ok=True,
all_manifests=not opt.this_manifest_only,
)
elif args:
projects_to_keep = self.GetProjects(
[], all_manifests=not opt.this_manifest_only
)
else:
all_projects = projects
projects_to_keep = projects
ret = self.delete_unused_projects(all_projects, opt)
ret = self.delete_unused_projects(projects_to_keep, opt)
if ret != 0:
return ret
+107
View File
@@ -14,9 +14,11 @@
"""Unittests for the subcmds/gc.py module."""
from pathlib import Path
import unittest
from unittest import mock
import manifest_xml
from subcmds import gc
@@ -80,3 +82,108 @@ class GcCommand(unittest.TestCase):
ret = self.cmd.Execute(self.opt, [])
self.assertEqual(ret, 1)
self.mock_repack.assert_not_called()
def test_gc_keep_manifest_projects(tmp_path: Path) -> None:
"""Keep inactive projects that are still in the manifest."""
repodir = tmp_path / ".repo"
manifest_dir = repodir / "manifests"
manifest_file = repodir / manifest_xml.MANIFEST_FILE_NAME
repodir.mkdir()
manifest_dir.mkdir()
manifest_gitdir = repodir / "manifests.git"
manifest_gitdir.mkdir()
(manifest_gitdir / "config").write_text(
"""[remote "origin"]
url = https://localhost:0/manifest
""",
encoding="utf-8",
)
manifest_file.write_text(
"""\
<manifest>
<remote name="origin" fetch="http://localhost" />
<default remote="origin" revision="refs/heads/main" />
<project name="included" groups="active-group" />
<project name="excluded" groups="other-group" />
</manifest>
""",
encoding="utf-8",
)
manifest = manifest_xml.XmlManifest(str(repodir), str(manifest_file))
manifest.manifestProject.config.SetString(
"manifest.groups",
"active-group",
)
projects = {project.name: project for project in manifest.projects}
included = projects["included"]
excluded = projects["excluded"]
Path(included.gitdir).mkdir(parents=True)
Path(included.objdir).mkdir(parents=True)
# Simulate an inactive project with only its object cache left.
Path(excluded.objdir).mkdir(parents=True)
assert not excluded.Exists
orphan_gitdir = repodir / "projects" / "orphan.git"
orphan_objdir = repodir / "project-objects" / "orphan.git"
orphan_gitdir.mkdir()
orphan_objdir.mkdir()
cmd = gc.Gc(repodir=str(repodir), manifest=manifest)
opt, args = cmd.OptionParser.parse_args(
["--yes", "--keep-manifest-projects"]
)
opt.quiet = True
selected_projects = cmd.GetProjects([], all_manifests=True)
assert included in selected_projects
assert excluded not in selected_projects
cmd.Execute(opt, args)
assert included.Exists
assert Path(excluded.objdir).exists()
assert not orphan_gitdir.exists()
assert not orphan_objdir.exists()
def test_gc_keep_manifest_projects_preserves_repack_targets() -> None:
"""Keep manifest projects without expanding repack targets."""
cmd = gc.Gc()
opt, _ = cmd.OptionParser.parse_args(
["--keep-manifest-projects", "--repack"]
)
opt.this_manifest_only = False
with mock.patch.object(
cmd,
"GetProjects",
side_effect=[["projA"], ["manifest_projects"]],
) as get_projects, mock.patch.object(
cmd, "delete_unused_projects", return_value=0
) as delete, mock.patch.object(
cmd, "repack_projects", return_value=0
) as repack:
cmd.Execute(opt, ["projA"])
get_projects.assert_has_calls(
[
mock.call(["projA"], all_manifests=True),
mock.call(
[],
groups="all",
missing_ok=True,
all_manifests=True,
),
]
)
delete.assert_called_once_with(["manifest_projects"], opt)
repack.assert_called_once_with(["projA"], opt)