release: cut 0.2.0 baseline
This commit is contained in:
135
cross_eval/worktree.py
Normal file
135
cross_eval/worktree.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""Git worktree lifecycle management for agentic mode."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WorktreeError(RuntimeError):
|
||||
"""Error during worktree operations."""
|
||||
|
||||
|
||||
def make_branch_name(preset_name: str) -> str:
|
||||
"""Generate a branch name for agentic results."""
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
return f"cross-eval/{preset_name}_{ts}"
|
||||
|
||||
|
||||
def create_worktree(base_cwd: Path, work_dir: Path, branch_name: str) -> Path:
|
||||
"""Create a git worktree on a new branch from HEAD.
|
||||
|
||||
1. Create branch from HEAD
|
||||
2. Create worktree checked out to that branch
|
||||
|
||||
The branch lives in the original repo, so it survives worktree removal.
|
||||
"""
|
||||
work_dir = work_dir.resolve()
|
||||
if work_dir.exists():
|
||||
shutil.rmtree(work_dir)
|
||||
|
||||
# Create the branch at HEAD
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "branch", branch_name, "HEAD"],
|
||||
cwd=base_cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise WorktreeError(
|
||||
f"Failed to create branch '{branch_name}': {e.stderr.strip()}"
|
||||
) from e
|
||||
|
||||
# Create worktree on that branch
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "worktree", "add", str(work_dir), branch_name],
|
||||
cwd=base_cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Clean up the branch if worktree creation fails
|
||||
subprocess.run(
|
||||
["git", "branch", "-D", branch_name],
|
||||
cwd=base_cwd,
|
||||
capture_output=True,
|
||||
)
|
||||
raise WorktreeError(
|
||||
f"Failed to create worktree at {work_dir}: {e.stderr.strip()}"
|
||||
) from e
|
||||
|
||||
logger.debug("Created worktree on branch '%s': %s", branch_name, work_dir)
|
||||
return work_dir
|
||||
|
||||
|
||||
def capture_diff(worktree_path: Path) -> str:
|
||||
"""Capture all changes made in the worktree as a unified diff.
|
||||
|
||||
Includes both tracked modifications and new untracked files.
|
||||
"""
|
||||
subprocess.run(
|
||||
["git", "add", "-A"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--cached", "HEAD"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def commit_worktree(worktree_path: Path, message: str) -> bool:
|
||||
"""Stage and commit all changes in the worktree.
|
||||
|
||||
Returns True if a commit was made, False if nothing to commit.
|
||||
"""
|
||||
subprocess.run(
|
||||
["git", "add", "-A"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
["git", "commit", "-m", message],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
# exit code 1 = nothing to commit
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def remove_worktree(base_cwd: Path, work_dir: Path) -> None:
|
||||
"""Remove a git worktree (branch is preserved in the original repo)."""
|
||||
work_dir = work_dir.resolve()
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "worktree", "remove", "--force", str(work_dir)],
|
||||
cwd=base_cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
if work_dir.exists():
|
||||
shutil.rmtree(work_dir, ignore_errors=True)
|
||||
subprocess.run(
|
||||
["git", "worktree", "prune"],
|
||||
cwd=base_cwd,
|
||||
capture_output=True,
|
||||
)
|
||||
logger.debug("Removed worktree: %s (branch preserved)", work_dir)
|
||||
Reference in New Issue
Block a user