codeberg-cli 0.1.0__tar.gz

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 (63) hide show
  1. codeberg_cli-0.1.0/.github/workflows/ci.yml +37 -0
  2. codeberg_cli-0.1.0/.github/workflows/release.yml +24 -0
  3. codeberg_cli-0.1.0/.gitignore +12 -0
  4. codeberg_cli-0.1.0/.python-version +1 -0
  5. codeberg_cli-0.1.0/Justfile +21 -0
  6. codeberg_cli-0.1.0/LICENSE +21 -0
  7. codeberg_cli-0.1.0/PKG-INFO +182 -0
  8. codeberg_cli-0.1.0/README.md +157 -0
  9. codeberg_cli-0.1.0/pyproject.toml +50 -0
  10. codeberg_cli-0.1.0/src/codeberg_cli/__main__.py +8 -0
  11. codeberg_cli-0.1.0/src/codeberg_cli/client.py +70 -0
  12. codeberg_cli-0.1.0/src/codeberg_cli/config.py +29 -0
  13. codeberg_cli-0.1.0/src/codeberg_cli/git.py +49 -0
  14. codeberg_cli-0.1.0/src/codeberg_cli/helpers.py +20 -0
  15. codeberg_cli-0.1.0/src/codeberg_cli/routes/__init__.py +6 -0
  16. codeberg_cli-0.1.0/src/codeberg_cli/routes/api.py +23 -0
  17. codeberg_cli-0.1.0/src/codeberg_cli/routes/auth/__init__.py +1 -0
  18. codeberg_cli-0.1.0/src/codeberg_cli/routes/auth/login.py +23 -0
  19. codeberg_cli-0.1.0/src/codeberg_cli/routes/auth/logout.py +17 -0
  20. codeberg_cli-0.1.0/src/codeberg_cli/routes/auth/status.py +21 -0
  21. codeberg_cli-0.1.0/src/codeberg_cli/routes/auth/whoami.py +14 -0
  22. codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/__init__.py +1 -0
  23. codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/close.py +28 -0
  24. codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/create.py +55 -0
  25. codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/list.py +49 -0
  26. codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/reopen.py +28 -0
  27. codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/view.py +46 -0
  28. codeberg_cli-0.1.0/src/codeberg_cli/routes/label/__init__.py +1 -0
  29. codeberg_cli-0.1.0/src/codeberg_cli/routes/label/create.py +35 -0
  30. codeberg_cli-0.1.0/src/codeberg_cli/routes/label/delete.py +26 -0
  31. codeberg_cli-0.1.0/src/codeberg_cli/routes/label/list.py +41 -0
  32. codeberg_cli-0.1.0/src/codeberg_cli/routes/milestone/__init__.py +1 -0
  33. codeberg_cli-0.1.0/src/codeberg_cli/routes/milestone/create.py +35 -0
  34. codeberg_cli-0.1.0/src/codeberg_cli/routes/milestone/list.py +46 -0
  35. codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/__init__.py +1 -0
  36. codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/checkout.py +42 -0
  37. codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/close.py +28 -0
  38. codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/create.py +68 -0
  39. codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/list.py +43 -0
  40. codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/merge.py +32 -0
  41. codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/view.py +42 -0
  42. codeberg_cli-0.1.0/src/codeberg_cli/routes/release/__init__.py +1 -0
  43. codeberg_cli-0.1.0/src/codeberg_cli/routes/release/create.py +40 -0
  44. codeberg_cli-0.1.0/src/codeberg_cli/routes/release/list.py +42 -0
  45. codeberg_cli-0.1.0/src/codeberg_cli/routes/release/upload.py +51 -0
  46. codeberg_cli-0.1.0/src/codeberg_cli/routes/release/view.py +50 -0
  47. codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/__init__.py +1 -0
  48. codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/clone.py +21 -0
  49. codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/create.py +77 -0
  50. codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/delete.py +27 -0
  51. codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/fork.py +28 -0
  52. codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/list.py +36 -0
  53. codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/view.py +41 -0
  54. codeberg_cli-0.1.0/tests/test_api.py +25 -0
  55. codeberg_cli-0.1.0/tests/test_auth.py +25 -0
  56. codeberg_cli-0.1.0/tests/test_client.py +57 -0
  57. codeberg_cli-0.1.0/tests/test_config.py +28 -0
  58. codeberg_cli-0.1.0/tests/test_git.py +34 -0
  59. codeberg_cli-0.1.0/tests/test_issue.py +22 -0
  60. codeberg_cli-0.1.0/tests/test_pr.py +23 -0
  61. codeberg_cli-0.1.0/tests/test_release.py +21 -0
  62. codeberg_cli-0.1.0/tests/test_repo.py +28 -0
  63. codeberg_cli-0.1.0/uv.lock +251 -0
