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.
- codeberg_cli-0.1.0/.github/workflows/ci.yml +37 -0
- codeberg_cli-0.1.0/.github/workflows/release.yml +24 -0
- codeberg_cli-0.1.0/.gitignore +12 -0
- codeberg_cli-0.1.0/.python-version +1 -0
- codeberg_cli-0.1.0/Justfile +21 -0
- codeberg_cli-0.1.0/LICENSE +21 -0
- codeberg_cli-0.1.0/PKG-INFO +182 -0
- codeberg_cli-0.1.0/README.md +157 -0
- codeberg_cli-0.1.0/pyproject.toml +50 -0
- codeberg_cli-0.1.0/src/codeberg_cli/__main__.py +8 -0
- codeberg_cli-0.1.0/src/codeberg_cli/client.py +70 -0
- codeberg_cli-0.1.0/src/codeberg_cli/config.py +29 -0
- codeberg_cli-0.1.0/src/codeberg_cli/git.py +49 -0
- codeberg_cli-0.1.0/src/codeberg_cli/helpers.py +20 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/__init__.py +6 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/api.py +23 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/auth/__init__.py +1 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/auth/login.py +23 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/auth/logout.py +17 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/auth/status.py +21 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/auth/whoami.py +14 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/__init__.py +1 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/close.py +28 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/create.py +55 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/list.py +49 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/reopen.py +28 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/issue/view.py +46 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/label/__init__.py +1 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/label/create.py +35 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/label/delete.py +26 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/label/list.py +41 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/milestone/__init__.py +1 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/milestone/create.py +35 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/milestone/list.py +46 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/__init__.py +1 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/checkout.py +42 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/close.py +28 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/create.py +68 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/list.py +43 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/merge.py +32 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/pr/view.py +42 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/release/__init__.py +1 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/release/create.py +40 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/release/list.py +42 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/release/upload.py +51 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/release/view.py +50 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/__init__.py +1 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/clone.py +21 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/create.py +77 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/delete.py +27 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/fork.py +28 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/list.py +36 -0
- codeberg_cli-0.1.0/src/codeberg_cli/routes/repo/view.py +41 -0
- codeberg_cli-0.1.0/tests/test_api.py +25 -0
- codeberg_cli-0.1.0/tests/test_auth.py +25 -0
- codeberg_cli-0.1.0/tests/test_client.py +57 -0
- codeberg_cli-0.1.0/tests/test_config.py +28 -0
- codeberg_cli-0.1.0/tests/test_git.py +34 -0
- codeberg_cli-0.1.0/tests/test_issue.py +22 -0
- codeberg_cli-0.1.0/tests/test_pr.py +23 -0
- codeberg_cli-0.1.0/tests/test_release.py +21 -0
- codeberg_cli-0.1.0/tests/test_repo.py +28 -0
- 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 @@
|
|
|
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
|
+
[](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
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# cb — A Codeberg CLI
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ThatXliner/cb/actions/workflows/ci.yml) [](https://pypi.org/project/codeberg-cli)
|
|
4
|
+
[](https://pypi.org/project/codeberg-cli)
|
|
5
|
+
[](#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,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
|