codeberg-cli 0.4.2__py3-none-any.whl → 0.5.0__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.
- codeberg_cli/client.py +6 -2
- codeberg_cli/routes/issue/assets/__init__.py +6 -0
- codeberg_cli/routes/issue/assets/delete.py +27 -0
- codeberg_cli/routes/issue/assets/list.py +47 -0
- codeberg_cli/routes/issue/assets/upload.py +50 -0
- codeberg_cli/routes/issue/deps/__init__.py +6 -0
- codeberg_cli/routes/issue/deps/add.py +27 -0
- codeberg_cli/routes/issue/deps/list.py +41 -0
- codeberg_cli/routes/issue/deps/remove.py +36 -0
- codeberg_cli/routes/issue/react.py +40 -0
- codeberg_cli/routes/issue/reactions.py +39 -0
- codeberg_cli/routes/keys/__init__.py +6 -0
- codeberg_cli/routes/keys/gpg/__init__.py +6 -0
- codeberg_cli/routes/keys/gpg/add.py +31 -0
- codeberg_cli/routes/keys/gpg/delete.py +17 -0
- codeberg_cli/routes/keys/gpg/list.py +29 -0
- codeberg_cli/routes/keys/ssh/__init__.py +6 -0
- codeberg_cli/routes/keys/ssh/add.py +31 -0
- codeberg_cli/routes/keys/ssh/delete.py +17 -0
- codeberg_cli/routes/keys/ssh/list.py +29 -0
- codeberg_cli/routes/notify/__init__.py +6 -0
- codeberg_cli/routes/{notifications.py → notify/list.py} +4 -2
- codeberg_cli/routes/notify/read.py +23 -0
- codeberg_cli/routes/notify/thread.py +31 -0
- codeberg_cli/routes/pr/review.py +44 -0
- codeberg_cli/routes/pr/reviews.py +48 -0
- codeberg_cli/routes/pr/status.py +51 -0
- codeberg_cli/routes/repo/hook/__init__.py +6 -0
- codeberg_cli/routes/repo/hook/create.py +46 -0
- codeberg_cli/routes/repo/hook/delete.py +26 -0
- codeberg_cli/routes/repo/hook/list.py +42 -0
- codeberg_cli/routes/repo/hook/test.py +26 -0
- codeberg_cli/routes/repo/hook/view.py +35 -0
- codeberg_cli/routes/repo/mirror/__init__.py +6 -0
- codeberg_cli/routes/repo/mirror/create.py +47 -0
- codeberg_cli/routes/repo/mirror/delete.py +26 -0
- codeberg_cli/routes/repo/mirror/list.py +47 -0
- codeberg_cli/routes/repo/mirror/sync.py +25 -0
- codeberg_cli/routes/repo/wiki/__init__.py +6 -0
- codeberg_cli/routes/repo/wiki/create.py +34 -0
- codeberg_cli/routes/repo/wiki/delete.py +27 -0
- codeberg_cli/routes/repo/wiki/edit.py +39 -0
- codeberg_cli/routes/repo/wiki/list.py +42 -0
- codeberg_cli/routes/repo/wiki/view.py +35 -0
- {codeberg_cli-0.4.2.dist-info → codeberg_cli-0.5.0.dist-info}/METADATA +15 -15
- {codeberg_cli-0.4.2.dist-info → codeberg_cli-0.5.0.dist-info}/RECORD +49 -7
- {codeberg_cli-0.4.2.dist-info → codeberg_cli-0.5.0.dist-info}/WHEEL +0 -0
- {codeberg_cli-0.4.2.dist-info → codeberg_cli-0.5.0.dist-info}/entry_points.txt +0 -0
- {codeberg_cli-0.4.2.dist-info → codeberg_cli-0.5.0.dist-info}/licenses/LICENSE +0 -0
codeberg_cli/client.py
CHANGED
|
@@ -84,9 +84,13 @@ class Client:
|
|
|
84
84
|
return self._request("PATCH", path, json=data, action=action)
|
|
85
85
|
|
|
86
86
|
def put(
|
|
87
|
-
self,
|
|
87
|
+
self,
|
|
88
|
+
path: str,
|
|
89
|
+
data: dict | None = None,
|
|
90
|
+
params: dict | None = None,
|
|
91
|
+
action: str | None = None,
|
|
88
92
|
) -> dict | list:
|
|
89
|
-
return self._request("PUT", path, json=data, action=action)
|
|
93
|
+
return self._request("PUT", path, json=data, params=params, action=action)
|
|
90
94
|
|
|
91
95
|
def delete(self, path: str, action: str | None = None) -> None:
|
|
92
96
|
self._request("DELETE", path, action=action)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.git import infer_repo
|
|
6
|
+
from codeberg_cli.helpers import require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("delete")
|
|
11
|
+
def _(
|
|
12
|
+
index: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
attachment_id: Annotated[int, Arg(description="Attachment ID")],
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Delete an issue attachment."""
|
|
17
|
+
client = require_client()
|
|
18
|
+
|
|
19
|
+
if not repo:
|
|
20
|
+
inferred = infer_repo()
|
|
21
|
+
if not inferred:
|
|
22
|
+
rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
|
|
23
|
+
return 1
|
|
24
|
+
repo = inferred
|
|
25
|
+
|
|
26
|
+
client.delete(f"/repos/{repo}/issues/{index}/assets/{attachment_id}")
|
|
27
|
+
rich.print(f"[bold green]Deleted[/bold green] attachment #{attachment_id}")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.git import infer_repo
|
|
6
|
+
from codeberg_cli.helpers import print_table, require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("list", "ls")
|
|
11
|
+
def _(
|
|
12
|
+
index: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
14
|
+
) -> None:
|
|
15
|
+
"""List issue attachments."""
|
|
16
|
+
client = require_client()
|
|
17
|
+
|
|
18
|
+
if not repo:
|
|
19
|
+
inferred = infer_repo()
|
|
20
|
+
if not inferred:
|
|
21
|
+
rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
|
|
22
|
+
return 1
|
|
23
|
+
repo = inferred
|
|
24
|
+
|
|
25
|
+
assets = client.get(f"/repos/{repo}/issues/{index}/assets")
|
|
26
|
+
|
|
27
|
+
if not assets:
|
|
28
|
+
rich.print("[dim]No attachments.[/dim]")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
rows = []
|
|
32
|
+
json_rows = []
|
|
33
|
+
for asset in assets:
|
|
34
|
+
rows.append((
|
|
35
|
+
str(asset["id"]),
|
|
36
|
+
asset["name"],
|
|
37
|
+
str(asset.get("size", "")),
|
|
38
|
+
asset.get("browser_download_url", ""),
|
|
39
|
+
))
|
|
40
|
+
json_rows.append({
|
|
41
|
+
"id": asset["id"],
|
|
42
|
+
"name": asset["name"],
|
|
43
|
+
"size": asset.get("size"),
|
|
44
|
+
"browser_download_url": asset.get("browser_download_url"),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
print_table(["ID", "Name", "Size", "Download"], rows, json_data=json_rows)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import rich
|
|
6
|
+
|
|
7
|
+
from codeberg_cli.git import infer_repo
|
|
8
|
+
from codeberg_cli.helpers import require_client
|
|
9
|
+
from xclif import Arg, Option, command
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@command("upload")
|
|
13
|
+
def _(
|
|
14
|
+
index: Annotated[int, Arg(description="Issue number")],
|
|
15
|
+
file: Annotated[str, Arg(description="File to upload")],
|
|
16
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Upload an issue attachment."""
|
|
19
|
+
client = require_client()
|
|
20
|
+
|
|
21
|
+
if not repo:
|
|
22
|
+
inferred = infer_repo()
|
|
23
|
+
if not inferred:
|
|
24
|
+
rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
|
|
25
|
+
return 1
|
|
26
|
+
repo = inferred
|
|
27
|
+
|
|
28
|
+
file_path = Path(file)
|
|
29
|
+
if not file_path.exists():
|
|
30
|
+
rich.print(f"[bold red]Error:[/bold red] File not found: {file}")
|
|
31
|
+
return 1
|
|
32
|
+
|
|
33
|
+
with open(file_path, "rb") as f:
|
|
34
|
+
content = f.read()
|
|
35
|
+
|
|
36
|
+
# Direct multipart upload (bypasses Client for multipart support)
|
|
37
|
+
token = client._client.headers.get("Authorization", "").replace("Bearer ", "")
|
|
38
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
39
|
+
|
|
40
|
+
response = httpx.post(
|
|
41
|
+
f"{client.base_url}/repos/{repo}/issues/{index}/assets",
|
|
42
|
+
headers=headers,
|
|
43
|
+
files={"attachment": (file_path.name, content)},
|
|
44
|
+
)
|
|
45
|
+
if response.is_success:
|
|
46
|
+
rich.print(f"[bold green]Uploaded[/bold green] {file_path.name} to issue #{index}")
|
|
47
|
+
else:
|
|
48
|
+
msg = response.json().get("message", "unknown error") if response.text else "unknown error"
|
|
49
|
+
rich.print(f"[bold red]Error:[/bold red] {response.status_code} {msg}")
|
|
50
|
+
return 1
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.git import infer_repo
|
|
6
|
+
from codeberg_cli.helpers import require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("add")
|
|
11
|
+
def _(
|
|
12
|
+
index: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
depends_on: Annotated[int, Arg(description="Issue number this one depends on")],
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Add a dependency to an issue."""
|
|
17
|
+
client = require_client()
|
|
18
|
+
|
|
19
|
+
if not repo:
|
|
20
|
+
inferred = infer_repo()
|
|
21
|
+
if not inferred:
|
|
22
|
+
rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
|
|
23
|
+
return 1
|
|
24
|
+
repo = inferred
|
|
25
|
+
|
|
26
|
+
client.post(f"/repos/{repo}/issues/{index}/dependencies", data={"index": depends_on})
|
|
27
|
+
rich.print(f"[bold green]Issue[/bold green] #{index} now depends on #{depends_on}")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.git import infer_repo
|
|
6
|
+
from codeberg_cli.helpers import print_table, require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("list", "ls")
|
|
11
|
+
def _(
|
|
12
|
+
index: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
14
|
+
) -> None:
|
|
15
|
+
"""List issue dependencies."""
|
|
16
|
+
client = require_client()
|
|
17
|
+
|
|
18
|
+
if not repo:
|
|
19
|
+
inferred = infer_repo()
|
|
20
|
+
if not inferred:
|
|
21
|
+
rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
|
|
22
|
+
return 1
|
|
23
|
+
repo = inferred
|
|
24
|
+
|
|
25
|
+
deps = client.get(f"/repos/{repo}/issues/{index}/dependencies")
|
|
26
|
+
|
|
27
|
+
if not deps:
|
|
28
|
+
rich.print("[dim]No dependencies.[/dim]")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
rows = []
|
|
32
|
+
json_rows = []
|
|
33
|
+
for dep in deps:
|
|
34
|
+
rows.append((str(dep["number"]), dep["title"], dep.get("state", "")))
|
|
35
|
+
json_rows.append({
|
|
36
|
+
"number": dep["number"],
|
|
37
|
+
"title": dep["title"],
|
|
38
|
+
"state": dep.get("state"),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
print_table(["#", "Title", "State"], rows, json_data=json_rows)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.git import infer_repo
|
|
6
|
+
from codeberg_cli.helpers import require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("remove")
|
|
11
|
+
def _(
|
|
12
|
+
index: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
depends_on: Annotated[int, Arg(description="Issue number this one depends on")],
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Remove a dependency from an issue."""
|
|
17
|
+
client = require_client()
|
|
18
|
+
|
|
19
|
+
if not repo:
|
|
20
|
+
inferred = infer_repo()
|
|
21
|
+
if not inferred:
|
|
22
|
+
rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
|
|
23
|
+
return 1
|
|
24
|
+
repo = inferred
|
|
25
|
+
|
|
26
|
+
response = client._client.request(
|
|
27
|
+
"DELETE",
|
|
28
|
+
f"/repos/{repo}/issues/{index}/dependencies",
|
|
29
|
+
json={"index": depends_on},
|
|
30
|
+
)
|
|
31
|
+
if not response.is_success:
|
|
32
|
+
msg = response.json().get("message", "unknown error") if response.text else "unknown error"
|
|
33
|
+
rich.print(f"[bold red]Error:[/bold red] {response.status_code} {msg}")
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
rich.print(f"[bold green]Removed[/bold green] dependency #{depends_on} from issue #{index}")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.git import infer_repo
|
|
6
|
+
from codeberg_cli.helpers import require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("react")
|
|
11
|
+
def _(
|
|
12
|
+
index: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
content: Annotated[str, Arg(description="Reaction emoji, e.g. +1, -1, heart, laugh")],
|
|
14
|
+
remove: Annotated[bool, Option(description="Remove the reaction instead of adding it", name="remove")] = False,
|
|
15
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Add or remove a reaction on an issue."""
|
|
18
|
+
client = require_client()
|
|
19
|
+
|
|
20
|
+
if not repo:
|
|
21
|
+
inferred = infer_repo()
|
|
22
|
+
if not inferred:
|
|
23
|
+
rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
|
|
24
|
+
return 1
|
|
25
|
+
repo = inferred
|
|
26
|
+
|
|
27
|
+
if remove:
|
|
28
|
+
response = client._client.request(
|
|
29
|
+
"DELETE",
|
|
30
|
+
f"/repos/{repo}/issues/{index}/reactions",
|
|
31
|
+
json={"content": content},
|
|
32
|
+
)
|
|
33
|
+
if not response.is_success:
|
|
34
|
+
msg = response.json().get("message", "unknown error") if response.text else "unknown error"
|
|
35
|
+
rich.print(f"[bold red]Error:[/bold red] {response.status_code} {msg}")
|
|
36
|
+
return 1
|
|
37
|
+
rich.print(f"[bold green]Removed[/bold green] reaction :{content}:")
|
|
38
|
+
else:
|
|
39
|
+
client.post(f"/repos/{repo}/issues/{index}/reactions", data={"content": content})
|
|
40
|
+
rich.print(f"[bold green]Added[/bold green] reaction :{content}:")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.git import infer_repo
|
|
6
|
+
from codeberg_cli.helpers import print_table, require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("reactions")
|
|
11
|
+
def _(
|
|
12
|
+
index: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
14
|
+
) -> None:
|
|
15
|
+
"""List reactions on an issue."""
|
|
16
|
+
client = require_client()
|
|
17
|
+
|
|
18
|
+
if not repo:
|
|
19
|
+
inferred = infer_repo()
|
|
20
|
+
if not inferred:
|
|
21
|
+
rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
|
|
22
|
+
return 1
|
|
23
|
+
repo = inferred
|
|
24
|
+
|
|
25
|
+
reactions = client.get(f"/repos/{repo}/issues/{index}/reactions")
|
|
26
|
+
|
|
27
|
+
if not reactions:
|
|
28
|
+
rich.print("[dim]No reactions.[/dim]")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
rows = []
|
|
32
|
+
json_rows = []
|
|
33
|
+
for reaction in reactions:
|
|
34
|
+
user = (reaction.get("user") or {}).get("login", "")
|
|
35
|
+
content = reaction.get("content", "")
|
|
36
|
+
rows.append((user, content))
|
|
37
|
+
json_rows.append({"user": user, "content": content})
|
|
38
|
+
|
|
39
|
+
print_table(["User", "Reaction"], rows, json_data=json_rows)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import rich
|
|
5
|
+
|
|
6
|
+
from codeberg_cli.helpers import require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("add")
|
|
11
|
+
def _(
|
|
12
|
+
armored_key: Annotated[str, Option(description="The armored public GPG key", name="armored-key")] = "",
|
|
13
|
+
key_file: Annotated[str, Option(description="Read the armored public key from a file path", name="key-file")] = "",
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Add a GPG key."""
|
|
16
|
+
client = require_client()
|
|
17
|
+
|
|
18
|
+
value = armored_key
|
|
19
|
+
if key_file:
|
|
20
|
+
file_path = Path(key_file)
|
|
21
|
+
if not file_path.exists():
|
|
22
|
+
rich.print(f"[bold red]Error:[/bold red] File not found: {key_file}")
|
|
23
|
+
return 1
|
|
24
|
+
value = file_path.read_text().strip()
|
|
25
|
+
|
|
26
|
+
if not value:
|
|
27
|
+
rich.print("[bold red]Error:[/bold red] No key provided (pass an armored_key argument or --key-file)")
|
|
28
|
+
return 1
|
|
29
|
+
|
|
30
|
+
result = client.post("/user/gpg_keys", data={"armored_public_key": value})
|
|
31
|
+
rich.print(f"[bold green]Added GPG key[/bold green] #{result['id']}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.helpers import require_client
|
|
6
|
+
from xclif import Arg, command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@command("delete")
|
|
10
|
+
def _(
|
|
11
|
+
id: Annotated[int, Arg(description="GPG key ID")],
|
|
12
|
+
) -> None:
|
|
13
|
+
"""Delete a GPG key."""
|
|
14
|
+
client = require_client()
|
|
15
|
+
|
|
16
|
+
client.delete(f"/user/gpg_keys/{id}")
|
|
17
|
+
rich.print(f"[bold green]Deleted GPG key[/bold green] #{id}")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import rich
|
|
2
|
+
|
|
3
|
+
from codeberg_cli.helpers import print_table, require_client
|
|
4
|
+
from xclif import command
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@command("list", "ls")
|
|
8
|
+
def _() -> None:
|
|
9
|
+
"""List your GPG keys."""
|
|
10
|
+
client = require_client()
|
|
11
|
+
|
|
12
|
+
keys = client.get("/user/gpg_keys")
|
|
13
|
+
|
|
14
|
+
if not keys:
|
|
15
|
+
rich.print("[dim]No GPG keys.[/dim]")
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
rows = []
|
|
19
|
+
json_rows = []
|
|
20
|
+
for key in keys:
|
|
21
|
+
rows.append((str(key["id"]), key.get("key_id", ""), str(key.get("can_sign", False))))
|
|
22
|
+
json_rows.append({
|
|
23
|
+
"id": key["id"],
|
|
24
|
+
"key_id": key.get("key_id"),
|
|
25
|
+
"can_sign": key.get("can_sign"),
|
|
26
|
+
"created_at": key.get("created_at"),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
print_table(["ID", "Key ID", "Can Sign"], rows, json_data=json_rows)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import rich
|
|
5
|
+
|
|
6
|
+
from codeberg_cli.helpers import require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("add")
|
|
11
|
+
def _(
|
|
12
|
+
title: Annotated[str, Arg(description="Title for the SSH key")],
|
|
13
|
+
key: Annotated[str, Option(description="The public key string", name="key")] = "",
|
|
14
|
+
key_file: Annotated[str, Option(description="Read the public key from a file path", name="key-file")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Add an SSH key."""
|
|
17
|
+
client = require_client()
|
|
18
|
+
|
|
19
|
+
if key_file:
|
|
20
|
+
file_path = Path(key_file)
|
|
21
|
+
if not file_path.exists():
|
|
22
|
+
rich.print(f"[bold red]Error:[/bold red] File not found: {key_file}")
|
|
23
|
+
return 1
|
|
24
|
+
key = file_path.read_text().strip()
|
|
25
|
+
|
|
26
|
+
if not key:
|
|
27
|
+
rich.print("[bold red]Error:[/bold red] No key provided (pass a key argument or --key-file)")
|
|
28
|
+
return 1
|
|
29
|
+
|
|
30
|
+
result = client.post("/user/keys", data={"title": title, "key": key})
|
|
31
|
+
rich.print(f"[bold green]Added SSH key[/bold green] #{result['id']}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.helpers import require_client
|
|
6
|
+
from xclif import Arg, command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@command("delete")
|
|
10
|
+
def _(
|
|
11
|
+
id: Annotated[int, Arg(description="SSH key ID")],
|
|
12
|
+
) -> None:
|
|
13
|
+
"""Delete an SSH key."""
|
|
14
|
+
client = require_client()
|
|
15
|
+
|
|
16
|
+
client.delete(f"/user/keys/{id}")
|
|
17
|
+
rich.print(f"[bold green]Deleted SSH key[/bold green] #{id}")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import rich
|
|
2
|
+
|
|
3
|
+
from codeberg_cli.helpers import print_table, require_client
|
|
4
|
+
from xclif import command
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@command("list", "ls")
|
|
8
|
+
def _() -> None:
|
|
9
|
+
"""List your SSH keys."""
|
|
10
|
+
client = require_client()
|
|
11
|
+
|
|
12
|
+
keys = client.get("/user/keys")
|
|
13
|
+
|
|
14
|
+
if not keys:
|
|
15
|
+
rich.print("[dim]No SSH keys.[/dim]")
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
rows = []
|
|
19
|
+
json_rows = []
|
|
20
|
+
for key in keys:
|
|
21
|
+
rows.append((str(key["id"]), key.get("title", ""), key.get("fingerprint", "")))
|
|
22
|
+
json_rows.append({
|
|
23
|
+
"id": key["id"],
|
|
24
|
+
"title": key.get("title"),
|
|
25
|
+
"fingerprint": key.get("fingerprint"),
|
|
26
|
+
"created_at": key.get("created_at"),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
print_table(["ID", "Title", "Fingerprint"], rows, json_data=json_rows)
|
|
@@ -6,7 +6,7 @@ from codeberg_cli.helpers import print_table, require_client
|
|
|
6
6
|
from xclif import Option, command
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
@command("notifications")
|
|
9
|
+
@command("list", "notifications")
|
|
10
10
|
def _(
|
|
11
11
|
limit: Annotated[int, Option(description="Maximum notifications to show", name="limit")] = 30,
|
|
12
12
|
all: Annotated[bool, Option(description="Show all notifications, not just unread", name="all")] = False,
|
|
@@ -15,7 +15,9 @@ def _(
|
|
|
15
15
|
client = require_client()
|
|
16
16
|
|
|
17
17
|
params: dict = {"limit": limit, "page": 1}
|
|
18
|
-
if
|
|
18
|
+
if all:
|
|
19
|
+
params["all"] = "true"
|
|
20
|
+
else:
|
|
19
21
|
params["status-types"] = "unread"
|
|
20
22
|
|
|
21
23
|
notifications = client.get("/notifications", params=params)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.helpers import require_client
|
|
6
|
+
from xclif import Option, command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@command("read")
|
|
10
|
+
def _(
|
|
11
|
+
all: Annotated[bool, Option(description="Mark all notifications as read", name="all")] = False,
|
|
12
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
13
|
+
) -> None:
|
|
14
|
+
"""Mark notifications as read."""
|
|
15
|
+
client = require_client()
|
|
16
|
+
|
|
17
|
+
params = {"all": "true"} if all else None
|
|
18
|
+
if repo:
|
|
19
|
+
client.put(f"/repos/{repo}/notifications", params=params)
|
|
20
|
+
else:
|
|
21
|
+
client.put("/notifications", params=params)
|
|
22
|
+
|
|
23
|
+
rich.print("[bold green]Marked[/bold green] notifications as read")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.helpers import is_json_mode, output, require_client
|
|
6
|
+
from xclif import Arg, Option, command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@command("thread")
|
|
10
|
+
def _(
|
|
11
|
+
id: Annotated[int, Arg(description="Thread ID")],
|
|
12
|
+
read: Annotated[bool, Option(description="Mark the thread as read", name="read")] = False,
|
|
13
|
+
) -> None:
|
|
14
|
+
"""View or mark a notification thread."""
|
|
15
|
+
client = require_client()
|
|
16
|
+
|
|
17
|
+
if read:
|
|
18
|
+
client.patch(f"/notifications/threads/{id}")
|
|
19
|
+
rich.print(f"[bold green]Marked[/bold green] thread #{id} read")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
thread = client.get(f"/notifications/threads/{id}")
|
|
23
|
+
|
|
24
|
+
if is_json_mode():
|
|
25
|
+
output(thread)
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
subject = thread.get("subject", {})
|
|
29
|
+
repo = thread.get("repository", {})
|
|
30
|
+
rich.print(f"[bold]#{thread['id']}[/bold] [bold]{subject.get('title', '')}[/bold]")
|
|
31
|
+
rich.print(f"[dim]{subject.get('type', '')}[/dim] in [dim]{repo.get('full_name', '')}[/dim]")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.git import infer_repo
|
|
6
|
+
from codeberg_cli.helpers import require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("review")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="PR number")],
|
|
13
|
+
approve: Annotated[bool, Option(description="Approve the pull request", name="approve")] = False,
|
|
14
|
+
request_changes: Annotated[bool, Option(description="Request changes", name="request-changes")] = False,
|
|
15
|
+
comment: Annotated[bool, Option(description="Submit a comment review", name="comment")] = False,
|
|
16
|
+
body: Annotated[str, Option(description="Review body", name="body")] = "",
|
|
17
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Submit a review on a pull request."""
|
|
20
|
+
client = require_client()
|
|
21
|
+
|
|
22
|
+
if not repo:
|
|
23
|
+
inferred = infer_repo()
|
|
24
|
+
if not inferred:
|
|
25
|
+
rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
|
|
26
|
+
return 1
|
|
27
|
+
repo = inferred
|
|
28
|
+
|
|
29
|
+
if sum([approve, request_changes, comment]) > 1:
|
|
30
|
+
rich.print("[bold red]Error:[/bold red] Choose at most one of --approve/--request-changes/--comment")
|
|
31
|
+
return 1
|
|
32
|
+
|
|
33
|
+
if approve:
|
|
34
|
+
event = "APPROVED"
|
|
35
|
+
elif request_changes:
|
|
36
|
+
event = "REQUEST_CHANGES"
|
|
37
|
+
elif comment:
|
|
38
|
+
event = "COMMENT"
|
|
39
|
+
else:
|
|
40
|
+
rich.print("[bold red]Error:[/bold red] Specify one of --approve, --request-changes, or --comment")
|
|
41
|
+
return 1
|
|
42
|
+
|
|
43
|
+
client.post(f"/repos/{repo}/pulls/{id}/reviews", data={"event": event, "body": body})
|
|
44
|
+
rich.print(f"[bold green]Submitted[/bold green] {event} review on PR #{id}")
|