mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-10-23 05:38:21 +00:00
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>
332 lines
10 KiB
Python
332 lines
10 KiB
Python
# 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 List, Set
|
|
|
|
from command import Command
|
|
from git_command import git_require
|
|
from git_command import GitCommand
|
|
import platform_utils
|
|
from progress import Progress
|
|
from project import Project
|
|
|
|
|
|
class Gc(Command):
|
|
COMMON = True
|
|
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):
|
|
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",
|
|
)
|
|
p.add_option(
|
|
"--repack",
|
|
default=False,
|
|
action="store_true",
|
|
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
|
|
) -> 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 delete_unused_projects(self, projects: List[Project], opt):
|
|
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 0
|
|
|
|
print("Identified the following projects are no longer used:")
|
|
print("\n".join(to_delete))
|
|
print("")
|
|
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()
|
|
|
|
return 0
|
|
|
|
def _generate_promisor_files(self, pack_dir: str):
|
|
"""Generates promisor files for all pack files in the given directory.
|
|
|
|
Promisor files are empty files with the same name as the corresponding
|
|
pack file but with the ".promisor" extension. They are used by Git.
|
|
"""
|
|
for root, _, files in platform_utils.walk(pack_dir):
|
|
for file in files:
|
|
if not file.endswith(".pack"):
|
|
continue
|
|
with open(os.path.join(root, f"{file[:-4]}promisor"), "w"):
|
|
pass
|
|
|
|
def repack_projects(self, projects: List[Project], opt):
|
|
repack_projects = []
|
|
# Find all projects eligible for repacking:
|
|
# - can't be shared
|
|
# - have a specific fetch filter
|
|
for project in projects:
|
|
if project.config.GetBoolean("extensions.preciousObjects"):
|
|
continue
|
|
if not project.clone_depth:
|
|
continue
|
|
if project.manifest.CloneFilterForDepth != "blob:none":
|
|
continue
|
|
|
|
repack_projects.append(project)
|
|
|
|
if opt.dryrun:
|
|
print(f"Would have repacked {len(repack_projects)} projects.")
|
|
return 0
|
|
|
|
pm = Progress(
|
|
"Repacking (this will take a while)",
|
|
len(repack_projects),
|
|
delay=False,
|
|
quiet=opt.quiet,
|
|
show_elapsed=True,
|
|
elide=True,
|
|
)
|
|
|
|
for project in repack_projects:
|
|
pm.update(msg=f"{project.name}")
|
|
|
|
pack_dir = os.path.join(project.gitdir, "tmp_repo_repack")
|
|
if os.path.isdir(pack_dir):
|
|
platform_utils.rmtree(pack_dir)
|
|
os.mkdir(pack_dir)
|
|
|
|
# Prepare workspace for repacking - remove all unreachable refs and
|
|
# their objects.
|
|
GitCommand(
|
|
project,
|
|
["reflog", "expire", "--expire-unreachable=all"],
|
|
verify_command=True,
|
|
).Wait()
|
|
pm.update(msg=f"{project.name} | gc", inc=0)
|
|
GitCommand(
|
|
project,
|
|
["gc"],
|
|
verify_command=True,
|
|
).Wait()
|
|
|
|
# Get all objects that are reachable from the remote, and pack them.
|
|
pm.update(msg=f"{project.name} | generating list of objects", inc=0)
|
|
remote_objects_cmd = GitCommand(
|
|
project,
|
|
[
|
|
"rev-list",
|
|
"--objects",
|
|
"--missing=allow-promisor",
|
|
f"--remotes={project.remote.name}",
|
|
"--filter=blob:none",
|
|
"--tags",
|
|
],
|
|
capture_stdout=True,
|
|
verify_command=True,
|
|
)
|
|
|
|
# Get all local objects and pack them.
|
|
local_head_objects_cmd = GitCommand(
|
|
project,
|
|
[
|
|
"rev-list",
|
|
"--objects",
|
|
"--missing=allow-promisor",
|
|
"HEAD^{tree}",
|
|
],
|
|
capture_stdout=True,
|
|
verify_command=True,
|
|
)
|
|
local_objects_cmd = GitCommand(
|
|
project,
|
|
[
|
|
"rev-list",
|
|
"--objects",
|
|
"--missing=allow-promisor",
|
|
"--all",
|
|
"--reflog",
|
|
"--indexed-objects",
|
|
"--not",
|
|
f"--remotes={project.remote.name}",
|
|
"--tags",
|
|
],
|
|
capture_stdout=True,
|
|
verify_command=True,
|
|
)
|
|
|
|
remote_objects_cmd.Wait()
|
|
|
|
pm.update(msg=f"{project.name} | remote repack", inc=0)
|
|
GitCommand(
|
|
project,
|
|
["pack-objects", os.path.join(pack_dir, "pack")],
|
|
input=remote_objects_cmd.stdout,
|
|
capture_stderr=True,
|
|
capture_stdout=True,
|
|
verify_command=True,
|
|
).Wait()
|
|
|
|
# create promisor file for each pack file
|
|
self._generate_promisor_files(pack_dir)
|
|
|
|
local_head_objects_cmd.Wait()
|
|
local_objects_cmd.Wait()
|
|
|
|
pm.update(msg=f"{project.name} | local repack", inc=0)
|
|
GitCommand(
|
|
project,
|
|
["pack-objects", os.path.join(pack_dir, "pack")],
|
|
input=local_head_objects_cmd.stdout + local_objects_cmd.stdout,
|
|
capture_stderr=True,
|
|
capture_stdout=True,
|
|
verify_command=True,
|
|
).Wait()
|
|
|
|
# Swap the old pack directory with the new one.
|
|
platform_utils.rename(
|
|
os.path.join(project.objdir, "objects", "pack"),
|
|
os.path.join(project.objdir, "objects", "pack_old"),
|
|
)
|
|
platform_utils.rename(
|
|
pack_dir,
|
|
os.path.join(project.objdir, "objects", "pack"),
|
|
)
|
|
platform_utils.rmtree(
|
|
os.path.join(project.objdir, "objects", "pack_old")
|
|
)
|
|
|
|
pm.end()
|
|
return 0
|
|
|
|
def Execute(self, opt, args):
|
|
projects: List[Project] = self.GetProjects(
|
|
args, all_manifests=not opt.this_manifest_only
|
|
)
|
|
|
|
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:
|
|
projects_to_keep = projects
|
|
|
|
ret = self.delete_unused_projects(projects_to_keep, opt)
|
|
if ret != 0:
|
|
return ret
|
|
|
|
if opt.repack:
|
|
git_require((2, 17, 0), fail=True, msg="--repack")
|
|
ret = self.repack_projects(projects, opt)
|
|
|
|
return ret
|