codeberg-cli 0.1.0__py3-none-any.whl → 0.2.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/__main__.py +1 -1
- codeberg_cli/client.py +20 -5
- codeberg_cli/git.py +18 -7
- codeberg_cli/helpers.py +30 -5
- codeberg_cli/routes/__init__.py +6 -3
- codeberg_cli/routes/auth/__init__.py +6 -1
- codeberg_cli/routes/auth/login.py +0 -2
- codeberg_cli/routes/auth/status.py +0 -2
- codeberg_cli/routes/auth/whoami.py +0 -2
- codeberg_cli/routes/issue/__init__.py +6 -1
- codeberg_cli/routes/issue/comment.py +41 -0
- codeberg_cli/routes/issue/create.py +2 -2
- codeberg_cli/routes/issue/edit.py +38 -0
- codeberg_cli/routes/issue/view.py +2 -2
- codeberg_cli/routes/label/__init__.py +6 -1
- codeberg_cli/routes/milestone/__init__.py +6 -1
- codeberg_cli/routes/notification/__init__.py +6 -0
- codeberg_cli/routes/notification/list.py +38 -0
- codeberg_cli/routes/pr/__init__.py +6 -1
- codeberg_cli/routes/pr/comment.py +41 -0
- codeberg_cli/routes/pr/create.py +2 -2
- codeberg_cli/routes/pr/edit.py +38 -0
- codeberg_cli/routes/pr/view.py +2 -2
- codeberg_cli/routes/release/__init__.py +6 -1
- codeberg_cli/routes/release/create.py +2 -2
- codeberg_cli/routes/release/upload.py +1 -1
- codeberg_cli/routes/release/view.py +2 -2
- codeberg_cli/routes/repo/__init__.py +6 -1
- codeberg_cli/routes/repo/clone.py +3 -2
- codeberg_cli/routes/repo/create.py +5 -4
- codeberg_cli/routes/repo/fork.py +1 -1
- codeberg_cli/routes/repo/star.py +25 -0
- codeberg_cli/routes/repo/unstar.py +25 -0
- codeberg_cli/routes/repo/view.py +4 -3
- codeberg_cli/routes/user/__init__.py +6 -0
- codeberg_cli/routes/user/view.py +26 -0
- codeberg_cli-0.2.0.dist-info/METADATA +118 -0
- codeberg_cli-0.2.0.dist-info/RECORD +59 -0
- codeberg_cli-0.1.0.dist-info/METADATA +0 -182
- codeberg_cli-0.1.0.dist-info/RECORD +0 -49
- {codeberg_cli-0.1.0.dist-info → codeberg_cli-0.2.0.dist-info}/WHEEL +0 -0
- {codeberg_cli-0.1.0.dist-info → codeberg_cli-0.2.0.dist-info}/entry_points.txt +0 -0
- {codeberg_cli-0.1.0.dist-info → codeberg_cli-0.2.0.dist-info}/licenses/LICENSE +0 -0
codeberg_cli/__main__.py
CHANGED
codeberg_cli/client.py
CHANGED
|
@@ -2,7 +2,7 @@ import httpx
|
|
|
2
2
|
from rich.status import Status
|
|
3
3
|
from xclif.context import get_context
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
DEFAULT_BASE_URL = "https://codeberg.org"
|
|
6
6
|
|
|
7
7
|
_VERBS = {
|
|
8
8
|
"GET": "Fetching",
|
|
@@ -19,7 +19,7 @@ class ClientError(RuntimeError):
|
|
|
19
19
|
class Client:
|
|
20
20
|
"""HTTP client for the Codeberg API."""
|
|
21
21
|
|
|
22
|
-
def __init__(self, token: str | None = None) -> None:
|
|
22
|
+
def __init__(self, token: str | None = None, base_url: str | None = None) -> None:
|
|
23
23
|
headers = {
|
|
24
24
|
"Accept": "application/json",
|
|
25
25
|
"User-Agent": "codeberg-cli/0.1.0",
|
|
@@ -27,7 +27,18 @@ class Client:
|
|
|
27
27
|
}
|
|
28
28
|
if token:
|
|
29
29
|
headers["Authorization"] = f"Bearer {token}"
|
|
30
|
-
|
|
30
|
+
raw = base_url or DEFAULT_BASE_URL
|
|
31
|
+
if "://" not in raw:
|
|
32
|
+
raw = "https://" + raw
|
|
33
|
+
raw = raw.rstrip("/")
|
|
34
|
+
if not raw.endswith("/api/v1"):
|
|
35
|
+
raw += "/api/v1"
|
|
36
|
+
self._client = httpx.Client(base_url=raw, headers=headers, timeout=30.0)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def base_url(self) -> str:
|
|
40
|
+
"""The API base URL used by this client."""
|
|
41
|
+
return str(self._client.base_url)
|
|
31
42
|
|
|
32
43
|
def get(
|
|
33
44
|
self, path: str, params: dict | None = None, action: str | None = None
|
|
@@ -50,10 +61,14 @@ class Client:
|
|
|
50
61
|
def _request(
|
|
51
62
|
self, method: str, path: str, action: str | None = None, **kwargs
|
|
52
63
|
) -> dict | list | None:
|
|
53
|
-
|
|
64
|
+
try:
|
|
65
|
+
ctx = get_context()
|
|
66
|
+
verbosity = ctx.verbosity
|
|
67
|
+
except RuntimeError:
|
|
68
|
+
verbosity = 0
|
|
54
69
|
label = action or (
|
|
55
70
|
f"{method} {path}"
|
|
56
|
-
if
|
|
71
|
+
if verbosity >= 2
|
|
57
72
|
else f"{_VERBS.get(method, method)} {path.split('/')[-1]}"
|
|
58
73
|
)
|
|
59
74
|
with Status(f"{label}..."):
|
codeberg_cli/git.py
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
|
-
import re
|
|
2
1
|
import subprocess
|
|
3
2
|
from pathlib import Path
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def parse_repo_from_remote(remote_url: str) -> str:
|
|
7
7
|
"""Extract owner/repo from a git remote URL."""
|
|
8
|
-
# HTTPS: https://
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
# HTTPS: https://hostname/owner/repo.git
|
|
9
|
+
parsed = urlparse(remote_url)
|
|
10
|
+
if parsed.scheme in ("http", "https"):
|
|
11
|
+
path = parsed.path
|
|
12
|
+
elif "@" in remote_url:
|
|
13
|
+
# SSH: git@hostname:owner/repo.git
|
|
14
|
+
path = remote_url.split(":", 1)[1]
|
|
15
|
+
else:
|
|
16
|
+
raise ValueError(f"Could not parse repo from remote: {remote_url}")
|
|
17
|
+
|
|
18
|
+
path = path.rstrip("/")
|
|
19
|
+
if path.endswith(".git"):
|
|
20
|
+
path = path[:-4]
|
|
21
|
+
owner_repo = path.removeprefix("/")
|
|
22
|
+
if "/" not in owner_repo:
|
|
23
|
+
raise ValueError(f"Could not parse repo from remote: {remote_url}")
|
|
24
|
+
return owner_repo
|
|
14
25
|
|
|
15
26
|
|
|
16
27
|
def get_default_branch(cwd: Path) -> str:
|
codeberg_cli/helpers.py
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
|
-
from codeberg_cli.client import Client, ClientError
|
|
1
|
+
from codeberg_cli.client import Client, ClientError, DEFAULT_BASE_URL
|
|
2
2
|
from codeberg_cli.config import load_config
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def
|
|
5
|
+
def get_base_url() -> str:
|
|
6
|
+
"""Get the web base URL from cascading CLI context or default."""
|
|
7
|
+
from xclif.context import get_context
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
ctx = get_context()
|
|
11
|
+
raw = ctx.get("base_url", DEFAULT_BASE_URL)
|
|
12
|
+
except RuntimeError:
|
|
13
|
+
raw = DEFAULT_BASE_URL
|
|
14
|
+
|
|
15
|
+
# Normalize: ensure scheme, strip API path suffix for backward compat
|
|
16
|
+
if "://" not in raw:
|
|
17
|
+
raw = "https://" + raw
|
|
18
|
+
raw = raw.rstrip("/")
|
|
19
|
+
if raw.endswith("/api/v1"):
|
|
20
|
+
raw = raw[:-7]
|
|
21
|
+
return raw
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_web_base_url() -> str:
|
|
25
|
+
"""Derive the web UI base URL from the API base URL."""
|
|
26
|
+
return get_base_url()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_authenticated_client(base_url: str | None = None) -> Client | None:
|
|
6
30
|
"""Return a Client if token is stored, else None."""
|
|
7
31
|
config = load_config()
|
|
8
32
|
token = config.get("token")
|
|
9
33
|
if not token:
|
|
10
34
|
return None
|
|
11
|
-
return Client(token=token)
|
|
35
|
+
return Client(token=token, base_url=base_url or get_base_url())
|
|
12
36
|
|
|
13
37
|
|
|
14
|
-
def require_client() -> Client:
|
|
38
|
+
def require_client(base_url: str | None = None) -> Client:
|
|
15
39
|
"""Return an authenticated Client or print error and exit."""
|
|
16
|
-
client = get_authenticated_client()
|
|
40
|
+
client = get_authenticated_client(base_url=base_url)
|
|
17
41
|
if client is None:
|
|
18
42
|
from xclif.errors import UsageError
|
|
43
|
+
|
|
19
44
|
raise UsageError("Not logged in. Run 'cb auth login' first.")
|
|
20
45
|
return client
|
codeberg_cli/routes/__init__.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
from
|
|
1
|
+
from codeberg_cli.client import DEFAULT_BASE_URL
|
|
2
|
+
from xclif import Cascade, WithConfig, command
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
@command("cb")
|
|
5
|
-
def _(
|
|
6
|
-
|
|
6
|
+
def _(
|
|
7
|
+
base_url: Cascade[WithConfig[str]] = DEFAULT_BASE_URL,
|
|
8
|
+
) -> None:
|
|
9
|
+
"""Interact with Codeberg or any Forgejo instance — manage repos, issues, PRs, releases, and more."""
|
|
@@ -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 require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("comment")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
message: Annotated[str, Option(description="Comment text", name="message")] = "",
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Comment on 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
|
+
if not message:
|
|
27
|
+
rich.print("[dim]Enter comment (Ctrl+D to finish):[/dim]")
|
|
28
|
+
try:
|
|
29
|
+
lines = []
|
|
30
|
+
while True:
|
|
31
|
+
line = input()
|
|
32
|
+
lines.append(line)
|
|
33
|
+
except (EOFError, KeyboardInterrupt):
|
|
34
|
+
pass
|
|
35
|
+
message = "\n".join(lines).strip()
|
|
36
|
+
if not message:
|
|
37
|
+
rich.print("[bold red]Error:[/bold red] Comment cannot be empty")
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
client.post(f"/repos/{repo}/issues/{id}/comments", data={"body": message})
|
|
41
|
+
rich.print(f"[bold green]Commented[/bold green] on issue #{id} in {repo}")
|
|
@@ -3,7 +3,7 @@ from typing import Annotated
|
|
|
3
3
|
import rich
|
|
4
4
|
|
|
5
5
|
from codeberg_cli.git import infer_repo
|
|
6
|
-
from codeberg_cli.helpers import require_client
|
|
6
|
+
from codeberg_cli.helpers import get_web_base_url, require_client
|
|
7
7
|
from xclif import Option, command
|
|
8
8
|
|
|
9
9
|
|
|
@@ -50,6 +50,6 @@ def _(
|
|
|
50
50
|
result = client.post(f"/repos/{repo}/issues", data=data)
|
|
51
51
|
|
|
52
52
|
rich.print(f"[bold green]Created issue[/bold green] #{result['number']}: {result['title']}")
|
|
53
|
-
rich.print(f"[dim]
|
|
53
|
+
rich.print(f"[dim]{get_web_base_url()}/{repo}/issues/{result['number']}[/dim]")
|
|
54
54
|
|
|
55
55
|
|
|
@@ -0,0 +1,38 @@
|
|
|
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("edit")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="Issue number")],
|
|
13
|
+
title: Annotated[str, Option(description="New title", name="title")] = "",
|
|
14
|
+
body: Annotated[str, Option(description="New body", name="body")] = "",
|
|
15
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Edit an issue's title or body."""
|
|
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
|
+
data: dict = {}
|
|
28
|
+
if title:
|
|
29
|
+
data["title"] = title
|
|
30
|
+
if body:
|
|
31
|
+
data["body"] = body
|
|
32
|
+
|
|
33
|
+
if not data:
|
|
34
|
+
rich.print("[bold yellow]Nothing to update.[/bold yellow] Pass --title and/or --body.")
|
|
35
|
+
return 1
|
|
36
|
+
|
|
37
|
+
client.patch(f"/repos/{repo}/issues/{id}", data=data)
|
|
38
|
+
rich.print(f"[bold green]Updated[/bold green] issue #{id} in {repo}")
|
|
@@ -4,7 +4,7 @@ from typing import Annotated
|
|
|
4
4
|
import rich
|
|
5
5
|
|
|
6
6
|
from codeberg_cli.git import infer_repo
|
|
7
|
-
from codeberg_cli.helpers import require_client
|
|
7
|
+
from codeberg_cli.helpers import get_web_base_url, require_client
|
|
8
8
|
from xclif import Arg, Option, command
|
|
9
9
|
|
|
10
10
|
|
|
@@ -25,7 +25,7 @@ def _(
|
|
|
25
25
|
repo = inferred
|
|
26
26
|
|
|
27
27
|
if web:
|
|
28
|
-
webbrowser.open(f"
|
|
28
|
+
webbrowser.open(f"{get_web_base_url()}/{repo}/issues/{id}")
|
|
29
29
|
return
|
|
30
30
|
|
|
31
31
|
issue = client.get(f"/repos/{repo}/issues/{id}")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from codeberg_cli.helpers import require_client
|
|
7
|
+
from xclif import Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("list", "ls")
|
|
11
|
+
def _(
|
|
12
|
+
limit: Annotated[int, Option(description="Maximum notifications to show", name="limit")] = 30,
|
|
13
|
+
all: Annotated[bool, Option(description="Show all notifications, not just unread", name="all")] = False,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""List notifications."""
|
|
16
|
+
client = require_client()
|
|
17
|
+
|
|
18
|
+
params: dict = {"limit": limit, "page": 1}
|
|
19
|
+
if not all:
|
|
20
|
+
params["status-types"] = "unread"
|
|
21
|
+
|
|
22
|
+
notifications = client.get("/notifications", params=params)
|
|
23
|
+
|
|
24
|
+
if not notifications:
|
|
25
|
+
rich.print("[dim]No notifications.[/dim]")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
table = Table("ID", "Type", "Subject", "Repo")
|
|
29
|
+
for n in notifications:
|
|
30
|
+
subject = n.get("subject", {})
|
|
31
|
+
repo = n.get("repository", {})
|
|
32
|
+
table.add_row(
|
|
33
|
+
str(n["id"]),
|
|
34
|
+
subject.get("type", ""),
|
|
35
|
+
subject.get("title", ""),
|
|
36
|
+
repo.get("full_name", ""),
|
|
37
|
+
)
|
|
38
|
+
rich.print(table)
|
|
@@ -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 require_client
|
|
7
|
+
from xclif import Arg, Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("comment")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="PR number")],
|
|
13
|
+
message: Annotated[str, Option(description="Comment text", name="message")] = "",
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Comment on a pull request."""
|
|
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
|
+
if not message:
|
|
27
|
+
rich.print("[dim]Enter comment (Ctrl+D to finish):[/dim]")
|
|
28
|
+
try:
|
|
29
|
+
lines = []
|
|
30
|
+
while True:
|
|
31
|
+
line = input()
|
|
32
|
+
lines.append(line)
|
|
33
|
+
except (EOFError, KeyboardInterrupt):
|
|
34
|
+
pass
|
|
35
|
+
message = "\n".join(lines).strip()
|
|
36
|
+
if not message:
|
|
37
|
+
rich.print("[bold red]Error:[/bold red] Comment cannot be empty")
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
client.post(f"/repos/{repo}/issues/{id}/comments", data={"body": message})
|
|
41
|
+
rich.print(f"[bold green]Commented[/bold green] on PR #{id} in {repo}")
|
codeberg_cli/routes/pr/create.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Annotated
|
|
|
5
5
|
import rich
|
|
6
6
|
|
|
7
7
|
from codeberg_cli.git import get_default_branch, infer_repo
|
|
8
|
-
from codeberg_cli.helpers import require_client
|
|
8
|
+
from codeberg_cli.helpers import get_web_base_url, require_client
|
|
9
9
|
from xclif import Option, command
|
|
10
10
|
|
|
11
11
|
|
|
@@ -63,6 +63,6 @@ def _(
|
|
|
63
63
|
|
|
64
64
|
result = client.post(f"/repos/{repo}/pulls", data=data)
|
|
65
65
|
rich.print(f"[bold green]Created PR[/bold green] #{result['number']}: {result['title']}")
|
|
66
|
-
rich.print(f"[dim]
|
|
66
|
+
rich.print(f"[dim]{get_web_base_url()}/{repo}/pulls/{result['number']}[/dim]")
|
|
67
67
|
|
|
68
68
|
|
|
@@ -0,0 +1,38 @@
|
|
|
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("edit")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="PR number")],
|
|
13
|
+
title: Annotated[str, Option(description="New title", name="title")] = "",
|
|
14
|
+
body: Annotated[str, Option(description="New body", name="body")] = "",
|
|
15
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Edit a pull request's title or body."""
|
|
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
|
+
data: dict = {}
|
|
28
|
+
if title:
|
|
29
|
+
data["title"] = title
|
|
30
|
+
if body:
|
|
31
|
+
data["body"] = body
|
|
32
|
+
|
|
33
|
+
if not data:
|
|
34
|
+
rich.print("[bold yellow]Nothing to update.[/bold yellow] Pass --title and/or --body.")
|
|
35
|
+
return 1
|
|
36
|
+
|
|
37
|
+
client.patch(f"/repos/{repo}/pulls/{id}", data=data)
|
|
38
|
+
rich.print(f"[bold green]Updated[/bold green] PR #{id} in {repo}")
|
codeberg_cli/routes/pr/view.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Annotated
|
|
|
4
4
|
import rich
|
|
5
5
|
|
|
6
6
|
from codeberg_cli.git import infer_repo
|
|
7
|
-
from codeberg_cli.helpers import require_client
|
|
7
|
+
from codeberg_cli.helpers import get_web_base_url, require_client
|
|
8
8
|
from xclif import Arg, Option, command
|
|
9
9
|
|
|
10
10
|
|
|
@@ -25,7 +25,7 @@ def _(
|
|
|
25
25
|
repo = inferred
|
|
26
26
|
|
|
27
27
|
if web:
|
|
28
|
-
webbrowser.open(f"
|
|
28
|
+
webbrowser.open(f"{get_web_base_url()}/{repo}/pulls/{id}")
|
|
29
29
|
return
|
|
30
30
|
|
|
31
31
|
pr = client.get(f"/repos/{repo}/pulls/{id}")
|
|
@@ -3,7 +3,7 @@ from typing import Annotated
|
|
|
3
3
|
import rich
|
|
4
4
|
|
|
5
5
|
from codeberg_cli.git import infer_repo
|
|
6
|
-
from codeberg_cli.helpers import require_client
|
|
6
|
+
from codeberg_cli.helpers import get_web_base_url, require_client
|
|
7
7
|
from xclif import Arg, Option, command
|
|
8
8
|
|
|
9
9
|
|
|
@@ -36,5 +36,5 @@ def _(
|
|
|
36
36
|
|
|
37
37
|
result = client.post(f"/repos/{repo}/releases", data=data)
|
|
38
38
|
rich.print(f"[bold green]Created release[/bold green] {result.get('name', tag)}")
|
|
39
|
-
rich.print(f"[dim]
|
|
39
|
+
rich.print(f"[dim]{get_web_base_url()}/{repo}/releases/tag/{tag}[/dim]")
|
|
40
40
|
|
|
@@ -38,7 +38,7 @@ def _(
|
|
|
38
38
|
headers = {"Authorization": f"Bearer {token}"}
|
|
39
39
|
|
|
40
40
|
response = httpx.post(
|
|
41
|
-
f"
|
|
41
|
+
f"{client.base_url}/repos/{repo}/releases/{id}/assets",
|
|
42
42
|
headers=headers,
|
|
43
43
|
files={"attachment": (file_path.name, content)},
|
|
44
44
|
)
|
|
@@ -4,7 +4,7 @@ from typing import Annotated
|
|
|
4
4
|
import rich
|
|
5
5
|
|
|
6
6
|
from codeberg_cli.git import infer_repo
|
|
7
|
-
from codeberg_cli.helpers import require_client
|
|
7
|
+
from codeberg_cli.helpers import get_web_base_url, require_client
|
|
8
8
|
from xclif import Arg, Option, command
|
|
9
9
|
|
|
10
10
|
|
|
@@ -27,7 +27,7 @@ def _(
|
|
|
27
27
|
release = client.get(f"/repos/{repo}/releases/{id}")
|
|
28
28
|
|
|
29
29
|
if web:
|
|
30
|
-
webbrowser.open(release.get("html_url", f"
|
|
30
|
+
webbrowser.open(release.get("html_url", f"{get_web_base_url()}/{repo}/releases/tag/{release['tag_name']}"))
|
|
31
31
|
return
|
|
32
32
|
|
|
33
33
|
rich.print(f"[bold]{release.get('name', release['tag_name'])}[/bold]")
|
|
@@ -3,6 +3,7 @@ from typing import Annotated
|
|
|
3
3
|
|
|
4
4
|
import rich
|
|
5
5
|
|
|
6
|
+
from codeberg_cli.helpers import get_web_base_url
|
|
6
7
|
from xclif import Arg, command
|
|
7
8
|
|
|
8
9
|
|
|
@@ -10,8 +11,8 @@ from xclif import Arg, command
|
|
|
10
11
|
def _(
|
|
11
12
|
repo: Annotated[str, Arg(description="Repository in owner/repo format")],
|
|
12
13
|
) -> None:
|
|
13
|
-
"""Clone a
|
|
14
|
-
url = f"
|
|
14
|
+
"""Clone a repository."""
|
|
15
|
+
url = f"{get_web_base_url()}/{repo}.git"
|
|
15
16
|
rich.print(f"Cloning [cyan]{repo}[/cyan]...")
|
|
16
17
|
result = subprocess.run(["git", "clone", url])
|
|
17
18
|
if result.returncode != 0:
|
|
@@ -5,7 +5,7 @@ import rich
|
|
|
5
5
|
from rich.prompt import Confirm, Prompt
|
|
6
6
|
|
|
7
7
|
from codeberg_cli.git import get_repo_remote
|
|
8
|
-
from codeberg_cli.helpers import require_client
|
|
8
|
+
from codeberg_cli.helpers import get_web_base_url, require_client
|
|
9
9
|
from xclif import Option, command
|
|
10
10
|
|
|
11
11
|
|
|
@@ -36,14 +36,15 @@ def _(
|
|
|
36
36
|
"auto_init": True,
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
web = get_web_base_url()
|
|
39
40
|
if org:
|
|
40
41
|
result = client.post(f"/orgs/{org}/repos", data=data, action=f"Creating repository in {org}")
|
|
41
42
|
owner = org
|
|
42
|
-
repo_url = f"
|
|
43
|
+
repo_url = f"{web}/{org}/{name}"
|
|
43
44
|
else:
|
|
44
45
|
result = client.post("/user/repos", data=data, action="Creating repository")
|
|
45
46
|
owner = result["owner"]["login"]
|
|
46
|
-
repo_url = f"
|
|
47
|
+
repo_url = f"{web}/{owner}/{name}"
|
|
47
48
|
|
|
48
49
|
rich.print(f"[bold green]Created repository[/bold green] {repo_url}")
|
|
49
50
|
|
|
@@ -62,7 +63,7 @@ def _(
|
|
|
62
63
|
|
|
63
64
|
try:
|
|
64
65
|
subprocess.run(
|
|
65
|
-
["git", "remote", "add", "origin", f"
|
|
66
|
+
["git", "remote", "add", "origin", f"{web}/{owner}/{name}.git"],
|
|
66
67
|
capture_output=True, text=True, check=True, cwd=cwd,
|
|
67
68
|
)
|
|
68
69
|
rich.print("[dim]Added remote 'origin'[/dim]")
|
codeberg_cli/routes/repo/fork.py
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
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 Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("star")
|
|
11
|
+
def _(
|
|
12
|
+
repo: Annotated[str, Option(description="Repository to star")] = "",
|
|
13
|
+
) -> None:
|
|
14
|
+
"""Star 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
|
+
client.put(f"/user/starred/{repo}", action=f"Starring {repo}")
|
|
25
|
+
rich.print(f"[bold green]Starred[/bold green] {repo}")
|
|
@@ -0,0 +1,25 @@
|
|
|
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 Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("unstar")
|
|
11
|
+
def _(
|
|
12
|
+
repo: Annotated[str, Option(description="Repository to unstar")] = "",
|
|
13
|
+
) -> None:
|
|
14
|
+
"""Unstar 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
|
+
client.delete(f"/user/starred/{repo}", action=f"Unstarring {repo}")
|
|
25
|
+
rich.print(f"[bold green]Unstarred[/bold green] {repo}")
|
codeberg_cli/routes/repo/view.py
CHANGED
|
@@ -5,7 +5,7 @@ import rich
|
|
|
5
5
|
from rich.markup import escape
|
|
6
6
|
|
|
7
7
|
from codeberg_cli.git import infer_repo
|
|
8
|
-
from codeberg_cli.helpers import require_client
|
|
8
|
+
from codeberg_cli.helpers import get_web_base_url, require_client
|
|
9
9
|
from xclif import Arg, Option, command
|
|
10
10
|
|
|
11
11
|
|
|
@@ -25,7 +25,7 @@ def _(
|
|
|
25
25
|
repo = inferred
|
|
26
26
|
|
|
27
27
|
if web:
|
|
28
|
-
webbrowser.open(f"
|
|
28
|
+
webbrowser.open(f"{get_web_base_url()}/{repo}")
|
|
29
29
|
return
|
|
30
30
|
|
|
31
31
|
data = client.get(f"/repos/{repo}")
|
|
@@ -36,6 +36,7 @@ def _(
|
|
|
36
36
|
rich.print(f" [dim]Stars:[/dim] {data.get('stars_count', 0)} [dim]Forks:[/dim] {data.get('forks_count', 0)}")
|
|
37
37
|
rich.print(f" [dim]Language:[/dim] {data.get('language', 'N/A')}")
|
|
38
38
|
rich.print(f" [dim]Visibility:[/dim] {'Private' if data.get('private') else 'Public'}")
|
|
39
|
-
|
|
39
|
+
web_url = f"{get_web_base_url()}/{repo}"
|
|
40
|
+
rich.print(f" [dim]URL:[/dim] {web_url}")
|
|
40
41
|
|
|
41
42
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
from rich.markup import escape
|
|
5
|
+
|
|
6
|
+
from codeberg_cli.helpers import require_client
|
|
7
|
+
from xclif import Option, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("view")
|
|
11
|
+
def _(
|
|
12
|
+
username: Annotated[str, Option(description="Username (defaults to authenticated user)", name="username")] = "",
|
|
13
|
+
) -> None:
|
|
14
|
+
"""View a user's profile (defaults to the authenticated user)."""
|
|
15
|
+
client = require_client()
|
|
16
|
+
|
|
17
|
+
user = client.get(f"/users/{username}" if username else "/user")
|
|
18
|
+
|
|
19
|
+
rich.print(f"[bold]{user['login']}[/bold]")
|
|
20
|
+
if user.get("full_name"):
|
|
21
|
+
rich.print(escape(user["full_name"]))
|
|
22
|
+
if user.get("email"):
|
|
23
|
+
rich.print(f" [dim]Email:[/dim] {user['email']}")
|
|
24
|
+
rich.print(f" [dim]Repos:[/dim] {user.get('num_repos', 0)} [dim]Stars:[/dim] {user.get('num_stars', 0)} [dim]Followers:[/dim] {user.get('followers_count', 0)} [dim]Following:[/dim] {user.get('following_count', 0)}")
|
|
25
|
+
if user.get("description"):
|
|
26
|
+
rich.print(f" [dim]Bio:[/dim] {escape(user['description'])}")
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codeberg-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A Forgejo CLI — works with Codeberg and any Forgejo instance
|
|
5
|
+
Project-URL: Homepage, https://codeberg.org/ThatXliner/codeberg-cli
|
|
6
|
+
Project-URL: Repository, https://codeberg.org/ThatXliner/codeberg-cli
|
|
7
|
+
Project-URL: Issues, https://codeberg.org/ThatXliner/codeberg-cli/issues
|
|
8
|
+
Author-email: Bryan Hu <thatxliner@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,codeberg,forgejo
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: httpx
|
|
21
|
+
Requires-Dist: platformdirs
|
|
22
|
+
Requires-Dist: tomlkit
|
|
23
|
+
Requires-Dist: xclif>=0.4.3
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# cb — A Codeberg CLI
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/codeberg-cli)
|
|
29
|
+
[](https://pypi.org/project/codeberg-cli)
|
|
30
|
+
[](#license)
|
|
31
|
+
|
|
32
|
+
`cb` is a CLI for [Codeberg](https://codeberg.org) (a [Forgejo](https://forgejo.org) instance) — think `gh` for Codeberg. It also works with any Forgejo instance. Built with [Xclif](https://xclif.readthedocs.io).
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
# One-time setup
|
|
36
|
+
cb auth login
|
|
37
|
+
|
|
38
|
+
# Work with repos, issues, PRs, releases
|
|
39
|
+
cb repo list
|
|
40
|
+
cb issue create --title "Fix the thing"
|
|
41
|
+
cb pr create --base main --head fix
|
|
42
|
+
cb release create v0.2.0
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install codeberg-cli # or: uv tool install codeberg-cli
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or from source:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://codeberg.org/ThatXliner/codeberg-cli.git
|
|
55
|
+
cd cb
|
|
56
|
+
uv tool install .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quickstart
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Authenticate (tokens at https://codeberg.org/user/settings/applications)
|
|
63
|
+
cb auth login
|
|
64
|
+
|
|
65
|
+
# Who am I?
|
|
66
|
+
cb auth whoami
|
|
67
|
+
|
|
68
|
+
# List your repos
|
|
69
|
+
cb repo list
|
|
70
|
+
|
|
71
|
+
# Clone one
|
|
72
|
+
cb repo clone ThatXliner/cb
|
|
73
|
+
|
|
74
|
+
# Open an issue
|
|
75
|
+
cb issue create --title "suggestion" --body "what about..."
|
|
76
|
+
|
|
77
|
+
# See everything you can do
|
|
78
|
+
cb --help
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Config
|
|
82
|
+
|
|
83
|
+
Token stored in `$XDG_CONFIG_HOME/codeberg-cli/config.toml` (managed by `cb auth login` / `cb auth logout`).
|
|
84
|
+
|
|
85
|
+
View or change config:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cb config path # Show config file location
|
|
89
|
+
cb config get # Print all config values
|
|
90
|
+
cb config set base_url "https://codeberg.org/api/v1" # Codeberg (default)
|
|
91
|
+
cb config set base_url "https://git.example.com/api/v1" # Self-hosted Forgejo
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Comparison
|
|
95
|
+
|
|
96
|
+
| | **cb** | **fj** (forgejo-cli) | **berg** (codeberg-cli) |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| Language | Python | Rust | Rust |
|
|
99
|
+
| Multi-instance | Yes (configurable via `cb config set base_url`) | Any Forgejo instance | Yes |
|
|
100
|
+
| Issues | create, list, view, close, reopen, comment, edit | open, edit, comment, close | yes |
|
|
101
|
+
| Pull requests | create, list, view, merge, checkout, close, comment, edit | create, merge | yes |
|
|
102
|
+
| Releases | create, list, view, upload | publish | — |
|
|
103
|
+
| Repos | create, list, clone, view, fork, delete, star, unstar | create, edit, star, watch | yes |
|
|
104
|
+
| Labels | create, list, delete | — | yes |
|
|
105
|
+
| Milestones | create, list | — | yes |
|
|
106
|
+
| Notifications | list | — | yes |
|
|
107
|
+
| User profiles | view | search, keys | — |
|
|
108
|
+
| Raw API | `cb api GET /path` | — | — |
|
|
109
|
+
| Config management | `cb config get/set/path` | — | `berg config` |
|
|
110
|
+
| AGit PRs (no-fork) | — | yes | — |
|
|
111
|
+
| Org/team mgmt | — | yes | — |
|
|
112
|
+
| Install | `pip install codeberg-cli` | prebuilt binaries | `cargo install codeberg-cli` |
|
|
113
|
+
|
|
114
|
+
**Choose `fj`** if you self-host Forgejo or need org/team management or Actions workflows. **Choose `berg`** if you want your tools to be written in Rust. **Choose `cb`** if you want a minimal, readable Python CLI — it's the only one that uploads release assets, the only one with a raw API command, and the most fully-featured for everyday issue/PR/repo workflows. We will be actively working on making ours the most feature complete, just open an issue!
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
codeberg_cli/__main__.py,sha256=35xiBzBbgDy9rcHqCJWHdIVsWhGEtPQNd-s9wt3k7yE,141
|
|
2
|
+
codeberg_cli/client.py,sha256=oObp9d5HN7YZgQ33I6qcnwPGqsk51wr45TQF2qoD5h8,2719
|
|
3
|
+
codeberg_cli/config.py,sha256=bjVHHM1sG3xTIU6EC7sakwR0nnP-ZsCtO4Jn2od6X4A,822
|
|
4
|
+
codeberg_cli/git.py,sha256=DIzHSaezhWQGl63f8pkYDNd-qDfoMe7SZBZ7TWVlCxc,1899
|
|
5
|
+
codeberg_cli/helpers.py,sha256=wa0wAyQbKI5Lb82IBKMisURUTfWJQ_lSPHGMlz1KKrs,1387
|
|
6
|
+
codeberg_cli/routes/__init__.py,sha256=y-lopyOkrIdwAZ5yRo2h_RZWtB1RmNq7bN91WU3uOP0,298
|
|
7
|
+
codeberg_cli/routes/api.py,sha256=o8she7_o32DNuYqr1v9D0hEHjujW1xEZEhHuxIzKO_8,720
|
|
8
|
+
codeberg_cli/routes/auth/__init__.py,sha256=eQHz7mep-kGESlmrv4tTAD3EPTqlNC2Dy9MeJkTxUi0,139
|
|
9
|
+
codeberg_cli/routes/auth/login.py,sha256=6QxaEz5cvSgJbr-dXpudP2VbGxsLx3xPw0cMwWZm1aU,654
|
|
10
|
+
codeberg_cli/routes/auth/logout.py,sha256=LfsYchVxrr77R5c2WNGbKNCLbkyQtbobkhcpw1DH8HA,351
|
|
11
|
+
codeberg_cli/routes/auth/status.py,sha256=jNhIPtcxQMslPhZp2dsWZiPTjzNBWY2ZCTuDSo0Gmvk,638
|
|
12
|
+
codeberg_cli/routes/auth/whoami.py,sha256=Zl5Pr7CKDocUmUMPPixQ9nqpD3zJLffRpG6yQQxAtUc,270
|
|
13
|
+
codeberg_cli/routes/issue/__init__.py,sha256=MTK7_DnakcurjWZydkccaCoLo6lPEiXXJUrsBykleIQ,122
|
|
14
|
+
codeberg_cli/routes/issue/close.py,sha256=RAAJiFPVV4FrNStlviMzUw9ZZjSj-h80sCfG0mZysl0,774
|
|
15
|
+
codeberg_cli/routes/issue/comment.py,sha256=qMkIx6kCq3qXjtiMEhLDTpnNomdEM2RNq0ZHpZANCC8,1326
|
|
16
|
+
codeberg_cli/routes/issue/create.py,sha256=UcOXeGSINLM4FsUSefi5MQOGC4lXDgkv1tT-V8dqTGI,1744
|
|
17
|
+
codeberg_cli/routes/issue/edit.py,sha256=Ctyq0AGAJg-M3S7DfeGuj1LYWx8f7Oem_9wGDFnPVNo,1170
|
|
18
|
+
codeberg_cli/routes/issue/list.py,sha256=bolDAmeI7saZ6AGnWsSOQmRHqgRWm21EP8rKINfXbUI,1502
|
|
19
|
+
codeberg_cli/routes/issue/reopen.py,sha256=QsxWU_1Qed6hdmFs9AiNKk191WkJLeeKInAINeSiiTA,782
|
|
20
|
+
codeberg_cli/routes/issue/view.py,sha256=km1N2Prgi9zxy_ohgwx79W771uR-LoWMbLruOEDNgVM,1533
|
|
21
|
+
codeberg_cli/routes/label/__init__.py,sha256=c3wahlhcqgG02ok34lYR4jmd0Z1KNwkQs_QZNePwVew,128
|
|
22
|
+
codeberg_cli/routes/label/create.py,sha256=Oqlf-xouSJg0nbDc-31e5b_d46d-XzCOXNIc9JVnKcc,1124
|
|
23
|
+
codeberg_cli/routes/label/delete.py,sha256=AGffQbsEwsGwFXtgIQltABWcLREAV1p4kpu4Xzamle4,759
|
|
24
|
+
codeberg_cli/routes/label/list.py,sha256=sVFfQsWkQVi4WqGIP03apabWzPFIgUvHktsRuqugw34,1248
|
|
25
|
+
codeberg_cli/routes/milestone/__init__.py,sha256=Gtqc5vKHpjnbzAZDoPkiCcrbYhby9kpyphqEVFPspI0,116
|
|
26
|
+
codeberg_cli/routes/milestone/create.py,sha256=VDA9q3y8uEDRJ3oIOPn2EWsUwvrNpeOviAXQFSLv7E4,1086
|
|
27
|
+
codeberg_cli/routes/milestone/list.py,sha256=LRoUmHMgamOqMuo085cEoIU9tdPhXduqN5DWd-JFUg8,1514
|
|
28
|
+
codeberg_cli/routes/notification/__init__.py,sha256=_oW9svvLRSiQZNb2NnYigNYvNqDCrfHRd94MDam08Cg,123
|
|
29
|
+
codeberg_cli/routes/notification/list.py,sha256=nprfT22MXZ1Ko6SUahS7hghJ4TwCqXxMJmFEwNAL74M,1082
|
|
30
|
+
codeberg_cli/routes/pr/__init__.py,sha256=QJ-u92u_rnQdsMoLgNhudbiSStmlAO5E-dn0Z4APmds,128
|
|
31
|
+
codeberg_cli/routes/pr/checkout.py,sha256=B5wYePvASjoFp3G1rMbpBjicbe0DPibFxIlMlO7W204,1221
|
|
32
|
+
codeberg_cli/routes/pr/close.py,sha256=Xge_cP9K0prup9CUvvSjuMj_NgF6ZG00SF0Ciets-t0,795
|
|
33
|
+
codeberg_cli/routes/pr/comment.py,sha256=AFjTeKlRi4xsTJTFs0jSklzG6GKN1JmLPWJcBKYee8o,1326
|
|
34
|
+
codeberg_cli/routes/pr/create.py,sha256=X-MlQ6FGqyGD12I0CeFl8Q3beVlyaNDK7f8zGBgRX4Q,2111
|
|
35
|
+
codeberg_cli/routes/pr/edit.py,sha256=saLp8j7gPcDnT2oGrmJWzcSrQgty6z_Nzz3bBaqbmmo,1169
|
|
36
|
+
codeberg_cli/routes/pr/list.py,sha256=bA4C9BN4E9xlmnDTHKkFh-zClLQ2agPU8fWz2lMT6rA,1286
|
|
37
|
+
codeberg_cli/routes/pr/merge.py,sha256=AIP-mm_Fvq0HG7v_vm7eYwtuU61JKLzjJqbFuZqnqgg,915
|
|
38
|
+
codeberg_cli/routes/pr/view.py,sha256=QQygrTIQEkCvCQwW2td4_9E2AfxmgPWcAuiXANR8jgU,1391
|
|
39
|
+
codeberg_cli/routes/release/__init__.py,sha256=GVDrIgpsXz3LCs2ivPnfVOrukYcSalNsRl6Rxradngk,134
|
|
40
|
+
codeberg_cli/routes/release/create.py,sha256=gZYCfzoA7tPBF8L4uvezBPGkgcdRtjcZBdnj3EVtK80,1405
|
|
41
|
+
codeberg_cli/routes/release/list.py,sha256=81mSZCYM0udIEt4j9-JV4o_A3AKUjbKvBg5D5veFqtg,1289
|
|
42
|
+
codeberg_cli/routes/release/upload.py,sha256=rTci3LEpW287PllqN75KOf-Q7BRuTZ-eusTaEiLILd0,1664
|
|
43
|
+
codeberg_cli/routes/release/view.py,sha256=BsM027YGSNBSfPMr6ya6sdNxowc-mvOaiTf-Vn-cgfM,1701
|
|
44
|
+
codeberg_cli/routes/repo/__init__.py,sha256=5mN6Vhtnt11V2hiFstJd2bnFk_Oezslu_XEgPQR0RP0,126
|
|
45
|
+
codeberg_cli/routes/repo/clone.py,sha256=KBdjX251MZfJd4xj_Z7C-XxYZCmuBGSgdLU65f9PnwQ,552
|
|
46
|
+
codeberg_cli/routes/repo/create.py,sha256=NfWaGmXOoHt4SwyjTNLHOW_HYDWgTXuJyeW2eDc2O3Y,2805
|
|
47
|
+
codeberg_cli/routes/repo/delete.py,sha256=ubUotlWGxLZ3MigHIrWd5Y3xV4LVlWdZZxyfVVKEz24,755
|
|
48
|
+
codeberg_cli/routes/repo/fork.py,sha256=qdutue-11POrsN-HM_No2vIIibQB1H4HqBLJcVtks_0,810
|
|
49
|
+
codeberg_cli/routes/repo/list.py,sha256=8Lledw3c_JVmmn3K4CFRxo6kINWErTa6Y2CdIqJ6mH0,1104
|
|
50
|
+
codeberg_cli/routes/repo/star.py,sha256=dwHM--10ehMtaIcI-s7e0NHW97hB9oHEhrwKQQmSEbo,680
|
|
51
|
+
codeberg_cli/routes/repo/unstar.py,sha256=IxDPa0EJKBz0qdTqvDPZOdPVWsSHi9A98Ka73yrExKE,693
|
|
52
|
+
codeberg_cli/routes/repo/view.py,sha256=0WyU_vj-uvkd712xZBmFw9tsLCg0CtupsZe4MYL_K9o,1356
|
|
53
|
+
codeberg_cli/routes/user/__init__.py,sha256=iAE6MlGEhCvzRzYsbgPdlXJ6WDs9Alcs7E0j3g5r8LQ,104
|
|
54
|
+
codeberg_cli/routes/user/view.py,sha256=-5xa2YPguCEaCw_S5n35r568dWFCgt95_uIaEVEE99U,1017
|
|
55
|
+
codeberg_cli-0.2.0.dist-info/METADATA,sha256=oSSDIdPwFIdVpzY3Oz4KtTjzcGv8yurbg1zDk0N1xtg,4148
|
|
56
|
+
codeberg_cli-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
57
|
+
codeberg_cli-0.2.0.dist-info/entry_points.txt,sha256=7Kg1K5av7D5TzPCqX_qyFCj09JTneIEvW0f2Gp4q1v4,49
|
|
58
|
+
codeberg_cli-0.2.0.dist-info/licenses/LICENSE,sha256=o71itnX05JiF5qOrKHEmWIvuf03sgYcwsc3r6AAW_h0,1065
|
|
59
|
+
codeberg_cli-0.2.0.dist-info/RECORD,,
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: codeberg-cli
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: A Codeberg CLI
|
|
5
|
-
Project-URL: Homepage, https://codeberg.org/ThatXliner/codeberg-cli
|
|
6
|
-
Project-URL: Repository, https://codeberg.org/ThatXliner/codeberg-cli
|
|
7
|
-
Project-URL: Issues, https://codeberg.org/ThatXliner/codeberg-cli/issues
|
|
8
|
-
Author-email: Bryan Hu <thatxliner@gmail.com>
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: cli,codeberg,forgejo
|
|
12
|
-
Classifier: Development Status :: 4 - Beta
|
|
13
|
-
Classifier: Environment :: Console
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
-
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
19
|
-
Requires-Python: >=3.12
|
|
20
|
-
Requires-Dist: httpx
|
|
21
|
-
Requires-Dist: platformdirs
|
|
22
|
-
Requires-Dist: tomlkit
|
|
23
|
-
Requires-Dist: xclif>=0.4.3
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
|
-
# cb — A Codeberg CLI
|
|
27
|
-
|
|
28
|
-
[](https://github.com/ThatXliner/cb/actions/workflows/ci.yml) [](https://pypi.org/project/codeberg-cli)
|
|
29
|
-
[](https://pypi.org/project/codeberg-cli)
|
|
30
|
-
[](#license)
|
|
31
|
-
|
|
32
|
-
`cb` is a native CLI for [Codeberg](https://codeberg.org) (a [Forgejo](https://forgejo.org) instance) — think `gh` for Codeberg. Built with [Xclif](https://xclif.readthedocs.io).
|
|
33
|
-
|
|
34
|
-
```text
|
|
35
|
-
# One-time setup
|
|
36
|
-
cb auth login
|
|
37
|
-
|
|
38
|
-
# Work with repos, issues, PRs, releases
|
|
39
|
-
cb repo list
|
|
40
|
-
cb issue create --title "Fix the thing"
|
|
41
|
-
cb pr create --base main --head fix
|
|
42
|
-
cb release create v0.2.0
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Install
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
pip install codeberg-cli # or: uv tool install codeberg-cli
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Or from source:
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
git clone https://codeberg.org/ThatXliner/codeberg-cli
|
|
55
|
-
cd cb
|
|
56
|
-
uv tool install .
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Quickstart
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
# Authenticate (tokens at https://codeberg.org/user/settings/applications)
|
|
63
|
-
cb auth login
|
|
64
|
-
|
|
65
|
-
# Who am I?
|
|
66
|
-
cb auth whoami
|
|
67
|
-
|
|
68
|
-
# List your repos
|
|
69
|
-
cb repo list
|
|
70
|
-
|
|
71
|
-
# Clone one
|
|
72
|
-
cb repo clone ThatXliner/cb
|
|
73
|
-
|
|
74
|
-
# Open an issue
|
|
75
|
-
cb issue create --title "suggestion" --body "what about..."
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Commands
|
|
79
|
-
|
|
80
|
-
### `cb auth` — authentication
|
|
81
|
-
|
|
82
|
-
| Command | Description |
|
|
83
|
-
|---------|-------------|
|
|
84
|
-
| `login` | Store a Codeberg access token |
|
|
85
|
-
| `logout` | Remove stored credentials |
|
|
86
|
-
| `status` | Show login state |
|
|
87
|
-
| `whoami` | Print current username |
|
|
88
|
-
|
|
89
|
-
### `cb repo` — repositories
|
|
90
|
-
|
|
91
|
-
Infer `owner/repo` from `git remote origin`. Override with `--repo`.
|
|
92
|
-
|
|
93
|
-
| Command | Description |
|
|
94
|
-
|---------|-------------|
|
|
95
|
-
| `create` | Create a repo (`--org`, `--private`, `--description`) |
|
|
96
|
-
| `list` | List repos for a user or org |
|
|
97
|
-
| `clone` | Clone via `git clone` |
|
|
98
|
-
| `view` | Show repo details (`--web` to open browser) |
|
|
99
|
-
| `fork` | Fork a repo |
|
|
100
|
-
| `delete` | Delete a repo (requires confirmation) |
|
|
101
|
-
|
|
102
|
-
### `cb label` — labels
|
|
103
|
-
|
|
104
|
-
| Command | Description |
|
|
105
|
-
|---------|-------------|
|
|
106
|
-
| `list` (alias: `ls`) | List repo labels |
|
|
107
|
-
| `create` | Create a label (`--color`, `--description`) |
|
|
108
|
-
| `delete` | Delete a label by ID |
|
|
109
|
-
|
|
110
|
-
### `cb milestone` — milestones
|
|
111
|
-
|
|
112
|
-
| Command | Description |
|
|
113
|
-
|---------|-------------|
|
|
114
|
-
| `list` (alias: `ls`) | List milestones (`--state`) |
|
|
115
|
-
| `create` | Create a milestone (`--description`, `--due-on`) |
|
|
116
|
-
|
|
117
|
-
### `cb issue` — issues
|
|
118
|
-
|
|
119
|
-
| Command | Description |
|
|
120
|
-
|---------|-------------|
|
|
121
|
-
| `create` | Create an issue (`--labels`, omit `--body` to open stdin) |
|
|
122
|
-
| `list` | List issues (`--state`, `--label`, `--limit`) |
|
|
123
|
-
| `view` | View an issue with comments (`--web`) |
|
|
124
|
-
| `close` | Close an issue |
|
|
125
|
-
| `reopen` | Reopen a closed issue |
|
|
126
|
-
|
|
127
|
-
### `cb pr` — pull requests
|
|
128
|
-
|
|
129
|
-
| Command | Description |
|
|
130
|
-
|---------|-------------|
|
|
131
|
-
| `create` | Open a PR (omit `--body` to open `$EDITOR`) |
|
|
132
|
-
| `list` | List PRs (alias: `ls`) |
|
|
133
|
-
| `view` | View a PR (`--web`) |
|
|
134
|
-
| `merge` | Merge with `--style merge|rebase|squash` |
|
|
135
|
-
| `checkout` | Fetch and checkout a PR locally (alias: `co`) |
|
|
136
|
-
| `close` | Close without merging |
|
|
137
|
-
|
|
138
|
-
### `cb release` — releases
|
|
139
|
-
|
|
140
|
-
| Command | Description |
|
|
141
|
-
|---------|-------------|
|
|
142
|
-
| `create` | Tag a release (`--prerelease`, `--draft`) |
|
|
143
|
-
| `list` | List releases |
|
|
144
|
-
| `view` | View a release (`--web`) |
|
|
145
|
-
| `upload` | Attach a file to a release |
|
|
146
|
-
|
|
147
|
-
### `cb api` — raw API access
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
cb api GET /version
|
|
151
|
-
cb api POST /repos/owner/repo/issues --data '{"title": "hi"}'
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Config
|
|
155
|
-
|
|
156
|
-
Token stored in `$XDG_CONFIG_HOME/cb/config.toml` (managed by `cb auth login` / `cb auth logout`).
|
|
157
|
-
|
|
158
|
-
## Comparison
|
|
159
|
-
|
|
160
|
-
There are two other CLI tools you can use with Codeberg:
|
|
161
|
-
|
|
162
|
-
| | **cb** | **fj** (forgejo-cli) | **berg** (codeberg-cli) |
|
|
163
|
-
|---|---|---|---|
|
|
164
|
-
| Language | Python | Rust | Rust |
|
|
165
|
-
| Codeberg-native | Yes (targets `codeberg.org/api/v1`) | Generic (any Forgejo instance) | Yes |
|
|
166
|
-
| Issues | create, list, view, close, reopen | open, edit, comment, close | yes |
|
|
167
|
-
| Pull requests | create, list, view, merge, checkout, close | create, merge | yes |
|
|
168
|
-
| Releases | create, list, view, upload | publish | — |
|
|
169
|
-
| Repos | create, list, clone, view, fork, delete | create, edit, star, watch | yes |
|
|
170
|
-
| Labels | create, list, delete | — | yes |
|
|
171
|
-
| Milestones | create, list | — | yes |
|
|
172
|
-
| Raw API | `cb api GET /path` | — | — |
|
|
173
|
-
| AGit PRs (no-fork) | — | yes | — |
|
|
174
|
-
| Org/team mgmt | — | yes | — |
|
|
175
|
-
| Install | `pip install codeberg-cli` | prebuilt binaries | `cargo install codeberg-cli` |
|
|
176
|
-
| Size | ~300 lines | Rust binary | Rust binary |
|
|
177
|
-
|
|
178
|
-
**Choose `fj`** if you self-host Forgejo or need org/team management. **Choose `berg`** if you want labels and milestones from a Rust binary. **Choose `cb`** if you want a minimal, readable Python CLI with release management and raw API access — `cb` is also the only one that uploads release assets.
|
|
179
|
-
|
|
180
|
-
## License
|
|
181
|
-
|
|
182
|
-
MIT
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
codeberg_cli/__main__.py,sha256=ysV0kHP-Q-sUwHOInLcB-QEryJfNNv-amdHP0EubG-8,113
|
|
2
|
-
codeberg_cli/client.py,sha256=wqf5mOR9Uyku7v_GZRjFQUZsYAfqiWXcJv_DMmubQWU,2242
|
|
3
|
-
codeberg_cli/config.py,sha256=bjVHHM1sG3xTIU6EC7sakwR0nnP-ZsCtO4Jn2od6X4A,822
|
|
4
|
-
codeberg_cli/git.py,sha256=oWOU8iV4FUbtZLA7RvDqRur6whYDoJKoIcUPtBxJZto,1544
|
|
5
|
-
codeberg_cli/helpers.py,sha256=0JGASGM_td_q2l9YOF1KybisZFofKeT5jC4q-G_lS48,627
|
|
6
|
-
codeberg_cli/routes/__init__.py,sha256=LPAm_RsDHeREJ-57n8CwFAdkXkTcykx9zjrKhQZFuvI,86
|
|
7
|
-
codeberg_cli/routes/api.py,sha256=o8she7_o32DNuYqr1v9D0hEHjujW1xEZEhHuxIzKO_8,720
|
|
8
|
-
codeberg_cli/routes/auth/__init__.py,sha256=TxwcK78EFqRSsHs2iSVRQ8Og4upvq91P3fcKcOFpbFQ,41
|
|
9
|
-
codeberg_cli/routes/auth/login.py,sha256=jcarfwPedC2xWqbs-ncNNZXI_5Kv4mdHt7IQa3azAqg,656
|
|
10
|
-
codeberg_cli/routes/auth/logout.py,sha256=LfsYchVxrr77R5c2WNGbKNCLbkyQtbobkhcpw1DH8HA,351
|
|
11
|
-
codeberg_cli/routes/auth/status.py,sha256=fkMgFiwEOBNNNDkJ4S0ahx9epfRQZzXVdYu6KEI4oQI,640
|
|
12
|
-
codeberg_cli/routes/auth/whoami.py,sha256=RjXbklkr0ajFX3BT9B8k1Qr6zSooaZWmQ-9oqjFcshs,272
|
|
13
|
-
codeberg_cli/routes/issue/__init__.py,sha256=RRWKFz7LRRGD3shDlBXRdLSO6IebeLLlzdjY3qRCrqQ,42
|
|
14
|
-
codeberg_cli/routes/issue/close.py,sha256=RAAJiFPVV4FrNStlviMzUw9ZZjSj-h80sCfG0mZysl0,774
|
|
15
|
-
codeberg_cli/routes/issue/create.py,sha256=UW-ilstUjQ2Lu16UZJOjBH8eCMNO3DDF0dPSR-0PE3g,1726
|
|
16
|
-
codeberg_cli/routes/issue/list.py,sha256=bolDAmeI7saZ6AGnWsSOQmRHqgRWm21EP8rKINfXbUI,1502
|
|
17
|
-
codeberg_cli/routes/issue/reopen.py,sha256=QsxWU_1Qed6hdmFs9AiNKk191WkJLeeKInAINeSiiTA,782
|
|
18
|
-
codeberg_cli/routes/issue/view.py,sha256=7pU0vJmgGiaRFDQ_MDESOtPVu9SEoWVH8piN9bzClYo,1515
|
|
19
|
-
codeberg_cli/routes/label/__init__.py,sha256=7yTJhxfd1ErLUoYJVKKhtbmSmC6kwIbUwNcIsu7dAas,42
|
|
20
|
-
codeberg_cli/routes/label/create.py,sha256=Oqlf-xouSJg0nbDc-31e5b_d46d-XzCOXNIc9JVnKcc,1124
|
|
21
|
-
codeberg_cli/routes/label/delete.py,sha256=AGffQbsEwsGwFXtgIQltABWcLREAV1p4kpu4Xzamle4,759
|
|
22
|
-
codeberg_cli/routes/label/list.py,sha256=sVFfQsWkQVi4WqGIP03apabWzPFIgUvHktsRuqugw34,1248
|
|
23
|
-
codeberg_cli/routes/milestone/__init__.py,sha256=0LihqWaCxPtc9yYPmXsorKagjKt065qsnPNfnO2MGAY,46
|
|
24
|
-
codeberg_cli/routes/milestone/create.py,sha256=VDA9q3y8uEDRJ3oIOPn2EWsUwvrNpeOviAXQFSLv7E4,1086
|
|
25
|
-
codeberg_cli/routes/milestone/list.py,sha256=LRoUmHMgamOqMuo085cEoIU9tdPhXduqN5DWd-JFUg8,1514
|
|
26
|
-
codeberg_cli/routes/pr/__init__.py,sha256=8WsMUvMJmcd5BJaSfMXVIaTNkEnrgFyCEGynl2VKKZI,39
|
|
27
|
-
codeberg_cli/routes/pr/checkout.py,sha256=B5wYePvASjoFp3G1rMbpBjicbe0DPibFxIlMlO7W204,1221
|
|
28
|
-
codeberg_cli/routes/pr/close.py,sha256=Xge_cP9K0prup9CUvvSjuMj_NgF6ZG00SF0Ciets-t0,795
|
|
29
|
-
codeberg_cli/routes/pr/create.py,sha256=VT5Xu_BQQKsgzZjKnNeVHq7jsQKbf6tMUO_1Rgufa6U,2093
|
|
30
|
-
codeberg_cli/routes/pr/list.py,sha256=bA4C9BN4E9xlmnDTHKkFh-zClLQ2agPU8fWz2lMT6rA,1286
|
|
31
|
-
codeberg_cli/routes/pr/merge.py,sha256=AIP-mm_Fvq0HG7v_vm7eYwtuU61JKLzjJqbFuZqnqgg,915
|
|
32
|
-
codeberg_cli/routes/pr/view.py,sha256=uaIGzGtMY21K9SAF2qRN58lLpMLQ2Dw1D19nbtke6gw,1373
|
|
33
|
-
codeberg_cli/routes/release/__init__.py,sha256=4NdEUIvolvOMJ8XFP-GkPleiTCo0xvf6JIcXbUf25kE,44
|
|
34
|
-
codeberg_cli/routes/release/create.py,sha256=nFTJCWEAc2S_lAoi-moeQqi4b6l_Q10FDdZYcMa24DQ,1387
|
|
35
|
-
codeberg_cli/routes/release/list.py,sha256=81mSZCYM0udIEt4j9-JV4o_A3AKUjbKvBg5D5veFqtg,1289
|
|
36
|
-
codeberg_cli/routes/release/upload.py,sha256=QG_jmUTamOsGajRVhtZ8N7H0_oMY_DWiIMobBkSeu3g,1674
|
|
37
|
-
codeberg_cli/routes/release/view.py,sha256=o9I1I9nkJjTcqMfxQdtXbTITZWxVYGiwmL6JrVPyg84,1683
|
|
38
|
-
codeberg_cli/routes/repo/__init__.py,sha256=UB56y2Kc_0altG6JrTvSYb5cFunqeQsDMdu5d4q_ROc,41
|
|
39
|
-
codeberg_cli/routes/repo/clone.py,sha256=mFk3i4Qf5MZObh_bcQniO36FuNNBJNWZQhB9QR_4lSI,511
|
|
40
|
-
codeberg_cli/routes/repo/create.py,sha256=XUw3TQjhwB6oL0FnshnBVEKvjSQiWTs8fUOZHh-EkwY,2803
|
|
41
|
-
codeberg_cli/routes/repo/delete.py,sha256=ubUotlWGxLZ3MigHIrWd5Y3xV4LVlWdZZxyfVVKEz24,755
|
|
42
|
-
codeberg_cli/routes/repo/fork.py,sha256=ApVdzAFXetNQJ8R9WUk-gejB2BDJ11cTFb3pn0V5MBg,810
|
|
43
|
-
codeberg_cli/routes/repo/list.py,sha256=8Lledw3c_JVmmn3K4CFRxo6kINWErTa6Y2CdIqJ6mH0,1104
|
|
44
|
-
codeberg_cli/routes/repo/view.py,sha256=ypJPFPsQKiQ1xw8LFZ04PFWNcP7N_ce9J5XWD2wlMXo,1311
|
|
45
|
-
codeberg_cli-0.1.0.dist-info/METADATA,sha256=zMqXfnXqXzE2UwiUgnO6ukwF_PLNNL7xyXT3NO3y5Is,5812
|
|
46
|
-
codeberg_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
47
|
-
codeberg_cli-0.1.0.dist-info/entry_points.txt,sha256=7Kg1K5av7D5TzPCqX_qyFCj09JTneIEvW0f2Gp4q1v4,49
|
|
48
|
-
codeberg_cli-0.1.0.dist-info/licenses/LICENSE,sha256=o71itnX05JiF5qOrKHEmWIvuf03sgYcwsc3r6AAW_h0,1065
|
|
49
|
-
codeberg_cli-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|