@@ -0,0 +1,37 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ pull_request:
5
+ jobs:
6
+ test:
7
+ strategy:
8
+ matrix:
9
+ python-version:
10
+ - "3.12"
11
+ os: [ubuntu-latest, macos-latest, windows-latest]
12
+ runs-on: ${{ matrix.os }}
13
+ steps:
14
+ - name: Check out repository code
15
+ uses: actions/checkout@v4
16
+ - name: Set up uv
17
+ uses: astral-sh/setup-uv@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ enable-cache: true
21
+ - name: Install just
22
+ uses: extractions/setup-just@v2
23
+ - name: Install dependencies
24
+ run: just install
25
+ - name: Run test suite
26
+ run: just test
27
+ build:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - name: Check out repository code
31
+ uses: actions/checkout@v4
32
+ - name: Set up uv
33
+ uses: astral-sh/setup-uv@v5
34
+ - name: Install just
35
+ uses: extractions/setup-just@v2
36
+ - name: Build project
37
+ run: just build
@@ -0,0 +1,24 @@
1
+ name: Release Asset Uploader
2
+ on:
3
+ release:
4
+ types:
5
+ - created
6
+ permissions:
7
+ contents: write
8
+ id-token: write
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Set up uv
15
+ uses: astral-sh/setup-uv@v5
16
+ - name: Build artifacts
17
+ run: uv build
18
+ - name: Upload artifacts
19
+ uses: shogo82148/actions-upload-release-asset@v1
20
+ with:
21
+ upload_url: ${{ github.event.release.upload_url }}
22
+ asset_path: dist/*
23
+ - name: Publish to PyPI
24
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .env
12
+ .DS_Store
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ default: test
2
+
3
+ # Run the test suite
4
+ test *args:
5
+ uv run pytest {{ args }}
6
+
7
+ # Run tests with coverage report
8
+ cov:
9
+ uv run pytest --cov=src/codeberg_cli --cov-report=term-missing
10
+
11
+ # Build the distribution (wheel + sdist)
12
+ build:
13
+ uv build
14
+
15
+ # Remove build artifacts
16
+ clean:
17
+ rm -rf dist
18
+
19
+ # Install all dependency groups
20
+ install:
21
+ uv sync --all-groups
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bryan Hu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,182 @@
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
+ [![CI](https://github.com/ThatXliner/cb/actions/workflows/ci.yml/badge.svg)](https://github.com/ThatXliner/cb/actions/workflows/ci.yml) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/codeberg-cli)](https://pypi.org/project/codeberg-cli)
29
+ [![PyPI](https://img.shields.io/pypi/v/codeberg-cli)](https://pypi.org/project/codeberg-cli)
30
+ [![PyPI - License](https://img.shields.io/pypi/l/codeberg-cli)](#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
@@ -0,0 +1,157 @@
1
+ # cb — A Codeberg CLI
2
+
3
+ [![CI](https://github.com/ThatXliner/cb/actions/workflows/ci.yml/badge.svg)](https://github.com/ThatXliner/cb/actions/workflows/ci.yml) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/codeberg-cli)](https://pypi.org/project/codeberg-cli)
4
+ [![PyPI](https://img.shields.io/pypi/v/codeberg-cli)](https://pypi.org/project/codeberg-cli)
5
+ [![PyPI - License](https://img.shields.io/pypi/l/codeberg-cli)](#license)
6
+
7
+ `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).
8
+
9
+ ```text
10
+ # One-time setup
11
+ cb auth login
12
+
13
+ # Work with repos, issues, PRs, releases
14
+ cb repo list
15
+ cb issue create --title "Fix the thing"
16
+ cb pr create --base main --head fix
17
+ cb release create v0.2.0
18
+ ```
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install codeberg-cli # or: uv tool install codeberg-cli
24
+ ```
25
+
26
+ Or from source:
27
+
28
+ ```bash
29
+ git clone https://codeberg.org/ThatXliner/codeberg-cli
30
+ cd cb
31
+ uv tool install .
32
+ ```
33
+
34
+ ## Quickstart
35
+
36
+ ```bash
37
+ # Authenticate (tokens at https://codeberg.org/user/settings/applications)
38
+ cb auth login
39
+
40
+ # Who am I?
41
+ cb auth whoami
42
+
43
+ # List your repos
44
+ cb repo list
45
+
46
+ # Clone one
47
+ cb repo clone ThatXliner/cb
48
+
49
+ # Open an issue
50
+ cb issue create --title "suggestion" --body "what about..."
51
+ ```
52
+
53
+ ## Commands
54
+
55
+ ### `cb auth` — authentication
56
+
57
+ | Command | Description |
58
+ |---------|-------------|
59
+ | `login` | Store a Codeberg access token |
60
+ | `logout` | Remove stored credentials |
61
+ | `status` | Show login state |
62
+ | `whoami` | Print current username |
63
+
64
+ ### `cb repo` — repositories
65
+
66
+ Infer `owner/repo` from `git remote origin`. Override with `--repo`.
67
+
68
+ | Command | Description |
69
+ |---------|-------------|
70
+ | `create` | Create a repo (`--org`, `--private`, `--description`) |
71
+ | `list` | List repos for a user or org |
72
+ | `clone` | Clone via `git clone` |
73
+ | `view` | Show repo details (`--web` to open browser) |
74
+ | `fork` | Fork a repo |
75
+ | `delete` | Delete a repo (requires confirmation) |
76
+
77
+ ### `cb label` — labels
78
+
79
+ | Command | Description |
80
+ |---------|-------------|
81
+ | `list` (alias: `ls`) | List repo labels |
82
+ | `create` | Create a label (`--color`, `--description`) |
83
+ | `delete` | Delete a label by ID |
84
+
85
+ ### `cb milestone` — milestones
86
+
87
+ | Command | Description |
88
+ |---------|-------------|
89
+ | `list` (alias: `ls`) | List milestones (`--state`) |
90
+ | `create` | Create a milestone (`--description`, `--due-on`) |
91
+
92
+ ### `cb issue` — issues
93
+
94
+ | Command | Description |
95
+ |---------|-------------|
96
+ | `create` | Create an issue (`--labels`, omit `--body` to open stdin) |
97
+ | `list` | List issues (`--state`, `--label`, `--limit`) |
98
+ | `view` | View an issue with comments (`--web`) |
99
+ | `close` | Close an issue |
100
+ | `reopen` | Reopen a closed issue |
101
+
102
+ ### `cb pr` — pull requests
103
+
104
+ | Command | Description |
105
+ |---------|-------------|
106
+ | `create` | Open a PR (omit `--body` to open `$EDITOR`) |
107
+ | `list` | List PRs (alias: `ls`) |
108
+ | `view` | View a PR (`--web`) |
109
+ | `merge` | Merge with `--style merge|rebase|squash` |
110
+ | `checkout` | Fetch and checkout a PR locally (alias: `co`) |
111
+ | `close` | Close without merging |
112
+
113
+ ### `cb release` — releases
114
+
115
+ | Command | Description |
116
+ |---------|-------------|
117
+ | `create` | Tag a release (`--prerelease`, `--draft`) |
118
+ | `list` | List releases |
119
+ | `view` | View a release (`--web`) |
120
+ | `upload` | Attach a file to a release |
121
+
122
+ ### `cb api` — raw API access
123
+
124
+ ```bash
125
+ cb api GET /version
126
+ cb api POST /repos/owner/repo/issues --data '{"title": "hi"}'
127
+ ```
128
+
129
+ ## Config
130
+
131
+ Token stored in `$XDG_CONFIG_HOME/cb/config.toml` (managed by `cb auth login` / `cb auth logout`).
132
+
133
+ ## Comparison
134
+
135
+ There are two other CLI tools you can use with Codeberg:
136
+
137
+ | | **cb** | **fj** (forgejo-cli) | **berg** (codeberg-cli) |
138
+ |---|---|---|---|
139
+ | Language | Python | Rust | Rust |
140
+ | Codeberg-native | Yes (targets `codeberg.org/api/v1`) | Generic (any Forgejo instance) | Yes |
141
+ | Issues | create, list, view, close, reopen | open, edit, comment, close | yes |
142
+ | Pull requests | create, list, view, merge, checkout, close | create, merge | yes |
143
+ | Releases | create, list, view, upload | publish | — |
144
+ | Repos | create, list, clone, view, fork, delete | create, edit, star, watch | yes |
145
+ | Labels | create, list, delete | — | yes |
146
+ | Milestones | create, list | — | yes |
147
+ | Raw API | `cb api GET /path` | — | — |
148
+ | AGit PRs (no-fork) | — | yes | — |
149
+ | Org/team mgmt | — | yes | — |
150
+ | Install | `pip install codeberg-cli` | prebuilt binaries | `cargo install codeberg-cli` |
151
+ | Size | ~300 lines | Rust binary | Rust binary |
152
+
153
+ **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.
154
+
155
+ ## License
156
+
157
+ MIT
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "codeberg-cli"
3
+ version = "0.1.0"
4
+ description = "A Codeberg CLI"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.12"
8
+ authors = [
9
+ { name = "Bryan Hu", email = "thatxliner@gmail.com" },
10
+ ]
11
+ keywords = ["codeberg", "forgejo", "cli"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Software Development :: Version Control :: Git",
20
+ ]
21
+ dependencies = [
22
+ "xclif>=0.4.3",
23
+ "httpx",
24
+ "tomlkit",
25
+ "platformdirs",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://codeberg.org/ThatXliner/codeberg-cli"
30
+ Repository = "https://codeberg.org/ThatXliner/codeberg-cli"
31
+ Issues = "https://codeberg.org/ThatXliner/codeberg-cli/issues"
32
+
33
+ [project.scripts]
34
+ cb = "codeberg_cli.__main__:cli"
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/codeberg_cli"]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "pytest>=8.0",
49
+ "pytest-httpx>=0.30.0",
50
+ ]
@@ -0,0 +1,8 @@
1
+ from xclif import Cli
2
+
3
+ from . import routes
4
+
5
+ cli = Cli.from_routes(routes)
6
+
7
+ if __name__ == "__main__":
8
+ cli()
@@ -0,0 +1,70 @@
1
+ import httpx
2
+ from rich.status import Status
3
+ from xclif.context import get_context
4
+
5
+ BASE_URL = "https://codeberg.org/api/v1"
6
+
7
+ _VERBS = {
8
+ "GET": "Fetching",
9
+ "POST": "Creating",
10
+ "PATCH": "Updating",
11
+ "DELETE": "Deleting",
12
+ }
13
+
14
+
15
+ class ClientError(RuntimeError):
16
+ """Raised when the API returns a non-2xx status."""
17
+
18
+
19
+ class Client:
20
+ """HTTP client for the Codeberg API."""
21
+
22
+ def __init__(self, token: str | None = None) -> None:
23
+ headers = {
24
+ "Accept": "application/json",
25
+ "User-Agent": "codeberg-cli/0.1.0",
26
+ "Content-Type": "application/json",
27
+ }
28
+ if token:
29
+ headers["Authorization"] = f"Bearer {token}"
30
+ self._client = httpx.Client(base_url=BASE_URL, headers=headers, timeout=30.0)
31
+
32
+ def get(
33
+ self, path: str, params: dict | None = None, action: str | None = None
34
+ ) -> dict | list:
35
+ return self._request("GET", path, params=params, action=action)
36
+
37
+ def post(
38
+ self, path: str, data: dict | None = None, action: str | None = None
39
+ ) -> dict | list:
40
+ return self._request("POST", path, json=data, action=action)
41
+
42
+ def patch(
43
+ self, path: str, data: dict | None = None, action: str | None = None
44
+ ) -> dict | list:
45
+ return self._request("PATCH", path, json=data, action=action)
46
+
47
+ def delete(self, path: str, action: str | None = None) -> None:
48
+ self._request("DELETE", path, action=action)
49
+
50
+ def _request(
51
+ self, method: str, path: str, action: str | None = None, **kwargs
52
+ ) -> dict | list | None:
53
+ ctx = get_context()
54
+ label = action or (
55
+ f"{method} {path}"
56
+ if ctx.verbosity >= 2
57
+ else f"{_VERBS.get(method, method)} {path.split('/')[-1]}"
58
+ )
59
+ with Status(f"{label}..."):
60
+ response = self._client.request(method, path, **kwargs)
61
+ if not response.is_success:
62
+ msg = (
63
+ response.json().get("message", "unknown error")
64
+ if response.text
65
+ else "unknown error"
66
+ )
67
+ raise ClientError(f"{response.status_code} {msg}")
68
+ if response.status_code == 204:
69
+ return None
70
+ return response.json()
@@ -0,0 +1,29 @@
1
+ from pathlib import Path
2
+
3
+ import tomlkit
4
+ from platformdirs import user_config_path
5
+
6
+
7
+ def get_config_path() -> Path:
8
+ """Return the config directory for codeberg-cli."""
9
+ return user_config_path("codeberg-cli", ensure_exists=False)
10
+
11
+
12
+ def config_file_path() -> Path:
13
+ """Return the path to the config file."""
14
+ return get_config_path() / "config.toml"
15
+
16
+
17
+ def load_config() -> dict:
18
+ """Load config from ~/.config/codeberg-cli/config.toml. Returns {} if missing."""
19
+ path = config_file_path()
20
+ if not path.exists():
21
+ return {}
22
+ return dict(tomlkit.parse(path.read_text()))
23
+
24
+
25
+ def save_config(data: dict) -> None:
26
+ """Save config to ~/.config/codeberg-cli/config.toml."""
27
+ path = config_file_path()
28
+ path.parent.mkdir(parents=True, exist_ok=True)
29
+ path.write_text(tomlkit.dumps(data))
@@ -0,0 +1,49 @@
1
+ import re
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+
6
+ def parse_repo_from_remote(remote_url: str) -> str:
7
+ """Extract owner/repo from a git remote URL."""
8
+ # HTTPS: https://codeberg.org/owner/repo.git
9
+ # SSH: git@codeberg.org:owner/repo.git
10
+ m = re.search(r"codeberg\.org[:/](.+?)(?:\.git)?/?$", remote_url)
11
+ if m:
12
+ return m.group(1)
13
+ raise ValueError(f"Could not parse repo from remote: {remote_url}")
14
+
15
+
16
+ def get_default_branch(cwd: Path) -> str:
17
+ """Get the default branch name for a repo."""
18
+ try:
19
+ result = subprocess.run(
20
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
21
+ capture_output=True, text=True, check=True, cwd=cwd,
22
+ )
23
+ return result.stdout.strip()
24
+ except (subprocess.CalledProcessError, FileNotFoundError):
25
+ return "main"
26
+
27
+
28
+ def get_repo_remote(cwd: Path | None = None) -> str | None:
29
+ """Get the 'origin' remote URL from a git repo, or None."""
30
+ try:
31
+ result = subprocess.run(
32
+ ["git", "remote", "get-url", "origin"],
33
+ capture_output=True, text=True, check=True,
34
+ cwd=cwd or Path.cwd(),
35
+ )
36
+ return result.stdout.strip()
37
+ except (subprocess.CalledProcessError, FileNotFoundError):
38
+ return None
39
+
40
+
41
+ def infer_repo(cwd: Path | None = None) -> str | None:
42
+ """Infer owner/repo from git remote origin."""
43
+ remote = get_repo_remote(cwd)
44
+ if remote is None:
45
+ return None
46
+ try:
47
+ return parse_repo_from_remote(remote)
48
+ except ValueError:
49
+ return None
@@ -0,0 +1,20 @@
1
+ from codeberg_cli.client import Client, ClientError
2
+ from codeberg_cli.config import load_config
3
+
4
+
5
+ def get_authenticated_client() -> Client | None:
6
+ """Return a Client if token is stored, else None."""
7
+ config = load_config()
8
+ token = config.get("token")
9
+ if not token:
10
+ return None
11
+ return Client(token=token)
12
+
13
+
14
+ def require_client() -> Client:
15
+ """Return an authenticated Client or print error and exit."""
16
+ client = get_authenticated_client()
17
+ if client is None:
18
+ from xclif.errors import UsageError
19
+ raise UsageError("Not logged in. Run 'cb auth login' first.")
20
+ return client