codeberg-cli 0.1.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 +8 -0
- codeberg_cli/client.py +70 -0
- codeberg_cli/config.py +29 -0
- codeberg_cli/git.py +49 -0
- codeberg_cli/helpers.py +20 -0
- codeberg_cli/routes/__init__.py +6 -0
- codeberg_cli/routes/api.py +23 -0
- codeberg_cli/routes/auth/__init__.py +1 -0
- codeberg_cli/routes/auth/login.py +23 -0
- codeberg_cli/routes/auth/logout.py +17 -0
- codeberg_cli/routes/auth/status.py +21 -0
- codeberg_cli/routes/auth/whoami.py +14 -0
- codeberg_cli/routes/issue/__init__.py +1 -0
- codeberg_cli/routes/issue/close.py +28 -0
- codeberg_cli/routes/issue/create.py +55 -0
- codeberg_cli/routes/issue/list.py +49 -0
- codeberg_cli/routes/issue/reopen.py +28 -0
- codeberg_cli/routes/issue/view.py +46 -0
- codeberg_cli/routes/label/__init__.py +1 -0
- codeberg_cli/routes/label/create.py +35 -0
- codeberg_cli/routes/label/delete.py +26 -0
- codeberg_cli/routes/label/list.py +41 -0
- codeberg_cli/routes/milestone/__init__.py +1 -0
- codeberg_cli/routes/milestone/create.py +35 -0
- codeberg_cli/routes/milestone/list.py +46 -0
- codeberg_cli/routes/pr/__init__.py +1 -0
- codeberg_cli/routes/pr/checkout.py +42 -0
- codeberg_cli/routes/pr/close.py +28 -0
- codeberg_cli/routes/pr/create.py +68 -0
- codeberg_cli/routes/pr/list.py +43 -0
- codeberg_cli/routes/pr/merge.py +32 -0
- codeberg_cli/routes/pr/view.py +42 -0
- codeberg_cli/routes/release/__init__.py +1 -0
- codeberg_cli/routes/release/create.py +40 -0
- codeberg_cli/routes/release/list.py +42 -0
- codeberg_cli/routes/release/upload.py +51 -0
- codeberg_cli/routes/release/view.py +50 -0
- codeberg_cli/routes/repo/__init__.py +1 -0
- codeberg_cli/routes/repo/clone.py +21 -0
- codeberg_cli/routes/repo/create.py +77 -0
- codeberg_cli/routes/repo/delete.py +27 -0
- codeberg_cli/routes/repo/fork.py +28 -0
- codeberg_cli/routes/repo/list.py +36 -0
- codeberg_cli/routes/repo/view.py +41 -0
- codeberg_cli-0.1.0.dist-info/METADATA +182 -0
- codeberg_cli-0.1.0.dist-info/RECORD +49 -0
- codeberg_cli-0.1.0.dist-info/WHEEL +4 -0
- codeberg_cli-0.1.0.dist-info/entry_points.txt +2 -0
- codeberg_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from codeberg_cli.git import infer_repo
|
|
7
|
+
from codeberg_cli.helpers import require_client
|
|
8
|
+
from xclif import Option, command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@command("list", "ls")
|
|
12
|
+
def _(
|
|
13
|
+
state: Annotated[str, Option(description="Filter by state: open, closed", name="state")] = "open",
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
limit: Annotated[int, Option(description="Maximum milestones to show", name="limit")] = 30,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""List milestones."""
|
|
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
|
+
milestones = client.get(f"/repos/{repo}/milestones", params={"state": state, "limit": limit})
|
|
28
|
+
|
|
29
|
+
if not milestones:
|
|
30
|
+
rich.print(f"[dim]No {state} milestones in {repo}.[/dim]")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
table = Table("#", "Title", "State", "Open / Closed", "Due Date")
|
|
34
|
+
for ms in milestones:
|
|
35
|
+
state_color = "green" if ms["state"] == "open" else "red"
|
|
36
|
+
due = ms.get("due_on", "") or ""
|
|
37
|
+
if due:
|
|
38
|
+
due = due[:10]
|
|
39
|
+
table.add_row(
|
|
40
|
+
str(ms["id"]),
|
|
41
|
+
ms["title"],
|
|
42
|
+
f"[{state_color}]{ms['state']}[/{state_color}]",
|
|
43
|
+
f"{ms.get('open_issues', 0)} / {ms.get('closed_issues', 0)}",
|
|
44
|
+
due,
|
|
45
|
+
)
|
|
46
|
+
rich.print(table)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Namespace package for pr subcommands
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import rich
|
|
5
|
+
|
|
6
|
+
from codeberg_cli.git import infer_repo
|
|
7
|
+
from codeberg_cli.helpers import require_client
|
|
8
|
+
from xclif import Arg, Option, command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@command("checkout", "co")
|
|
12
|
+
def _(
|
|
13
|
+
id: Annotated[int, Arg(description="PR number")],
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Checkout a pull request locally."""
|
|
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
|
+
pr = client.get(f"/repos/{repo}/pulls/{id}")
|
|
27
|
+
branch = pr["head"]["ref"]
|
|
28
|
+
repo_url = pr["head"]["repo"]["clone_url"]
|
|
29
|
+
|
|
30
|
+
rich.print(f"Fetching PR #{id} ({branch})...")
|
|
31
|
+
subprocess.run(
|
|
32
|
+
["git", "fetch", repo_url, branch],
|
|
33
|
+
check=True,
|
|
34
|
+
)
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
["git", "checkout", branch],
|
|
37
|
+
)
|
|
38
|
+
if result.returncode != 0:
|
|
39
|
+
subprocess.run(["git", "checkout", "-b", branch, "FETCH_HEAD"], check=True)
|
|
40
|
+
rich.print(f"[bold green]Checked out[/bold green] PR #{id} as [bold]{branch}[/bold]")
|
|
41
|
+
|
|
42
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
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("close")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="PR number")],
|
|
13
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Close a pull request without merging."""
|
|
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.patch(f"/repos/{repo}/pulls/{id}", data={"state": "closed"})
|
|
26
|
+
rich.print(f"[bold green]Closed[/bold green] PR #{id} in {repo}")
|
|
27
|
+
|
|
28
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import rich
|
|
6
|
+
|
|
7
|
+
from codeberg_cli.git import get_default_branch, infer_repo
|
|
8
|
+
from codeberg_cli.helpers import require_client
|
|
9
|
+
from xclif import Option, command
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@command("create")
|
|
13
|
+
def _(
|
|
14
|
+
title: Annotated[str, Option(description="PR title", name="title")] = "",
|
|
15
|
+
body: Annotated[str, Option(description="PR body", name="body")] = "",
|
|
16
|
+
base: Annotated[str, Option(description="Base branch", name="base")] = "",
|
|
17
|
+
head: Annotated[str, Option(description="Head branch (default: current branch)", name="head")] = "",
|
|
18
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Create a pull request."""
|
|
21
|
+
client = require_client()
|
|
22
|
+
|
|
23
|
+
if not repo:
|
|
24
|
+
inferred = infer_repo()
|
|
25
|
+
if not inferred:
|
|
26
|
+
rich.print("[bold red]Error:[/bold red] No repo specified and not in a git directory")
|
|
27
|
+
return 1
|
|
28
|
+
repo = inferred
|
|
29
|
+
|
|
30
|
+
if not base:
|
|
31
|
+
base = get_default_branch(Path.cwd())
|
|
32
|
+
|
|
33
|
+
if not head:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
36
|
+
capture_output=True, text=True,
|
|
37
|
+
)
|
|
38
|
+
head = result.stdout.strip()
|
|
39
|
+
|
|
40
|
+
if not title:
|
|
41
|
+
title = input("Title: ").strip()
|
|
42
|
+
if not title:
|
|
43
|
+
rich.print("[bold red]Error:[/bold red] Title is required")
|
|
44
|
+
return 1
|
|
45
|
+
|
|
46
|
+
if not body:
|
|
47
|
+
rich.print("[dim]Enter body (Ctrl+D to finish, or leave empty):[/dim]")
|
|
48
|
+
try:
|
|
49
|
+
lines = []
|
|
50
|
+
while True:
|
|
51
|
+
line = input()
|
|
52
|
+
lines.append(line)
|
|
53
|
+
except (EOFError, KeyboardInterrupt):
|
|
54
|
+
pass
|
|
55
|
+
body = "\n".join(lines).strip()
|
|
56
|
+
|
|
57
|
+
data = {
|
|
58
|
+
"title": title,
|
|
59
|
+
"body": body,
|
|
60
|
+
"base": base,
|
|
61
|
+
"head": head,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
result = client.post(f"/repos/{repo}/pulls", data=data)
|
|
65
|
+
rich.print(f"[bold green]Created PR[/bold green] #{result['number']}: {result['title']}")
|
|
66
|
+
rich.print(f"[dim]https://codeberg.org/{repo}/pulls/{result['number']}[/dim]")
|
|
67
|
+
|
|
68
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from codeberg_cli.git import infer_repo
|
|
7
|
+
from codeberg_cli.helpers import require_client
|
|
8
|
+
from xclif import Option, command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@command("list", "ls")
|
|
12
|
+
def _(
|
|
13
|
+
state: Annotated[str, Option(description="Filter by state: open, closed", name="state")] = "open",
|
|
14
|
+
limit: Annotated[int, Option(description="Maximum PRs to show", name="limit")] = 30,
|
|
15
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
16
|
+
) -> None:
|
|
17
|
+
"""List pull requests."""
|
|
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
|
+
pulls = client.get(f"/repos/{repo}/pulls", params={"state": state, "limit": limit, "page": 1})
|
|
28
|
+
|
|
29
|
+
if not pulls:
|
|
30
|
+
rich.print(f"[dim]No {state} PRs in {repo}.[/dim]")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
table = Table("#", "Title", "Author", "Base \u2192 Head")
|
|
34
|
+
for pr in pulls:
|
|
35
|
+
table.add_row(
|
|
36
|
+
str(pr["number"]),
|
|
37
|
+
pr["title"],
|
|
38
|
+
pr["user"]["login"],
|
|
39
|
+
f"{pr['base']['ref']} \u2192 {pr['head']['ref']}",
|
|
40
|
+
)
|
|
41
|
+
rich.print(table)
|
|
42
|
+
|
|
43
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
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("merge")
|
|
11
|
+
def _(
|
|
12
|
+
id: Annotated[int, Arg(description="PR number")],
|
|
13
|
+
style: Annotated[str, Option(description="Merge style: merge, rebase, squash", name="style")] = "merge",
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Merge 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
|
+
data = {
|
|
27
|
+
"Do": style,
|
|
28
|
+
}
|
|
29
|
+
client.post(f"/repos/{repo}/pulls/{id}/merge", data=data)
|
|
30
|
+
rich.print(f"[bold green]Merged[/bold green] #{id} in {repo}")
|
|
31
|
+
|
|
32
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import webbrowser
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import rich
|
|
5
|
+
|
|
6
|
+
from codeberg_cli.git import infer_repo
|
|
7
|
+
from codeberg_cli.helpers import require_client
|
|
8
|
+
from xclif import Arg, Option, command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@command("view")
|
|
12
|
+
def _(
|
|
13
|
+
id: Annotated[int, Arg(description="PR number")],
|
|
14
|
+
web: Annotated[bool, Option(description="Open in browser", name="web")] = False,
|
|
15
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
16
|
+
) -> None:
|
|
17
|
+
"""View a pull request."""
|
|
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 web:
|
|
28
|
+
webbrowser.open(f"https://codeberg.org/{repo}/pulls/{id}")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
pr = client.get(f"/repos/{repo}/pulls/{id}")
|
|
32
|
+
|
|
33
|
+
state_color = "green" if pr["state"] == "open" else "red"
|
|
34
|
+
merged = "[bold magenta]MERGED[/bold magenta] " if pr.get("merged") else ""
|
|
35
|
+
rich.print(f"[bold]#{pr['number']}[/bold] [bold]{pr['title']}[/bold]")
|
|
36
|
+
rich.print(f"{merged}[{state_color}]{pr['state']}[/{state_color}] [dim]by {pr['user']['login']}[/dim]")
|
|
37
|
+
rich.print(f" [dim]{pr['base']['ref']}[/dim] \u2190 [dim]{pr['head']['ref']}[/dim]")
|
|
38
|
+
if pr.get("body"):
|
|
39
|
+
rich.print()
|
|
40
|
+
rich.print(pr["body"])
|
|
41
|
+
|
|
42
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Namespace package for release subcommands
|
|
@@ -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("create")
|
|
11
|
+
def _(
|
|
12
|
+
tag: Annotated[str, Arg(description="Git tag for the release")],
|
|
13
|
+
title: Annotated[str, Option(description="Release title", name="title")] = "",
|
|
14
|
+
notes: Annotated[str, Option(description="Release notes", name="notes")] = "",
|
|
15
|
+
prerelease: Annotated[bool, Option(description="Mark as prerelease", name="prerelease")] = False,
|
|
16
|
+
draft: Annotated[bool, Option(description="Create as draft", name="draft")] = False,
|
|
17
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Create a release."""
|
|
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
|
+
data = {
|
|
30
|
+
"tag_name": tag,
|
|
31
|
+
"name": title or tag,
|
|
32
|
+
"body": notes,
|
|
33
|
+
"prerelease": prerelease,
|
|
34
|
+
"draft": draft,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
result = client.post(f"/repos/{repo}/releases", data=data)
|
|
38
|
+
rich.print(f"[bold green]Created release[/bold green] {result.get('name', tag)}")
|
|
39
|
+
rich.print(f"[dim]https://codeberg.org/{repo}/releases/tag/{tag}[/dim]")
|
|
40
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from codeberg_cli.git import infer_repo
|
|
7
|
+
from codeberg_cli.helpers import require_client
|
|
8
|
+
from xclif import Option, command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@command("list", "ls")
|
|
12
|
+
def _(
|
|
13
|
+
limit: Annotated[int, Option(description="Maximum releases to show", name="limit")] = 30,
|
|
14
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
15
|
+
) -> None:
|
|
16
|
+
"""List releases."""
|
|
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
|
+
releases = client.get(f"/repos/{repo}/releases", params={"limit": limit, "page": 1})
|
|
27
|
+
|
|
28
|
+
if not releases:
|
|
29
|
+
rich.print(f"[dim]No releases in {repo}.[/dim]")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
table = Table("Tag", "Title", "Type", "Assets")
|
|
33
|
+
for r in releases:
|
|
34
|
+
release_type = "[yellow]draft[/yellow]" if r.get("draft") else "[cyan]prerelease[/cyan]" if r.get("prerelease") else "[green]release[/green]"
|
|
35
|
+
table.add_row(
|
|
36
|
+
r["tag_name"],
|
|
37
|
+
r.get("name", ""),
|
|
38
|
+
release_type,
|
|
39
|
+
str(len(r.get("assets", []))),
|
|
40
|
+
)
|
|
41
|
+
rich.print(table)
|
|
42
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
id: Annotated[int, Arg(description="Release ID")],
|
|
15
|
+
file: Annotated[str, Arg(description="File to upload")],
|
|
16
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Upload a release asset."""
|
|
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"https://codeberg.org/api/v1/repos/{repo}/releases/{id}/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 release #{id}")
|
|
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
|
|
51
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import webbrowser
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import rich
|
|
5
|
+
|
|
6
|
+
from codeberg_cli.git import infer_repo
|
|
7
|
+
from codeberg_cli.helpers import require_client
|
|
8
|
+
from xclif import Arg, Option, command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@command("view")
|
|
12
|
+
def _(
|
|
13
|
+
id: Annotated[int, Arg(description="Release ID")],
|
|
14
|
+
web: Annotated[bool, Option(description="Open in browser", name="web")] = False,
|
|
15
|
+
repo: Annotated[str, Option(description="Repository (owner/repo)", name="repo")] = "",
|
|
16
|
+
) -> None:
|
|
17
|
+
"""View a release."""
|
|
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
|
+
release = client.get(f"/repos/{repo}/releases/{id}")
|
|
28
|
+
|
|
29
|
+
if web:
|
|
30
|
+
webbrowser.open(release.get("html_url", f"https://codeberg.org/{repo}/releases/tag/{release['tag_name']}"))
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
rich.print(f"[bold]{release.get('name', release['tag_name'])}[/bold]")
|
|
34
|
+
rich.print(f" [dim]Tag:[/dim] {release['tag_name']}")
|
|
35
|
+
rich.print(f" [dim]Draft:[/dim] {release.get('draft', False)}")
|
|
36
|
+
rich.print(f" [dim]Prerelease:[/dim] {release.get('prerelease', False)}")
|
|
37
|
+
|
|
38
|
+
if release.get("body"):
|
|
39
|
+
rich.print()
|
|
40
|
+
rich.print(release["body"])
|
|
41
|
+
|
|
42
|
+
assets = release.get("assets", [])
|
|
43
|
+
if assets:
|
|
44
|
+
rich.print()
|
|
45
|
+
rich.print(f"[bold]{len(assets)} asset(s)[/bold]")
|
|
46
|
+
for asset in assets:
|
|
47
|
+
size = asset.get("size", 0)
|
|
48
|
+
size_str = f"{size / 1024:.1f} KB" if size < 1024 * 1024 else f"{size / 1024 / 1024:.1f} MB"
|
|
49
|
+
rich.print(f" [dim]-[/dim] {asset['name']} ({size_str})")
|
|
50
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Namespace package for repo subcommands
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import rich
|
|
5
|
+
|
|
6
|
+
from xclif import Arg, command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@command("clone")
|
|
10
|
+
def _(
|
|
11
|
+
repo: Annotated[str, Arg(description="Repository in owner/repo format")],
|
|
12
|
+
) -> None:
|
|
13
|
+
"""Clone a Codeberg repository."""
|
|
14
|
+
url = f"https://codeberg.org/{repo}.git"
|
|
15
|
+
rich.print(f"Cloning [cyan]{repo}[/cyan]...")
|
|
16
|
+
result = subprocess.run(["git", "clone", url])
|
|
17
|
+
if result.returncode != 0:
|
|
18
|
+
rich.print(f"[bold red]Error:[/bold red] git clone failed")
|
|
19
|
+
return 1
|
|
20
|
+
|
|
21
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import rich
|
|
5
|
+
from rich.prompt import Confirm, Prompt
|
|
6
|
+
|
|
7
|
+
from codeberg_cli.git import get_repo_remote
|
|
8
|
+
from codeberg_cli.helpers import require_client
|
|
9
|
+
from xclif import Option, command
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@command("create")
|
|
13
|
+
def _(
|
|
14
|
+
name: Annotated[str, Option(description="Repository name")] = "",
|
|
15
|
+
org: Annotated[str, Option(description="Organization to create repo under")] = "",
|
|
16
|
+
description: Annotated[str, Option(description="Repository description")] = "",
|
|
17
|
+
private: Annotated[bool, Option(description="Make repository private")] = False,
|
|
18
|
+
remote: Annotated[bool, Option(description="Add git remote and push")] = False,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Create a repository."""
|
|
21
|
+
client = require_client()
|
|
22
|
+
|
|
23
|
+
if not name:
|
|
24
|
+
name = Prompt.ask("Repository name", default=Path.cwd().name)
|
|
25
|
+
if not description:
|
|
26
|
+
description = Prompt.ask("Description", default="")
|
|
27
|
+
private = not Confirm.ask("Public repository?", default=True)
|
|
28
|
+
is_git = Path.cwd().joinpath(".git").exists()
|
|
29
|
+
if is_git and not get_repo_remote(Path.cwd()):
|
|
30
|
+
remote = Confirm.ask("Add remote and push to Codeberg?", default=True)
|
|
31
|
+
|
|
32
|
+
data = {
|
|
33
|
+
"name": name,
|
|
34
|
+
"description": description,
|
|
35
|
+
"private": private,
|
|
36
|
+
"auto_init": True,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if org:
|
|
40
|
+
result = client.post(f"/orgs/{org}/repos", data=data, action=f"Creating repository in {org}")
|
|
41
|
+
owner = org
|
|
42
|
+
repo_url = f"https://codeberg.org/{org}/{name}"
|
|
43
|
+
else:
|
|
44
|
+
result = client.post("/user/repos", data=data, action="Creating repository")
|
|
45
|
+
owner = result["owner"]["login"]
|
|
46
|
+
repo_url = f"https://codeberg.org/{owner}/{name}"
|
|
47
|
+
|
|
48
|
+
rich.print(f"[bold green]Created repository[/bold green] {repo_url}")
|
|
49
|
+
|
|
50
|
+
if remote:
|
|
51
|
+
import subprocess
|
|
52
|
+
|
|
53
|
+
cwd = Path.cwd()
|
|
54
|
+
if not (cwd / ".git").exists():
|
|
55
|
+
rich.print("[yellow]Not a git repository, skipping remote setup[/yellow]")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
existing = get_repo_remote(cwd)
|
|
59
|
+
if existing:
|
|
60
|
+
rich.print(f"[yellow]Remote 'origin' already exists ({existing}), skipping[/yellow]")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
subprocess.run(
|
|
65
|
+
["git", "remote", "add", "origin", f"https://codeberg.org/{owner}/{name}.git"],
|
|
66
|
+
capture_output=True, text=True, check=True, cwd=cwd,
|
|
67
|
+
)
|
|
68
|
+
rich.print("[dim]Added remote 'origin'[/dim]")
|
|
69
|
+
subprocess.run(
|
|
70
|
+
["git", "push", "-u", "origin", "HEAD"],
|
|
71
|
+
capture_output=True, text=True, cwd=cwd,
|
|
72
|
+
)
|
|
73
|
+
rich.print("[dim]Pushed current branch[/dim]")
|
|
74
|
+
except subprocess.CalledProcessError as e:
|
|
75
|
+
rich.print(f"[red]Failed to set up remote: {e.stderr.strip()}[/red]")
|
|
76
|
+
|
|
77
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import rich
|
|
4
|
+
|
|
5
|
+
from codeberg_cli.helpers import require_client
|
|
6
|
+
from xclif import Arg, Option, command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@command("delete", "rm")
|
|
10
|
+
def _(
|
|
11
|
+
repo: Annotated[str, Arg(description="Repository to delete")],
|
|
12
|
+
yes: Annotated[bool, Option(description="Skip confirmation")] = False,
|
|
13
|
+
) -> None:
|
|
14
|
+
"""Delete a repository."""
|
|
15
|
+
client = require_client()
|
|
16
|
+
|
|
17
|
+
if not yes:
|
|
18
|
+
rich.print(f"[bold red]Warning:[/bold red] This will permanently delete [bold]{repo}[/bold]")
|
|
19
|
+
confirm = input("Type the repo name to confirm: ").strip()
|
|
20
|
+
if confirm != repo:
|
|
21
|
+
rich.print("[dim]Aborted.[/dim]")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
client.delete(f"/repos/{repo}")
|
|
25
|
+
rich.print(f"[bold green]Deleted[/bold green] {repo}")
|
|
26
|
+
|
|
27
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
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, Arg, command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@command("fork")
|
|
11
|
+
def _(
|
|
12
|
+
repo: Annotated[str, Option(description="Repository to fork")] = "",
|
|
13
|
+
) -> None:
|
|
14
|
+
"""Fork 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
|
+
result = client.post(f"/repos/{repo}/forks", action=f"Forking {repo}")
|
|
25
|
+
fork_name = result.get("full_name", f"{result['owner']['login']}/{result['name']}")
|
|
26
|
+
rich.print(f"[bold green]Forked[/bold green] {repo} -> [bold]{fork_name}[/bold]")
|
|
27
|
+
|
|
28
|
+
|