16
16
from typing import List , Set
17
17
18
18
from command import Command
19
+ from git_command import GitCommand
19
20
import platform_utils
20
21
from progress import Progress
21
22
from project import Project
22
23
23
24
24
25
class Gc (Command ):
25
26
COMMON = True
26
- helpSummary = "Cleaning up internal repo state."
27
+ helpSummary = "Cleaning up internal repo and Git state."
27
28
helpUsage = """
28
29
%prog
29
30
"""
@@ -44,6 +45,13 @@ def _Options(self, p):
44
45
action = "store_true" ,
45
46
help = "answer yes to all safe prompts" ,
46
47
)
48
+ p .add_option (
49
+ "--repack" ,
50
+ default = False ,
51
+ action = "store_true" ,
52
+ help = "repack all projects that use partial clone with "
53
+ "filter=blob:none" ,
54
+ )
47
55
48
56
def _find_git_to_delete (
49
57
self , to_keep : Set [str ], start_dir : str
@@ -126,9 +134,159 @@ def delete_unused_projects(self, projects: List[Project], opt):
126
134
127
135
return 0
128
136
137
+ def _generate_promisor_files (self , pack_dir : str ):
138
+ """Generates promisor files for all pack files in the given directory.
139
+
140
+ Promisor files are empty files with the same name as the corresponding
141
+ pack file but with the ".promisor" extension. They are used by Git.
142
+ """
143
+ for root , _ , files in platform_utils .walk (pack_dir ):
144
+ for file in files :
145
+ if not file .endswith (".pack" ):
146
+ continue
147
+ with open (os .path .join (root , f"{ file [:- 4 ]} promisor" ), "w" ):
148
+ pass
149
+
150
+ def repack_projects (self , projects : List [Project ], opt ):
151
+ repack_projects = []
152
+ # Find all projects eligible for repacking:
153
+ # - can't be shared
154
+ # - have a specific fetch filter
155
+ for project in projects :
156
+ if project .config .GetBoolean ("extensions.preciousObjects" ):
157
+ continue
158
+ if not project .clone_depth :
159
+ continue
160
+ if project .manifest .CloneFilterForDepth != "blob:none" :
161
+ continue
162
+
163
+ repack_projects .append (project )
164
+
165
+ if opt .dryrun :
166
+ print (f"Would have repacked { len (repack_projects )} projects." )
167
+ return 0
168
+
169
+ pm = Progress (
170
+ "Repacking (this will take a while)" ,
171
+ len (repack_projects ),
172
+ delay = False ,
173
+ quiet = opt .quiet ,
174
+ show_elapsed = True ,
175
+ elide = True ,
176
+ )
177
+
178
+ for project in repack_projects :
179
+ pm .update (msg = f"{ project .name } " )
180
+
181
+ pack_dir = os .path .join (project .gitdir , "tmp_repo_repack" )
182
+ if os .path .isdir (pack_dir ):
183
+ platform_utils .rmtree (pack_dir )
184
+ os .mkdir (pack_dir )
185
+
186
+ # Prepare workspace for repacking - remove all unreachable refs and
187
+ # their objects.
188
+ GitCommand (
189
+ project ,
190
+ ["reflog" , "expire" , "--expire-unreachable=all" ],
191
+ verify_command = True ,
192
+ ).Wait ()
193
+ pm .update (msg = f"{ project .name } | gc" , inc = 0 )
194
+ GitCommand (
195
+ project ,
196
+ ["gc" ],
197
+ verify_command = True ,
198
+ ).Wait ()
199
+
200
+ # Get all objects that are reachable from the remote, and pack them.
201
+ pm .update (msg = f"{ project .name } | generating list of objects" , inc = 0 )
202
+ remote_objects_cmd = GitCommand (
203
+ project ,
204
+ [
205
+ "rev-list" ,
206
+ "--objects" ,
207
+ f"--remotes={ project .remote .name } " ,
208
+ "--filter=blob:none" ,
209
+ ],
210
+ capture_stdout = True ,
211
+ verify_command = True ,
212
+ )
213
+
214
+ # Get all local objects and pack them.
215
+ local_head_objects_cmd = GitCommand (
216
+ project ,
217
+ ["rev-list" , "--objects" , "HEAD^{tree}" ],
218
+ capture_stdout = True ,
219
+ verify_command = True ,
220
+ )
221
+ local_objects_cmd = GitCommand (
222
+ project ,
223
+ [
224
+ "rev-list" ,
225
+ "--objects" ,
226
+ "--all" ,
227
+ "--reflog" ,
228
+ "--indexed-objects" ,
229
+ "--not" ,
230
+ f"--remotes={ project .remote .name } " ,
231
+ ],
232
+ capture_stdout = True ,
233
+ verify_command = True ,
234
+ )
235
+
236
+ remote_objects_cmd .Wait ()
237
+
238
+ pm .update (msg = f"{ project .name } | remote repack" , inc = 0 )
239
+ GitCommand (
240
+ project ,
241
+ ["pack-objects" , os .path .join (pack_dir , "pack" )],
242
+ input = remote_objects_cmd .stdout ,
243
+ capture_stderr = True ,
244
+ capture_stdout = True ,
245
+ verify_command = True ,
246
+ ).Wait ()
247
+
248
+ # create promisor file for each pack file
249
+ self ._generate_promisor_files (pack_dir )
250
+
251
+ local_head_objects_cmd .Wait ()
252
+ local_objects_cmd .Wait ()
253
+
254
+ pm .update (msg = f"{ project .name } | local repack" , inc = 0 )
255
+ GitCommand (
256
+ project ,
257
+ ["pack-objects" , os .path .join (pack_dir , "pack" )],
258
+ input = local_head_objects_cmd .stdout + local_objects_cmd .stdout ,
259
+ capture_stderr = True ,
260
+ capture_stdout = True ,
261
+ verify_command = True ,
262
+ ).Wait ()
263
+
264
+ # Swap the old pack directory with the new one.
265
+ platform_utils .rename (
266
+ os .path .join (project .objdir , "objects" , "pack" ),
267
+ os .path .join (project .objdir , "objects" , "pack_old" ),
268
+ )
269
+ platform_utils .rename (
270
+ pack_dir ,
271
+ os .path .join (project .objdir , "objects" , "pack" ),
272
+ )
273
+ platform_utils .rmtree (
274
+ os .path .join (project .objdir , "objects" , "pack_old" )
275
+ )
276
+
277
+ pm .end ()
278
+ return 0
279
+
129
280
def Execute (self , opt , args ):
130
281
projects : List [Project ] = self .GetProjects (
131
282
args , all_manifests = not opt .this_manifest_only
132
283
)
133
284
134
- return self .delete_unused_projects (projects , opt )
285
+ ret = self .delete_unused_projects (projects , opt )
286
+ if ret != 0 :
287
+ return ret
288
+
289
+ if not opt .repack :
290
+ return
291
+
292
+ return self .repack_projects (projects , opt )
0 commit comments