framework-m-studio 0.2.2__py3-none-any.whl
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.
- framework_m_studio/__init__.py +16 -0
- framework_m_studio/app.py +283 -0
- framework_m_studio/cli.py +247 -0
- framework_m_studio/codegen/__init__.py +34 -0
- framework_m_studio/codegen/generator.py +291 -0
- framework_m_studio/codegen/parser.py +545 -0
- framework_m_studio/codegen/templates/doctype.py.jinja2 +69 -0
- framework_m_studio/codegen/templates/test_doctype.py.jinja2 +58 -0
- framework_m_studio/codegen/test_generator.py +368 -0
- framework_m_studio/codegen/transformer.py +406 -0
- framework_m_studio/discovery.py +193 -0
- framework_m_studio/docs_generator.py +318 -0
- framework_m_studio/git/__init__.py +1 -0
- framework_m_studio/git/adapter.py +309 -0
- framework_m_studio/git/github_provider.py +321 -0
- framework_m_studio/git/protocol.py +249 -0
- framework_m_studio/py.typed +0 -0
- framework_m_studio/routes.py +552 -0
- framework_m_studio/sdk_generator.py +239 -0
- framework_m_studio/workspace.py +295 -0
- framework_m_studio-0.2.2.dist-info/METADATA +65 -0
- framework_m_studio-0.2.2.dist-info/RECORD +24 -0
- framework_m_studio-0.2.2.dist-info/WHEEL +4 -0
- framework_m_studio-0.2.2.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""GitHub Provider for Studio Cloud Mode.
|
|
2
|
+
|
|
3
|
+
Provides GitHub-specific API operations like creating pull requests,
|
|
4
|
+
managing branches, and repository operations using the GitHub REST API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.error import HTTPError
|
|
13
|
+
from urllib.parse import urljoin
|
|
14
|
+
from urllib.request import Request, urlopen
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class PullRequest:
|
|
19
|
+
"""GitHub Pull Request data."""
|
|
20
|
+
|
|
21
|
+
number: int
|
|
22
|
+
"""PR number."""
|
|
23
|
+
|
|
24
|
+
title: str
|
|
25
|
+
"""PR title."""
|
|
26
|
+
|
|
27
|
+
url: str
|
|
28
|
+
"""Web URL to the PR."""
|
|
29
|
+
|
|
30
|
+
state: str
|
|
31
|
+
"""PR state: open, closed, merged."""
|
|
32
|
+
|
|
33
|
+
head_branch: str
|
|
34
|
+
"""Source branch name."""
|
|
35
|
+
|
|
36
|
+
base_branch: str
|
|
37
|
+
"""Target branch name."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Repository:
|
|
42
|
+
"""GitHub Repository data."""
|
|
43
|
+
|
|
44
|
+
full_name: str
|
|
45
|
+
"""Full repo name (owner/repo)."""
|
|
46
|
+
|
|
47
|
+
default_branch: str
|
|
48
|
+
"""Default branch name."""
|
|
49
|
+
|
|
50
|
+
clone_url: str
|
|
51
|
+
"""HTTPS clone URL."""
|
|
52
|
+
|
|
53
|
+
private: bool
|
|
54
|
+
"""Whether the repo is private."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GitHubError(Exception):
|
|
58
|
+
"""GitHub API error."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, message: str, status_code: int = 0):
|
|
61
|
+
super().__init__(message)
|
|
62
|
+
self.status_code = status_code
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GitHubAuthError(GitHubError):
|
|
66
|
+
"""Authentication failed."""
|
|
67
|
+
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class GitHubProvider:
|
|
72
|
+
"""GitHub API provider.
|
|
73
|
+
|
|
74
|
+
Provides GitHub-specific operations using the REST API.
|
|
75
|
+
Uses personal access tokens for authentication.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
API_BASE = "https://api.github.com"
|
|
79
|
+
|
|
80
|
+
def __init__(self, token: str):
|
|
81
|
+
"""Initialize GitHubProvider.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
token: GitHub personal access token.
|
|
85
|
+
"""
|
|
86
|
+
self._token = token
|
|
87
|
+
|
|
88
|
+
def _request(
|
|
89
|
+
self,
|
|
90
|
+
method: str,
|
|
91
|
+
endpoint: str,
|
|
92
|
+
data: dict[str, Any] | None = None,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
"""Make an authenticated request to GitHub API.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
method: HTTP method (GET, POST, PATCH, DELETE).
|
|
98
|
+
endpoint: API endpoint (e.g., /repos/owner/repo).
|
|
99
|
+
data: Request body data.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Response JSON as dict.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
GitHubError: If request fails.
|
|
106
|
+
"""
|
|
107
|
+
url = urljoin(self.API_BASE, endpoint)
|
|
108
|
+
headers = {
|
|
109
|
+
"Authorization": f"Bearer {self._token}",
|
|
110
|
+
"Accept": "application/vnd.github+json",
|
|
111
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
body = None
|
|
115
|
+
if data:
|
|
116
|
+
body = json.dumps(data).encode("utf-8")
|
|
117
|
+
headers["Content-Type"] = "application/json"
|
|
118
|
+
|
|
119
|
+
request = Request(url, data=body, headers=headers, method=method)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
with urlopen(request, timeout=30) as response:
|
|
123
|
+
response_body = response.read().decode("utf-8")
|
|
124
|
+
if response_body:
|
|
125
|
+
result: dict[str, Any] = json.loads(response_body)
|
|
126
|
+
return result
|
|
127
|
+
return {}
|
|
128
|
+
except HTTPError as e:
|
|
129
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
130
|
+
if e.code == 401:
|
|
131
|
+
raise GitHubAuthError(
|
|
132
|
+
f"Authentication failed: {error_body}", e.code
|
|
133
|
+
) from e
|
|
134
|
+
raise GitHubError(f"GitHub API error: {error_body}", e.code) from e
|
|
135
|
+
|
|
136
|
+
def get_repository(self, owner: str, repo: str) -> Repository:
|
|
137
|
+
"""Get repository information.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
owner: Repository owner (user or org).
|
|
141
|
+
repo: Repository name.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Repository data.
|
|
145
|
+
"""
|
|
146
|
+
data = self._request("GET", f"/repos/{owner}/{repo}")
|
|
147
|
+
return Repository(
|
|
148
|
+
full_name=data["full_name"],
|
|
149
|
+
default_branch=data["default_branch"],
|
|
150
|
+
clone_url=data["clone_url"],
|
|
151
|
+
private=data["private"],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def create_branch(
|
|
155
|
+
self,
|
|
156
|
+
owner: str,
|
|
157
|
+
repo: str,
|
|
158
|
+
branch_name: str,
|
|
159
|
+
from_ref: str = "HEAD",
|
|
160
|
+
) -> str:
|
|
161
|
+
"""Create a new branch.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
owner: Repository owner.
|
|
165
|
+
repo: Repository name.
|
|
166
|
+
branch_name: New branch name.
|
|
167
|
+
from_ref: Base reference (branch, tag, or SHA).
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
The created ref name.
|
|
171
|
+
"""
|
|
172
|
+
# Get the SHA of the base ref
|
|
173
|
+
ref_data = self._request(
|
|
174
|
+
"GET", f"/repos/{owner}/{repo}/git/ref/heads/{from_ref}"
|
|
175
|
+
)
|
|
176
|
+
sha = ref_data["object"]["sha"]
|
|
177
|
+
|
|
178
|
+
# Create new branch
|
|
179
|
+
self._request(
|
|
180
|
+
"POST",
|
|
181
|
+
f"/repos/{owner}/{repo}/git/refs",
|
|
182
|
+
data={"ref": f"refs/heads/{branch_name}", "sha": sha},
|
|
183
|
+
)
|
|
184
|
+
return f"refs/heads/{branch_name}"
|
|
185
|
+
|
|
186
|
+
def create_pull_request(
|
|
187
|
+
self,
|
|
188
|
+
owner: str,
|
|
189
|
+
repo: str,
|
|
190
|
+
title: str,
|
|
191
|
+
head: str,
|
|
192
|
+
base: str,
|
|
193
|
+
body: str = "",
|
|
194
|
+
draft: bool = False,
|
|
195
|
+
) -> PullRequest:
|
|
196
|
+
"""Create a pull request.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
owner: Repository owner.
|
|
200
|
+
repo: Repository name.
|
|
201
|
+
title: PR title.
|
|
202
|
+
head: Source branch.
|
|
203
|
+
base: Target branch.
|
|
204
|
+
body: PR description.
|
|
205
|
+
draft: Create as draft PR.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Created PullRequest.
|
|
209
|
+
"""
|
|
210
|
+
data = self._request(
|
|
211
|
+
"POST",
|
|
212
|
+
f"/repos/{owner}/{repo}/pulls",
|
|
213
|
+
data={
|
|
214
|
+
"title": title,
|
|
215
|
+
"head": head,
|
|
216
|
+
"base": base,
|
|
217
|
+
"body": body,
|
|
218
|
+
"draft": draft,
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
return PullRequest(
|
|
222
|
+
number=data["number"],
|
|
223
|
+
title=data["title"],
|
|
224
|
+
url=data["html_url"],
|
|
225
|
+
state=data["state"],
|
|
226
|
+
head_branch=data["head"]["ref"],
|
|
227
|
+
base_branch=data["base"]["ref"],
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def get_pull_request(
|
|
231
|
+
self,
|
|
232
|
+
owner: str,
|
|
233
|
+
repo: str,
|
|
234
|
+
pr_number: int,
|
|
235
|
+
) -> PullRequest:
|
|
236
|
+
"""Get a pull request.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
owner: Repository owner.
|
|
240
|
+
repo: Repository name.
|
|
241
|
+
pr_number: PR number.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
PullRequest data.
|
|
245
|
+
"""
|
|
246
|
+
data = self._request("GET", f"/repos/{owner}/{repo}/pulls/{pr_number}")
|
|
247
|
+
return PullRequest(
|
|
248
|
+
number=data["number"],
|
|
249
|
+
title=data["title"],
|
|
250
|
+
url=data["html_url"],
|
|
251
|
+
state=data["state"],
|
|
252
|
+
head_branch=data["head"]["ref"],
|
|
253
|
+
base_branch=data["base"]["ref"],
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def list_pull_requests(
|
|
257
|
+
self,
|
|
258
|
+
owner: str,
|
|
259
|
+
repo: str,
|
|
260
|
+
state: str = "open",
|
|
261
|
+
head: str | None = None,
|
|
262
|
+
) -> list[PullRequest]:
|
|
263
|
+
"""List pull requests.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
owner: Repository owner.
|
|
267
|
+
repo: Repository name.
|
|
268
|
+
state: Filter by state (open, closed, all).
|
|
269
|
+
head: Filter by head branch (owner:branch).
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of PullRequests.
|
|
273
|
+
"""
|
|
274
|
+
endpoint = f"/repos/{owner}/{repo}/pulls?state={state}"
|
|
275
|
+
if head:
|
|
276
|
+
endpoint += f"&head={head}"
|
|
277
|
+
|
|
278
|
+
data = self._request("GET", endpoint)
|
|
279
|
+
# data is a list of dicts, but mypy sees it as dict[str, Any]
|
|
280
|
+
if not isinstance(data, list):
|
|
281
|
+
return []
|
|
282
|
+
return [
|
|
283
|
+
PullRequest(
|
|
284
|
+
number=int(pr["number"])
|
|
285
|
+
if isinstance(pr.get("number"), (int, str))
|
|
286
|
+
else 0,
|
|
287
|
+
title=str(pr["title"]) if "title" in pr else "",
|
|
288
|
+
url=str(pr["html_url"]) if "html_url" in pr else "",
|
|
289
|
+
state=str(pr["state"]) if "state" in pr else "",
|
|
290
|
+
head_branch=str(pr["head"]["ref"])
|
|
291
|
+
if "head" in pr and "ref" in pr["head"]
|
|
292
|
+
else "",
|
|
293
|
+
base_branch=str(pr["base"]["ref"])
|
|
294
|
+
if "base" in pr and "ref" in pr["base"]
|
|
295
|
+
else "",
|
|
296
|
+
)
|
|
297
|
+
for pr in data
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
def get_authenticated_user(self) -> dict[str, Any]:
|
|
301
|
+
"""Get the authenticated user.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
User data including login, name, email.
|
|
305
|
+
"""
|
|
306
|
+
return self._request("GET", "/user")
|
|
307
|
+
|
|
308
|
+
def validate_token(self) -> bool:
|
|
309
|
+
"""Validate the access token.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
True if token is valid.
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
GitHubAuthError: If token is invalid.
|
|
316
|
+
"""
|
|
317
|
+
try:
|
|
318
|
+
self.get_authenticated_user()
|
|
319
|
+
return True
|
|
320
|
+
except GitHubAuthError:
|
|
321
|
+
return False
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Git Adapter Protocol.
|
|
2
|
+
|
|
3
|
+
Defines the interface for Git operations following Hexagonal Architecture.
|
|
4
|
+
The core logic depends on this protocol, not on specific implementations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Protocol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class GitStatus:
|
|
16
|
+
"""Status of a Git workspace."""
|
|
17
|
+
|
|
18
|
+
branch: str
|
|
19
|
+
"""Current branch name."""
|
|
20
|
+
|
|
21
|
+
is_clean: bool
|
|
22
|
+
"""True if working directory has no changes."""
|
|
23
|
+
|
|
24
|
+
modified_files: list[str] = field(default_factory=list)
|
|
25
|
+
"""List of modified file paths relative to workspace root."""
|
|
26
|
+
|
|
27
|
+
staged_files: list[str] = field(default_factory=list)
|
|
28
|
+
"""List of staged file paths."""
|
|
29
|
+
|
|
30
|
+
untracked_files: list[str] = field(default_factory=list)
|
|
31
|
+
"""List of untracked file paths."""
|
|
32
|
+
|
|
33
|
+
ahead: int = 0
|
|
34
|
+
"""Number of commits ahead of remote."""
|
|
35
|
+
|
|
36
|
+
behind: int = 0
|
|
37
|
+
"""Number of commits behind remote."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CommitResult:
|
|
42
|
+
"""Result of a commit operation."""
|
|
43
|
+
|
|
44
|
+
sha: str
|
|
45
|
+
"""The commit SHA."""
|
|
46
|
+
|
|
47
|
+
message: str
|
|
48
|
+
"""The commit message."""
|
|
49
|
+
|
|
50
|
+
files_changed: int
|
|
51
|
+
"""Number of files changed in this commit."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GitAdapterProtocol(Protocol):
|
|
55
|
+
"""Port for Git operations.
|
|
56
|
+
|
|
57
|
+
Implementations can use git CLI, dulwich, gitpython, etc.
|
|
58
|
+
This protocol abstracts the Git implementation details.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
async def clone(
|
|
62
|
+
self,
|
|
63
|
+
repo_url: str,
|
|
64
|
+
target_dir: Path,
|
|
65
|
+
*,
|
|
66
|
+
auth_token: str | None = None,
|
|
67
|
+
branch: str | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Clone a repository to the target directory.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
repo_url: The repository URL (HTTPS or SSH).
|
|
73
|
+
target_dir: Local path to clone into.
|
|
74
|
+
auth_token: Personal access token for HTTPS auth.
|
|
75
|
+
branch: Specific branch to clone (default: default branch).
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
GitError: If clone fails (auth, network, etc.).
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
async def commit(
|
|
83
|
+
self,
|
|
84
|
+
workspace: Path,
|
|
85
|
+
message: str,
|
|
86
|
+
*,
|
|
87
|
+
author: str | None = None,
|
|
88
|
+
) -> CommitResult:
|
|
89
|
+
"""Stage all changes and create a commit.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
workspace: Path to the Git workspace.
|
|
93
|
+
message: Commit message.
|
|
94
|
+
author: Author string (default: from git config).
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
CommitResult with SHA and metadata.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
GitError: If commit fails or no changes to commit.
|
|
101
|
+
"""
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
async def push(
|
|
105
|
+
self,
|
|
106
|
+
workspace: Path,
|
|
107
|
+
branch: str | None = None,
|
|
108
|
+
*,
|
|
109
|
+
force: bool = False,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Push commits to remote.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
workspace: Path to the Git workspace.
|
|
115
|
+
branch: Branch to push (default: current branch).
|
|
116
|
+
force: Force push (use with caution).
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
GitError: If push fails (auth, conflicts, etc.).
|
|
120
|
+
"""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
async def pull(
|
|
124
|
+
self,
|
|
125
|
+
workspace: Path,
|
|
126
|
+
*,
|
|
127
|
+
rebase: bool = True,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Pull latest changes from remote.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
workspace: Path to the Git workspace.
|
|
133
|
+
rebase: If True, use --rebase (default).
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
GitError: If pull fails (conflicts, etc.).
|
|
137
|
+
"""
|
|
138
|
+
...
|
|
139
|
+
|
|
140
|
+
async def create_branch(
|
|
141
|
+
self,
|
|
142
|
+
workspace: Path,
|
|
143
|
+
name: str,
|
|
144
|
+
*,
|
|
145
|
+
checkout: bool = True,
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Create a new branch.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
workspace: Path to the Git workspace.
|
|
151
|
+
name: Branch name.
|
|
152
|
+
checkout: If True, switch to the new branch.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
GitError: If branch creation fails.
|
|
156
|
+
"""
|
|
157
|
+
...
|
|
158
|
+
|
|
159
|
+
async def checkout(
|
|
160
|
+
self,
|
|
161
|
+
workspace: Path,
|
|
162
|
+
ref: str,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Checkout a branch or commit.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
workspace: Path to the Git workspace.
|
|
168
|
+
ref: Branch name, tag, or commit SHA.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
GitError: If checkout fails.
|
|
172
|
+
"""
|
|
173
|
+
...
|
|
174
|
+
|
|
175
|
+
async def get_status(
|
|
176
|
+
self,
|
|
177
|
+
workspace: Path,
|
|
178
|
+
) -> GitStatus:
|
|
179
|
+
"""Get the current status of the workspace.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
workspace: Path to the Git workspace.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
GitStatus with branch info and file changes.
|
|
186
|
+
"""
|
|
187
|
+
...
|
|
188
|
+
|
|
189
|
+
async def get_current_branch(
|
|
190
|
+
self,
|
|
191
|
+
workspace: Path,
|
|
192
|
+
) -> str:
|
|
193
|
+
"""Get the name of the current branch.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
workspace: Path to the Git workspace.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Branch name.
|
|
200
|
+
"""
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
async def fetch(
|
|
204
|
+
self,
|
|
205
|
+
workspace: Path,
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Fetch updates from remote without merging.
|
|
208
|
+
|
|
209
|
+
Used to check for available updates (Updates Available indicator).
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
workspace: Path to the Git workspace.
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
GitError: If fetch fails (auth, network, etc.).
|
|
216
|
+
"""
|
|
217
|
+
...
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class GitError(Exception):
|
|
221
|
+
"""Base exception for Git operations."""
|
|
222
|
+
|
|
223
|
+
def __init__(self, message: str, returncode: int = 1):
|
|
224
|
+
"""Initialize GitError.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
message: Error description.
|
|
228
|
+
returncode: Git command exit code.
|
|
229
|
+
"""
|
|
230
|
+
super().__init__(message)
|
|
231
|
+
self.returncode = returncode
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class GitAuthError(GitError):
|
|
235
|
+
"""Authentication failed."""
|
|
236
|
+
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class GitConflictError(GitError):
|
|
241
|
+
"""Merge/rebase conflict occurred."""
|
|
242
|
+
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class GitNetworkError(GitError):
|
|
247
|
+
"""Network-related error (timeout, DNS, etc.)."""
|
|
248
|
+
|
|
249
|
+
pass
|
|
File without changes
|