elizaos-plugin-github 2.0.0a4__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.
- elizaos_plugin_github/__init__.py +113 -0
- elizaos_plugin_github/actions/__init__.py +27 -0
- elizaos_plugin_github/actions/create_branch.py +62 -0
- elizaos_plugin_github/actions/create_comment.py +67 -0
- elizaos_plugin_github/actions/create_issue.py +93 -0
- elizaos_plugin_github/actions/create_pull_request.py +73 -0
- elizaos_plugin_github/actions/merge_pull_request.py +82 -0
- elizaos_plugin_github/actions/push_code.py +83 -0
- elizaos_plugin_github/actions/review_pull_request.py +84 -0
- elizaos_plugin_github/config.py +68 -0
- elizaos_plugin_github/error.py +165 -0
- elizaos_plugin_github/generated/specs/__init__.py +1 -0
- elizaos_plugin_github/generated/specs/specs.py +171 -0
- elizaos_plugin_github/providers/__init__.py +12 -0
- elizaos_plugin_github/providers/issue_context.py +136 -0
- elizaos_plugin_github/providers/repository_state.py +87 -0
- elizaos_plugin_github/py.typed +0 -0
- elizaos_plugin_github/service.py +716 -0
- elizaos_plugin_github/types.py +413 -0
- elizaos_plugin_github-2.0.0a4.dist-info/METADATA +178 -0
- elizaos_plugin_github-2.0.0a4.dist-info/RECORD +22 -0
- elizaos_plugin_github-2.0.0a4.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from github import Auth, Github, GithubException
|
|
6
|
+
from github.Issue import Issue
|
|
7
|
+
from github.IssueComment import IssueComment
|
|
8
|
+
from github.PullRequest import PullRequest
|
|
9
|
+
from github.PullRequestReview import PullRequestReview
|
|
10
|
+
from github.Repository import Repository
|
|
11
|
+
|
|
12
|
+
from elizaos_plugin_github.config import GitHubConfig
|
|
13
|
+
from elizaos_plugin_github.error import (
|
|
14
|
+
BranchExistsError,
|
|
15
|
+
BranchNotFoundError,
|
|
16
|
+
ClientNotInitializedError,
|
|
17
|
+
FileNotFoundError,
|
|
18
|
+
GitHubApiError,
|
|
19
|
+
IssueNotFoundError,
|
|
20
|
+
MergeConflictError,
|
|
21
|
+
PermissionDeniedError,
|
|
22
|
+
PullRequestNotFoundError,
|
|
23
|
+
RateLimitedError,
|
|
24
|
+
RepositoryNotFoundError,
|
|
25
|
+
)
|
|
26
|
+
from elizaos_plugin_github.types import (
|
|
27
|
+
CreateBranchParams,
|
|
28
|
+
CreateCommentParams,
|
|
29
|
+
CreateCommitParams,
|
|
30
|
+
CreateIssueParams,
|
|
31
|
+
CreatePullRequestParams,
|
|
32
|
+
CreateReviewParams,
|
|
33
|
+
GitHubBranch,
|
|
34
|
+
GitHubBranchRef,
|
|
35
|
+
GitHubComment,
|
|
36
|
+
GitHubCommit,
|
|
37
|
+
GitHubCommitAuthor,
|
|
38
|
+
GitHubDirectoryEntry,
|
|
39
|
+
GitHubFileContent,
|
|
40
|
+
GitHubIssue,
|
|
41
|
+
GitHubLabel,
|
|
42
|
+
GitHubMilestone,
|
|
43
|
+
GitHubPullRequest,
|
|
44
|
+
GitHubRepository,
|
|
45
|
+
GitHubReview,
|
|
46
|
+
GitHubUser,
|
|
47
|
+
IssueState,
|
|
48
|
+
IssueStateReason,
|
|
49
|
+
ListIssuesParams,
|
|
50
|
+
ListPullRequestsParams,
|
|
51
|
+
MergeableState,
|
|
52
|
+
MergePullRequestParams,
|
|
53
|
+
PullRequestState,
|
|
54
|
+
RepositoryRef,
|
|
55
|
+
ReviewState,
|
|
56
|
+
UpdateIssueParams,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class GitHubService:
|
|
63
|
+
def __init__(self, config: GitHubConfig) -> None:
|
|
64
|
+
self._config = config
|
|
65
|
+
self._client: Github | None = None
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def config(self) -> GitHubConfig:
|
|
69
|
+
return self._config
|
|
70
|
+
|
|
71
|
+
def _get_client(self) -> Github:
|
|
72
|
+
if self._client is None:
|
|
73
|
+
raise ClientNotInitializedError()
|
|
74
|
+
return self._client
|
|
75
|
+
|
|
76
|
+
async def start(self) -> None:
|
|
77
|
+
logger.info("Starting GitHub service...")
|
|
78
|
+
|
|
79
|
+
self._config.validate_all()
|
|
80
|
+
|
|
81
|
+
auth = Auth.Token(self._config.api_token)
|
|
82
|
+
self._client = Github(auth=auth)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
user = self._client.get_user()
|
|
86
|
+
logger.info(f"GitHub service started - authenticated as {user.login}")
|
|
87
|
+
except GithubException as e:
|
|
88
|
+
raise self._map_exception(e, "", "") from e
|
|
89
|
+
|
|
90
|
+
async def stop(self) -> None:
|
|
91
|
+
logger.info("Stopping GitHub service...")
|
|
92
|
+
if self._client:
|
|
93
|
+
self._client.close()
|
|
94
|
+
self._client = None
|
|
95
|
+
logger.info("GitHub service stopped")
|
|
96
|
+
|
|
97
|
+
async def get_repository(self, params: RepositoryRef) -> GitHubRepository:
|
|
98
|
+
client = self._get_client()
|
|
99
|
+
owner, repo_name = self._resolve_repo_ref(params.owner, params.repo)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
repo = client.get_repo(f"{owner}/{repo_name}")
|
|
103
|
+
return self._map_repository(repo)
|
|
104
|
+
except GithubException as e:
|
|
105
|
+
raise self._map_exception(e, owner, repo_name) from e
|
|
106
|
+
|
|
107
|
+
async def create_issue(self, params: CreateIssueParams) -> GitHubIssue:
|
|
108
|
+
client = self._get_client()
|
|
109
|
+
owner, repo_name = self._resolve_repo_ref(params.owner, params.repo)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
repo = client.get_repo(f"{owner}/{repo_name}")
|
|
113
|
+
issue = repo.create_issue(
|
|
114
|
+
title=params.title,
|
|
115
|
+
body=params.body or "",
|
|
116
|
+
assignees=params.assignees,
|
|
117
|
+
labels=params.labels,
|
|
118
|
+
milestone=repo.get_milestone(params.milestone) if params.milestone else None,
|
|
119
|
+
)
|
|
120
|
+
return self._map_issue(issue)
|
|
121
|
+
except GithubException as e:
|
|
122
|
+
raise self._map_exception(e, owner, repo_name) from e
|
|
123
|
+
|
|
124
|
+
async def get_issue(self, owner: str, repo: str, issue_number: int) -> GitHubIssue:
|
|
125
|
+
client = self._get_client()
|
|
126
|
+
owner, repo_name = self._resolve_repo_ref(owner, repo)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
repo_obj = client.get_repo(f"{owner}/{repo_name}")
|
|
130
|
+
issue = repo_obj.get_issue(issue_number)
|
|
131
|
+
return self._map_issue(issue)
|
|
132
|
+
except GithubException as e:
|
|
133
|
+
exc = self._map_exception(e, owner, repo_name)
|
|
134
|
+
if isinstance(exc, RepositoryNotFoundError):
|
|
135
|
+
raise IssueNotFoundError(issue_number, owner, repo_name) from e
|
|
136
|
+
raise exc from e
|
|
137
|
+
|
|
138
|
+
async def update_issue(self, params: UpdateIssueParams) -> GitHubIssue:
|
|
139
|
+
client = self._get_client()
|
|
140
|
+
owner, repo_name = self._resolve_repo_ref(params.owner, params.repo)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
repo = client.get_repo(f"{owner}/{repo_name}")
|
|
144
|
+
issue = repo.get_issue(params.issue_number)
|
|
145
|
+
|
|
146
|
+
kwargs: dict[str, object] = {}
|
|
147
|
+
if params.title is not None:
|
|
148
|
+
kwargs["title"] = params.title
|
|
149
|
+
if params.body is not None:
|
|
150
|
+
kwargs["body"] = params.body
|
|
151
|
+
if params.state is not None:
|
|
152
|
+
kwargs["state"] = params.state.value
|
|
153
|
+
if params.assignees is not None:
|
|
154
|
+
kwargs["assignees"] = params.assignees
|
|
155
|
+
if params.labels is not None:
|
|
156
|
+
kwargs["labels"] = params.labels
|
|
157
|
+
if params.milestone is not None:
|
|
158
|
+
kwargs["milestone"] = repo.get_milestone(params.milestone)
|
|
159
|
+
|
|
160
|
+
issue.edit(**kwargs)
|
|
161
|
+
return self._map_issue(issue)
|
|
162
|
+
except GithubException as e:
|
|
163
|
+
raise self._map_exception(e, owner, repo_name) from e
|
|
164
|
+
|
|
165
|
+
async def list_issues(self, params: ListIssuesParams) -> list[GitHubIssue]:
|
|
166
|
+
client = self._get_client()
|
|
167
|
+
owner, repo_name = self._resolve_repo_ref(params.owner, params.repo)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
repo = client.get_repo(f"{owner}/{repo_name}")
|
|
171
|
+
issues = repo.get_issues(
|
|
172
|
+
state=params.state,
|
|
173
|
+
sort=params.sort,
|
|
174
|
+
direction=params.direction,
|
|
175
|
+
labels=params.labels.split(",") if params.labels else [],
|
|
176
|
+
assignee=params.assignee or "none",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
result: list[GitHubIssue] = []
|
|
180
|
+
start = (params.page - 1) * params.per_page
|
|
181
|
+
end = start + params.per_page
|
|
182
|
+
|
|
183
|
+
for i, issue in enumerate(issues):
|
|
184
|
+
if i < start:
|
|
185
|
+
continue
|
|
186
|
+
if i >= end:
|
|
187
|
+
break
|
|
188
|
+
if issue.pull_request is None:
|
|
189
|
+
result.append(self._map_issue(issue))
|
|
190
|
+
|
|
191
|
+
return result
|
|
192
|
+
except GithubException as e:
|
|
193
|
+
raise self._map_exception(e, owner, repo_name) from e
|
|
194
|
+
|
|
195
|
+
async def create_pull_request(self, params: CreatePullRequestParams) -> GitHubPullRequest:
|
|
196
|
+
client = self._get_client()
|
|
197
|
+
owner, repo_name = self._resolve_repo_ref(params.owner, params.repo)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
repo = client.get_repo(f"{owner}/{repo_name}")
|
|
201
|
+
pr = repo.create_pull(
|
|
202
|
+
title=params.title,
|
|
203
|
+
body=params.body or "",
|
|
204
|
+
head=params.head,
|
|
205
|
+
base=params.base,
|
|
206
|
+
draft=params.draft,
|
|
207
|
+
maintainer_can_modify=params.maintainer_can_modify,
|
|
208
|
+
)
|
|
209
|
+
return self._map_pull_request(pr)
|
|
210
|
+
except GithubException as e:
|
|
211
|
+
raise self._map_exception(e, owner, repo_name) from e
|
|
212
|
+
|
|
213
|
+
async def get_pull_request(self, owner: str, repo: str, pull_number: int) -> GitHubPullRequest:
|
|
214
|
+
client = self._get_client()
|
|
215
|
+
owner, repo_name = self._resolve_repo_ref(owner, repo)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
repo_obj = client.get_repo(f"{owner}/{repo_name}")
|
|
219
|
+
pr = repo_obj.get_pull(pull_number)
|
|
220
|
+
return self._map_pull_request(pr)
|
|
221
|
+
except GithubException as e:
|
|
222
|
+
exc = self._map_exception(e, owner, repo_name)
|
|
223
|
+
if isinstance(exc, RepositoryNotFoundError):
|
|
224
|
+
raise PullRequestNotFoundError(pull_number, owner, repo_name) from e
|
|
225
|
+
raise exc from e
|
|
226
|
+
|
|
227
|
+
async def list_pull_requests(self, params: ListPullRequestsParams) -> list[GitHubPullRequest]:
|
|
228
|
+
client = self._get_client()
|
|
229
|
+
owner, repo_name = self._resolve_repo_ref(params.owner, params.repo)
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
repo = client.get_repo(f"{owner}/{repo_name}")
|
|
233
|
+
prs = repo.get_pulls(
|
|
234
|
+
state=params.state,
|
|
235
|
+
sort=params.sort,
|
|
236
|
+
direction=params.direction,
|
|
237
|
+
head=params.head,
|
|
238
|
+
base=params.base,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
result: list[GitHubPullRequest] = []
|
|
242
|
+
start = (params.page - 1) * params.per_page
|
|
243
|
+
end = start + params.per_page
|
|
244
|
+
|
|
245
|
+
for i, pr in enumerate(prs):
|
|
246
|
+
if i < start:
|
|
247
|
+
continue
|
|
248
|
+
if i >= end:
|
|
249
|
+
break
|
|
250
|
+
result.append(self._map_pull_request(pr))
|
|
251
|
+
|
|
252
|
+
return result
|
|
253
|
+
except GithubException as e:
|
|
254
|
+
raise self._map_exception(e, owner, repo_name) from e
|
|
255
|
+
|
|
256
|
+
async def merge_pull_request(self, params: MergePullRequestParams) -> tuple[str, bool, str]:
|
|
257
|
+
client = self._get_client()
|
|
258
|
+
owner, repo_name = self._resolve_repo_ref(params.owner, params.repo)
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
repo = client.get_repo(f"{owner}/{repo_name}")
|
|
262
|
+
pr = repo.get_pull(params.pull_number)
|
|
263
|
+
|
|
264
|
+
result = pr.merge(
|
|
265
|
+
commit_title=params.commit_title,
|
|
266
|
+
commit_message=params.commit_message,
|
|
267
|
+
merge_method=params.merge_method,
|
|
268
|
+
sha=params.sha,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return (result.sha, result.merged, result.message)
|
|
272
|
+
except GithubException as e:
|
|
273
|
+
exc = self._map_exception(e, owner, repo_name)
|
|
274
|
+
if isinstance(exc, GitHubApiError) and exc.status == 405:
|
|
275
|
+
raise MergeConflictError(params.pull_number, owner, repo_name) from e
|
|
276
|
+
raise exc from e
|
|
277
|
+
|
|
278
|
+
async def create_review(self, params: CreateReviewParams) -> GitHubReview:
|
|
279
|
+
client = self._get_client()
|
|
280
|
+
owner, repo_name = self._resolve_repo_ref(params.owner, params.repo)
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
repo = client.get_repo(f"{owner}/{repo_name}")
|
|
284
|
+
pr = repo.get_pull(params.pull_number)
|
|
285
|
+
|
|
286
|
+
review = pr.create_review(
|
|
287
|
+
body=params.body,
|
|
288
|
+
event=params.event.value,
|
|
289
|
+
commit=repo.get_commit(params.commit_id) if params.commit_id else None,
|
|
290
|
+
comments=[
|
|
291
|
+
{
|
|
292
|
+
"path": c.path,
|
|
293
|
+
"line": c.line,
|
|
294
|
+
"body": c.body,
|
|
295
|
+
"side": c.side,
|
|
296
|
+
}
|
|
297
|
+
for c in params.comments
|
|
298
|
+
],
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return self._map_review(review)
|
|
302
|
+
except GithubException as e:
|
|
303
|
+
raise self._map_exception(e, owner, repo_name) from e
|
|
304
|
+
|
|
305
|
+
async def create_comment(self, params: CreateCommentParams) -> GitHubComment:
|
|
306
|
+
client = self._get_client()
|
|
307
|
+
owner, repo_name = self._resolve_repo_ref(params.owner, params.repo)
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
repo = client.get_repo(f"{owner}/{repo_name}")
|
|
311
|
+
issue = repo.get_issue(params.issue_number)
|
|
312
|
+
comment = issue.create_comment(params.body)
|
|
313
|
+
return self._map_comment(comment)
|
|
314
|
+
except GithubException as e:
|
|
315
|
+
raise self._map_exception(e, owner, repo_name) from e
|
|
316
|
+
|
|
317
|
+
# ===========================================================================
|
|
318
|
+
# Branch Operations
|
|
319
|
+
# ===========================================================================
|
|
320
|
+
|
|
321
|
+
async def create_branch(self, params: CreateBranchParams) -> GitHubBranch:
|
|
322
|
+
"""Create a new branch."""
|
|
323
|
+
client = self._get_client()
|
|
324
|
+
owner, repo_name = self._resolve_repo_ref(params.owner, params.repo)
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
repo = client.get_repo(f"{owner}/{repo_name}")
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
ref = repo.get_git_ref(f"heads/{params.from_ref}")
|
|
331
|
+
sha = ref.object.sha
|
|
332
|
+
except GithubException:
|
|
333
|
+
sha = params.from_ref
|
|
334
|
+
|
|
335
|
+
repo.create_git_ref(f"refs/heads/{params.branch_name}", sha)
|
|
336
|
+
|
|
337
|
+
return GitHubBranch(
|
|
338
|
+
name=params.branch_name,
|
|
339
|
+
sha=sha,
|
|
340
|
+
protected=False,
|
|
341
|
+
)
|
|
342
|
+
except GithubException as e:
|
|
343
|
+
exc = self._map_exception(e, owner, repo_name)
|
|
344
|
+
if "already exists" in str(e):
|
|
345
|
+
raise BranchExistsError(params.branch_name, owner, repo_name) from e
|
|
346
|
+
raise exc from e
|
|
347
|
+
|
|
348
|
+
async def delete_branch(self, owner: str, repo: str, branch_name: str) -> None:
|
|
349
|
+
client = self._get_client()
|
|
350
|
+
owner, repo_name = self._resolve_repo_ref(owner, repo)
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
repo_obj = client.get_repo(f"{owner}/{repo_name}")
|
|
354
|
+
ref = repo_obj.get_git_ref(f"heads/{branch_name}")
|
|
355
|
+
ref.delete()
|
|
356
|
+
except GithubException as e:
|
|
357
|
+
exc = self._map_exception(e, owner, repo_name)
|
|
358
|
+
if isinstance(exc, RepositoryNotFoundError):
|
|
359
|
+
raise BranchNotFoundError(branch_name, owner, repo_name) from e
|
|
360
|
+
raise exc from e
|
|
361
|
+
|
|
362
|
+
async def list_branches(
|
|
363
|
+
self, owner: str, repo: str, per_page: int = 30, page: int = 1
|
|
364
|
+
) -> list[GitHubBranch]:
|
|
365
|
+
client = self._get_client()
|
|
366
|
+
owner, repo_name = self._resolve_repo_ref(owner, repo)
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
repo_obj = client.get_repo(f"{owner}/{repo_name}")
|
|
370
|
+
branches = repo_obj.get_branches()
|
|
371
|
+
|
|
372
|
+
result: list[GitHubBranch] = []
|
|
373
|
+
start = (page - 1) * per_page
|
|
374
|
+
end = start + per_page
|
|
375
|
+
|
|
376
|
+
for i, branch in enumerate(branches):
|
|
377
|
+
if i < start:
|
|
378
|
+
continue
|
|
379
|
+
if i >= end:
|
|
380
|
+
break
|
|
381
|
+
result.append(
|
|
382
|
+
GitHubBranch(
|
|
383
|
+
name=branch.name,
|
|
384
|
+
sha=branch.commit.sha,
|
|
385
|
+
protected=branch.protected,
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return result
|
|
390
|
+
except GithubException as e:
|
|
391
|
+
raise self._map_exception(e, owner, repo_name) from e
|
|
392
|
+
|
|
393
|
+
async def get_file(
|
|
394
|
+
self, owner: str, repo: str, path: str, branch: str | None = None
|
|
395
|
+
) -> GitHubFileContent:
|
|
396
|
+
client = self._get_client()
|
|
397
|
+
owner, repo_name = self._resolve_repo_ref(owner, repo)
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
repo_obj = client.get_repo(f"{owner}/{repo_name}")
|
|
401
|
+
content = repo_obj.get_contents(path, ref=branch or self._config.branch)
|
|
402
|
+
|
|
403
|
+
if isinstance(content, list):
|
|
404
|
+
raise FileNotFoundError(f"{path} is a directory, not a file", owner, repo_name)
|
|
405
|
+
|
|
406
|
+
decoded_content = ""
|
|
407
|
+
if content.content:
|
|
408
|
+
decoded_content = base64.b64decode(content.content).decode("utf-8")
|
|
409
|
+
|
|
410
|
+
return GitHubFileContent(
|
|
411
|
+
name=content.name,
|
|
412
|
+
path=content.path,
|
|
413
|
+
content=decoded_content,
|
|
414
|
+
sha=content.sha,
|
|
415
|
+
size=content.size,
|
|
416
|
+
type="file",
|
|
417
|
+
encoding=content.encoding or "base64",
|
|
418
|
+
html_url=content.html_url,
|
|
419
|
+
download_url=content.download_url,
|
|
420
|
+
)
|
|
421
|
+
except GithubException as e:
|
|
422
|
+
exc = self._map_exception(e, owner, repo_name)
|
|
423
|
+
if isinstance(exc, RepositoryNotFoundError):
|
|
424
|
+
raise FileNotFoundError(path, owner, repo_name) from e
|
|
425
|
+
raise exc from e
|
|
426
|
+
|
|
427
|
+
async def list_directory(
|
|
428
|
+
self, owner: str, repo: str, path: str, branch: str | None = None
|
|
429
|
+
) -> list[GitHubDirectoryEntry]:
|
|
430
|
+
"""List directory contents."""
|
|
431
|
+
client = self._get_client()
|
|
432
|
+
owner, repo_name = self._resolve_repo_ref(owner, repo)
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
repo_obj = client.get_repo(f"{owner}/{repo_name}")
|
|
436
|
+
contents = repo_obj.get_contents(path, ref=branch or self._config.branch)
|
|
437
|
+
|
|
438
|
+
if not isinstance(contents, list):
|
|
439
|
+
raise FileNotFoundError(f"{path} is a file, not a directory", owner, repo_name)
|
|
440
|
+
|
|
441
|
+
return [
|
|
442
|
+
GitHubDirectoryEntry(
|
|
443
|
+
name=c.name,
|
|
444
|
+
path=c.path,
|
|
445
|
+
sha=c.sha,
|
|
446
|
+
size=c.size,
|
|
447
|
+
type=c.type,
|
|
448
|
+
html_url=c.html_url,
|
|
449
|
+
download_url=c.download_url,
|
|
450
|
+
)
|
|
451
|
+
for c in contents
|
|
452
|
+
]
|
|
453
|
+
except GithubException as e:
|
|
454
|
+
raise self._map_exception(e, owner, repo_name) from e
|
|
455
|
+
|
|
456
|
+
async def create_commit(self, params: CreateCommitParams) -> GitHubCommit:
|
|
457
|
+
client = self._get_client()
|
|
458
|
+
owner, repo_name = self._resolve_repo_ref(params.owner, params.repo)
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
repo = client.get_repo(f"{owner}/{repo_name}")
|
|
462
|
+
|
|
463
|
+
ref = repo.get_git_ref(f"heads/{params.branch}")
|
|
464
|
+
parent_sha = params.parent_sha or ref.object.sha
|
|
465
|
+
|
|
466
|
+
parent_commit = repo.get_git_commit(parent_sha)
|
|
467
|
+
|
|
468
|
+
tree_elements = []
|
|
469
|
+
for file in params.files:
|
|
470
|
+
if file.operation == "delete":
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
content = file.content
|
|
474
|
+
if file.encoding == "base64":
|
|
475
|
+
blob = repo.create_git_blob(content, "base64")
|
|
476
|
+
else:
|
|
477
|
+
blob = repo.create_git_blob(content, "utf-8")
|
|
478
|
+
|
|
479
|
+
tree_elements.append(
|
|
480
|
+
{
|
|
481
|
+
"path": file.path,
|
|
482
|
+
"mode": "100644",
|
|
483
|
+
"type": "blob",
|
|
484
|
+
"sha": blob.sha,
|
|
485
|
+
}
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# Create tree
|
|
489
|
+
new_tree = repo.create_git_tree(tree_elements, parent_commit.tree)
|
|
490
|
+
|
|
491
|
+
# Create commit
|
|
492
|
+
author_name = params.author_name or client.get_user().name or "elizaos"
|
|
493
|
+
author_email = params.author_email or f"{author_name}@users.noreply.github.com"
|
|
494
|
+
|
|
495
|
+
commit = repo.create_git_commit(
|
|
496
|
+
message=params.message,
|
|
497
|
+
tree=new_tree,
|
|
498
|
+
parents=[parent_commit],
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
ref.edit(commit.sha)
|
|
502
|
+
|
|
503
|
+
return GitHubCommit(
|
|
504
|
+
sha=commit.sha,
|
|
505
|
+
message=commit.message,
|
|
506
|
+
author=GitHubCommitAuthor(
|
|
507
|
+
name=author_name,
|
|
508
|
+
email=author_email,
|
|
509
|
+
date=datetime.now(UTC).isoformat(),
|
|
510
|
+
),
|
|
511
|
+
committer=GitHubCommitAuthor(
|
|
512
|
+
name=author_name,
|
|
513
|
+
email=author_email,
|
|
514
|
+
date=datetime.now(UTC).isoformat(),
|
|
515
|
+
),
|
|
516
|
+
timestamp=datetime.now(UTC).isoformat(),
|
|
517
|
+
html_url=commit.html_url,
|
|
518
|
+
parents=[parent_sha],
|
|
519
|
+
)
|
|
520
|
+
except GithubException as e:
|
|
521
|
+
raise self._map_exception(e, owner, repo_name) from e
|
|
522
|
+
|
|
523
|
+
async def get_authenticated_user(self) -> GitHubUser:
|
|
524
|
+
client = self._get_client()
|
|
525
|
+
user = client.get_user()
|
|
526
|
+
return self._map_user_from_named_user(user)
|
|
527
|
+
|
|
528
|
+
def _resolve_repo_ref(self, owner: str | None, repo: str | None) -> tuple[str, str]:
|
|
529
|
+
return self._config.get_repository_ref(owner, repo)
|
|
530
|
+
|
|
531
|
+
def _map_exception(self, e: GithubException, owner: str, repo: str) -> Exception:
|
|
532
|
+
status = e.status
|
|
533
|
+
|
|
534
|
+
if status == 401:
|
|
535
|
+
return PermissionDeniedError("Invalid or missing authentication token")
|
|
536
|
+
|
|
537
|
+
if status == 403:
|
|
538
|
+
# Check for rate limiting
|
|
539
|
+
rate_limit = e.headers.get("X-RateLimit-Remaining")
|
|
540
|
+
if rate_limit == "0":
|
|
541
|
+
reset = e.headers.get("X-RateLimit-Reset")
|
|
542
|
+
if reset:
|
|
543
|
+
reset_time = datetime.fromtimestamp(int(reset), tz=UTC)
|
|
544
|
+
retry_after = int((reset_time - datetime.now(UTC)).total_seconds() * 1000)
|
|
545
|
+
return RateLimitedError(max(retry_after, 0), 0, reset_time)
|
|
546
|
+
return PermissionDeniedError(str(e.data))
|
|
547
|
+
|
|
548
|
+
if status == 404:
|
|
549
|
+
return RepositoryNotFoundError(owner, repo)
|
|
550
|
+
|
|
551
|
+
if status == 409:
|
|
552
|
+
message = str(e.data) if e.data else ""
|
|
553
|
+
if "merge" in message.lower():
|
|
554
|
+
return MergeConflictError(0, owner, repo)
|
|
555
|
+
if "already exists" in message.lower():
|
|
556
|
+
return BranchExistsError("unknown", owner, repo)
|
|
557
|
+
|
|
558
|
+
if status == 422:
|
|
559
|
+
return GitHubApiError(status, str(e.data))
|
|
560
|
+
|
|
561
|
+
return GitHubApiError(status, str(e.data))
|
|
562
|
+
|
|
563
|
+
def _map_repository(self, repo: Repository) -> GitHubRepository:
|
|
564
|
+
return GitHubRepository(
|
|
565
|
+
id=repo.id,
|
|
566
|
+
name=repo.name,
|
|
567
|
+
full_name=repo.full_name,
|
|
568
|
+
owner=self._map_user_from_named_user(repo.owner),
|
|
569
|
+
description=repo.description,
|
|
570
|
+
private=repo.private,
|
|
571
|
+
fork=repo.fork,
|
|
572
|
+
default_branch=repo.default_branch,
|
|
573
|
+
language=repo.language,
|
|
574
|
+
stargazers_count=repo.stargazers_count,
|
|
575
|
+
forks_count=repo.forks_count,
|
|
576
|
+
open_issues_count=repo.open_issues_count,
|
|
577
|
+
watchers_count=repo.watchers_count,
|
|
578
|
+
html_url=repo.html_url,
|
|
579
|
+
clone_url=repo.clone_url,
|
|
580
|
+
ssh_url=repo.ssh_url,
|
|
581
|
+
created_at=repo.created_at.isoformat() if repo.created_at else "",
|
|
582
|
+
updated_at=repo.updated_at.isoformat() if repo.updated_at else "",
|
|
583
|
+
pushed_at=repo.pushed_at.isoformat() if repo.pushed_at else "",
|
|
584
|
+
topics=repo.topics or [],
|
|
585
|
+
license=None, # Would need mapping
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
def _map_user_from_named_user(self, user: object) -> GitHubUser:
|
|
589
|
+
return GitHubUser(
|
|
590
|
+
id=user.id, # type: ignore
|
|
591
|
+
login=user.login, # type: ignore
|
|
592
|
+
name=getattr(user, "name", None),
|
|
593
|
+
avatar_url=user.avatar_url, # type: ignore
|
|
594
|
+
html_url=user.html_url, # type: ignore
|
|
595
|
+
type=user.type, # type: ignore
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
def _map_issue(self, issue: Issue) -> GitHubIssue:
|
|
599
|
+
return GitHubIssue(
|
|
600
|
+
number=issue.number,
|
|
601
|
+
title=issue.title,
|
|
602
|
+
body=issue.body,
|
|
603
|
+
state=IssueState(issue.state),
|
|
604
|
+
state_reason=IssueStateReason(issue.state_reason) if issue.state_reason else None,
|
|
605
|
+
user=self._map_user_from_named_user(issue.user),
|
|
606
|
+
assignees=[self._map_user_from_named_user(a) for a in issue.assignees],
|
|
607
|
+
labels=[
|
|
608
|
+
GitHubLabel(
|
|
609
|
+
id=label.id,
|
|
610
|
+
name=label.name,
|
|
611
|
+
color=label.color,
|
|
612
|
+
description=label.description,
|
|
613
|
+
default=label.default,
|
|
614
|
+
)
|
|
615
|
+
for label in issue.labels
|
|
616
|
+
],
|
|
617
|
+
milestone=(
|
|
618
|
+
GitHubMilestone(
|
|
619
|
+
number=issue.milestone.number,
|
|
620
|
+
title=issue.milestone.title,
|
|
621
|
+
description=issue.milestone.description,
|
|
622
|
+
state=issue.milestone.state,
|
|
623
|
+
due_on=issue.milestone.due_on.isoformat() if issue.milestone.due_on else None,
|
|
624
|
+
created_at=issue.milestone.created_at.isoformat(),
|
|
625
|
+
updated_at=issue.milestone.updated_at.isoformat(),
|
|
626
|
+
closed_at=(
|
|
627
|
+
issue.milestone.closed_at.isoformat() if issue.milestone.closed_at else None
|
|
628
|
+
),
|
|
629
|
+
open_issues=issue.milestone.open_issues,
|
|
630
|
+
closed_issues=issue.milestone.closed_issues,
|
|
631
|
+
)
|
|
632
|
+
if issue.milestone
|
|
633
|
+
else None
|
|
634
|
+
),
|
|
635
|
+
created_at=issue.created_at.isoformat(),
|
|
636
|
+
updated_at=issue.updated_at.isoformat(),
|
|
637
|
+
closed_at=issue.closed_at.isoformat() if issue.closed_at else None,
|
|
638
|
+
html_url=issue.html_url,
|
|
639
|
+
comments=issue.comments,
|
|
640
|
+
is_pull_request=issue.pull_request is not None,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
def _map_pull_request(self, pr: PullRequest) -> GitHubPullRequest:
|
|
644
|
+
"""Map pull request to type."""
|
|
645
|
+
return GitHubPullRequest(
|
|
646
|
+
number=pr.number,
|
|
647
|
+
title=pr.title,
|
|
648
|
+
body=pr.body,
|
|
649
|
+
state=PullRequestState(pr.state),
|
|
650
|
+
draft=pr.draft,
|
|
651
|
+
merged=pr.merged,
|
|
652
|
+
mergeable=pr.mergeable,
|
|
653
|
+
mergeable_state=MergeableState(pr.mergeable_state)
|
|
654
|
+
if pr.mergeable_state
|
|
655
|
+
else MergeableState.UNKNOWN,
|
|
656
|
+
user=self._map_user_from_named_user(pr.user),
|
|
657
|
+
head=GitHubBranchRef(
|
|
658
|
+
ref=pr.head.ref,
|
|
659
|
+
label=pr.head.label,
|
|
660
|
+
sha=pr.head.sha,
|
|
661
|
+
repo=RepositoryRef(owner=pr.head.repo.owner.login, repo=pr.head.repo.name)
|
|
662
|
+
if pr.head.repo
|
|
663
|
+
else None,
|
|
664
|
+
),
|
|
665
|
+
base=GitHubBranchRef(
|
|
666
|
+
ref=pr.base.ref,
|
|
667
|
+
label=pr.base.label,
|
|
668
|
+
sha=pr.base.sha,
|
|
669
|
+
repo=RepositoryRef(owner=pr.base.repo.owner.login, repo=pr.base.repo.name)
|
|
670
|
+
if pr.base.repo
|
|
671
|
+
else None,
|
|
672
|
+
),
|
|
673
|
+
assignees=[self._map_user_from_named_user(a) for a in pr.assignees],
|
|
674
|
+
requested_reviewers=[self._map_user_from_named_user(r) for r in pr.requested_reviewers],
|
|
675
|
+
labels=[
|
|
676
|
+
GitHubLabel(
|
|
677
|
+
id=label.id,
|
|
678
|
+
name=label.name,
|
|
679
|
+
color=label.color,
|
|
680
|
+
description=label.description,
|
|
681
|
+
default=label.default,
|
|
682
|
+
)
|
|
683
|
+
for label in pr.labels
|
|
684
|
+
],
|
|
685
|
+
milestone=None, # Would need full mapping
|
|
686
|
+
created_at=pr.created_at.isoformat(),
|
|
687
|
+
updated_at=pr.updated_at.isoformat(),
|
|
688
|
+
closed_at=pr.closed_at.isoformat() if pr.closed_at else None,
|
|
689
|
+
merged_at=pr.merged_at.isoformat() if pr.merged_at else None,
|
|
690
|
+
html_url=pr.html_url,
|
|
691
|
+
commits=pr.commits,
|
|
692
|
+
additions=pr.additions,
|
|
693
|
+
deletions=pr.deletions,
|
|
694
|
+
changed_files=pr.changed_files,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
def _map_review(self, review: PullRequestReview) -> GitHubReview:
|
|
698
|
+
return GitHubReview(
|
|
699
|
+
id=review.id,
|
|
700
|
+
user=self._map_user_from_named_user(review.user),
|
|
701
|
+
body=review.body,
|
|
702
|
+
state=ReviewState(review.state),
|
|
703
|
+
commit_id=review.commit_id,
|
|
704
|
+
html_url=review.html_url,
|
|
705
|
+
submitted_at=review.submitted_at.isoformat() if review.submitted_at else None,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
def _map_comment(self, comment: IssueComment) -> GitHubComment:
|
|
709
|
+
return GitHubComment(
|
|
710
|
+
id=comment.id,
|
|
711
|
+
body=comment.body,
|
|
712
|
+
user=self._map_user_from_named_user(comment.user),
|
|
713
|
+
created_at=comment.created_at.isoformat(),
|
|
714
|
+
updated_at=comment.updated_at.isoformat(),
|
|
715
|
+
html_url=comment.html_url,
|
|
716
|
+
)
|