1+ #!/usr/bin/env python3
2+ # -*- coding: utf-8 -*-
3+
4+ import argparse
5+ import os
6+ import sys
7+ from typing import List
8+
9+ from splitpatch .patch import Patch
10+ from splitpatch .tree import DirNode
11+ from splitpatch .merge import Merge
12+ from splitpatch import __version__ , logger , setup_logging
13+
14+
15+ def setup_args () -> argparse .Namespace :
16+ """Set up and validate command line arguments
17+
18+ Includes:
19+ 1. Parse command line arguments
20+ 2. Validate argument validity
21+ 3. Configure logging level
22+ 4. Print argument information
23+
24+ Returns:
25+ argparse.Namespace: Parsed argument object
26+ """
27+ parser = argparse .ArgumentParser (description = 'Split patch tool' )
28+
29+ # Base parameters
30+ parser .add_argument ('patch_files' , type = str , nargs = '+' , help = 'Input patch file paths, multiple files can be specified' )
31+ parser .add_argument ('--out-dir' , type = str , help = 'Output directory path' )
32+
33+ # Split parameters
34+ parser .add_argument ('--level' , type = int , default = 1 , help = 'Merge level limit (default: 1)' )
35+ parser .add_argument ('--threshold' , type = int , default = 10 ,
36+ help = 'Module change file count threshold, merge to parent directory if below this value (default: 10)' )
37+
38+ # Other parameters
39+ parser .add_argument ('--dry-run' , action = 'store_true' , help = 'Only show operations to be performed, do not execute' )
40+ parser .add_argument ('--log-level' , type = str , default = 'WARNING' ,
41+ choices = ['DEBUG' , 'INFO' , 'WARNING' , 'ERROR' , 'CRITICAL' ],
42+ help = 'Logging level (default: WARNING)' )
43+ parser .add_argument ('--version' , action = 'version' , version = f'%(prog)s { __version__ } ' )
44+
45+ args = parser .parse_args ()
46+ # Validate arguments
47+ if not args .dry_run and not args .out_dir :
48+ parser .error ("--out-dir is required in non-dry-run mode" )
49+
50+ # Configure logging
51+ setup_logging (args .log_level )
52+ logger .debug ("Starting to process patch files" )
53+
54+ # Print argument information
55+ logger .info ("Current arguments:" )
56+ logger .info (f" Input files: { ', ' .join (args .patch_files )} " )
57+ if args .out_dir :
58+ logger .info (f" Output directory: { args .out_dir } " )
59+ logger .info (f" Merge level: { args .level } " )
60+ logger .info (f" File count threshold: { args .threshold } " )
61+ logger .info (f" Log level: { args .log_level } " )
62+ logger .info (f" Dry run: { 'yes' if args .dry_run else 'no' } " )
63+
64+ return args
65+
66+
67+ def parse_patches (patch_files : List [str ]) -> Patch :
68+ """Parse and validate all patch files
69+
70+ Args:
71+ patch_files: List of patch file paths
72+
73+ Returns:
74+ Patch: Combined patch object
75+
76+ Raises:
77+ SystemExit: When invalid patch files are found
78+ """
79+ logger .debug ("Starting to parse patch files" )
80+ combined_patch = Patch ("combined.patch" )
81+ invalid_files = []
82+
83+ for patch_file in patch_files :
84+ patch = Patch (patch_file )
85+ if not patch .is_valid ():
86+ invalid_files .append (patch_file )
87+ continue
88+
89+ logger .debug (f"Parsing patch file: { patch_file } " )
90+ patch .parse_patch ()
91+
92+ # Merge into combined data
93+ for file_path , changes in patch .items ():
94+ if file_path in combined_patch :
95+ # If file exists, extend content
96+ combined_patch [file_path ].extend (changes )
97+ else :
98+ combined_patch [file_path ] = changes
99+
100+ if invalid_files :
101+ logger .error ("The following patch files are invalid:" )
102+ for file in invalid_files :
103+ logger .error (f" - { file } " )
104+ sys .exit (1 )
105+
106+ logger .debug (f"All patch files parsed: { combined_patch } " )
107+ return combined_patch
108+
109+ def split_patch (patch : Patch , level : int , threshold : int ) -> List [Patch ]:
110+ """Split and merge patch data based on level and threshold parameters
111+
112+ Args:
113+ patch: Patch object to be processed
114+ level: Level limit for merging
115+ threshold: File count threshold for module merging
116+
117+ Returns:
118+ List[Patch]: List of merged patches
119+ """
120+ logger .debug ("Processing patch data" )
121+
122+ # Build file tree
123+ root = DirNode .from_patch (patch )
124+ logger .debug (f"Built file tree structure:\n { root } " )
125+
126+ # Apply merge strategy
127+ strategy = Merge (root , level , threshold )
128+ strategy .merge ()
129+ logger .debug (f"Merged file tree structure:\n { root } " )
130+
131+ # Convert merged tree to patch list
132+ return root .to_patches ()
133+
134+
135+ def output_patches (patches : List [Patch ], out_dir : str , dry_run : bool ) -> None :
136+ """Output processed patches to specified directory
137+
138+ Args:
139+ patches: List of patches to output
140+ out_dir: Output directory path
141+ dry_run: If True, only print info without actually writing files
142+ """
143+ if dry_run :
144+ logger .info ("Dry run mode - files will not be written" )
145+ for i , patch in enumerate (patches , 1 ):
146+ normalized_path = patch .path .lstrip ('/' ).replace ('/' , '_' )
147+ logger .info (f"Patch { i :03d} _{ normalized_path } .patch" )
148+ return
149+
150+ os .makedirs (out_dir , exist_ok = True )
151+ for i , patch in enumerate (patches , 1 ):
152+ normalized_path = patch .path .lstrip ('/' ).replace ('/' , '_' )
153+ output_file = os .path .join (out_dir , f"{ i :03d} _{ normalized_path } .patch" )
154+
155+ try :
156+ patch .path = output_file
157+ patch .write_patch ()
158+ logger .info (f"Output file: { patch } " )
159+ except IOError as e :
160+ logger .error (f"Failed to write file { output_file } : { e } " )
161+ sys .exit (1 )
162+
163+
164+ def main () -> None :
165+ try :
166+ # Parse command line arguments
167+ args = setup_args ()
168+
169+ # Parse patch files
170+ combined_patch = parse_patches (args .patch_files )
171+
172+ # Process patch data
173+ patches = split_patch (combined_patch , args .level , args .threshold )
174+
175+ # Output results
176+ output_patches (patches , args .out_dir , args .dry_run )
177+
178+ except Exception as e :
179+ logger .error (f"An error occurred during processing: { e } " )
180+ sys .exit (1 )
181+
182+
183+ if __name__ == "__main__" :
184+ main ()
0 commit comments