From 6a133736152d6b4fa47351aeae79d67784972dc6 Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:04:34 +0800 Subject: [PATCH 1/6] Fix issue with progress directory not found in progress sync --- app/commands/progress/sync/on.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/commands/progress/sync/on.py b/app/commands/progress/sync/on.py index b879e08..cdf8cba 100644 --- a/app/commands/progress/sync/on.py +++ b/app/commands/progress/sync/on.py @@ -90,6 +90,9 @@ def on() -> None: key=lambda entry: (entry["exercise_name"], entry["started_at"]) ) + # Ensure the directory exists before writing + os.makedirs(os.path.dirname(local_progress_filepath), exist_ok=True) + with open(local_progress_filepath, "w") as file: file.write(json.dumps(synced_progress, indent=2)) From 62da93a08e105a43a6d9ed0dcb532bff5be54c17 Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:31:18 +0800 Subject: [PATCH 2/6] Update fix to resolve race condition for Windows in deleting folder --- app/commands/progress/sync/on.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/app/commands/progress/sync/on.py b/app/commands/progress/sync/on.py index cdf8cba..d498b01 100644 --- a/app/commands/progress/sync/on.py +++ b/app/commands/progress/sync/on.py @@ -2,6 +2,7 @@ import os import click +import time from app.commands.check.git import git from app.commands.check.github import github @@ -67,8 +68,30 @@ def on() -> None: local_progress = json.load(file) rmtree(PROGRESS_LOCAL_FOLDER_NAME) + # Wait for folder to be fully deleted (Windows can be slow with deleting folder) + max_retries = 10 + for _ in range(max_retries): + if not os.path.exists(PROGRESS_LOCAL_FOLDER_NAME): + break + time.sleep(0.1) + + # If folder still exists after retries, force fail + if os.path.exists(PROGRESS_LOCAL_FOLDER_NAME): + raise Exception(f"Failed to delete {PROGRESS_LOCAL_FOLDER_NAME} before cloning") + clone_with_custom_name(f"{username}/{fork_name}", PROGRESS_LOCAL_FOLDER_NAME) + # Verify clone succeeded by checking folder exists + if not os.path.exists(PROGRESS_LOCAL_FOLDER_NAME): + # Clone failed: recreate the local progress to avoid data loss + os.makedirs(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 Exception( + f"Clone failed - {PROGRESS_LOCAL_FOLDER_NAME} does not exist. Your local progress has been restored." + ) + # To reconcile the difference between local and remote progress, we merge by # (exercise_name, start_time) which should be unique remote_progress = [] @@ -90,9 +113,6 @@ def on() -> None: key=lambda entry: (entry["exercise_name"], entry["started_at"]) ) - # Ensure the directory exists before writing - os.makedirs(os.path.dirname(local_progress_filepath), exist_ok=True) - with open(local_progress_filepath, "w") as file: file.write(json.dumps(synced_progress, indent=2)) From c041e3d7edcee4ea947ab7d92b39fc3e773f338c Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:42:41 +0800 Subject: [PATCH 3/6] Address copilot comments --- app/commands/progress/sync/on.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/commands/progress/sync/on.py b/app/commands/progress/sync/on.py index d498b01..801237a 100644 --- a/app/commands/progress/sync/on.py +++ b/app/commands/progress/sync/on.py @@ -69,15 +69,17 @@ def on() -> None: rmtree(PROGRESS_LOCAL_FOLDER_NAME) # Wait for folder to be fully deleted (Windows can be slow with deleting folder) - max_retries = 10 + max_retries = 20 for _ in range(max_retries): if not os.path.exists(PROGRESS_LOCAL_FOLDER_NAME): break - time.sleep(0.1) + time.sleep(0.2) # If folder still exists after retries, force fail if os.path.exists(PROGRESS_LOCAL_FOLDER_NAME): - raise Exception(f"Failed to delete {PROGRESS_LOCAL_FOLDER_NAME} before cloning") + raise RuntimeError( + f"Failed to delete {PROGRESS_LOCAL_FOLDER_NAME} before cloning" + ) clone_with_custom_name(f"{username}/{fork_name}", PROGRESS_LOCAL_FOLDER_NAME) @@ -85,10 +87,9 @@ def on() -> None: if not os.path.exists(PROGRESS_LOCAL_FOLDER_NAME): # Clone failed: recreate the local progress to avoid data loss os.makedirs(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 Exception( + raise RuntimeError( f"Clone failed - {PROGRESS_LOCAL_FOLDER_NAME} does not exist. Your local progress has been restored." ) From 2b57e8dcbc944b126946c63989159d668dd46528 Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Wed, 14 Jan 2026 23:13:20 +0800 Subject: [PATCH 4/6] Add fix for race condition for all callsites to rmtree for Windows --- app/commands/progress/sync/on.py | 14 -------------- app/utils/cli.py | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/commands/progress/sync/on.py b/app/commands/progress/sync/on.py index 801237a..dbef2ed 100644 --- a/app/commands/progress/sync/on.py +++ b/app/commands/progress/sync/on.py @@ -2,7 +2,6 @@ import os import click -import time from app.commands.check.git import git from app.commands.check.github import github @@ -68,19 +67,6 @@ def on() -> None: local_progress = json.load(file) rmtree(PROGRESS_LOCAL_FOLDER_NAME) - # Wait for folder to be fully deleted (Windows can be slow with deleting folder) - max_retries = 20 - for _ in range(max_retries): - if not os.path.exists(PROGRESS_LOCAL_FOLDER_NAME): - break - time.sleep(0.2) - - # If folder still exists after retries, force fail - if os.path.exists(PROGRESS_LOCAL_FOLDER_NAME): - raise RuntimeError( - f"Failed to delete {PROGRESS_LOCAL_FOLDER_NAME} before cloning" - ) - clone_with_custom_name(f"{username}/{fork_name}", PROGRESS_LOCAL_FOLDER_NAME) # Verify clone succeeded by checking folder exists diff --git a/app/utils/cli.py b/app/utils/cli.py index a49fdc8..1add0c8 100644 --- a/app/utils/cli.py +++ b/app/utils/cli.py @@ -1,13 +1,32 @@ import os import shutil import stat +import time from pathlib import Path from typing import Union 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 = 20 + for _ in range(max_retries): + if not os.path.exists(folder_name): + return + time.sleep(0.2) + + # If folder still exists after retries, raise error + raise RuntimeError(f"Failed to delete {folder_name} after {max_retries} retries") From 98396959d97416858296cd47e64313f48d523fe4 Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Wed, 14 Jan 2026 23:43:07 +0800 Subject: [PATCH 5/6] Fix progress sync off and clean up code --- app/commands/progress/sync/off.py | 1 + app/commands/progress/sync/on.py | 8 ++++---- app/utils/cli.py | 7 +++++-- 3 files changed, 10 insertions(+), 6 deletions(-) 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 dbef2ed..16cf80e 100644 --- a/app/commands/progress/sync/on.py +++ b/app/commands/progress/sync/on.py @@ -69,14 +69,14 @@ def on() -> None: clone_with_custom_name(f"{username}/{fork_name}", PROGRESS_LOCAL_FOLDER_NAME) - # Verify clone succeeded by checking folder exists + # Verify clone succeeded, else restore local progress before failing if not os.path.exists(PROGRESS_LOCAL_FOLDER_NAME): - # Clone failed: recreate the local progress to avoid data loss - os.makedirs(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 - {PROGRESS_LOCAL_FOLDER_NAME} does not exist. Your local progress has been restored." + f"Clone failed for {PROGRESS_LOCAL_FOLDER_NAME}. " + "Your local progress has been restored." ) # To reconcile the difference between local and remote progress, we merge by diff --git a/app/utils/cli.py b/app/utils/cli.py index 1add0c8..fe24236 100644 --- a/app/utils/cli.py +++ b/app/utils/cli.py @@ -5,6 +5,9 @@ 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: """ @@ -22,11 +25,11 @@ def force_remove_readonly(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 = 20 + max_retries = MAX_DELETE_RETRIES for _ in range(max_retries): if not os.path.exists(folder_name): return - time.sleep(0.2) + 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") From fbe295cb404d724b8edf5bf83c185fcc4308cf54 Mon Sep 17 00:00:00 2001 From: jovnc <95868357+jovnc@users.noreply.github.com> Date: Thu, 15 Jan 2026 19:55:01 +0800 Subject: [PATCH 6/6] Address comments --- app/commands/progress/sync/on.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/commands/progress/sync/on.py b/app/commands/progress/sync/on.py index 16cf80e..6d5fdb5 100644 --- a/app/commands/progress/sync/on.py +++ b/app/commands/progress/sync/on.py @@ -77,6 +77,7 @@ def on() -> None: 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