251 lines
11 KiB
Python
Executable File
251 lines
11 KiB
Python
Executable File
#!/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()
|