gitea-cli 0.12.3__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.
- gitea_cli-0.12.3/MANIFEST.in +23 -0
- gitea_cli-0.12.3/PKG-INFO +45 -0
- gitea_cli-0.12.3/README.md +14 -0
- gitea_cli-0.12.3/gitea_cli/__init__.py +47 -0
- gitea_cli-0.12.3/gitea_cli/__version__.py +1 -0
- gitea_cli-0.12.3/gitea_cli/main.py +439 -0
- gitea_cli-0.12.3/gitea_cli.egg-info/PKG-INFO +45 -0
- gitea_cli-0.12.3/gitea_cli.egg-info/SOURCES.txt +13 -0
- gitea_cli-0.12.3/gitea_cli.egg-info/dependency_links.txt +1 -0
- gitea_cli-0.12.3/gitea_cli.egg-info/entry_points.txt +2 -0
- gitea_cli-0.12.3/gitea_cli.egg-info/requires.txt +5 -0
- gitea_cli-0.12.3/gitea_cli.egg-info/top_level.txt +1 -0
- gitea_cli-0.12.3/pyproject.toml +45 -0
- gitea_cli-0.12.3/setup.cfg +4 -0
- gitea_cli-0.12.3/setup.py +95 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include requirements.txt
|
|
5
|
+
include pyproject.toml
|
|
6
|
+
include setup.py
|
|
7
|
+
include setup.cfg
|
|
8
|
+
recursive-include src *.py
|
|
9
|
+
recursive-include docs *.rst *.md
|
|
10
|
+
recursive-exclude * __pycache__
|
|
11
|
+
recursive-exclude * *.py[co]
|
|
12
|
+
recursive-exclude * .DS_Store
|
|
13
|
+
prune tests
|
|
14
|
+
prune .git
|
|
15
|
+
prune .github
|
|
16
|
+
global-exclude *.pyc *.pyo
|
|
17
|
+
|
|
18
|
+
# Added entries (2 items) - 2026-08-31 03:15:31
|
|
19
|
+
exclude gitea_cli.ini.sha256
|
|
20
|
+
prune gitignore_backup
|
|
21
|
+
|
|
22
|
+
# Added entries (1 items) - 2026-08-31 03:16:03
|
|
23
|
+
exclude gitea_cli.ini
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitea-cli
|
|
3
|
+
Version: 0.12.3
|
|
4
|
+
Summary: A production-grade CLI tool for interacting with Gitea API (Migration, Mirroring, Forking, Polling).
|
|
5
|
+
Home-page: https://github.com/cumulus13/gitea_cli
|
|
6
|
+
Author: Hadi Cahyadi
|
|
7
|
+
Author-email: cumulus13@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/cumulus13/gitea_cli
|
|
10
|
+
Project-URL: Repository, https://github.com/cumulus13/gitea_cli
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
21
|
+
Requires-Python: >=3.7
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: requests>=2.28.0
|
|
24
|
+
Requires-Dist: rich>=12.0.0
|
|
25
|
+
Requires-Dist: rich-argparse>=1.0.0
|
|
26
|
+
Requires-Dist: configset>=0.2.0
|
|
27
|
+
Requires-Dist: clipboard>=0.0.4
|
|
28
|
+
Dynamic: author-email
|
|
29
|
+
Dynamic: home-page
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
|
|
32
|
+
# Gitea CLI
|
|
33
|
+
|
|
34
|
+
Simple gitea cli
|
|
35
|
+
|
|
36
|
+
## 👤 Author
|
|
37
|
+
|
|
38
|
+
[Hadi Cahyadi](mailto:cumulus13@gmail.com)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
[](https://www.buymeacoffee.com/cumulus13)
|
|
42
|
+
|
|
43
|
+
[](https://ko-fi.com/cumulus13)
|
|
44
|
+
|
|
45
|
+
[Support me on Patreon](https://www.patreon.com/cumulus13)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Gitea CLI
|
|
2
|
+
|
|
3
|
+
Simple gitea cli
|
|
4
|
+
|
|
5
|
+
## 👤 Author
|
|
6
|
+
|
|
7
|
+
[Hadi Cahyadi](mailto:cumulus13@gmail.com)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
[](https://www.buymeacoffee.com/cumulus13)
|
|
11
|
+
|
|
12
|
+
[](https://ko-fi.com/cumulus13)
|
|
13
|
+
|
|
14
|
+
[Support me on Patreon](https://www.patreon.com/cumulus13)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# File: gitea_cli/__init__.py
|
|
4
|
+
# Author: Hadi Cahyadi <cumulus13@gmail.com>
|
|
5
|
+
# Date: 2026-08-31
|
|
6
|
+
# Description: Gitea CLI - Interactive tool for Gitea API operations.
|
|
7
|
+
# License: MIT
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
Gitea CLI - Interactive tool for Gitea API operations.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import traceback
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
# Read version from __init__.py
|
|
18
|
+
def get_version():
|
|
19
|
+
"""
|
|
20
|
+
Get the version.
|
|
21
|
+
Version is taken from the __version__.py file if it exists.
|
|
22
|
+
The content of __version__.py should be:
|
|
23
|
+
version = "0.33"
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
version_file = Path(__file__).parent / "__version__.py"
|
|
27
|
+
if not version_file.is_file():
|
|
28
|
+
version_file = Path(__file__).parent.parent / "__version__.py"
|
|
29
|
+
if version_file.is_file():
|
|
30
|
+
with open(version_file, "r") as f:
|
|
31
|
+
for line in f:
|
|
32
|
+
if line.strip().startswith("version"):
|
|
33
|
+
parts = line.split("=")
|
|
34
|
+
if len(parts) == 2:
|
|
35
|
+
return parts[1].strip().strip('"').strip("'")
|
|
36
|
+
else:
|
|
37
|
+
print("No __version__ file found for __init__")
|
|
38
|
+
|
|
39
|
+
except Exception as e:
|
|
40
|
+
if os.getenv('TRACEBACK') and os.getenv('TRACEBACK') in ('1', 'true', 'True'):
|
|
41
|
+
print(traceback.format_exc())
|
|
42
|
+
else:
|
|
43
|
+
print(f"ERROR: {e}")
|
|
44
|
+
|
|
45
|
+
return "0.1.0"
|
|
46
|
+
|
|
47
|
+
__version__ = get_version()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "0.12.3"
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# File: gitea_cli/main.py
|
|
4
|
+
# Author: Hadi Cahyadi <cumulus13@gmail.com>
|
|
5
|
+
# Date: 2026-08-31
|
|
6
|
+
# Description: Simple gitea cli
|
|
7
|
+
# License: MIT
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import getpass
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import ClassVar, Optional
|
|
16
|
+
|
|
17
|
+
import clipboard
|
|
18
|
+
import requests
|
|
19
|
+
from configset import configset # type: ignore
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich_argparse import RichHelpFormatter, _lazy_rich as rr
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CustomRichHelpFormatter(RichHelpFormatter):
|
|
27
|
+
"""A custom RichHelpFormatter with modified styles."""
|
|
28
|
+
|
|
29
|
+
styles: ClassVar[dict[str, rr.StyleType]] = {
|
|
30
|
+
"argparse.args": "bold #FFFF00",
|
|
31
|
+
"argparse.groups": "#AA55FF",
|
|
32
|
+
"argparse.help": "bold #00FFFF",
|
|
33
|
+
"argparse.metavar": "bold #FF00FF",
|
|
34
|
+
"argparse.syntax": "underline",
|
|
35
|
+
"argparse.text": "white",
|
|
36
|
+
"argparse.prog": "bold #00AAFF italic",
|
|
37
|
+
"argparse.default": "bold",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CLI:
|
|
42
|
+
"""A command-line interface for interacting with Gitea API."""
|
|
43
|
+
|
|
44
|
+
CONFIGFILE = str(Path.home() / ".gitea_cli.ini")
|
|
45
|
+
CONFIG = configset(CONFIGFILE)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def setup_parser(cls):
|
|
49
|
+
subparsers = cls.parser.add_subparsers(dest="command", required=True)
|
|
50
|
+
|
|
51
|
+
# REPO subparser
|
|
52
|
+
repo_parser = subparsers.add_parser(
|
|
53
|
+
"repo",
|
|
54
|
+
help="Repository operations",
|
|
55
|
+
formatter_class=CustomRichHelpFormatter,
|
|
56
|
+
)
|
|
57
|
+
repo_parser.add_argument(
|
|
58
|
+
"-a", "--add", metavar="REPO_NAME", help="Create new repository"
|
|
59
|
+
)
|
|
60
|
+
repo_parser.add_argument(
|
|
61
|
+
"-l", "--list", action="store_true", help="List repositories"
|
|
62
|
+
)
|
|
63
|
+
repo_parser.add_argument(
|
|
64
|
+
"-rm", "--remove", metavar="REPO_NAME", help="Remove repository"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Migration and Mirroring
|
|
68
|
+
repo_parser.add_argument(
|
|
69
|
+
"-m",
|
|
70
|
+
"--migrate",
|
|
71
|
+
metavar="REMOTE_URL",
|
|
72
|
+
help="Migrate/clone repository from remote server (GitHub, GitLab, Gogs, private Git)",
|
|
73
|
+
)
|
|
74
|
+
repo_parser.add_argument(
|
|
75
|
+
"--service",
|
|
76
|
+
choices=["git", "github", "gitlab", "gogs", "gitea", "codeberg"],
|
|
77
|
+
default="git",
|
|
78
|
+
help="Remote Git service type (default: git)",
|
|
79
|
+
)
|
|
80
|
+
repo_parser.add_argument(
|
|
81
|
+
"--mirror",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="Set repository as a continuously syncing mirror",
|
|
84
|
+
)
|
|
85
|
+
repo_parser.add_argument(
|
|
86
|
+
"-n",
|
|
87
|
+
"--name",
|
|
88
|
+
metavar="REPO_NAME",
|
|
89
|
+
help="Target repository name on Gitea",
|
|
90
|
+
)
|
|
91
|
+
repo_parser.add_argument(
|
|
92
|
+
"--organization",
|
|
93
|
+
metavar="ORG_NAME",
|
|
94
|
+
help="Target organization on Gitea (optional)",
|
|
95
|
+
)
|
|
96
|
+
repo_parser.add_argument(
|
|
97
|
+
"--private",
|
|
98
|
+
action="store_true",
|
|
99
|
+
help="Make target repository private on Gitea",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Forking
|
|
103
|
+
repo_parser.add_argument(
|
|
104
|
+
"-fk",
|
|
105
|
+
"--fork",
|
|
106
|
+
metavar="OWNER/REPO",
|
|
107
|
+
help="Fork repository (e.g. owner/repo_name)",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Source Authentication Flags
|
|
111
|
+
auth_group = repo_parser.add_argument_group("Source Repository Credentials")
|
|
112
|
+
auth_group.add_argument(
|
|
113
|
+
"--clone-user",
|
|
114
|
+
help="Username for remote repo authentication (HTTPS Basic Auth)",
|
|
115
|
+
)
|
|
116
|
+
auth_group.add_argument(
|
|
117
|
+
"--clone-pass",
|
|
118
|
+
help="Password for remote repo authentication",
|
|
119
|
+
)
|
|
120
|
+
auth_group.add_argument(
|
|
121
|
+
"--clone-token",
|
|
122
|
+
help="Personal Access Token for remote repo (GitHub/GitLab/Gitea token)",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Migration Scope Toggles
|
|
126
|
+
toggle_group = repo_parser.add_argument_group("Migration Scope Options")
|
|
127
|
+
toggle_group.add_argument("--no-wiki", action="store_true", help="Do not migrate wiki")
|
|
128
|
+
toggle_group.add_argument("--no-labels", action="store_true", help="Do not migrate labels")
|
|
129
|
+
toggle_group.add_argument("--no-issues", action="store_true", help="Do not migrate issues")
|
|
130
|
+
toggle_group.add_argument("--no-pull-requests", action="store_true", help="Do not migrate pull requests")
|
|
131
|
+
toggle_group.add_argument("--no-releases", action="store_true", help="Do not migrate releases")
|
|
132
|
+
toggle_group.add_argument("--no-milestones", action="store_true", help="Do not migrate milestones")
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def usage(cls):
|
|
136
|
+
cls.parser = argparse.ArgumentParser(
|
|
137
|
+
prog="gitea-cli",
|
|
138
|
+
description="Gitea CLI - Interact with Gitea API",
|
|
139
|
+
formatter_class=CustomRichHelpFormatter,
|
|
140
|
+
)
|
|
141
|
+
cls.parser.add_argument("-u", "--username", help="Gitea username")
|
|
142
|
+
cls.parser.add_argument("-p", "--password", help="Gitea password")
|
|
143
|
+
cls.parser.add_argument(
|
|
144
|
+
"--api",
|
|
145
|
+
help="Gitea API token/key",
|
|
146
|
+
default=cls.CONFIG.get_config("api", "key", ""),
|
|
147
|
+
)
|
|
148
|
+
cls.parser.add_argument(
|
|
149
|
+
"--url",
|
|
150
|
+
help="Gitea API endpoint URL",
|
|
151
|
+
default=cls.CONFIG.get_config("api", "url", "http://localhost:3000/api/v1"),
|
|
152
|
+
)
|
|
153
|
+
cls.setup_parser()
|
|
154
|
+
|
|
155
|
+
if len(sys.argv) == 1:
|
|
156
|
+
cls.parser.print_help()
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
args = cls.parser.parse_args()
|
|
160
|
+
cls.handle_args(args)
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def get_auth_headers(cls, args):
|
|
164
|
+
api_key = args.api or cls.CONFIG.get_config("api", "key", "")
|
|
165
|
+
headers = {"Authorization": f"token {api_key}"} if api_key else {}
|
|
166
|
+
auth = None
|
|
167
|
+
if not api_key:
|
|
168
|
+
username = args.username or cls.CONFIG.get_config("auth", "username", "")
|
|
169
|
+
password = args.password or cls.CONFIG.get_config("auth", "password", "")
|
|
170
|
+
if username and not password:
|
|
171
|
+
password = getpass.getpass(f"Enter password for Gitea user '{username}': ")
|
|
172
|
+
if username and password:
|
|
173
|
+
auth = (username, password)
|
|
174
|
+
return auth, headers
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def get_current_user(cls, args) -> Optional[dict]:
|
|
178
|
+
"""Fetch details of authenticated user."""
|
|
179
|
+
url = f"{args.url.rstrip('/')}/user"
|
|
180
|
+
auth, headers = cls.get_auth_headers(args)
|
|
181
|
+
try:
|
|
182
|
+
r = requests.get(url, auth=auth, headers=headers, timeout=15)
|
|
183
|
+
if r.status_code == 200:
|
|
184
|
+
return r.json()
|
|
185
|
+
console.print(
|
|
186
|
+
f"❌ [red]Failed to get current user: {r.status_code} {r.text}[/]"
|
|
187
|
+
)
|
|
188
|
+
return None
|
|
189
|
+
except Exception as e:
|
|
190
|
+
console.print(f"❌ [red]Error fetching user details:[/] {e}")
|
|
191
|
+
if os.getenv("TRACEBACK", "").lower() in ["1", "true"]:
|
|
192
|
+
console.print_exception()
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def poll_repo_completion(
|
|
197
|
+
cls, args, owner: str, repo_name: str, timeout: int = 600, interval: int = 4
|
|
198
|
+
) -> bool:
|
|
199
|
+
"""Poll repo state until asynchronous migration, mirror, or fork action completes."""
|
|
200
|
+
url = f"{args.url.rstrip('/')}/repos/{owner}/{repo_name}"
|
|
201
|
+
auth, headers = cls.get_auth_headers(args)
|
|
202
|
+
start_time = time.time()
|
|
203
|
+
|
|
204
|
+
with console.status(
|
|
205
|
+
f"[bold yellow]Waiting for task completion on '{owner}/{repo_name}'...[/]"
|
|
206
|
+
) as status:
|
|
207
|
+
while time.time() - start_time < timeout:
|
|
208
|
+
try:
|
|
209
|
+
r = requests.get(url, auth=auth, headers=headers, timeout=15)
|
|
210
|
+
if r.status_code == 200:
|
|
211
|
+
data = r.json()
|
|
212
|
+
if not data.get("empty", True) or data.get("size", 0) > 0:
|
|
213
|
+
status.stop()
|
|
214
|
+
console.print(
|
|
215
|
+
f"✅ [bold green]Repository '{owner}/{repo_name}' is ready![/]"
|
|
216
|
+
)
|
|
217
|
+
return True
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
time.sleep(interval)
|
|
221
|
+
|
|
222
|
+
console.print(
|
|
223
|
+
f"⚠️ [bold yellow]Polling timed out for '{owner}/{repo_name}'. Check Gitea Web UI.[/]"
|
|
224
|
+
)
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def handle_args(cls, args):
|
|
229
|
+
if args.command == "repo":
|
|
230
|
+
if args.add:
|
|
231
|
+
cls.create_repo(args)
|
|
232
|
+
elif args.list:
|
|
233
|
+
cls.list_repos(args)
|
|
234
|
+
elif args.remove:
|
|
235
|
+
cls.remove_repo(args)
|
|
236
|
+
elif args.migrate:
|
|
237
|
+
repo_name = (
|
|
238
|
+
args.name
|
|
239
|
+
or args.migrate.rstrip("/").split("/")[-1].replace(".git", "")
|
|
240
|
+
)
|
|
241
|
+
cls.migrate_repo(
|
|
242
|
+
args,
|
|
243
|
+
repo_name=repo_name,
|
|
244
|
+
remote_url=args.migrate,
|
|
245
|
+
)
|
|
246
|
+
elif args.fork:
|
|
247
|
+
cls.fork_repo(args)
|
|
248
|
+
else:
|
|
249
|
+
console.print("⚠️ [red]No repo action specified.[/]")
|
|
250
|
+
sys.exit(1)
|
|
251
|
+
|
|
252
|
+
@classmethod
|
|
253
|
+
def migrate_repo(cls, args, repo_name: str, remote_url: str):
|
|
254
|
+
"""Migrate or Mirror a remote repository with full auth and service options."""
|
|
255
|
+
action_label = "Mirroring" if args.mirror else "Migrating"
|
|
256
|
+
console.print(
|
|
257
|
+
f"🚩 [#FFFF00]{action_label} repository[/] [bold #00FFFF]'{repo_name}'[/] from [bold #00FFAA]{remote_url}[/]..."
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
user_info = cls.get_current_user(args)
|
|
261
|
+
if not user_info:
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
owner = args.organization or user_info.get("login") or user_info.get("username")
|
|
265
|
+
uid = user_info.get("id")
|
|
266
|
+
|
|
267
|
+
url = f"{args.url.rstrip('/')}/repos/migrate"
|
|
268
|
+
auth, headers = cls.get_auth_headers(args)
|
|
269
|
+
|
|
270
|
+
payload = {
|
|
271
|
+
"clone_addr": remote_url,
|
|
272
|
+
"repo_name": repo_name,
|
|
273
|
+
"service": args.service,
|
|
274
|
+
"mirror": args.mirror,
|
|
275
|
+
"private": args.private,
|
|
276
|
+
"wiki": not args.no_wiki,
|
|
277
|
+
"labels": not args.no_labels,
|
|
278
|
+
"issues": not args.no_issues,
|
|
279
|
+
"pull_requests": not args.no_pull_requests,
|
|
280
|
+
"releases": not args.no_releases,
|
|
281
|
+
"milestones": not args.no_milestones,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if args.organization:
|
|
285
|
+
payload["repo_owner"] = args.organization
|
|
286
|
+
else:
|
|
287
|
+
payload["uid"] = uid
|
|
288
|
+
|
|
289
|
+
if args.clone_token:
|
|
290
|
+
payload["auth_token"] = args.clone_token
|
|
291
|
+
elif args.clone_user:
|
|
292
|
+
clone_pass = args.clone_pass or getpass.getpass(f"Enter password for remote user '{args.clone_user}': ")
|
|
293
|
+
payload["auth_username"] = args.clone_user
|
|
294
|
+
payload["auth_password"] = clone_pass
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
r = requests.post(url, auth=auth, headers=headers, json=payload, timeout=30)
|
|
298
|
+
if r.status_code in (201, 202):
|
|
299
|
+
console.print(
|
|
300
|
+
"🔄 [#00FFAA]Task accepted by Gitea. Polling migration status...[/]"
|
|
301
|
+
)
|
|
302
|
+
cls.poll_repo_completion(args, owner=owner, repo_name=repo_name)
|
|
303
|
+
else:
|
|
304
|
+
console.print(
|
|
305
|
+
f"❌ [red]Failed to start migration:[/] [#FFFF00]{r.status_code}[/] [#00FFFF]{r.text}[/]"
|
|
306
|
+
)
|
|
307
|
+
except Exception as e:
|
|
308
|
+
console.print(f"❌ [red]Migration Request Error:[/] {e}")
|
|
309
|
+
if os.getenv("TRACEBACK", "").lower() in ["1", "true"]:
|
|
310
|
+
console.print_exception()
|
|
311
|
+
|
|
312
|
+
@classmethod
|
|
313
|
+
def fork_repo(cls, args):
|
|
314
|
+
"""Fork an existing repository."""
|
|
315
|
+
parts = args.fork.strip("/").split("/")
|
|
316
|
+
if len(parts) != 2:
|
|
317
|
+
console.print(
|
|
318
|
+
"❌ [red]Invalid fork target format. Use 'owner/repo' (e.g. upstream_user/target_repo).[/]"
|
|
319
|
+
)
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
src_owner, src_repo = parts[0], parts[1]
|
|
323
|
+
user_info = cls.get_current_user(args)
|
|
324
|
+
if not user_info:
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
target_owner = args.organization or user_info.get("login") or user_info.get("username")
|
|
328
|
+
target_name = args.name or src_repo
|
|
329
|
+
|
|
330
|
+
url = f"{args.url.rstrip('/')}/repos/{src_owner}/{src_repo}/forks"
|
|
331
|
+
auth, headers = cls.get_auth_headers(args)
|
|
332
|
+
|
|
333
|
+
payload = {}
|
|
334
|
+
if args.organization:
|
|
335
|
+
payload["organization"] = args.organization
|
|
336
|
+
if args.name:
|
|
337
|
+
payload["name"] = args.name
|
|
338
|
+
|
|
339
|
+
console.print(
|
|
340
|
+
f"🍴 [#FFFF00]Forking[/] [bold #00FFFF]'{src_owner}/{src_repo}'[/] to [bold #00FFAA]'{target_owner}/{target_name}'[/]..."
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
r = requests.post(url, auth=auth, headers=headers, json=payload, timeout=30)
|
|
345
|
+
if r.status_code in (201, 202):
|
|
346
|
+
console.print("🔄 [#00FFAA]Fork task accepted. Polling status...[/]")
|
|
347
|
+
cls.poll_repo_completion(args, owner=target_owner, repo_name=target_name)
|
|
348
|
+
else:
|
|
349
|
+
console.print(
|
|
350
|
+
f"❌ [red]Failed to fork repo:[/] [#FFFF00]{r.status_code}[/] [#00FFFF]{r.text}[/]"
|
|
351
|
+
)
|
|
352
|
+
except Exception as e:
|
|
353
|
+
console.print(f"❌ [red]Fork Error:[/] {e}")
|
|
354
|
+
if os.getenv("TRACEBACK", "").lower() in ["1", "true"]:
|
|
355
|
+
console.print_exception()
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
def create_repo(cls, args):
|
|
359
|
+
url = f"{args.url.rstrip('/')}/user/repos"
|
|
360
|
+
auth, headers = cls.get_auth_headers(args)
|
|
361
|
+
data = {"name": args.add, "private": args.private}
|
|
362
|
+
try:
|
|
363
|
+
r = requests.post(url, auth=auth, headers=headers, json=data, timeout=15)
|
|
364
|
+
if r.status_code == 201:
|
|
365
|
+
console.print(
|
|
366
|
+
f"✅ [green]Repository '{args.add}' created successfully.[/]"
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
console.print(
|
|
370
|
+
f"❌ [red]Failed to create repo: {r.status_code} {r.text}[/]"
|
|
371
|
+
)
|
|
372
|
+
except Exception as e:
|
|
373
|
+
console.print(f"❌ [red]Error creating repository:[/] {e}")
|
|
374
|
+
if os.getenv("TRACEBACK", "").lower() in ["1", "true"]:
|
|
375
|
+
console.print_exception()
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def list_repos(cls, args):
|
|
379
|
+
url = f"{args.url.rstrip('/')}/user/repos"
|
|
380
|
+
auth, headers = cls.get_auth_headers(args)
|
|
381
|
+
r = None
|
|
382
|
+
try:
|
|
383
|
+
r = requests.get(url, auth=auth, headers=headers, timeout=15)
|
|
384
|
+
if r.status_code == 200:
|
|
385
|
+
repos = r.json()
|
|
386
|
+
if repos:
|
|
387
|
+
console.print("🔄 [bold green]Repositories:[/]")
|
|
388
|
+
for repo in repos:
|
|
389
|
+
console.print(f"- {repo['full_name']}")
|
|
390
|
+
else:
|
|
391
|
+
console.print("⚠️ [yellow]No repositories found.[/]")
|
|
392
|
+
else:
|
|
393
|
+
console.print(
|
|
394
|
+
f"❌ [red]Failed to list repos: {r.status_code} {r.text}[/]"
|
|
395
|
+
)
|
|
396
|
+
except Exception as e:
|
|
397
|
+
console.print(f"❌ [red]Error listing repositories:[/] {e}")
|
|
398
|
+
if os.getenv("TRACEBACK", "").lower() in ["1", "true"]:
|
|
399
|
+
console.print_exception()
|
|
400
|
+
if r:
|
|
401
|
+
clipboard.copy(r.content.decode())
|
|
402
|
+
|
|
403
|
+
@classmethod
|
|
404
|
+
def remove_repo(cls, args):
|
|
405
|
+
user_info = cls.get_current_user(args)
|
|
406
|
+
if not user_info:
|
|
407
|
+
console.print("\n❌ [red]Cannot determine owner from API credentials.[/]")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
owner = user_info.get("login") or user_info.get("username")
|
|
411
|
+
url = f"{args.url.rstrip('/')}/repos/{owner}/{args.remove}"
|
|
412
|
+
auth, headers = cls.get_auth_headers(args)
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
r = requests.delete(url, auth=auth, headers=headers, timeout=15)
|
|
416
|
+
if r.status_code == 204:
|
|
417
|
+
console.print(
|
|
418
|
+
f"🚩 [green]Repository '{args.remove}' deleted successfully.[/]"
|
|
419
|
+
)
|
|
420
|
+
elif r.status_code == 404:
|
|
421
|
+
console.print(
|
|
422
|
+
f"⚠️ [yellow]Repository '{args.remove}' not found.[/]"
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
console.print(
|
|
426
|
+
f"❌ [red]Failed to delete repo: {r.status_code} {r.text}[/]"
|
|
427
|
+
)
|
|
428
|
+
except Exception as e:
|
|
429
|
+
console.print(f"❌ [red]Error deleting repository:[/] {e}")
|
|
430
|
+
if os.getenv("TRACEBACK", "").lower() in ["1", "true"]:
|
|
431
|
+
console.print_exception()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def main():
|
|
435
|
+
CLI.usage()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
if __name__ == "__main__":
|
|
439
|
+
main()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitea-cli
|
|
3
|
+
Version: 0.12.3
|
|
4
|
+
Summary: A production-grade CLI tool for interacting with Gitea API (Migration, Mirroring, Forking, Polling).
|
|
5
|
+
Home-page: https://github.com/cumulus13/gitea_cli
|
|
6
|
+
Author: Hadi Cahyadi
|
|
7
|
+
Author-email: cumulus13@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/cumulus13/gitea_cli
|
|
10
|
+
Project-URL: Repository, https://github.com/cumulus13/gitea_cli
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
21
|
+
Requires-Python: >=3.7
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: requests>=2.28.0
|
|
24
|
+
Requires-Dist: rich>=12.0.0
|
|
25
|
+
Requires-Dist: rich-argparse>=1.0.0
|
|
26
|
+
Requires-Dist: configset>=0.2.0
|
|
27
|
+
Requires-Dist: clipboard>=0.0.4
|
|
28
|
+
Dynamic: author-email
|
|
29
|
+
Dynamic: home-page
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
|
|
32
|
+
# Gitea CLI
|
|
33
|
+
|
|
34
|
+
Simple gitea cli
|
|
35
|
+
|
|
36
|
+
## 👤 Author
|
|
37
|
+
|
|
38
|
+
[Hadi Cahyadi](mailto:cumulus13@gmail.com)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
[](https://www.buymeacoffee.com/cumulus13)
|
|
42
|
+
|
|
43
|
+
[](https://ko-fi.com/cumulus13)
|
|
44
|
+
|
|
45
|
+
[Support me on Patreon](https://www.patreon.com/cumulus13)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
gitea_cli/__init__.py
|
|
6
|
+
gitea_cli/__version__.py
|
|
7
|
+
gitea_cli/main.py
|
|
8
|
+
gitea_cli.egg-info/PKG-INFO
|
|
9
|
+
gitea_cli.egg-info/SOURCES.txt
|
|
10
|
+
gitea_cli.egg-info/dependency_links.txt
|
|
11
|
+
gitea_cli.egg-info/entry_points.txt
|
|
12
|
+
gitea_cli.egg-info/requires.txt
|
|
13
|
+
gitea_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gitea_cli
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=61.0",
|
|
4
|
+
"wheel",
|
|
5
|
+
]
|
|
6
|
+
build-backend = "setuptools.build_meta"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "gitea-cli"
|
|
10
|
+
version = "0.12.3"
|
|
11
|
+
description = "A production-grade CLI tool for interacting with Gitea API (Migration, Mirroring, Forking, Polling)."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Hadi Cahyadi" },
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">=3.9"
|
|
29
|
+
dependencies = [
|
|
30
|
+
"requests>=2.28.0",
|
|
31
|
+
"rich>=12.0.0",
|
|
32
|
+
"rich-argparse>=1.0.0",
|
|
33
|
+
"configset>=0.2.0",
|
|
34
|
+
"clipboard>=0.0.4",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.license]
|
|
38
|
+
text = "MIT"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/cumulus13/gitea_cli"
|
|
42
|
+
Repository = "https://github.com/cumulus13/gitea_cli"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
gitea-cli = "gitea_cli.main:main"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
from setuptools import setup, find_packages
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
NAME = "gitea_cli"
|
|
10
|
+
this_directory = os.path.abspath(os.path.dirname(__file__))
|
|
11
|
+
|
|
12
|
+
if (Path(__file__).parent / '__version__.py').is_file():
|
|
13
|
+
shutil.copy(str((Path(__file__).parent / '__version__.py')), os.path.join(this_directory, NAME, '__version__.py'))
|
|
14
|
+
|
|
15
|
+
# Read the contents of README file
|
|
16
|
+
with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f:
|
|
17
|
+
long_description = f.read()
|
|
18
|
+
|
|
19
|
+
# Read version from __init__.py
|
|
20
|
+
def get_version():
|
|
21
|
+
"""
|
|
22
|
+
Get the version.
|
|
23
|
+
Version is taken from the __version__.py file if it exists.
|
|
24
|
+
The content of __version__.py should be:
|
|
25
|
+
version = "0.33"
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
version_file = Path(__file__).parent / "__version__.py"
|
|
29
|
+
if not version_file.is_file():
|
|
30
|
+
version_file = Path(__file__).parent / NAME / "__version__.py"
|
|
31
|
+
if version_file.is_file():
|
|
32
|
+
with open(version_file, "r") as f:
|
|
33
|
+
for line in f:
|
|
34
|
+
if line.strip().startswith("version"):
|
|
35
|
+
parts = line.split("=")
|
|
36
|
+
if len(parts) == 2:
|
|
37
|
+
return parts[1].strip().strip('"').strip("'")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
if os.getenv('TRACEBACK') and os.getenv('TRACEBACK') in ['1', 'true', 'True']:
|
|
40
|
+
print(traceback.format_exc())
|
|
41
|
+
else:
|
|
42
|
+
print(f"ERROR: {e}")
|
|
43
|
+
|
|
44
|
+
return "0.0.0"
|
|
45
|
+
|
|
46
|
+
print(f"NAME : {NAME}")
|
|
47
|
+
print(f"VERSION: {get_version()}")
|
|
48
|
+
|
|
49
|
+
setup(
|
|
50
|
+
name=NAME,
|
|
51
|
+
version=get_version(),
|
|
52
|
+
author="Hadi Cahyadi",
|
|
53
|
+
author_email="cumulus13@gmail.com",
|
|
54
|
+
description="simple gitea cli",
|
|
55
|
+
long_description=long_description,
|
|
56
|
+
long_description_content_type="text/markdown",
|
|
57
|
+
url=f"https://github.com/cumulus13/{NAME}",
|
|
58
|
+
# packages=find_packages(),
|
|
59
|
+
packages=[NAME],
|
|
60
|
+
classifiers=[
|
|
61
|
+
"Development Status :: 4 - Beta",
|
|
62
|
+
"Intended Audience :: Developers",
|
|
63
|
+
"License :: OSI Approved :: MIT License",
|
|
64
|
+
"Operating System :: OS Independent",
|
|
65
|
+
"Programming Language :: Python :: 3",
|
|
66
|
+
"Programming Language :: Python :: 3.7",
|
|
67
|
+
"Programming Language :: Python :: 3.8",
|
|
68
|
+
"Programming Language :: Python :: 3.9",
|
|
69
|
+
"Programming Language :: Python :: 3.10",
|
|
70
|
+
"Programming Language :: Python :: 3.11",
|
|
71
|
+
"Programming Language :: Python :: 3.12",
|
|
72
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
73
|
+
"Topic :: System :: Logging",
|
|
74
|
+
"Topic :: Utilities",
|
|
75
|
+
],
|
|
76
|
+
python_requires=">=3.7",
|
|
77
|
+
install_requires=[
|
|
78
|
+
"rich>=10.0.0",
|
|
79
|
+
"rich_argparse",
|
|
80
|
+
"configset",
|
|
81
|
+
"clipboard"
|
|
82
|
+
],
|
|
83
|
+
entry_points = {
|
|
84
|
+
"console_scripts":
|
|
85
|
+
[
|
|
86
|
+
"gitea-cli = gitea_cli.main:main",
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
keywords="gitea tools",
|
|
90
|
+
project_urls={
|
|
91
|
+
"Bug Reports": f"https://github.com/cumulus13/{NAME}/issues",
|
|
92
|
+
"Source": f"https://github.com/cumulus13/{NAME}",
|
|
93
|
+
"Documentation": f"https://github.com/cumulus13/{NAME}#readme",
|
|
94
|
+
},
|
|
95
|
+
)
|