|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import json |
3 | 4 | import os |
4 | 5 | import shutil |
5 | 6 | import subprocess |
6 | 7 | import sys |
7 | 8 | import tempfile |
8 | 9 | import time |
9 | | -from functools import cache |
| 10 | +from functools import cache, lru_cache |
10 | 11 | from io import StringIO |
11 | 12 | from pathlib import Path |
12 | 13 | from typing import TYPE_CHECKING, Optional |
13 | 14 |
|
14 | 15 | import git |
| 16 | +from filelock import FileLock |
15 | 17 | from rich.prompt import Confirm |
16 | 18 | from unidiff import PatchSet |
17 | 19 |
|
|
20 | 22 | from codeflash.code_utils.config_consts import N_CANDIDATES |
21 | 23 |
|
22 | 24 | if TYPE_CHECKING: |
| 25 | + from typing import Any |
| 26 | + |
23 | 27 | from git import Repo |
24 | 28 |
|
25 | 29 |
|
@@ -199,6 +203,14 @@ def get_last_commit_author_if_pr_exists(repo: Repo | None = None) -> str | None: |
199 | 203 | patches_dir = codeflash_cache_dir / "patches" |
200 | 204 |
|
201 | 205 |
|
| 206 | +@lru_cache(maxsize=1) |
| 207 | +def get_git_project_id() -> str: |
| 208 | + """Return the first commit sha of the repo.""" |
| 209 | + repo: Repo = git.Repo(search_parent_directories=True) |
| 210 | + root_commits = list(repo.iter_commits(rev="HEAD", max_parents=0)) |
| 211 | + return root_commits[0].hexsha |
| 212 | + |
| 213 | + |
202 | 214 | def create_worktree_snapshot_commit(worktree_dir: Path, commit_message: str) -> None: |
203 | 215 | repository = git.Repo(worktree_dir, search_parent_directories=True) |
204 | 216 | repository.git.add(".") |
@@ -257,20 +269,70 @@ def remove_worktree(worktree_dir: Path) -> None: |
257 | 269 | logger.exception(f"Failed to remove worktree: {worktree_dir}") |
258 | 270 |
|
259 | 271 |
|
260 | | -def create_diff_patch_from_worktree(worktree_dir: Path, files: list[str], fto_name: str) -> Path: |
| 272 | +def get_patches_dir_for_project() -> Path: |
| 273 | + project_id = get_git_project_id() or "" |
| 274 | + return Path(patches_dir / project_id) |
| 275 | + |
| 276 | + |
| 277 | +def get_patches_metadata() -> dict[str, Any]: |
| 278 | + project_patches_dir = get_patches_dir_for_project() |
| 279 | + meta_file = project_patches_dir / "metadata.json" |
| 280 | + if meta_file.exists(): |
| 281 | + return json.loads(meta_file.read_text()) |
| 282 | + return {"id": get_git_project_id() or "", "patches": []} |
| 283 | + |
| 284 | + |
| 285 | +def save_patches_metadata(patch_metadata: dict) -> dict: |
| 286 | + project_patches_dir = get_patches_dir_for_project() |
| 287 | + meta_file = project_patches_dir / "metadata.json" |
| 288 | + lock_file = project_patches_dir / "metadata.json.lock" |
| 289 | + |
| 290 | + with FileLock(lock_file, timeout=10): |
| 291 | + metadata = get_patches_metadata() |
| 292 | + |
| 293 | + patch_metadata["id"] = time.strftime("%Y%m%d-%H%M%S") |
| 294 | + metadata["patches"].append(patch_metadata) |
| 295 | + |
| 296 | + meta_file.write_text(json.dumps(metadata, indent=2)) |
| 297 | + |
| 298 | + return patch_metadata |
| 299 | + |
| 300 | + |
| 301 | +def overwrite_patch_metadata(patches: list[dict]) -> bool: |
| 302 | + project_patches_dir = get_patches_dir_for_project() |
| 303 | + meta_file = project_patches_dir / "metadata.json" |
| 304 | + lock_file = project_patches_dir / "metadata.json.lock" |
| 305 | + |
| 306 | + with FileLock(lock_file, timeout=10): |
| 307 | + metadata = get_patches_metadata() |
| 308 | + metadata["patches"] = patches |
| 309 | + meta_file.write_text(json.dumps(metadata, indent=2)) |
| 310 | + return True |
| 311 | + |
| 312 | + |
| 313 | +def create_diff_patch_from_worktree( |
| 314 | + worktree_dir: Path, files: list[str], metadata_input: dict[str, Any] |
| 315 | +) -> dict[str, Any]: |
261 | 316 | repository = git.Repo(worktree_dir, search_parent_directories=True) |
262 | 317 | uni_diff_text = repository.git.diff(None, "HEAD", *files, ignore_blank_lines=True, ignore_space_at_eol=True) |
263 | 318 |
|
264 | 319 | if not uni_diff_text: |
265 | 320 | logger.warning("No changes found in worktree.") |
266 | | - return None |
| 321 | + return {} |
267 | 322 |
|
268 | 323 | if not uni_diff_text.endswith("\n"): |
269 | 324 | uni_diff_text += "\n" |
270 | 325 |
|
271 | | - # write to patches_dir |
272 | | - patches_dir.mkdir(parents=True, exist_ok=True) |
273 | | - patch_path = patches_dir / f"{worktree_dir.name}.{fto_name}.patch" |
| 326 | + project_patches_dir = get_patches_dir_for_project() |
| 327 | + project_patches_dir.mkdir(parents=True, exist_ok=True) |
| 328 | + |
| 329 | + patch_path = project_patches_dir / f"{worktree_dir.name}.{metadata_input['fto_name']}.patch" |
274 | 330 | with patch_path.open("w", encoding="utf8") as f: |
275 | 331 | f.write(uni_diff_text) |
276 | | - return patch_path |
| 332 | + |
| 333 | + final_metadata = {} |
| 334 | + if metadata_input: |
| 335 | + metadata_input["patch_path"] = str(patch_path) |
| 336 | + final_metadata = save_patches_metadata(metadata_input) |
| 337 | + |
| 338 | + return final_metadata |
0 commit comments