diff --git a/app/commands/progress/sync/off.py b/app/commands/progress/sync/off.py index 256e4f3..549c8ce 100644 --- a/app/commands/progress/sync/off.py +++ b/app/commands/progress/sync/off.py @@ -53,6 +53,7 @@ def off() -> None: local_progress = json.load(file) rmtree(PROGRESS_LOCAL_FOLDER_NAME) + os.makedirs(os.path.dirname(local_progress_filepath), exist_ok=True) # Re-create just the progress folder with open(local_progress_filepath, "a") as progress_file: diff --git a/app/commands/progress/sync/on.py b/app/commands/progress/sync/on.py index b879e08..6d5fdb5 100644 --- a/app/commands/progress/sync/on.py +++ b/app/commands/progress/sync/on.py @@ -69,6 +69,17 @@ def on() -> None: clone_with_custom_name(f"{username}/{fork_name}", PROGRESS_LOCAL_FOLDER_NAME) + # Verify clone succeeded, else restore local progress before failing + if not os.path.exists(PROGRESS_LOCAL_FOLDER_NAME): + os.makedirs(os.path.dirname(local_progress_filepath), exist_ok=True) + with open(local_progress_filepath, "w") as file: + file.write(json.dumps(local_progress, indent=2)) + raise RuntimeError( + f"Clone failed for {PROGRESS_LOCAL_FOLDER_NAME}. " + "Your local progress has been restored." + "Re-run the command `gitmastery progress sync on` to try again." + ) + # To reconcile the difference between local and remote progress, we merge by # (exercise_name, start_time) which should be unique remote_progress = [] diff --git a/app/utils/cli.py b/app/utils/cli.py index a49fdc8..fe24236 100644 --- a/app/utils/cli.py +++ b/app/utils/cli.py @@ -1,13 +1,35 @@ import os import shutil import stat +import time from pathlib import Path from typing import Union +MAX_DELETE_RETRIES = 20 +MAX_RETRY_INTERVAL = 0.2 + def rmtree(folder_name: Union[str, Path]) -> None: + """ + Remove a directory tree. + + Raises RuntimeError if the folder still exists after max retries. + """ + if not os.path.exists(folder_name): + return + def force_remove_readonly(func, path, _): os.chmod(path, stat.S_IWRITE) func(path) shutil.rmtree(folder_name, onerror=force_remove_readonly) + + # Wait for folder to be fully deleted (Windows can be slow with permissions) + max_retries = MAX_DELETE_RETRIES + for _ in range(max_retries): + if not os.path.exists(folder_name): + return + time.sleep(MAX_RETRY_INTERVAL) + + # If folder still exists after retries, raise error + raise RuntimeError(f"Failed to delete {folder_name} after {max_retries} retries")