git-alternative 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.
- git_alternative/__init__.py +63 -0
- git_alternative/ci.py +162 -0
- git_alternative/cli.py +117 -0
- git_alternative/client.py +72 -0
- git_alternative/exceptions.py +43 -0
- git_alternative/http.py +137 -0
- git_alternative/managers/__init__.py +1 -0
- git_alternative/managers/issues.py +292 -0
- git_alternative/managers/repos.py +218 -0
- git_alternative/models.py +291 -0
- git_alternative/pagination.py +45 -0
- git_alternative/resources/__init__.py +1 -0
- git_alternative/resources/issues.py +291 -0
- git_alternative/resources/labels.py +91 -0
- git_alternative/resources/pipelines.py +118 -0
- git_alternative/resources/pulls.py +207 -0
- git_alternative/resources/repos.py +184 -0
- git_alternative/resources/ssh_keys.py +64 -0
- git_alternative/ssh_keys.py +80 -0
- git_alternative/utils.py +91 -0
- git_alternative/webhooks.py +104 -0
- git_alternative-0.2.2.dist-info/METADATA +36 -0
- git_alternative-0.2.2.dist-info/RECORD +26 -0
- git_alternative-0.2.2.dist-info/WHEEL +4 -0
- git_alternative-0.2.2.dist-info/entry_points.txt +3 -0
- git_alternative-0.2.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Data models for the Forge Git collaboration platform SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Generic, TypeVar
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IssueState(str, Enum):
|
|
14
|
+
"""Lifecycle state of an issue."""
|
|
15
|
+
|
|
16
|
+
OPEN = "open"
|
|
17
|
+
CLOSED = "closed"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PrState(str, Enum):
|
|
21
|
+
"""Lifecycle state of a pull request."""
|
|
22
|
+
|
|
23
|
+
OPEN = "open"
|
|
24
|
+
MERGED = "merged"
|
|
25
|
+
CLOSED = "closed"
|
|
26
|
+
DRAFT = "draft"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MergeStrategy(str, Enum):
|
|
30
|
+
"""Strategy used to merge a pull request."""
|
|
31
|
+
|
|
32
|
+
MERGE = "merge"
|
|
33
|
+
SQUASH = "squash"
|
|
34
|
+
REBASE = "rebase"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PipelineState(str, Enum):
|
|
38
|
+
"""Execution state of a CI pipeline or job."""
|
|
39
|
+
|
|
40
|
+
PENDING = "pending"
|
|
41
|
+
RUNNING = "running"
|
|
42
|
+
SUCCESS = "success"
|
|
43
|
+
FAILED = "failed"
|
|
44
|
+
CANCELLED = "cancelled"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PipelineTrigger(str, Enum):
|
|
48
|
+
"""Event that triggered a CI pipeline."""
|
|
49
|
+
|
|
50
|
+
PUSH = "push"
|
|
51
|
+
PULL_REQUEST = "pull_request"
|
|
52
|
+
MANUAL = "manual"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class User:
|
|
57
|
+
"""Represents a Forge user account."""
|
|
58
|
+
|
|
59
|
+
id: str
|
|
60
|
+
username: str
|
|
61
|
+
email: str
|
|
62
|
+
display_name: str
|
|
63
|
+
created_at: datetime
|
|
64
|
+
updated_at: datetime
|
|
65
|
+
bio: str | None = None
|
|
66
|
+
avatar_url: str | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class Repository:
|
|
71
|
+
"""Represents a Forge repository."""
|
|
72
|
+
|
|
73
|
+
id: str
|
|
74
|
+
owner_id: str
|
|
75
|
+
name: str
|
|
76
|
+
description: str | None
|
|
77
|
+
is_private: bool
|
|
78
|
+
is_fork: bool
|
|
79
|
+
default_branch: str
|
|
80
|
+
created_at: datetime
|
|
81
|
+
updated_at: datetime
|
|
82
|
+
star_count: int = 0
|
|
83
|
+
fork_count: int = 0
|
|
84
|
+
fork_of: str | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class CreateRepoRequest:
|
|
89
|
+
"""Request payload for creating a new repository."""
|
|
90
|
+
|
|
91
|
+
name: str
|
|
92
|
+
description: str | None = None
|
|
93
|
+
is_private: bool = False
|
|
94
|
+
default_branch: str = "main"
|
|
95
|
+
auto_init: bool = False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class Branch:
|
|
100
|
+
"""Represents a git branch."""
|
|
101
|
+
|
|
102
|
+
name: str
|
|
103
|
+
sha: str
|
|
104
|
+
is_default: bool = False
|
|
105
|
+
is_protected: bool = False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class CommitSummary:
|
|
110
|
+
"""Summary of a git commit."""
|
|
111
|
+
|
|
112
|
+
sha: str
|
|
113
|
+
message: str
|
|
114
|
+
author_name: str
|
|
115
|
+
author_email: str
|
|
116
|
+
authored_at: datetime
|
|
117
|
+
committer_name: str
|
|
118
|
+
committer_email: str
|
|
119
|
+
committed_at: datetime
|
|
120
|
+
parents: list[str] = field(default_factory=list)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class Label:
|
|
125
|
+
"""Represents an issue/PR label."""
|
|
126
|
+
|
|
127
|
+
id: str
|
|
128
|
+
name: str
|
|
129
|
+
color: str
|
|
130
|
+
description: str | None = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class Issue:
|
|
135
|
+
"""Represents a Forge issue."""
|
|
136
|
+
|
|
137
|
+
id: str
|
|
138
|
+
repo_id: str
|
|
139
|
+
number: int
|
|
140
|
+
title: str
|
|
141
|
+
author_id: str
|
|
142
|
+
state: IssueState
|
|
143
|
+
created_at: datetime
|
|
144
|
+
updated_at: datetime
|
|
145
|
+
body: str | None = None
|
|
146
|
+
labels: list[Label] = field(default_factory=list)
|
|
147
|
+
assignees: list[str] = field(default_factory=list)
|
|
148
|
+
milestone_id: str | None = None
|
|
149
|
+
closed_at: datetime | None = None
|
|
150
|
+
comment_count: int = 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class Comment:
|
|
155
|
+
"""Represents a comment on an issue or pull request."""
|
|
156
|
+
|
|
157
|
+
id: str
|
|
158
|
+
author_id: str
|
|
159
|
+
body: str
|
|
160
|
+
created_at: datetime
|
|
161
|
+
updated_at: datetime
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class Reviewer:
|
|
166
|
+
"""Represents a PR reviewer and their approval status."""
|
|
167
|
+
|
|
168
|
+
user_id: str
|
|
169
|
+
approved: bool = False
|
|
170
|
+
reviewed_at: datetime | None = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class PullRequest:
|
|
175
|
+
"""Represents a Forge pull request."""
|
|
176
|
+
|
|
177
|
+
id: str
|
|
178
|
+
repo_id: str
|
|
179
|
+
number: int
|
|
180
|
+
title: str
|
|
181
|
+
author_id: str
|
|
182
|
+
state: PrState
|
|
183
|
+
head_branch: str
|
|
184
|
+
head_sha: str
|
|
185
|
+
base_branch: str
|
|
186
|
+
base_sha: str
|
|
187
|
+
created_at: datetime
|
|
188
|
+
updated_at: datetime
|
|
189
|
+
body: str | None = None
|
|
190
|
+
merge_sha: str | None = None
|
|
191
|
+
merge_strategy: MergeStrategy | None = None
|
|
192
|
+
reviewers: list[Reviewer] = field(default_factory=list)
|
|
193
|
+
merged_at: datetime | None = None
|
|
194
|
+
has_conflicts: bool = False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass
|
|
198
|
+
class PipelineJob:
|
|
199
|
+
"""Represents a single job within a CI pipeline."""
|
|
200
|
+
|
|
201
|
+
id: str
|
|
202
|
+
name: str
|
|
203
|
+
state: PipelineState
|
|
204
|
+
started_at: datetime | None = None
|
|
205
|
+
finished_at: datetime | None = None
|
|
206
|
+
steps: list[dict[str, Any]] = field(default_factory=list)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass
|
|
210
|
+
class Pipeline:
|
|
211
|
+
"""Represents a CI pipeline run."""
|
|
212
|
+
|
|
213
|
+
id: str
|
|
214
|
+
repo_id: str
|
|
215
|
+
commit_sha: str
|
|
216
|
+
branch: str
|
|
217
|
+
trigger: PipelineTrigger
|
|
218
|
+
state: PipelineState
|
|
219
|
+
created_at: datetime
|
|
220
|
+
jobs: list[PipelineJob] = field(default_factory=list)
|
|
221
|
+
started_at: datetime | None = None
|
|
222
|
+
finished_at: datetime | None = None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass
|
|
226
|
+
class CommitStatus:
|
|
227
|
+
"""Aggregated CI status for a commit."""
|
|
228
|
+
|
|
229
|
+
sha: str
|
|
230
|
+
state: PipelineState
|
|
231
|
+
pipeline_count: int = 0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class SshKey:
|
|
236
|
+
"""Represents an SSH public key attached to a user account."""
|
|
237
|
+
|
|
238
|
+
id: str
|
|
239
|
+
user_id: str
|
|
240
|
+
title: str
|
|
241
|
+
key: str
|
|
242
|
+
fingerprint: str
|
|
243
|
+
created_at: datetime
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class PaginatedResponse(Generic[T]):
|
|
248
|
+
"""A paginated list of items from the API."""
|
|
249
|
+
|
|
250
|
+
items: list[T]
|
|
251
|
+
page: int
|
|
252
|
+
per_page: int
|
|
253
|
+
has_next_page: bool
|
|
254
|
+
total_count: int | None = None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass
|
|
258
|
+
class CIJobStep:
|
|
259
|
+
"""A single step within a CI job definition."""
|
|
260
|
+
|
|
261
|
+
name: str
|
|
262
|
+
run: str | None = None
|
|
263
|
+
uses: str | None = None
|
|
264
|
+
with_params: dict[str, Any] = field(default_factory=dict)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass
|
|
268
|
+
class CIJobDefinition:
|
|
269
|
+
"""Definition of a CI job from the pipeline config file."""
|
|
270
|
+
|
|
271
|
+
name: str
|
|
272
|
+
runs_on: str
|
|
273
|
+
steps: list[CIJobStep] = field(default_factory=list)
|
|
274
|
+
needs: list[str] = field(default_factory=list)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@dataclass
|
|
278
|
+
class CITrigger:
|
|
279
|
+
"""Trigger conditions for a CI pipeline."""
|
|
280
|
+
|
|
281
|
+
push_branches: list[str] = field(default_factory=list)
|
|
282
|
+
pr_branches: list[str] = field(default_factory=list)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass
|
|
286
|
+
class CIConfig:
|
|
287
|
+
"""Parsed representation of a .forge/ci.yml configuration file."""
|
|
288
|
+
|
|
289
|
+
name: str
|
|
290
|
+
trigger: CITrigger
|
|
291
|
+
jobs: dict[str, CIJobDefinition] = field(default_factory=dict)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Pagination utilities for the Forge API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from git_alternative.models import PaginatedResponse, T
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def paginate_commits(
|
|
9
|
+
raw_commits: list[T],
|
|
10
|
+
page: int,
|
|
11
|
+
per_page: int,
|
|
12
|
+
) -> PaginatedResponse[T]:
|
|
13
|
+
"""Apply server-side pagination logic to a list of commits.
|
|
14
|
+
|
|
15
|
+
Fetches ``per_page + 1`` items and uses the extra item as a sentinel to
|
|
16
|
+
determine whether a next page exists, without requiring an expensive
|
|
17
|
+
``COUNT(*)`` query.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
raw_commits: List of commit objects fetched with ``limit = per_page + 1``.
|
|
21
|
+
page: The current page number (1-based).
|
|
22
|
+
per_page: Maximum number of items per page.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A :class:`~git_alternative.models.PaginatedResponse` with the correct
|
|
26
|
+
items slice and ``has_next_page`` flag.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> items = list(range(11)) # fetched with limit=11
|
|
30
|
+
>>> result = paginate_commits(items, page=1, per_page=10)
|
|
31
|
+
>>> result.has_next_page
|
|
32
|
+
True
|
|
33
|
+
>>> len(result.items)
|
|
34
|
+
10
|
|
35
|
+
"""
|
|
36
|
+
per_page = min(per_page, 100)
|
|
37
|
+
has_next = len(raw_commits) > per_page
|
|
38
|
+
items = raw_commits[:per_page] if has_next else raw_commits
|
|
39
|
+
|
|
40
|
+
return PaginatedResponse(
|
|
41
|
+
items=items,
|
|
42
|
+
page=page,
|
|
43
|
+
per_page=per_page,
|
|
44
|
+
has_next_page=has_next,
|
|
45
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Resource sub-clients for the Forge API."""
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Issues resource client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from git_alternative.http import HttpClient
|
|
8
|
+
from git_alternative.models import Comment, Issue, IssueState, Label, PaginatedResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IssuesResource:
|
|
12
|
+
"""Client for issue-related API endpoints.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
http: Shared HTTP client instance.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, http: HttpClient) -> None:
|
|
19
|
+
self._http = http
|
|
20
|
+
|
|
21
|
+
def list(
|
|
22
|
+
self,
|
|
23
|
+
owner: str,
|
|
24
|
+
repo: str,
|
|
25
|
+
state: str = "open",
|
|
26
|
+
label: str | None = None,
|
|
27
|
+
assignee: str | None = None,
|
|
28
|
+
page: int = 1,
|
|
29
|
+
per_page: int = 30,
|
|
30
|
+
) -> PaginatedResponse[Issue]:
|
|
31
|
+
"""List issues in a repository.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
owner: Repository owner.
|
|
35
|
+
repo: Repository name.
|
|
36
|
+
state: Filter by state — ``"open"``, ``"closed"``, or ``"all"``
|
|
37
|
+
(default: ``"open"``).
|
|
38
|
+
label: Filter by label name.
|
|
39
|
+
assignee: Filter by assignee username.
|
|
40
|
+
page: Page number (1-based, default: 1).
|
|
41
|
+
per_page: Items per page (max: 100, default: 30).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A :class:`~git_alternative.models.PaginatedResponse` of
|
|
45
|
+
:class:`~git_alternative.models.Issue` instances.
|
|
46
|
+
"""
|
|
47
|
+
params: dict[str, Any] = {"state": state, "page": page, "per_page": per_page}
|
|
48
|
+
if label:
|
|
49
|
+
params["label"] = label
|
|
50
|
+
if assignee:
|
|
51
|
+
params["assignee"] = assignee
|
|
52
|
+
data = self._http.get(f"/api/v1/repos/{owner}/{repo}/issues", params=params)
|
|
53
|
+
items = data.get("items", data if isinstance(data, list) else [])
|
|
54
|
+
issues = [Issue(**i) for i in items]
|
|
55
|
+
if isinstance(data, dict):
|
|
56
|
+
return PaginatedResponse[Issue](
|
|
57
|
+
items=issues,
|
|
58
|
+
page=data.get("page", page),
|
|
59
|
+
per_page=data.get("per_page", per_page),
|
|
60
|
+
has_next_page=data.get("has_next_page", False),
|
|
61
|
+
total_count=data.get("total_count"),
|
|
62
|
+
)
|
|
63
|
+
return PaginatedResponse[Issue](
|
|
64
|
+
items=issues,
|
|
65
|
+
page=page,
|
|
66
|
+
per_page=per_page,
|
|
67
|
+
has_next_page=False,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def get(self, owner: str, repo: str, number: int) -> Issue:
|
|
71
|
+
"""Fetch a single issue by number.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
owner: Repository owner.
|
|
75
|
+
repo: Repository name.
|
|
76
|
+
number: Issue number.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
An :class:`~git_alternative.models.Issue` instance.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ForgeNotFoundError: If the issue does not exist.
|
|
83
|
+
"""
|
|
84
|
+
data = self._http.get(f"/api/v1/repos/{owner}/{repo}/issues/{number}")
|
|
85
|
+
return Issue(**data)
|
|
86
|
+
|
|
87
|
+
def create(
|
|
88
|
+
self,
|
|
89
|
+
owner: str,
|
|
90
|
+
repo: str,
|
|
91
|
+
title: str,
|
|
92
|
+
body: str | None = None,
|
|
93
|
+
labels: list[str] | None = None,
|
|
94
|
+
assignees: list[str] | None = None,
|
|
95
|
+
milestone_id: str | None = None,
|
|
96
|
+
) -> Issue:
|
|
97
|
+
"""Create a new issue.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
owner: Repository owner.
|
|
101
|
+
repo: Repository name.
|
|
102
|
+
title: Issue title.
|
|
103
|
+
body: Optional Markdown body.
|
|
104
|
+
labels: Optional list of label names to attach.
|
|
105
|
+
assignees: Optional list of usernames to assign.
|
|
106
|
+
milestone_id: Optional milestone UUID string.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The newly created :class:`~git_alternative.models.Issue`.
|
|
110
|
+
"""
|
|
111
|
+
payload: dict[str, Any] = {"title": title}
|
|
112
|
+
if body is not None:
|
|
113
|
+
payload["body"] = body
|
|
114
|
+
if labels:
|
|
115
|
+
payload["labels"] = labels
|
|
116
|
+
if assignees:
|
|
117
|
+
payload["assignees"] = assignees
|
|
118
|
+
if milestone_id:
|
|
119
|
+
payload["milestone_id"] = milestone_id
|
|
120
|
+
data = self._http.post(f"/api/v1/repos/{owner}/{repo}/issues", json=payload)
|
|
121
|
+
return Issue(**data)
|
|
122
|
+
|
|
123
|
+
def update(
|
|
124
|
+
self,
|
|
125
|
+
owner: str,
|
|
126
|
+
repo: str,
|
|
127
|
+
number: int,
|
|
128
|
+
title: str | None = None,
|
|
129
|
+
body: str | None = None,
|
|
130
|
+
state: IssueState | None = None,
|
|
131
|
+
) -> Issue:
|
|
132
|
+
"""Update an existing issue.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
owner: Repository owner.
|
|
136
|
+
repo: Repository name.
|
|
137
|
+
number: Issue number.
|
|
138
|
+
title: New title (omit to leave unchanged).
|
|
139
|
+
body: New body (omit to leave unchanged).
|
|
140
|
+
state: New state (omit to leave unchanged).
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The updated :class:`~git_alternative.models.Issue`.
|
|
144
|
+
"""
|
|
145
|
+
payload: dict[str, Any] = {}
|
|
146
|
+
if title is not None:
|
|
147
|
+
payload["title"] = title
|
|
148
|
+
if body is not None:
|
|
149
|
+
payload["body"] = body
|
|
150
|
+
if state is not None:
|
|
151
|
+
payload["state"] = state.value
|
|
152
|
+
data = self._http.patch(f"/api/v1/repos/{owner}/{repo}/issues/{number}", json=payload)
|
|
153
|
+
return Issue(**data)
|
|
154
|
+
|
|
155
|
+
def close(self, owner: str, repo: str, number: int) -> Issue:
|
|
156
|
+
"""Close an issue.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
owner: Repository owner.
|
|
160
|
+
repo: Repository name.
|
|
161
|
+
number: Issue number.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The updated :class:`~git_alternative.models.Issue`.
|
|
165
|
+
"""
|
|
166
|
+
return self.update(owner, repo, number, state=IssueState.CLOSED)
|
|
167
|
+
|
|
168
|
+
def reopen(self, owner: str, repo: str, number: int) -> Issue:
|
|
169
|
+
"""Reopen a closed issue.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
owner: Repository owner.
|
|
173
|
+
repo: Repository name.
|
|
174
|
+
number: Issue number.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The updated :class:`~git_alternative.models.Issue`.
|
|
178
|
+
"""
|
|
179
|
+
return self.update(owner, repo, number, state=IssueState.OPEN)
|
|
180
|
+
|
|
181
|
+
def delete(self, owner: str, repo: str, number: int) -> None:
|
|
182
|
+
"""Delete an issue.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
owner: Repository owner.
|
|
186
|
+
repo: Repository name.
|
|
187
|
+
number: Issue number.
|
|
188
|
+
"""
|
|
189
|
+
self._http.delete(f"/api/v1/repos/{owner}/{repo}/issues/{number}")
|
|
190
|
+
|
|
191
|
+
def list_comments(self, owner: str, repo: str, number: int) -> list[Comment]:
|
|
192
|
+
"""List comments on an issue.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
owner: Repository owner.
|
|
196
|
+
repo: Repository name.
|
|
197
|
+
number: Issue number.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of :class:`~git_alternative.models.Comment` instances.
|
|
201
|
+
"""
|
|
202
|
+
data = self._http.get(f"/api/v1/repos/{owner}/{repo}/issues/{number}/comments")
|
|
203
|
+
return [Comment(**c) for c in data]
|
|
204
|
+
|
|
205
|
+
def add_comment(self, owner: str, repo: str, issue_number: int, body: str) -> Comment:
|
|
206
|
+
"""Add a comment to an issue.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
owner: Repository owner.
|
|
210
|
+
repo: Repository name.
|
|
211
|
+
issue_number: Issue number.
|
|
212
|
+
body: Comment body (Markdown supported).
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
The newly created :class:`~git_alternative.models.Comment`.
|
|
216
|
+
"""
|
|
217
|
+
data = self._http.post(
|
|
218
|
+
f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments",
|
|
219
|
+
json={"body": body},
|
|
220
|
+
)
|
|
221
|
+
return Comment(**data)
|
|
222
|
+
|
|
223
|
+
def update_comment(
|
|
224
|
+
self, owner: str, repo: str, issue_number: int, comment_id: str, body: str
|
|
225
|
+
) -> Comment:
|
|
226
|
+
"""Update an existing comment.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
owner: Repository owner.
|
|
230
|
+
repo: Repository name.
|
|
231
|
+
issue_number: Issue number.
|
|
232
|
+
comment_id: Comment UUID string.
|
|
233
|
+
body: New comment body.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
The updated :class:`~git_alternative.models.Comment`.
|
|
237
|
+
"""
|
|
238
|
+
data = self._http.patch(
|
|
239
|
+
f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments/{comment_id}",
|
|
240
|
+
json={"body": body},
|
|
241
|
+
)
|
|
242
|
+
return Comment(**data)
|
|
243
|
+
|
|
244
|
+
def delete_comment(
|
|
245
|
+
self, owner: str, repo: str, issue_number: int, comment_id: str
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Delete a comment from an issue.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
owner: Repository owner.
|
|
251
|
+
repo: Repository name.
|
|
252
|
+
issue_number: Issue number.
|
|
253
|
+
comment_id: Comment UUID string.
|
|
254
|
+
"""
|
|
255
|
+
self._http.delete(
|
|
256
|
+
f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments/{comment_id}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def list_labels(self, owner: str, repo: str) -> list[Label]:
|
|
260
|
+
"""List all labels in a repository.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
owner: Repository owner.
|
|
264
|
+
repo: Repository name.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List of :class:`~git_alternative.models.Label` instances.
|
|
268
|
+
"""
|
|
269
|
+
data = self._http.get(f"/api/v1/repos/{owner}/{repo}/labels")
|
|
270
|
+
return [Label(**lb) for lb in data]
|
|
271
|
+
|
|
272
|
+
def create_label(
|
|
273
|
+
self, owner: str, repo: str, name: str, color: str, description: str | None = None
|
|
274
|
+
) -> Label:
|
|
275
|
+
"""Create a new label in a repository.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
owner: Repository owner.
|
|
279
|
+
repo: Repository name.
|
|
280
|
+
name: Label name.
|
|
281
|
+
color: Hex color string (e.g., ``"#ff0000"``).
|
|
282
|
+
description: Optional label description.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
The newly created :class:`~git_alternative.models.Label`.
|
|
286
|
+
"""
|
|
287
|
+
payload: dict[str, Any] = {"name": name, "color": color}
|
|
288
|
+
if description is not None:
|
|
289
|
+
payload["description"] = description
|
|
290
|
+
data = self._http.post(f"/api/v1/repos/{owner}/{repo}/labels", json=payload)
|
|
291
|
+
return Label(**data)
|