autopilot-code 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .git_ops import GitOperations, GitResult, MergeStatus
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import shutil
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from enum import Enum, auto
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MergeStatus(Enum):
|
|
11
|
+
MERGEABLE = auto()
|
|
12
|
+
CONFLICTING = auto()
|
|
13
|
+
UNKNOWN = auto()
|
|
14
|
+
BLOCKED = auto()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class GitResult:
|
|
19
|
+
success: bool
|
|
20
|
+
output: str
|
|
21
|
+
error: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GitOperations:
|
|
25
|
+
def __init__(self, repo_root: Path):
|
|
26
|
+
self.repo_root = repo_root
|
|
27
|
+
|
|
28
|
+
def create_worktree(
|
|
29
|
+
self, worktree_path: Path, branch: str, base_branch: str = "main"
|
|
30
|
+
) -> GitResult:
|
|
31
|
+
self.prune_worktrees()
|
|
32
|
+
|
|
33
|
+
if worktree_path.exists():
|
|
34
|
+
return GitResult(
|
|
35
|
+
success=True, output=f"Reusing existing worktree: {worktree_path}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if self.branch_exists(branch):
|
|
39
|
+
return self._run_git(["worktree", "add", str(worktree_path), branch])
|
|
40
|
+
else:
|
|
41
|
+
return self._run_git(
|
|
42
|
+
["worktree", "add", str(worktree_path), "-b", branch, base_branch]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def remove_worktree(self, worktree_path: Path) -> GitResult:
|
|
46
|
+
return self._run_git(["worktree", "remove", str(worktree_path)])
|
|
47
|
+
|
|
48
|
+
def prune_worktrees(self) -> GitResult:
|
|
49
|
+
return self._run_git(["worktree", "prune"])
|
|
50
|
+
|
|
51
|
+
def worktree_exists(self, worktree_path: Path) -> bool:
|
|
52
|
+
result = self._run_git(["worktree", "list"])
|
|
53
|
+
if not result.success:
|
|
54
|
+
return False
|
|
55
|
+
return str(worktree_path) in result.output
|
|
56
|
+
|
|
57
|
+
def branch_exists(self, branch: str, remote: bool = False) -> bool:
|
|
58
|
+
if remote:
|
|
59
|
+
result = self._run_git(["ls-remote", "--heads", "origin", branch])
|
|
60
|
+
else:
|
|
61
|
+
result = self._run_git(
|
|
62
|
+
["show-ref", "--verify", f"refs/heads/{branch}"], cwd=self.repo_root
|
|
63
|
+
)
|
|
64
|
+
return result.success
|
|
65
|
+
|
|
66
|
+
def fetch(self, remote: str = "origin", branch: Optional[str] = None) -> GitResult:
|
|
67
|
+
args = ["fetch", remote]
|
|
68
|
+
if branch:
|
|
69
|
+
args.append(branch)
|
|
70
|
+
return self._run_git(args)
|
|
71
|
+
|
|
72
|
+
def push(
|
|
73
|
+
self,
|
|
74
|
+
branch: str,
|
|
75
|
+
remote: str = "origin",
|
|
76
|
+
force: bool = False,
|
|
77
|
+
set_upstream: bool = True,
|
|
78
|
+
) -> GitResult:
|
|
79
|
+
args = ["push", remote, branch]
|
|
80
|
+
if force:
|
|
81
|
+
args.append("-f")
|
|
82
|
+
if set_upstream:
|
|
83
|
+
args.append("-u")
|
|
84
|
+
return self._run_git(args)
|
|
85
|
+
|
|
86
|
+
def has_changes(self, worktree: Path) -> bool:
|
|
87
|
+
result = self._run_git(["status", "--porcelain"], cwd=worktree)
|
|
88
|
+
return bool(result.output.strip())
|
|
89
|
+
|
|
90
|
+
def stage_all(self, worktree: Path) -> GitResult:
|
|
91
|
+
return self._run_git(["add", "-A"], cwd=worktree)
|
|
92
|
+
|
|
93
|
+
def commit(self, worktree: Path, message: str) -> GitResult:
|
|
94
|
+
return self._run_git(["commit", "-m", message], cwd=worktree)
|
|
95
|
+
|
|
96
|
+
def get_commit_sha(self, worktree: Path, short: bool = True) -> Optional[str]:
|
|
97
|
+
args = ["rev-parse"]
|
|
98
|
+
if short:
|
|
99
|
+
args.append("--short")
|
|
100
|
+
args.append("HEAD")
|
|
101
|
+
result = self._run_git(args, cwd=worktree)
|
|
102
|
+
if result.success:
|
|
103
|
+
return result.output.strip()
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def get_changed_files_count(self, worktree: Path) -> int:
|
|
107
|
+
result = self._run_git(
|
|
108
|
+
["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"], cwd=worktree
|
|
109
|
+
)
|
|
110
|
+
if result.success:
|
|
111
|
+
return len([line for line in result.output.splitlines() if line.strip()])
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
def rebase(self, worktree: Path, onto: str = "origin/main") -> GitResult:
|
|
115
|
+
return self._run_git(["rebase", onto], cwd=worktree)
|
|
116
|
+
|
|
117
|
+
def abort_rebase(self, worktree: Path) -> GitResult:
|
|
118
|
+
return self._run_git(["rebase", "--abort"], cwd=worktree)
|
|
119
|
+
|
|
120
|
+
def merge(self, worktree: Path, branch: str = "origin/main") -> GitResult:
|
|
121
|
+
return self._run_git(["merge", branch], cwd=worktree)
|
|
122
|
+
|
|
123
|
+
def abort_merge(self, worktree: Path) -> GitResult:
|
|
124
|
+
return self._run_git(["merge", "--abort"], cwd=worktree)
|
|
125
|
+
|
|
126
|
+
def has_conflicts(self, worktree: Path) -> bool:
|
|
127
|
+
result = self._run_git(["diff", "--name-only", "--diff-filter=U"], cwd=worktree)
|
|
128
|
+
if result.success:
|
|
129
|
+
return bool(result.output.strip())
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def get_conflicted_files(self, worktree: Path) -> list[str]:
|
|
133
|
+
result = self._run_git(["diff", "--name-only", "--diff-filter=U"], cwd=worktree)
|
|
134
|
+
if result.success:
|
|
135
|
+
return [line.strip() for line in result.output.splitlines() if line.strip()]
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
def is_merge_in_progress(self, worktree: Path) -> bool:
|
|
139
|
+
merge_head = worktree / ".git" / "MERGE_HEAD"
|
|
140
|
+
if merge_head.exists():
|
|
141
|
+
return True
|
|
142
|
+
rebase_merge = worktree / ".git" / "rebase-merge"
|
|
143
|
+
rebase_apply = worktree / ".git" / "rebase-apply"
|
|
144
|
+
return rebase_merge.exists() or rebase_apply.exists()
|
|
145
|
+
|
|
146
|
+
def get_pr_merge_status(self, repo: str, branch: str) -> MergeStatus:
|
|
147
|
+
result = self._run_gh(
|
|
148
|
+
[
|
|
149
|
+
"pr",
|
|
150
|
+
"view",
|
|
151
|
+
"--repo",
|
|
152
|
+
repo,
|
|
153
|
+
"--head",
|
|
154
|
+
branch,
|
|
155
|
+
"--json",
|
|
156
|
+
"mergeable,mergeStateStatus",
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if not result.success:
|
|
161
|
+
return MergeStatus.UNKNOWN
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
data = json.loads(result.output)
|
|
165
|
+
mergeable = data.get("mergeable")
|
|
166
|
+
merge_state = data.get("mergeStateStatus")
|
|
167
|
+
|
|
168
|
+
if mergeable == "CONFLICTING" or merge_state == "DIRTY":
|
|
169
|
+
return MergeStatus.CONFLICTING
|
|
170
|
+
elif merge_state == "CLEAN" or merge_state == "HAS_HOOKS":
|
|
171
|
+
return MergeStatus.MERGEABLE
|
|
172
|
+
elif merge_state == "BLOCKED":
|
|
173
|
+
return MergeStatus.BLOCKED
|
|
174
|
+
else:
|
|
175
|
+
return MergeStatus.UNKNOWN
|
|
176
|
+
except (json.JSONDecodeError, TypeError):
|
|
177
|
+
return MergeStatus.UNKNOWN
|
|
178
|
+
|
|
179
|
+
def _run_git(self, args: list[str], cwd: Optional[Path] = None) -> GitResult:
|
|
180
|
+
cwd = cwd or self.repo_root
|
|
181
|
+
try:
|
|
182
|
+
result = subprocess.run(
|
|
183
|
+
["git"] + args, cwd=cwd, capture_output=True, text=True
|
|
184
|
+
)
|
|
185
|
+
return GitResult(
|
|
186
|
+
success=result.returncode == 0,
|
|
187
|
+
output=result.stdout,
|
|
188
|
+
error=result.stderr if result.returncode != 0 else None,
|
|
189
|
+
)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
return GitResult(success=False, output="", error=str(e))
|
|
192
|
+
|
|
193
|
+
def _run_gh(self, args: list[str]) -> GitResult:
|
|
194
|
+
try:
|
|
195
|
+
result = subprocess.run(
|
|
196
|
+
["gh"] + args, cwd=self.repo_root, capture_output=True, text=True
|
|
197
|
+
)
|
|
198
|
+
return GitResult(
|
|
199
|
+
success=result.returncode == 0,
|
|
200
|
+
output=result.stdout,
|
|
201
|
+
error=result.stderr if result.returncode != 0 else None,
|
|
202
|
+
)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
return GitResult(success=False, output="", error=str(e))
|