|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import configparser |
| 4 | +import os |
4 | 5 | import shutil |
| 6 | +import stat |
5 | 7 | import subprocess |
6 | 8 | import tempfile |
7 | 9 | import time |
8 | 10 | from pathlib import Path |
9 | 11 | from typing import Optional |
10 | 12 |
|
11 | 13 | import git |
12 | | -from git.exc import GitCommandError |
13 | 14 |
|
14 | 15 | from codeflash.cli_cmds.console import logger |
15 | 16 | from codeflash.code_utils.compat import codeflash_cache_dir |
@@ -97,62 +98,24 @@ def create_detached_worktree(module_root: Path) -> Optional[Path]: |
97 | 98 | return worktree_dir |
98 | 99 |
|
99 | 100 |
|
100 | | -def _fallback_remove_worktree(worktree_dir: Path) -> None: |
101 | | - """Fallback worktree removal using shutil.rmtree when git commands fail.""" |
102 | | - if worktree_dir.exists(): |
103 | | - shutil.rmtree(worktree_dir, ignore_errors=True) |
104 | | - logger.debug(f"Removed worktree directory using fallback method: {worktree_dir}") |
| 101 | +def _handle_remove_readonly(func, path, exc_info): |
| 102 | + """Error handler for shutil.rmtree to handle read-only files on Windows.""" |
| 103 | + if isinstance(exc_info[1], PermissionError): |
| 104 | + os.chmod(path, stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR) |
| 105 | + func(path) |
| 106 | + else: |
| 107 | + raise exc_info[1] |
105 | 108 |
|
106 | 109 |
|
107 | 110 | def remove_worktree(worktree_dir: Path) -> None: |
108 | | - """Remove a git worktree, with retry logic for Windows permission errors.""" |
109 | | - # Try to get repository reference |
110 | | - try: |
111 | | - repository = git.Repo(worktree_dir, search_parent_directories=True) |
112 | | - except git.InvalidGitRepositoryError: |
113 | | - # Worktree is not a valid git repository (corrupted or partially created) |
114 | | - logger.debug(f"Worktree is not a valid git repository, using fallback deletion: {worktree_dir}") |
115 | | - _fallback_remove_worktree(worktree_dir) |
| 111 | + """Remove a git worktree directory.""" |
| 112 | + if not worktree_dir.exists(): |
116 | 113 | return |
| 114 | + try: |
| 115 | + shutil.rmtree(worktree_dir, onerror=_handle_remove_readonly) |
| 116 | + logger.debug(f"Removed worktree: {worktree_dir}") |
117 | 117 | except Exception: |
118 | | - logger.exception(f"Failed to open worktree repository: {worktree_dir}") |
119 | | - _fallback_remove_worktree(worktree_dir) |
120 | | - return |
121 | | - |
122 | | - # Try git worktree remove first |
123 | | - for attempt in range(2): |
124 | | - try: |
125 | | - repository.git.worktree("remove", "--force", worktree_dir) |
126 | | - logger.debug(f"Successfully removed worktree: {worktree_dir}") |
127 | | - return |
128 | | - except GitCommandError as e: |
129 | | - error_msg = str(e).lower() |
130 | | - # Check if it's a permission error or not a git repository error |
131 | | - if "permission denied" in error_msg or "failed to delete" in error_msg: |
132 | | - if attempt == 0: |
133 | | - # Retry once with a small delay to allow file handles to close |
134 | | - logger.debug(f"Permission error removing worktree (attempt {attempt + 1}), retrying after delay: {worktree_dir}") |
135 | | - time.sleep(0.5) |
136 | | - continue |
137 | | - elif "not a git repository" in error_msg: |
138 | | - # Worktree reference is broken, just delete the directory |
139 | | - logger.debug(f"Worktree git reference is broken, using fallback deletion: {worktree_dir}") |
140 | | - _fallback_remove_worktree(worktree_dir) |
141 | | - return |
142 | | - |
143 | | - # Fallback to shutil.rmtree for any persistent error |
144 | | - logger.warning(f"Git worktree remove failed, using fallback deletion: {worktree_dir}") |
145 | | - _fallback_remove_worktree(worktree_dir) |
146 | | - # Try to prune stale worktree references |
147 | | - try: |
148 | | - repository.git.worktree("prune") |
149 | | - except Exception: |
150 | | - pass |
151 | | - return |
152 | | - except Exception: |
153 | | - logger.exception(f"Failed to remove worktree: {worktree_dir}") |
154 | | - _fallback_remove_worktree(worktree_dir) |
155 | | - return |
| 118 | + logger.exception(f"Failed to remove worktree: {worktree_dir}") |
156 | 119 |
|
157 | 120 |
|
158 | 121 | def create_diff_patch_from_worktree( |
|
0 commit comments