codeberg-cli 0.2.0__py3-none-any.whl → 0.3.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 -0
- codeberg_cli/helpers.py +75 -0
- codeberg_cli/routes/__init__.py +5 -1
- codeberg_cli/routes/actions/__init__.py +6 -0
- codeberg_cli/routes/actions/dispatch.py +31 -0
- codeberg_cli/routes/actions/run.py +40 -0
- codeberg_cli/routes/actions/runs.py +52 -0
- codeberg_cli/routes/actions/workflows.py +46 -0
- codeberg_cli/routes/issue/delete.py +31 -0
- codeberg_cli/routes/issue/labels/__init__.py +6 -0
- codeberg_cli/routes/issue/labels/add.py +27 -0
- codeberg_cli/routes/issue/labels/list.py +43 -0
- codeberg_cli/routes/issue/labels/remove.py +27 -0
- codeberg_cli/routes/issue/list.py +15 -10
- codeberg_cli/routes/issue/pin.py +26 -0
- codeberg_cli/routes/issue/search.py +59 -0
- codeberg_cli/routes/issue/subscribe.py +27 -0
- codeberg_cli/routes/issue/time/__init__.py +6 -0
- codeberg_cli/routes/issue/time/add.py +31 -0
- codeberg_cli/routes/issue/time/list.py +42 -0
- codeberg_cli/routes/issue/unpin.py +26 -0
- codeberg_cli/routes/issue/unsubscribe.py +27 -0
- codeberg_cli/routes/issue/view.py +5 -1
- codeberg_cli/routes/label/edit.py +41 -0
- codeberg_cli/routes/label/list.py +12 -10
- codeberg_cli/routes/milestone/delete.py +38 -0
- codeberg_cli/routes/milestone/edit.py +44 -0
- codeberg_cli/routes/milestone/list.py +15 -15
- codeberg_cli/routes/milestone/view.py +42 -0
- codeberg_cli/routes/{notification/list.py → notifications.py} +15 -11
- codeberg_cli/routes/org/__init__.py +6 -0
- codeberg_cli/routes/org/list.py +28 -0
- codeberg_cli/routes/org/members.py +31 -0
- codeberg_cli/routes/org/teams.py +38 -0
- codeberg_cli/routes/org/view.py +33 -0
- codeberg_cli/routes/pr/check_merge.py +33 -0
- codeberg_cli/routes/pr/commits.py +44 -0
- codeberg_cli/routes/pr/diff.py +31 -0
- codeberg_cli/routes/pr/files.py +42 -0
- codeberg_cli/routes/pr/list.py +16 -10
- codeberg_cli/routes/pr/reopen.py +26 -0
- codeberg_cli/routes/pr/update.py +27 -0
- codeberg_cli/routes/pr/view.py +5 -1
- codeberg_cli/routes/release/delete.py +38 -0
- codeberg_cli/routes/release/edit.py +56 -0
- codeberg_cli/routes/release/list.py +14 -11
- codeberg_cli/routes/release/view.py +12 -3
- codeberg_cli/routes/repo/archive.py +36 -0
- codeberg_cli/routes/repo/branch/__init__.py +6 -0
- codeberg_cli/routes/repo/branch/create.py +31 -0
- codeberg_cli/routes/repo/branch/delete.py +26 -0
- codeberg_cli/routes/repo/branch/list.py +36 -0
- codeberg_cli/routes/repo/branch/view.py +36 -0
- codeberg_cli/routes/repo/collaborator/__init__.py +6 -0
- codeberg_cli/routes/repo/collaborator/add.py +31 -0
- codeberg_cli/routes/repo/collaborator/delete.py +26 -0
- codeberg_cli/routes/repo/collaborator/list.py +37 -0
- codeberg_cli/routes/repo/commits.py +51 -0
- codeberg_cli/routes/repo/contents.py +55 -0
- codeberg_cli/routes/repo/edit.py +64 -0
- codeberg_cli/routes/repo/forks/sync.py +32 -0
- codeberg_cli/routes/repo/forks.py +42 -0
- codeberg_cli/routes/repo/languages.py +45 -0
- codeberg_cli/routes/repo/list.py +16 -10
- codeberg_cli/routes/repo/migrate.py +33 -0
- codeberg_cli/routes/repo/search.py +58 -0
- codeberg_cli/routes/repo/stargazers.py +40 -0
- codeberg_cli/routes/repo/tag/__init__.py +6 -0
- codeberg_cli/routes/repo/tag/create.py +34 -0
- codeberg_cli/routes/repo/tag/delete.py +26 -0
- codeberg_cli/routes/repo/tag/list.py +36 -0
- codeberg_cli/routes/repo/topics/__init__.py +6 -0
- codeberg_cli/routes/repo/topics/list.py +36 -0
- codeberg_cli/routes/repo/topics/set.py +27 -0
- codeberg_cli/routes/repo/transfer.py +35 -0
- codeberg_cli/routes/repo/unwatch.py +25 -0
- codeberg_cli/routes/repo/view.py +6 -2
- codeberg_cli/routes/repo/watch.py +25 -0
- codeberg_cli/routes/repo/watchers.py +40 -0
- codeberg_cli/routes/{user/view.py → user.py} +6 -2
- codeberg_cli-0.3.0.dist-info/METADATA +232 -0
- codeberg_cli-0.3.0.dist-info/RECORD +122 -0
- codeberg_cli/routes/notification/__init__.py +0 -6
- codeberg_cli/routes/user/__init__.py +0 -6
- codeberg_cli-0.2.0.dist-info/METADATA +0 -118
- codeberg_cli-0.2.0.dist-info/RECORD +0 -59
- {codeberg_cli-0.2.0.dist-info → codeberg_cli-0.3.0.dist-info}/WHEEL +0 -0
- {codeberg_cli-0.2.0.dist-info → codeberg_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {codeberg_cli-0.2.0.dist-info → codeberg_cli-0.3.0.dist-info}/licenses/LICENSE +0 -0
codeberg_cli/client.py
CHANGED
|
@@ -7,6 +7,7 @@ DEFAULT_BASE_URL = "https://codeberg.org"
|
|
|
7
7
|
_VERBS = {
|
|
8
8
|
"GET": "Fetching",
|
|
9
9
|
"POST": "Creating",
|
|
10
|
+
"PUT": "Updating",
|
|
10
11
|
"PATCH": "Updating",
|
|
11
12
|
"DELETE": "Deleting",
|
|
12
13
|
}
|
|
@@ -55,6 +56,11 @@ class Client:
|
|
|
55
56
|
) -> dict | list:
|
|
56
57
|
return self._request("PATCH", path, json=data, action=action)
|
|
57
58
|
|
|
59
|
+
def put(
|
|
60
|
+
self, path: str, data: dict | None = None, action: str | None = None
|
|
61
|
+
) -> dict | list:
|
|
62
|
+
return self._request("PUT", path, json=data, action=action)
|
|
63
|
+
|
|
58
64
|
def delete(self, path: str, action: str | None = None) -> None:
|
|
59
65
|
self._request("DELETE", path, action=action)
|
|
60
66
|
|
codeberg_cli/helpers.py
CHANGED
|
@@ -1,7 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json as _json
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import rich
|
|
9
|
+
|
|
1
10
|
from codeberg_cli.client import Client, ClientError, DEFAULT_BASE_URL
|
|
2
11
|
from codeberg_cli.config import load_config
|
|
3
12
|
|
|
4
13
|
|
|
14
|
+
def is_json_mode() -> bool:
|
|
15
|
+
"""Check whether ``--json`` was passed on the CLI."""
|
|
16
|
+
return "--json" in sys.argv[1:]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def output(
|
|
20
|
+
data: Any,
|
|
21
|
+
*,
|
|
22
|
+
json: bool | None = None,
|
|
23
|
+
default: str | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Print *data* as JSON or fall back to *default* text.
|
|
26
|
+
|
|
27
|
+
When ``json=True`` (or global ``--json`` was passed), data is printed as
|
|
28
|
+
prettified JSON. When ``json=False`` the *default* string (if given) is
|
|
29
|
+
printed. When *default* is ``None`` and JSON mode is off, this is a no-op
|
|
30
|
+
— the caller is expected to handle rich printing themselves.
|
|
31
|
+
"""
|
|
32
|
+
if json is None:
|
|
33
|
+
json = is_json_mode()
|
|
34
|
+
if json:
|
|
35
|
+
_json.dump(
|
|
36
|
+
data,
|
|
37
|
+
sys.stdout,
|
|
38
|
+
indent=2,
|
|
39
|
+
default=str,
|
|
40
|
+
)
|
|
41
|
+
sys.stdout.write("\n")
|
|
42
|
+
elif default is not None:
|
|
43
|
+
rich.print(default)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def print_table(
|
|
47
|
+
columns: list[str],
|
|
48
|
+
rows: Iterable[Iterable[str]],
|
|
49
|
+
*,
|
|
50
|
+
json: bool | None = None,
|
|
51
|
+
json_data: Any = None,
|
|
52
|
+
title: str | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Print a table, or JSON when ``--json`` is active.
|
|
55
|
+
|
|
56
|
+
When JSON mode is on, *json_data* is printed as JSON (or *rows* converted
|
|
57
|
+
to a list if no explicit json_data is given). When not in JSON mode, a
|
|
58
|
+
rich ``Table`` is printed.
|
|
59
|
+
"""
|
|
60
|
+
if json is None:
|
|
61
|
+
json = is_json_mode()
|
|
62
|
+
if json:
|
|
63
|
+
_json.dump(
|
|
64
|
+
json_data if json_data is not None else list(rows),
|
|
65
|
+
sys.stdout,
|
|
66
|
+
indent=2,
|
|
67
|
+
default=str,
|
|
68
|
+
)
|
|
69
|
+
sys.stdout.write("\n")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
from rich.table import Table
|
|
73
|
+
|
|
74
|
+
table = Table(*columns, title=title)
|
|
75
|
+
for row in rows:
|
|
76
|
+
table.add_row(*row)
|
|
77
|
+
rich.print(table)
|
|
78
|
+
|
|
79
|
+
|
|
5
80
|
def get_base_url() -> str:
|
|
6
81
|
"""Get the web base URL from cascading CLI context or default."""
|
|
7
82
|
from xclif.context import get_context
|
codeberg_cli/routes/__init__.py
CHANGED
|
@@ -5,5 +5,9 @@ from xclif import Cascade, WithConfig, command
|
|
|
5
5
|
@command("cb")
|
|
6
6
|
def _(
|
|
7
7
|
base_url: Cascade[WithConfig[str]] = DEFAULT_BASE_URL,
|
|
8
|
+
json: bool = False,
|
|
8
9
|
) -> None:
|
|
9
|
-
"""Interact with Codeberg or any Forgejo instance — manage repos, issues, PRs, releases, and more.
|
|
10
|
+
"""Interact with Codeberg or any Forgejo instance — manage repos, issues, PRs, releases, actions, and more.
|
|
11
|
+
|
|
12
|
+
Use --json on any command for machine-readable output.
|
|
13
|
+
"""
|
|
@@ -0,0 +1,31 @@
|
|
|
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("dispatch")
|
|
11
|
+
def _(
|
|
12
|
+
workflow: Annotated[str, Arg(description="Workflow filename (e.g. ci.yml)")],
|
|
13
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
14
|
+
ref: Annotated[str, Option(description="Branch or tag to dispatch on", name="ref")] = "main",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Dispatch a workflow (trigger a workflow_dispatch event)."""
|
|
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(
|
|
27
|
+
f"/repos/{repo}/actions/workflows/{workflow}/dispatches",
|
|
28
|
+
data={"ref": ref},
|
|
29
|
+
action="Dispatching workflow",
|
|
30
|
+
)
|
|
31
|
+
rich.print(f"[green]Dispatched {workflow} on {ref} in {repo}.[/green]")
|
|
@@ -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 is_json_mode, output, require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("run")
|
|
11
|
+
def _(
|
|
12
|
+
run_id: Annotated[int, Arg(description="Run ID")],
|
|
13
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
14
|
+
) -> None:
|
|
15
|
+
"""View details of a specific action run."""
|
|
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
|
+
run = client.get(f"/repos/{repo}/actions/runs/{run_id}")
|
|
26
|
+
|
|
27
|
+
if is_json_mode():
|
|
28
|
+
output(run)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
rich.print(f"[bold]Run #{run['id']}:[/bold] {run['display_title']}")
|
|
32
|
+
rich.print(f" Status: {run.get('status', '?')}")
|
|
33
|
+
rich.print(f" Conclusion: {run.get('conclusion', 'N/A')}")
|
|
34
|
+
rich.print(f" Event: {run.get('event', '?')}")
|
|
35
|
+
rich.print(f" Branch: {run.get('head_branch', '?')}")
|
|
36
|
+
rich.print(f" Commit: {run.get('head_sha', '?')[:12]}")
|
|
37
|
+
rich.print(f" Created: {run.get('created_at', '?')}")
|
|
38
|
+
rich.print(f" Updated: {run.get('updated_at', '?')}")
|
|
39
|
+
if run.get("url"):
|
|
40
|
+
rich.print(f" URL: {run['url']}")
|
|
@@ -0,0 +1,52 @@
|
|
|
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 Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("runs")
|
|
11
|
+
def _(
|
|
12
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
13
|
+
limit: Annotated[int, Option(description="Maximum runs to show", name="limit")] = 20,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""List action runs for a repository."""
|
|
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
|
+
runs = client.get(f"/repos/{repo}/actions/runs", params={"limit": limit, "page": 1})
|
|
26
|
+
|
|
27
|
+
if not runs:
|
|
28
|
+
rich.print(f"[dim]No action runs in {repo}.[/dim]")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
rows = []
|
|
32
|
+
json_rows = []
|
|
33
|
+
for run in runs:
|
|
34
|
+
status = run.get("status", "?")
|
|
35
|
+
conclusion = run.get("conclusion", "")
|
|
36
|
+
label = f"{status}" if not conclusion else f"{conclusion}"
|
|
37
|
+
rows.append((str(run["id"]), run["display_title"][:72], label, run["event"]))
|
|
38
|
+
json_rows.append({
|
|
39
|
+
"id": run["id"],
|
|
40
|
+
"title": run["display_title"],
|
|
41
|
+
"status": status,
|
|
42
|
+
"conclusion": conclusion,
|
|
43
|
+
"event": run["event"],
|
|
44
|
+
"created_at": run.get("created_at", ""),
|
|
45
|
+
"head_branch": run.get("head_branch", ""),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
print_table(
|
|
49
|
+
["ID", "Title", "Status", "Event"],
|
|
50
|
+
rows,
|
|
51
|
+
json_data=json_rows,
|
|
52
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
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 Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("workflows")
|
|
11
|
+
def _(
|
|
12
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
13
|
+
) -> None:
|
|
14
|
+
"""List workflows for a repository."""
|
|
15
|
+
client = require_client()
|
|
16
|
+
|
|
17
|
+
if not repo:
|
|
18
|
+
inferred = infer_repo()
|
|
19
|
+
if not inferred:
|
|
20
|
+
rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
|
|
21
|
+
return 1
|
|
22
|
+
repo = inferred
|
|
23
|
+
|
|
24
|
+
workflows = client.get(f"/repos/{repo}/actions/workflows")
|
|
25
|
+
|
|
26
|
+
if not workflows:
|
|
27
|
+
rich.print(f"[dim]No workflows in {repo}.[/dim]")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
rows = []
|
|
31
|
+
json_rows = []
|
|
32
|
+
for wf in workflows:
|
|
33
|
+
state = wf.get("state", "?")
|
|
34
|
+
rows.append((str(wf["id"]), wf["name"], wf.get("filename", ""), state))
|
|
35
|
+
json_rows.append({
|
|
36
|
+
"id": wf["id"],
|
|
37
|
+
"name": wf["name"],
|
|
38
|
+
"filename": wf.get("filename", ""),
|
|
39
|
+
"state": state,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
print_table(
|
|
43
|
+
["ID", "Name", "Filename", "State"],
|
|
44
|
+
rows,
|
|
45
|
+
json_data=json_rows,
|
|
46
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
id: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Delete 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
|
+
confirm = input(f"Are you sure you want to delete issue #{id} in {repo}? [y/N] ").strip().lower()
|
|
26
|
+
if confirm != "y":
|
|
27
|
+
rich.print("[dim]Cancelled.[/dim]")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
client.delete(f"/repos/{repo}/issues/{id}")
|
|
31
|
+
rich.print(f"[bold green]Deleted[/bold green] issue #{id} in {repo}")
|
|
@@ -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
|
+
id: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
label: Annotated[str, Arg(description="Label name")],
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Add a label 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/{id}/labels", data={"labels": [label]})
|
|
27
|
+
rich.print(f"[bold green]Added label[/bold green] \"{label}\" to issue #{id} in {repo}")
|
|
@@ -0,0 +1,43 @@
|
|
|
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")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
14
|
+
) -> None:
|
|
15
|
+
"""List labels 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
|
+
labels = client.get(f"/repos/{repo}/issues/{id}/labels")
|
|
26
|
+
|
|
27
|
+
if not labels:
|
|
28
|
+
rich.print(f"[dim]No labels on issue #{id} in {repo}.[/dim]")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
rows = []
|
|
32
|
+
json_rows = []
|
|
33
|
+
for label in labels:
|
|
34
|
+
color = label.get("color", "ffffff")
|
|
35
|
+
rows.append((str(label["id"]), f"#{color} {label['name']}", f"#{color}", label.get("description", "") or ""))
|
|
36
|
+
json_rows.append({
|
|
37
|
+
"id": label["id"],
|
|
38
|
+
"name": label["name"],
|
|
39
|
+
"color": f"#{color}",
|
|
40
|
+
"description": label.get("description", ""),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
print_table(["#", "Name", "Color", "Description"], rows, json_data=json_rows)
|
|
@@ -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("remove")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
label: Annotated[str, Arg(description="Label identifier")],
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Remove a label 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
|
+
client.delete(f"/repos/{repo}/issues/{id}/labels/{label}")
|
|
27
|
+
rich.print(f"[bold green]Removed label[/bold green] \"{label}\" from issue #{id} in {repo}")
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
2
|
|
|
3
3
|
import rich
|
|
4
|
-
from rich.table import Table
|
|
5
4
|
|
|
6
5
|
from codeberg_cli.git import infer_repo
|
|
7
|
-
from codeberg_cli.helpers import require_client
|
|
6
|
+
from codeberg_cli.helpers import print_table, require_client
|
|
8
7
|
from xclif import Option, command
|
|
9
8
|
|
|
10
9
|
|
|
@@ -35,15 +34,21 @@ def _(
|
|
|
35
34
|
rich.print(f"[dim]No {state} issues in {repo}.[/dim]")
|
|
36
35
|
return
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
rows = []
|
|
38
|
+
json_rows = []
|
|
39
39
|
for issue in issues:
|
|
40
40
|
labels_str = ", ".join(l["name"] for l in (issue.get("labels") or []))
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
issue["
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
rows.append((str(issue["number"]), issue["title"], issue["user"]["login"], labels_str))
|
|
42
|
+
json_rows.append({
|
|
43
|
+
"number": issue["number"],
|
|
44
|
+
"title": issue["title"],
|
|
45
|
+
"author": issue["user"]["login"],
|
|
46
|
+
"state": issue.get("state"),
|
|
47
|
+
"labels": [l["name"] for l in (issue.get("labels") or [])],
|
|
48
|
+
"created_at": issue.get("created_at"),
|
|
49
|
+
"updated_at": issue.get("updated_at"),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
print_table(["#", "Title", "Author", "Labels"], rows, json_data=json_rows)
|
|
48
53
|
|
|
49
54
|
|
|
@@ -0,0 +1,26 @@
|
|
|
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("pin")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Pin 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
|
+
client.post(f"/repos/{repo}/issues/{id}/pin")
|
|
26
|
+
rich.print(f"[bold green]Pinned[/bold green] issue #{id} in {repo}")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.helpers import print_table, require_client
|
|
6
|
+
from xclif import Arg, Option, command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@command("search")
|
|
10
|
+
def _(
|
|
11
|
+
query: Annotated[str, Arg(description="Search query")],
|
|
12
|
+
state: Annotated[str, Option(description="Filter by state: open, closed", name="state")] = "open",
|
|
13
|
+
limit: Annotated[int, Option(description="Maximum results to show", name="limit")] = 30,
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo) to scope search", name="repo")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Search issues across repositories."""
|
|
17
|
+
client = require_client()
|
|
18
|
+
|
|
19
|
+
params = {"q": query, "state": state, "limit": limit, "page": 1}
|
|
20
|
+
|
|
21
|
+
if repo:
|
|
22
|
+
# Scope search to a specific repository
|
|
23
|
+
if "/" in repo:
|
|
24
|
+
owner, name = repo.split("/", 1)
|
|
25
|
+
params["owner"] = owner
|
|
26
|
+
params["repo"] = name
|
|
27
|
+
else:
|
|
28
|
+
params["repo"] = repo
|
|
29
|
+
|
|
30
|
+
results = client.get("/repos/issues/search", params=params)
|
|
31
|
+
|
|
32
|
+
if not results:
|
|
33
|
+
rich.print(f"[dim]No issues found matching \"{query}\".[/dim]")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
rows = []
|
|
37
|
+
json_rows = []
|
|
38
|
+
for issue in results:
|
|
39
|
+
labels_str = ", ".join(l["name"] for l in (issue.get("labels") or []))
|
|
40
|
+
rows.append((
|
|
41
|
+
str(issue["number"]),
|
|
42
|
+
issue["title"],
|
|
43
|
+
issue["user"]["login"],
|
|
44
|
+
issue.get("repository", {}).get("full_name", ""),
|
|
45
|
+
issue["state"],
|
|
46
|
+
labels_str,
|
|
47
|
+
))
|
|
48
|
+
json_rows.append({
|
|
49
|
+
"number": issue["number"],
|
|
50
|
+
"title": issue["title"],
|
|
51
|
+
"author": issue["user"]["login"],
|
|
52
|
+
"repo": issue.get("repository", {}).get("full_name", ""),
|
|
53
|
+
"state": issue["state"],
|
|
54
|
+
"labels": [l["name"] for l in (issue.get("labels") or [])],
|
|
55
|
+
"created_at": issue.get("created_at"),
|
|
56
|
+
"updated_at": issue.get("updated_at"),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
print_table(["#", "Title", "Author", "Repo", "State", "Labels"], rows, json_data=json_rows)
|
|
@@ -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("subscribe")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Subscribe to 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
|
+
user = client.get("/user")["login"]
|
|
26
|
+
client.put(f"/repos/{repo}/issues/{id}/subscriptions/{user}")
|
|
27
|
+
rich.print(f"[bold green]Subscribed[/bold green] to issue #{id} in {repo}")
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
id: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
time: Annotated[str, Arg(description="Time to add (e.g. 1h30m)")],
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
user: Annotated[str, Option(description="User to add time for", name="user")] = "",
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Add tracked time to 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 not user:
|
|
28
|
+
user = client.get("/user")["login"]
|
|
29
|
+
|
|
30
|
+
client.post(f"/repos/{repo}/issues/{id}/times", data={"time": time, "user": user})
|
|
31
|
+
rich.print(f"[bold green]Added[/bold green] {time} to issue #{id} in {repo}")
|
|
@@ -0,0 +1,42 @@
|
|
|
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")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
14
|
+
) -> None:
|
|
15
|
+
"""List tracked time 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
|
+
times = client.get(f"/repos/{repo}/issues/{id}/times")
|
|
26
|
+
|
|
27
|
+
if not times:
|
|
28
|
+
rich.print(f"[dim]No tracked time on issue #{id} in {repo}.[/dim]")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
rows = []
|
|
32
|
+
json_rows = []
|
|
33
|
+
for t in times:
|
|
34
|
+
rows.append((str(t["id"]), t["user"]["login"], str(t.get("time", "")), t.get("created", "")[:10]))
|
|
35
|
+
json_rows.append({
|
|
36
|
+
"id": t["id"],
|
|
37
|
+
"user": t["user"]["login"],
|
|
38
|
+
"time": t.get("time"),
|
|
39
|
+
"created": t.get("created"),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
print_table(["#", "User", "Time (s)", "Date"], rows, json_data=json_rows)
|