33import json
44import gzip
55import hashlib
6- from datetime import datetime
6+ from datetime import datetime , timezone
77from pathlib import Path
88
99from codesage .snapshot .versioning import SnapshotVersionManager
@@ -46,7 +46,7 @@ def snapshot():
4646from codesage .audit .models import AuditEvent
4747
4848
49- def _create_snapshot_data (path ):
49+ def _create_snapshot_data (path , project_name ):
5050 file_snapshots = []
5151 for root , dirs , files in os .walk (path ):
5252 dirs [:] = [d for d in dirs if d not in DEFAULT_EXCLUDE_DIRS ]
@@ -81,7 +81,7 @@ def _create_snapshot_data(path):
8181 metadata = SnapshotMetadata (
8282 version = "" ,
8383 timestamp = datetime .now (),
84- project_name = os . path . basename ( os . path . abspath ( path )) ,
84+ project_name = project_name ,
8585 file_count = len (file_snapshots ),
8686 total_size = total_size ,
8787 tool_version = tool_version ,
@@ -97,15 +97,16 @@ def _create_snapshot_data(path):
9797
9898@snapshot .command ('create' )
9999@click .argument ('path' , type = click .Path (exists = True , dir_okay = True ))
100+ @click .option ('--project' , '-p' , 'project_name_override' , help = 'Override the project name.' )
100101@click .option ('--format' , '-f' , type = click .Choice (['json' , 'python-semantic-digest' ]), default = 'json' , help = 'Snapshot format.' )
101102@click .option ('--output' , '-o' , type = click .Path (), default = None , help = 'Output file path.' )
102103@click .option ('--compress' , is_flag = True , help = 'Enable compression.' )
103104@click .option ('--language' , '-l' , type = click .Choice (['python' , 'go' , 'shell' , 'java' , 'auto' ]), default = 'auto' , help = 'Language to analyze.' )
104105@click .pass_context
105- def create (ctx , path , format , output , compress , language ):
106+ def create (ctx , path , project_name_override , format , output , compress , language ):
106107 """Create a new snapshot from the given path."""
107108 audit_logger = ctx .obj .audit_logger
108- project_name = os .path .basename (os .path .abspath (path ))
109+ project_name = project_name_override or os .path .basename (os .path .abspath (path ))
109110 try :
110111 root_path = Path (path )
111112
@@ -168,31 +169,38 @@ def create(ctx, path, format, output, compress, language):
168169 click .echo (f"{ language .capitalize ()} semantic digest created at { output } " )
169170 return
170171
171- snapshot_data = _create_snapshot_data (path )
172+ snapshot_data = _create_snapshot_data (path , project_name )
172173
173174 if output :
174175 output_path = Path (output )
175176 output_path .parent .mkdir (parents = True , exist_ok = True )
176- with open (output_path , 'w' ) as f :
177+ # Use model_dump_json for consistency
178+ with open (output_path , 'w' , encoding = 'utf-8' ) as f :
177179 f .write (snapshot_data .model_dump_json (indent = 2 ))
178180
179181 click .echo (f"Snapshot created at { output } " )
180182 else :
181- manager = SnapshotVersionManager (SNAPSHOT_DIR , DEFAULT_SNAPSHOT_CONFIG ['snapshot' ])
183+ manager = SnapshotVersionManager (SNAPSHOT_DIR , project_name , DEFAULT_SNAPSHOT_CONFIG ['snapshot' ])
184+
185+ # The format for saving via manager is 'json', not the input format for semantic digests
186+ save_format = 'json'
187+
182188 if compress :
183- snapshot_path = manager .save_snapshot (snapshot_data , format )
189+ snapshot_path = manager .save_snapshot (snapshot_data , save_format )
190+
191+ # Compress the file
184192 with open (snapshot_path , 'rb' ) as f_in :
185193 with gzip .open (f"{ snapshot_path } .gz" , 'wb' ) as f_out :
186194 f_out .writelines (f_in )
187195 os .remove (snapshot_path )
188196 click .echo (f"Compressed snapshot created at { snapshot_path } .gz" )
189197 else :
190- snapshot_path = manager .save_snapshot (snapshot_data , format )
198+ snapshot_path = manager .save_snapshot (snapshot_data , save_format )
191199 click .echo (f"Snapshot created at { snapshot_path } " )
192200 finally :
193201 audit_logger .log (
194202 AuditEvent (
195- timestamp = datetime .now (),
203+ timestamp = datetime .now (timezone . utc ),
196204 event_type = "cli.snapshot.create" ,
197205 project_name = project_name ,
198206 command = "snapshot create" ,
@@ -207,56 +215,52 @@ def create(ctx, path, format, output, compress, language):
207215 )
208216
209217@snapshot .command ('list' )
210- def list_snapshots ():
211- """List all available snapshots."""
212- manager = SnapshotVersionManager (SNAPSHOT_DIR , DEFAULT_SNAPSHOT_CONFIG ['snapshot' ])
218+ @click .option ('--project' , '-p' , required = True , help = 'The name of the project.' )
219+ def list_snapshots (project ):
220+ """List all available snapshots for a project."""
221+ manager = SnapshotVersionManager (SNAPSHOT_DIR , project , DEFAULT_SNAPSHOT_CONFIG ['snapshot' ])
213222 snapshots = manager .list_snapshots ()
214223 if not snapshots :
215- click .echo ("No snapshots found." )
224+ click .echo (f "No snapshots found for project ' { project } ' ." )
216225 return
217226 for s in snapshots :
218227 click .echo (f"- { s ['version' ]} ({ s ['timestamp' ]} )" )
219228
220229@snapshot .command ('show' )
221230@click .argument ('version' )
222- def show (version ):
231+ @click .option ('--project' , '-p' , required = True , help = 'The name of the project.' )
232+ def show (version , project ):
223233 """Show details of a specific snapshot."""
224- manager = SnapshotVersionManager (SNAPSHOT_DIR , DEFAULT_SNAPSHOT_CONFIG ['snapshot' ])
234+ manager = SnapshotVersionManager (SNAPSHOT_DIR , project , DEFAULT_SNAPSHOT_CONFIG ['snapshot' ])
225235 snapshot_data = manager .load_snapshot (version )
226236 if not snapshot_data :
227- click .echo (f"Snapshot { version } not found." , err = True )
237+ click .echo (f"Snapshot { version } not found for project ' { project } ' ." , err = True )
228238 return
229239 click .echo (snapshot_data .model_dump_json (indent = 2 ))
230240
231241@snapshot .command ('cleanup' )
242+ @click .option ('--project' , '-p' , required = True , help = 'The name of the project.' )
232243@click .option ('--dry-run' , is_flag = True , help = 'Show which snapshots would be deleted.' )
233- def cleanup (dry_run ):
234- """Clean up old snapshots."""
235- from datetime import timedelta
236-
237- manager = SnapshotVersionManager (SNAPSHOT_DIR , DEFAULT_SNAPSHOT_CONFIG ['snapshot' ])
244+ def cleanup (project , dry_run ):
245+ """Clean up old snapshots for a project."""
246+ manager = SnapshotVersionManager (SNAPSHOT_DIR , project , DEFAULT_SNAPSHOT_CONFIG ['snapshot' ])
238247
239248 if dry_run :
240249 index = manager ._load_index ()
241- now = datetime .now ()
242-
243- expired_by_date = [
244- s for s in index
245- if now - datetime .fromisoformat (s ["timestamp" ]) > timedelta (days = manager .retention_days )
246- ]
247-
248- sorted_by_date = sorted (index , key = lambda s : s ["timestamp" ], reverse = True )
249- expired_by_count = sorted_by_date [manager .max_versions :]
250+ if not index :
251+ click .echo (f"No snapshots to clean up for project '{ project } '." )
252+ return
250253
251- expired = {s ['version' ]: s for s in expired_by_date + expired_by_count }.values ()
254+ now = datetime .now (timezone .utc )
255+ expired_snapshots = manager ._get_expired_snapshots (index , now )
252256
253- if not expired :
254- click .echo ("No snapshots to clean up." )
257+ if not expired_snapshots :
258+ click .echo (f "No snapshots to clean up for project ' { project } ' ." )
255259 return
256260
257261 click .echo ("Snapshots to be deleted:" )
258- for s in expired :
262+ for s in expired_snapshots :
259263 click .echo (f"- { s ['version' ]} " )
260264 else :
261265 manager .cleanup_expired_snapshots ()
262- click .echo ("Expired snapshots have been cleaned up." )
266+ click .echo (f "Expired snapshots for project ' { project } ' have been cleaned up." )
0 commit comments