@@ -200,7 +200,7 @@ impl SnapshotManager {
200200 Ok ( ( ) )
201201 }
202202
203- /// Restore tree recursively
203+ /// Restore tree recursively (always clean - deletes extras after successful restore)
204204 fn restore_tree < ' a > (
205205 & ' a self ,
206206 tree_hash : & ' a str ,
@@ -209,11 +209,19 @@ impl SnapshotManager {
209209 Box :: pin ( async move {
210210 fs:: create_dir_all ( dest) . await ?;
211211
212- // Load tree object
212+ // Load tree object - tells us what SHOULD exist
213213 let tree_json = self . store . get_blob ( tree_hash) . await ?;
214214 let tree: Tree = serde_json:: from_slice ( & tree_json) ?;
215215
216- // Restore each entry
216+ // Build set of expected names in this directory (owned strings to avoid borrow issues)
217+ let expected_names: std:: collections:: HashSet < String > = tree
218+ . entries
219+ . iter ( )
220+ . map ( |e| e. name . clone ( ) )
221+ . collect ( ) ;
222+
223+ // Phase 1: Restore each entry from snapshot
224+ // Do this FIRST - if restore fails, extras remain untouched (safer)
217225 for entry in tree. entries {
218226 let entry_path = dest. join ( & entry. name ) ;
219227
@@ -281,6 +289,29 @@ impl SnapshotManager {
281289 }
282290 }
283291
292+ // Phase 2: Clean this directory - delete extras (only after successful restore)
293+ // Cleanup failures are warned but don't fail the operation
294+ let mut read_dir = fs:: read_dir ( dest) . await ?;
295+ while let Some ( entry) = read_dir. next_entry ( ) . await ? {
296+ let name = entry. file_name ( ) ;
297+ let name_str = name. to_string_lossy ( ) . to_string ( ) ;
298+
299+ if !expected_names. contains ( & name_str) {
300+ let path = entry. path ( ) ;
301+
302+ // Not in snapshot - delete it
303+ if path. is_dir ( ) {
304+ if let Err ( e) = fs:: remove_dir_all ( & path) . await {
305+ tracing:: warn!( "Failed to delete directory {}: {}" , path. display( ) , e) ;
306+ }
307+ } else {
308+ if let Err ( e) = fs:: remove_file ( & path) . await {
309+ tracing:: warn!( "Failed to delete file {}: {}" , path. display( ) , e) ;
310+ }
311+ }
312+ }
313+ }
314+
284315 Ok ( ( ) )
285316 } )
286317 }
@@ -1189,4 +1220,69 @@ mod tests {
11891220 assert ! ( result. is_err( ) ) ;
11901221 assert ! ( result. unwrap_err( ) . to_string( ) . contains( "does not exist" ) ) ;
11911222 }
1223+
1224+ #[ tokio:: test]
1225+ async fn test_restore_always_clean ( ) {
1226+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1227+ let store_dir = temp_dir. path ( ) . join ( "store" ) ;
1228+ let workspace = temp_dir. path ( ) . join ( "workspace" ) ;
1229+ let restore_dir = temp_dir. path ( ) . join ( "restored" ) ;
1230+
1231+ // Create snapshot with specific files
1232+ fs:: create_dir_all ( & workspace) . await . unwrap ( ) ;
1233+ fs:: write ( workspace. join ( "file1.txt" ) , "content1" )
1234+ . await
1235+ . unwrap ( ) ;
1236+ fs:: create_dir_all ( workspace. join ( "dir1" ) ) . await . unwrap ( ) ;
1237+ fs:: write ( workspace. join ( "dir1/file2.txt" ) , "content2" )
1238+ . await
1239+ . unwrap ( ) ;
1240+
1241+ let manager = SnapshotManager :: new ( store_dir) . unwrap ( ) ;
1242+ let snapshot_id = manager
1243+ . create_snapshot ( & workspace, Some ( "Clean test" . to_string ( ) ) , None )
1244+ . await
1245+ . unwrap ( ) ;
1246+
1247+ // Restore to directory with extra files
1248+ fs:: create_dir_all ( & restore_dir) . await . unwrap ( ) ;
1249+ fs:: write ( restore_dir. join ( "extra_file.txt" ) , "should be deleted" )
1250+ . await
1251+ . unwrap ( ) ;
1252+ fs:: create_dir_all ( restore_dir. join ( "extra_dir" ) )
1253+ . await
1254+ . unwrap ( ) ;
1255+ fs:: write ( restore_dir. join ( "extra_dir/nested.txt" ) , "also deleted" )
1256+ . await
1257+ . unwrap ( ) ;
1258+ fs:: create_dir_all ( restore_dir. join ( "dir1" ) ) . await . unwrap ( ) ;
1259+ fs:: write ( restore_dir. join ( "dir1/extra_in_dir.txt" ) , "delete me" )
1260+ . await
1261+ . unwrap ( ) ;
1262+
1263+ // Restore snapshot (should clean extras)
1264+ manager
1265+ . restore_snapshot ( & snapshot_id, & restore_dir)
1266+ . await
1267+ . unwrap ( ) ;
1268+
1269+ // Verify exact match - only snapshot files exist
1270+ assert ! ( restore_dir. join( "file1.txt" ) . exists( ) ) ;
1271+ assert ! ( restore_dir. join( "dir1/file2.txt" ) . exists( ) ) ;
1272+
1273+ // Verify extras are deleted
1274+ assert ! ( !restore_dir. join( "extra_file.txt" ) . exists( ) ) ;
1275+ assert ! ( !restore_dir. join( "extra_dir" ) . exists( ) ) ;
1276+ assert ! ( !restore_dir. join( "dir1/extra_in_dir.txt" ) . exists( ) ) ;
1277+
1278+ // Verify content is correct
1279+ let content1 = fs:: read_to_string ( restore_dir. join ( "file1.txt" ) )
1280+ . await
1281+ . unwrap ( ) ;
1282+ assert_eq ! ( content1, "content1" ) ;
1283+ let content2 = fs:: read_to_string ( restore_dir. join ( "dir1/file2.txt" ) )
1284+ . await
1285+ . unwrap ( ) ;
1286+ assert_eq ! ( content2, "content2" ) ;
1287+ }
11921288}
0 commit comments