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.
Files changed (49) hide show
  1. codeberg_cli/__main__.py +8 -0
  2. codeberg_cli/client.py +70 -0
  3. codeberg_cli/config.py +29 -0
  4. codeberg_cli/git.py +49 -0
  5. codeberg_cli/helpers.py +20 -0
  6. codeberg_cli/routes/__init__.py +6 -0
  7. codeberg_cli/routes/api.py +23 -0
  8. codeberg_cli/routes/auth/__init__.py +1 -0
  9. codeberg_cli/routes/auth/login.py +23 -0
  10. codeberg_cli/routes/auth/logout.py +17 -0
  11. codeberg_cli/routes/auth/status.py +21 -0
  12. codeberg_cli/routes/auth/whoami.py +14 -0
  13. codeberg_cli/routes/issue/__init__.py +1 -0
  14. codeberg_cli/routes/issue/close.py +28 -0
  15. codeberg_cli/routes/issue/create.py +55 -0
  16. codeberg_cli/routes/issue/list.py +49 -0
  17. codeberg_cli/routes/issue/reopen.py +28 -0
  18. codeberg_cli/routes/issue/view.py +46 -0
  19. codeberg_cli/routes/label/__init__.py +1 -0
  20. codeberg_cli/routes/label/create.py +35 -0
  21. codeberg_cli/routes/label/delete.py +26 -0
  22. codeberg_cli/routes/label/list.py +41 -0
  23. codeberg_cli/routes/milestone/__init__.py +1 -0
  24. codeberg_cli/routes/milestone/create.py +35 -0
  25. codeberg_cli/routes/milestone/list.py +46 -0
  26. codeberg_cli/routes/pr/__init__.py +1 -0
  27. codeberg_cli/routes/pr/checkout.py +42 -0
  28. codeberg_cli/routes/pr/close.py +28 -0
  29. codeberg_cli/routes/pr/create.py +68 -0
  30. codeberg_cli/routes/pr/list.py +43 -0
  31. codeberg_cli/routes/pr/merge.py +32 -0
  32. codeberg_cli/routes/pr/view.py +42 -0
  33. codeberg_cli/routes/release/__init__.py +1 -0
  34. codeberg_cli/routes/release/create.py +40 -0
  35. codeberg_cli/routes/release/list.py +42 -0
  36. codeberg_cli/routes/release/upload.py +51 -0
  37. codeberg_cli/routes/release/view.py +50 -0
  38. codeberg_cli/routes/repo/__init__.py +1 -0
  39. codeberg_cli/routes/repo/clone.py +21 -0
  40. codeberg_cli/routes/repo/create.py +77 -0
  41. codeberg_cli/routes/repo/delete.py +27 -0
  42. codeberg_cli/routes/repo/fork.py +28 -0
  43. codeberg_cli/routes/repo/list.py +36 -0
  44. codeberg_cli/routes/repo/view.py +41 -0
  45. codeberg_cli-0.1.0.dist-info/METADATA +182 -0
  46. codeberg_cli-0.1.0.dist-info/RECORD +49 -0
  47. codeberg_cli-0.1.0.dist-info/WHEEL +4 -0
  48. codeberg_cli-0.1.0.dist-info/entry_points.txt +2 -0
  49. 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
+