MWSE/tools/gitea

251 lines
11 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""gitea — git.saqut.com (Gitea) CLI (MWSE otonom ajanı için).
- Cloudflare 1010'u aşmak için tarayıcı User-Agent'ı kullanır (düz curl takılır).
- Kimlik: önce ortam değişkenleri (GITEA_HOST/GITEA_USER/GITEA_TOKEN|GITEA_PASS/GITEA_REPO),
yoksa repo kökündeki .gitea-auth.json (gitignore'lu).
- Varsayılan repo: saqut/MWSE (.gitea-auth.json içindeki "repo" ile değiştirilebilir).
Örnekler:
./tools/gitea issue list --state open
./tools/gitea issue view 22
./tools/gitea issue comment 22 --body "WSTS portu başladı"
./tools/gitea issue close 21 --comment "tamamlandı, testler yeşil"
./tools/gitea issue label 33 --add bug --remove docs
./tools/gitea milestone list
./tools/gitea wiki list ; ./tools/gitea wiki view Home
./tools/gitea pr create --title "Go engine 0.1.0" --head go-rewrite --base stable --body-file PR.md
./tools/gitea pr list
"""
import os, sys, json, base64, argparse, urllib.request, urllib.error, urllib.parse
UA = "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"
HOST = USER = AUTHHDR = REPO = None
def _find_auth():
here = os.path.dirname(os.path.abspath(__file__))
for d in (os.getcwd(), here, os.path.dirname(here)):
p = os.path.join(d, ".gitea-auth.json")
if os.path.isfile(p):
return p
return None
def load_auth():
global HOST, USER, AUTHHDR, REPO
host = os.environ.get("GITEA_HOST"); user = os.environ.get("GITEA_USER")
token = os.environ.get("GITEA_TOKEN"); pw = os.environ.get("GITEA_PASS")
repo = os.environ.get("GITEA_REPO")
if not (host and user and (token or pw)):
f = _find_auth()
if f:
try:
d = json.load(open(f, encoding="utf-8"))
except Exception as e:
sys.exit(f"HATA: {f} okunamadı: {e}")
host = host or d.get("host"); user = user or d.get("user")
token = token or d.get("token"); pw = pw or d.get("password")
repo = repo or d.get("repo")
if not (host and user and (token or pw)):
sys.exit("HATA: kimlik yok. Repo kökünde .gitea-auth.json oluştur "
"(host,user,password|token) ya da GITEA_* ortam değişkenlerini ver.")
HOST = host.rstrip("/")
USER = user
AUTHHDR = ("token " + token) if token else ("Basic " + base64.b64encode(f"{user}:{pw}".encode()).decode())
REPO = repo or "saqut/MWSE"
def api(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(HOST + "/api/v1" + path, data=data, method=method)
req.add_header("User-Agent", UA)
req.add_header("Authorization", AUTHHDR)
req.add_header("Accept", "application/json")
if data is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=40) as r:
t = r.read().decode()
return r.status, (json.loads(t) if t.strip() else {})
except urllib.error.HTTPError as e:
t = e.read().decode()
try: t = json.loads(t)
except Exception: pass
return e.code, t
except Exception as e:
return "ERR", str(e)
def R(p): return f"/repos/{REPO}{p}"
def die(s, r): sys.exit(f"API hata {s}: {r}")
def ok(s): return s in (200, 201, 204)
def body_text(a):
if getattr(a, "body_file", None):
return open(a.body_file, encoding="utf-8").read()
return a.body or ""
def labels_map():
s, r = api("GET", R("/labels?limit=100"))
if not ok(s): die(s, r)
return {x["name"]: x["id"] for x in r}
def milestone_id(title):
s, r = api("GET", R("/milestones?state=all&limit=100"))
if not ok(s): die(s, r)
for m in r:
if m["title"] == title: return m["id"]
sys.exit(f"milestone bulunamadı: {title}")
# ---- issue ----
def c_issue_list(a):
q = f"?state={a.state}&type=issues&limit={a.limit}"
if a.labels: q += "&labels=" + urllib.parse.quote(a.labels)
if a.milestone: q += "&milestones=" + urllib.parse.quote(a.milestone)
s, r = api("GET", R("/issues" + q))
if not ok(s): die(s, r)
if a.json: print(json.dumps(r, ensure_ascii=False, indent=2)); return
for i in r:
ms = (i.get("milestone") or {}).get("title", "-")
lbl = ",".join(l["name"] for l in i.get("labels", []))
print(f"#{i['number']:<3} [{i['state']:<6}] ({ms:<6}) {i['title']}" + (f" [{lbl}]" if lbl else ""))
print(f"-- {len(r)} issue", file=sys.stderr)
def c_issue_view(a):
s, i = api("GET", R(f"/issues/{a.num}"))
if not ok(s): die(s, i)
ms = (i.get("milestone") or {}).get("title", "-")
lbl = ",".join(l["name"] for l in i.get("labels", []))
print(f"#{i['number']} [{i['state']}] milestone={ms} labels={lbl}\n{i['title']}\n{'-'*60}\n{i.get('body') or ''}")
s, c = api("GET", R(f"/issues/{a.num}/comments"))
if ok(s) and c:
print("\n--- yorumlar ---")
for cm in c: print(f"@{cm['user']['login']}: {cm['body']}")
def c_issue_create(a):
b = {"title": a.title, "body": body_text(a)}
if a.milestone: b["milestone"] = milestone_id(a.milestone)
if a.labels:
m = labels_map(); b["labels"] = [m[x] for x in a.labels.split(",") if x in m]
s, r = api("POST", R("/issues"), b)
print(f"oluşturuldu #{r['number']}" if ok(s) else f"hata {s}: {r}")
def c_issue_close(a):
if a.comment: api("POST", R(f"/issues/{a.num}/comments"), {"body": a.comment})
s, r = api("PATCH", R(f"/issues/{a.num}"), {"state": "closed"})
print(f"kapatıldı #{a.num}" if ok(s) else f"hata {s}: {r}")
def c_issue_reopen(a):
s, r = api("PATCH", R(f"/issues/{a.num}"), {"state": "open"})
print(f"ıldı #{a.num}" if ok(s) else f"hata {s}: {r}")
def c_issue_comment(a):
s, r = api("POST", R(f"/issues/{a.num}/comments"), {"body": body_text(a)})
print("yorum eklendi" if ok(s) else f"hata {s}: {r}")
def c_issue_edit(a):
b = {}
if a.title: b["title"] = a.title
if a.body or a.body_file: b["body"] = body_text(a)
if a.milestone: b["milestone"] = milestone_id(a.milestone)
s, r = api("PATCH", R(f"/issues/{a.num}"), b)
print(f"güncellendi #{a.num}" if ok(s) else f"hata {s}: {r}")
def c_issue_label(a):
m = labels_map()
if a.add:
ids = [m[x] for x in a.add.split(",") if x in m]
s, r = api("POST", R(f"/issues/{a.num}/labels"), {"labels": ids})
print(f"eklendi: {a.add}" if ok(s) else f"hata {s}: {r}")
if a.remove:
for x in a.remove.split(","):
if x in m: api("DELETE", R(f"/issues/{a.num}/labels/{m[x]}"))
print(f"çıkarıldı: {a.remove}")
# ---- label / milestone ----
def c_label_list(a):
s, r = api("GET", R("/labels?limit=100"))
if not ok(s): die(s, r)
for l in r: print(f"{l['id']:<4} {l['name']:<16} #{l['color']}")
def c_milestone_list(a):
s, r = api("GET", R("/milestones?state=all&limit=100"))
if not ok(s): die(s, r)
for m in r:
print(f"{m['title']:<8}ık={m['open_issues']:<3} kapalı={m['closed_issues']:<3} {m.get('description','')[:60]}")
# ---- wiki ----
def c_wiki_list(a):
s, r = api("GET", R("/wiki/pages?limit=100"))
if not ok(s): die(s, r)
for p in r: print(p["title"])
def c_wiki_view(a):
s, r = api("GET", R("/wiki/page/" + urllib.parse.quote(a.page)))
if not ok(s): die(s, r)
print(base64.b64decode(r["content_base64"]).decode("utf-8", "replace"))
def c_wiki_edit(a):
content = open(a.content_file, encoding="utf-8").read()
cb = base64.b64encode(content.encode()).decode()
s, _ = api("GET", R("/wiki/page/" + urllib.parse.quote(a.page)))
if ok(s):
s, r = api("PATCH", R("/wiki/page/" + urllib.parse.quote(a.page)),
{"title": a.page, "content_base64": cb, "message": a.message})
else:
s, r = api("POST", R("/wiki/new"), {"title": a.page, "content_base64": cb, "message": a.message})
print("wiki yazıldı" if ok(s) else f"hata {s}: {r}")
# ---- pr ----
def c_pr_list(a):
s, r = api("GET", R(f"/pulls?state={a.state}&limit=50"))
if not ok(s): die(s, r)
for p in r:
print(f"#{p['number']} [{p['state']}] {p['head']['ref']}{p['base']['ref']} {p['title']}")
def c_pr_view(a):
s, p = api("GET", R(f"/pulls/{a.num}"))
if not ok(s): die(s, p)
print(f"#{p['number']} [{p['state']}] {p['head']['ref']}{p['base']['ref']}\n{p['title']}\n{'-'*60}\n{p.get('body') or ''}")
def c_pr_create(a):
s, r = api("POST", R("/pulls"), {"title": a.title, "body": body_text(a), "head": a.head, "base": a.base})
print(f"PR #{r['number']} oluşturuldu" if ok(s) else f"hata {s}: {r}")
def build_parser():
p = argparse.ArgumentParser(prog="gitea", description="git.saqut.com CLI")
sub = p.add_subparsers(dest="grp", required=True)
gi = sub.add_parser("issue").add_subparsers(dest="act", required=True)
x = gi.add_parser("list"); x.add_argument("--state", default="open", choices=["open","closed","all"])
x.add_argument("--labels"); x.add_argument("--milestone"); x.add_argument("--limit", default=50); x.add_argument("--json", action="store_true"); x.set_defaults(f=c_issue_list)
x = gi.add_parser("view"); x.add_argument("num"); x.set_defaults(f=c_issue_view)
x = gi.add_parser("create"); x.add_argument("--title", required=True); x.add_argument("--body"); x.add_argument("--body-file"); x.add_argument("--milestone"); x.add_argument("--labels"); x.set_defaults(f=c_issue_create)
x = gi.add_parser("edit"); x.add_argument("num"); x.add_argument("--title"); x.add_argument("--body"); x.add_argument("--body-file"); x.add_argument("--milestone"); x.set_defaults(f=c_issue_edit)
x = gi.add_parser("close"); x.add_argument("num"); x.add_argument("--comment"); x.set_defaults(f=c_issue_close)
x = gi.add_parser("reopen"); x.add_argument("num"); x.set_defaults(f=c_issue_reopen)
x = gi.add_parser("comment"); x.add_argument("num"); x.add_argument("--body"); x.add_argument("--body-file"); x.set_defaults(f=c_issue_comment)
x = gi.add_parser("label"); x.add_argument("num"); x.add_argument("--add"); x.add_argument("--remove"); x.set_defaults(f=c_issue_label)
gl = sub.add_parser("label").add_subparsers(dest="act", required=True)
gl.add_parser("list").set_defaults(f=c_label_list)
gm = sub.add_parser("milestone").add_subparsers(dest="act", required=True)
gm.add_parser("list").set_defaults(f=c_milestone_list)
gw = sub.add_parser("wiki").add_subparsers(dest="act", required=True)
gw.add_parser("list").set_defaults(f=c_wiki_list)
x = gw.add_parser("view"); x.add_argument("page"); x.set_defaults(f=c_wiki_view)
x = gw.add_parser("edit"); x.add_argument("page"); x.add_argument("--content-file", required=True); x.add_argument("--message", default="wiki güncelleme"); x.set_defaults(f=c_wiki_edit)
gp = sub.add_parser("pr").add_subparsers(dest="act", required=True)
x = gp.add_parser("list"); x.add_argument("--state", default="open", choices=["open","closed","all"]); x.set_defaults(f=c_pr_list)
x = gp.add_parser("view"); x.add_argument("num"); x.set_defaults(f=c_pr_view)
x = gp.add_parser("create"); x.add_argument("--title", required=True); x.add_argument("--body"); x.add_argument("--body-file"); x.add_argument("--head", required=True); x.add_argument("--base", default="stable"); x.set_defaults(f=c_pr_create)
return p
def main():
args = build_parser().parse_args()
load_auth()
args.f(args)
if __name__ == "__main__":
main()