1616from typing import List , Set
1717
1818from command import Command
19+ from git_command import GitCommand
1920import platform_utils
2021from progress import Progress
2122from project import Project
2223
2324
2425class Gc (Command ):
2526 COMMON = True
26- helpSummary = "Cleaning up internal repo state."
27+ helpSummary = "Cleaning up internal repo and Git state."
2728 helpUsage = """
2829%prog
2930"""
@@ -44,6 +45,13 @@ def _Options(self, p):
4445 action = "store_true" ,
4546 help = "answer yes to all safe prompts" ,
4647 )
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+ )
4755
4856 def _find_git_to_delete (
4957 self , to_keep : Set [str ], start_dir : str
@@ -126,9 +134,159 @@ def delete_unused_projects(self, projects: List[Project], opt):
126134
127135 return 0
128136
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+
129280 def Execute (self , opt , args ):
130281 projects : List [Project ] = self .GetProjects (
131282 args , all_manifests = not opt .this_manifest_only
132283 )
133284
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