summaryrefslogtreecommitdiffstats
path: root/subcmds/wipe.py
diff options
context:
space:
mode:
Diffstat (limited to 'subcmds/wipe.py')
-rw-r--r--subcmds/wipe.py184
1 files changed, 184 insertions, 0 deletions
diff --git a/subcmds/wipe.py b/subcmds/wipe.py
new file mode 100644
index 00000000..51938649
--- /dev/null
+++ b/subcmds/wipe.py
@@ -0,0 +1,184 @@
1# Copyright (C) 2025 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import os
16import sys
17from typing import List
18
19from command import Command
20from error import GitError
21from error import RepoExitError
22import platform_utils
23from project import DeleteWorktreeError
24
25
26class Error(RepoExitError):
27 """Exit error when wipe command fails."""
28
29
30class Wipe(Command):
31 """Delete projects from the worktree and .repo"""
32
33 COMMON = True
34 helpSummary = "Wipe projects from the worktree"
35 helpUsage = """
36%prog <project>...
37"""
38 helpDescription = """
39The '%prog' command removes the specified projects from the worktree
40(the checked out source code) and deletes the project's git data from `.repo`.
41
42This is a destructive operation and cannot be undone.
43
44Projects can be specified either by name, or by a relative or absolute path
45to the project's local directory.
46
47Examples:
48
49 # Wipe the project "platform/build" by name:
50 $ repo wipe platform/build
51
52 # Wipe the project at the path "build/make":
53 $ repo wipe build/make
54"""
55
56 def _Options(self, p):
57 # TODO(crbug.com/gerrit/393383056): Add --broken option to scan and
58 # wipe broken projects.
59 p.add_option(
60 "-f",
61 "--force",
62 action="store_true",
63 help="force wipe shared projects and uncommitted changes",
64 )
65 p.add_option(
66 "--force-uncommitted",
67 action="store_true",
68 help="force wipe even if there are uncommitted changes",
69 )
70 p.add_option(
71 "--force-shared",
72 action="store_true",
73 help="force wipe even if the project shares an object directory",
74 )
75
76 def ValidateOptions(self, opt, args: List[str]):
77 if not args:
78 self.Usage()
79
80 def Execute(self, opt, args: List[str]):
81 # Get all projects to handle shared object directories.
82 all_projects = self.GetProjects(None, all_manifests=True, groups="all")
83 projects_to_wipe = self.GetProjects(args, all_manifests=True)
84 relpaths_to_wipe = {p.relpath for p in projects_to_wipe}
85
86 # Build a map from objdir to the relpaths of projects that use it.
87 objdir_map = {}
88 for p in all_projects:
89 objdir_map.setdefault(p.objdir, set()).add(p.relpath)
90
91 uncommitted_projects = []
92 shared_objdirs = {}
93 objdirs_to_delete = set()
94
95 for project in projects_to_wipe:
96 if project == self.manifest.manifestProject:
97 raise Error(
98 f"error: cannot wipe the manifest project: {project.name}"
99 )
100
101 try:
102 if project.HasChanges():
103 uncommitted_projects.append(project.name)
104 except GitError:
105 uncommitted_projects.append(f"{project.name} (corrupted)")
106
107 users = objdir_map.get(project.objdir, {project.relpath})
108 is_shared = not users.issubset(relpaths_to_wipe)
109 if is_shared:
110 shared_objdirs.setdefault(project.objdir, set()).update(users)
111 else:
112 objdirs_to_delete.add(project.objdir)
113
114 block_uncommitted = uncommitted_projects and not (
115 opt.force or opt.force_uncommitted
116 )
117 block_shared = shared_objdirs and not (opt.force or opt.force_shared)
118
119 if block_uncommitted or block_shared:
120 error_messages = []
121 if block_uncommitted:
122 error_messages.append(
123 "The following projects have uncommitted changes or are "
124 "corrupted:\n"
125 + "\n".join(f" - {p}" for p in sorted(uncommitted_projects))
126 )
127 if block_shared:
128 shared_dir_messages = []
129 for objdir, users in sorted(shared_objdirs.items()):
130 other_users = users - relpaths_to_wipe
131 projects_to_wipe_in_dir = users & relpaths_to_wipe
132 message = f"""Object directory {objdir} is shared by:
133 Projects to be wiped: {', '.join(sorted(projects_to_wipe_in_dir))}
134 Projects not to be wiped: {', '.join(sorted(other_users))}"""
135 shared_dir_messages.append(message)
136 error_messages.append(
137 "The following projects have shared object directories:\n"
138 + "\n".join(sorted(shared_dir_messages))
139 )
140
141 if block_uncommitted and block_shared:
142 error_messages.append(
143 "Use --force to wipe anyway, or --force-uncommitted and "
144 "--force-shared to specify."
145 )
146 elif block_uncommitted:
147 error_messages.append("Use --force-uncommitted to wipe anyway.")
148 else:
149 error_messages.append("Use --force-shared to wipe anyway.")
150
151 raise Error("\n\n".join(error_messages))
152
153 # If we are here, either there were no issues, or --force was used.
154 # Proceed with wiping.
155 successful_wipes = set()
156
157 for project in projects_to_wipe:
158 try:
159 # Force the delete here since we've already performed our
160 # own safety checks above.
161 project.DeleteWorktree(force=True, verbose=opt.verbose)
162 successful_wipes.add(project.relpath)
163 except DeleteWorktreeError as e:
164 print(
165 f"error: failed to wipe {project.name}: {e}",
166 file=sys.stderr,
167 )
168
169 # Clean up object directories only if all projects using them were
170 # successfully wiped.
171 for objdir in objdirs_to_delete:
172 users = objdir_map.get(objdir, set())
173 # Check if every project that uses this objdir has been
174 # successfully processed. If a project failed to be wiped, don't
175 # delete the object directory, or we'll corrupt the remaining
176 # project.
177 if users.issubset(successful_wipes):
178 if os.path.exists(objdir):
179 if opt.verbose:
180 print(
181 f"Deleting objects directory: {objdir}",
182 file=sys.stderr,
183 )
184 platform_utils.rmtree(objdir)