#!/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"açı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} açı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()