2024-12-16 22:30:07 +00:00
|
|
|
# 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
|
2025-01-14 19:16:28 +00:00
|
|
|
from typing import List, Set
|
2024-12-16 22:30:07 +00:00
|
|
|
|
|
|
|
from command import Command
|
2025-01-14 19:20:21 +00:00
|
|
|
from git_command import GitCommand
|
2024-12-16 22:30:07 +00:00
|
|
|
import platform_utils
|
|
|
|
from progress import Progress
|
2025-01-14 19:16:28 +00:00
|
|
|
from project import Project
|
2024-12-16 22:30:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Gc(Command):
|
|
|
|
COMMON = True
|
2025-01-14 19:20:21 +00:00
|
|
|
helpSummary = "Cleaning up internal repo and Git state."
|
2024-12-16 22:30:07 +00:00
|
|
|
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",
|
|
|
|
)
|
2025-01-14 19:20:21 +00:00
|
|
|
p.add_option(
|
|
|
|
"--repack",
|
|
|
|
default=False,
|
|
|
|
action="store_true",
|
|
|
|
help="repack all projects that use partial clone with "
|
|
|
|
"filter=blob:none",
|
|
|
|
)
|
2024-12-16 22:30:07 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2025-01-14 19:16:28 +00:00
|
|
|
def delete_unused_projects(self, projects: List[Project], opt):
|
2024-12-16 22:30:07 +00:00
|
|
|
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.")
|
2025-01-14 19:16:28 +00:00
|
|
|
return 0
|
2024-12-16 22:30:07 +00:00
|
|
|
|
|
|
|
print("Identified the following projects are no longer used:")
|
|
|
|
print("\n".join(to_delete))
|
2025-01-14 19:16:28 +00:00
|
|
|
print("")
|
2024-12-16 22:30:07 +00:00
|
|
|
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()
|
2025-01-14 19:16:28 +00:00
|
|
|
|
|
|
|
return 0
|
|
|
|
|
2025-01-14 19:20:21 +00:00
|
|
|
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",
|
|
|
|
f"--remotes={project.remote.name}",
|
|
|
|
"--filter=blob:none",
|
|
|
|
],
|
|
|
|
capture_stdout=True,
|
|
|
|
verify_command=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
# Get all local objects and pack them.
|
|
|
|
local_head_objects_cmd = GitCommand(
|
|
|
|
project,
|
|
|
|
["rev-list", "--objects", "HEAD^{tree}"],
|
|
|
|
capture_stdout=True,
|
|
|
|
verify_command=True,
|
|
|
|
)
|
|
|
|
local_objects_cmd = GitCommand(
|
|
|
|
project,
|
|
|
|
[
|
|
|
|
"rev-list",
|
|
|
|
"--objects",
|
|
|
|
"--all",
|
|
|
|
"--reflog",
|
|
|
|
"--indexed-objects",
|
|
|
|
"--not",
|
|
|
|
f"--remotes={project.remote.name}",
|
|
|
|
],
|
|
|
|
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
|
|
|
|
|
2025-01-14 19:16:28 +00:00
|
|
|
def Execute(self, opt, args):
|
|
|
|
projects: List[Project] = self.GetProjects(
|
|
|
|
args, all_manifests=not opt.this_manifest_only
|
|
|
|
)
|
|
|
|
|
2025-01-14 19:20:21 +00:00
|
|
|
ret = self.delete_unused_projects(projects, opt)
|
|
|
|
if ret != 0:
|
|
|
|
return ret
|
|
|
|
|
|
|
|
if not opt.repack:
|
|
|
|
return
|
|
|
|
|
|
|
|
return self.repack_projects(projects, opt)
|