11#!/usr/bin/env python3
22"""
3- Git Repository Cleaner
3+ Git Repository Cleaner and OSS Stack Cleaner
44
5- This script provides various cleanup operations for git repositories and GitHub issues.
5+ This script provides various cleanup operations for git repositories, GitHub issues,
6+ and OSS stack files.
67
78Usage:
89 python cleaner.py tags -n 3 [--dry-run] [--confirm]
910 python cleaner.py issues --max-issues 100 [--dry-run]
11+ python cleaner.py oss-stacks -n 3 [--dry-run]
1012"""
1113
1214import argparse
1315import subprocess
1416import sys
1517import os
1618import requests
19+ import oss2
20+ import re
1721from collections import defaultdict
1822from typing import List , Dict , Tuple
23+ from packaging import version
1924
2025
2126def run_command (command : List [str ], dry_run : bool = False ) -> str :
@@ -286,6 +291,164 @@ def _close_issue(issue_number: int, issue_title: str, headers: Dict[str, str]) -
286291 print (f"Error closing issue #{ issue_number } : { e } " )
287292
288293
294+ def parse_oss_filename (filename : str ) -> Tuple [str , str , str ]:
295+ s0 , s1 = filename .split ("/" )[- 1 ].split ("-linux-" )
296+ def parse_version (full_name ):
297+ parts = full_name .split ('-' )
298+ for i in range (len (parts )):
299+ potential_version = '-' .join (parts [i :])
300+ try :
301+ version .parse (potential_version )
302+ software_name = '-' .join (parts [:i ]) if i > 0 else None
303+ return software_name , potential_version
304+ except version .InvalidVersion :
305+ continue
306+ raise ValueError (f"Could not parse version from { full_name } " )
307+ os_name = s1 .rstrip ('.tar.gz' )
308+ stack_name , stack_version = parse_version (s0 )
309+ return stack_name , stack_version , os_name
310+
311+
312+ def clean_oss_stacks (args ):
313+ """Clean up old OSS stack files, keeping only the latest n versions per stack per OS."""
314+ # Initialize OSS bucket
315+ try :
316+ bucket = oss2 .Bucket (
317+ oss2 .Auth (
318+ os .environ .get ("OSS_ACCESS_KEY_ID" ),
319+ os .environ .get ("OSS_ACCESS_KEY_SECRET" ),
320+ ),
321+ os .environ .get ("OSS_ENDPOINT" , "http://oss-accelerate.aliyuncs.com" ),
322+ 'drycc'
323+ )
324+ except Exception as e :
325+ print (f"Error initializing OSS bucket: { e } " )
326+ sys .exit (1 )
327+
328+ # List all objects in the stacks directory
329+ print ("Listing OSS objects..." )
330+ object_keys = []
331+ try :
332+ for obj in oss2 .ObjectIterator (bucket , prefix = 'stacks/' ):
333+ if obj .key .endswith ('.tar.gz' ):
334+ object_keys .append (obj .key )
335+ except Exception as e :
336+ print (f"Error listing OSS objects: { e } " )
337+ sys .exit (1 )
338+
339+ if not object_keys :
340+ print ("No stack files found in OSS" )
341+ return
342+
343+ print (f"Found { len (object_keys )} stack files" )
344+
345+ # Handle suffix-based deletion
346+ if hasattr (args , 'subfix' ) and args .subfix is not None :
347+ print (f"OSS Stack Cleaner - Deleting files with suffix: { args .subfix } " )
348+
349+ # Find files matching the suffix
350+ files_to_delete = []
351+ for obj_key in object_keys :
352+ # Check if the filename ends with the specified suffix
353+ filename = obj_key .split ('/' )[- 1 ] # Get just the filename part
354+ if filename .endswith (args .subfix ):
355+ files_to_delete .append (obj_key )
356+
357+ if not files_to_delete :
358+ print (f"No files found matching suffix: { args .subfix } " )
359+ return
360+
361+ print (f"\n Found { len (files_to_delete )} files to delete:" )
362+ for obj_key in sorted (files_to_delete ):
363+ print (f" - { obj_key } " )
364+
365+ # Confirm deletion
366+ if not args .dry_run :
367+ response = input (f"\n Delete these { len (files_to_delete )} files? (y/N): " )
368+ if response .lower () != 'y' :
369+ print ("Aborted by user" )
370+ return
371+
372+ # Delete files
373+ success_count = 0
374+ for obj_key in files_to_delete :
375+ try :
376+ if args .dry_run :
377+ print (f"[DRY RUN] Would delete: { obj_key } " )
378+ else :
379+ bucket .delete_object (obj_key )
380+ print (f"Deleted: { obj_key } " )
381+ success_count += 1
382+ except Exception as e :
383+ print (f"Error deleting { obj_key } : { e } " , file = sys .stderr )
384+
385+ print (f"\n Completed: { success_count } /{ len (files_to_delete )} files processed" )
386+ return
387+
388+ # Handle keep-count based deletion (existing logic)
389+ if hasattr (args , 'keep_count' ) and args .keep_count is not None :
390+ if args .keep_count < 1 :
391+ print ("Error: keep-count must be at least 1" , file = sys .stderr )
392+ sys .exit (1 )
393+ print (f"OSS Stack Cleaner - Keeping { args .keep_count } latest versions per stack per OS" )
394+
395+ # Parse and group files by stack and OS
396+ stack_os_files = defaultdict (list )
397+
398+ for obj_key in object_keys :
399+ try :
400+ stack_name , stack_version , os_name = parse_oss_filename (obj_key )
401+ package_version = version .parse (stack_version )
402+ main_version = f"{ package_version .major } .{ package_version .micro } "
403+ stack_os_files [(stack_name , os_name , main_version )].append ((obj_key , stack_version ))
404+ except ValueError as e :
405+ print (f"Warning: Skipping invalid filename { obj_key } : { e } " )
406+ continue
407+
408+ # Sort versions for each stack-OS combination (newest first)
409+ for key in stack_os_files :
410+ stack_os_files [key ].sort (key = lambda x : version .parse (x [1 ]), reverse = True )
411+
412+ # Determine which files to delete
413+ files_to_delete = []
414+
415+ for (stack_name , os_name , _ ), files in stack_os_files .items ():
416+ if len (files ) > args .keep_count :
417+ # Keep the first n files (newest), delete the rest
418+ files_to_delete .extend ([file_info [0 ] for file_info in files [args .keep_count :]])
419+ print (f"Stack '{ stack_name } ' OS '{ os_name } ': keeping { args .keep_count } versions, deleting { len (files ) - args .keep_count } " )
420+
421+ if not files_to_delete :
422+ print ("No files to delete - all stacks have <= {} versions per OS" .format (args .keep_count ))
423+ return
424+
425+ print (f"\n Found { len (files_to_delete )} files to delete:" )
426+ for obj_key in sorted (files_to_delete ):
427+ print (f" - { obj_key } " )
428+
429+ # Confirm deletion
430+ if not args .dry_run :
431+ response = input (f"\n Delete these { len (files_to_delete )} files? (y/N): " )
432+ if response .lower () != 'y' :
433+ print ("Aborted by user" )
434+ return
435+
436+ # Delete files
437+ success_count = 0
438+ for obj_key in files_to_delete :
439+ try :
440+ if args .dry_run :
441+ print (f"[DRY RUN] Would delete: { obj_key } " )
442+ else :
443+ bucket .delete_object (obj_key )
444+ print (f"Deleted: { obj_key } " )
445+ success_count += 1
446+ except Exception as e :
447+ print (f"Error deleting { obj_key } : { e } " , file = sys .stderr )
448+
449+ print (f"\n Completed: { success_count } /{ len (files_to_delete )} files processed" )
450+
451+
289452def main ():
290453 parser = argparse .ArgumentParser (
291454 description = 'Git repository cleaner with various cleanup operations' ,
@@ -297,6 +460,10 @@ def main():
297460 python cleaner.py tags -n 2 --confirm # Skip confirmation prompt
298461 python cleaner.py issues --max-issues 50 # Keep 50 most recent issues
299462 python cleaner.py issues --dry-run # Preview issue cleanup
463+ python cleaner.py oss-stacks -n 3 # Keep 3 latest versions per stack per OS
464+ python cleaner.py oss-stacks -n 2 --dry-run # Preview OSS stack cleanup
465+ python cleaner.py oss-stacks -s linux-arm64-debian-12.tar.gz # Delete files with specific suffix
466+ python cleaner.py oss-stacks -s linux-amd64-debian-12.tar.gz --dry-run # Preview suffix-based deletion
300467
301468Note: Issues cleanup requires admin permissions to delete issues.
302469If deletion fails, issues will be closed instead.
@@ -321,6 +488,18 @@ def main():
321488 issues_parser .add_argument ('--dry-run' , action = 'store_true' ,
322489 help = 'Only print commands without executing them' )
323490
491+ # OSS stacks cleanup parser
492+ oss_parser = subparsers .add_parser ('oss-stacks' , help = 'Clean up old OSS stack files' )
493+ oss_parser .add_argument ('--dry-run' , action = 'store_true' ,
494+ help = 'Only print commands without executing them' )
495+
496+ # Make -n and -s mutually exclusive
497+ oss_group = oss_parser .add_mutually_exclusive_group (required = True )
498+ oss_group .add_argument ('-n' , '--keep-count' , type = int ,
499+ help = 'Number of latest versions to keep for each stack per OS' )
500+ oss_group .add_argument ('-s' , '--subfix' , type = str ,
501+ help = 'Suffix pattern to match files for deletion (e.g., linux-arm64-debian-12.tar.gz)' )
502+
324503 args = parser .parse_args ()
325504
326505 if not args .action :
@@ -331,6 +510,8 @@ def main():
331510 clean_tags (args )
332511 elif args .action == 'issues' :
333512 clean_github_issues (args )
513+ elif args .action == 'oss-stacks' :
514+ clean_oss_stacks (args )
334515 else :
335516 parser .error (f"Unknown action: { args .action } " )
336517
0 commit comments