Files
diary-news/scripts/push_to_gitea.py
2026-06-11 17:24:46 +08:00

209 lines
7.2 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""一键推送到 Gitea 远程。
流程(每步都先预览,要你确认才进下一步):
1. git status + diff --stat → 让你看要推哪些文件
2. 检查 .env / secrets 是否会进 git(check-ignore + 内容嗅探)
3. 检查 APK / 大文件
4. git add <你指定的文件> → 不 add . / 全量,精准 add
5. git status 再次确认
6. git commit -m <你给的 message> → 没 message 就退出
7. git push origin main → 推
退出码:
0 = 成功推到 origin/main
1 = 你取消
2 = 推送失败(网络 / 认证 / 冲突)
"""
from __future__ import annotations
import argparse
import os
import subprocess
import sys
REPO_DIR = r"D:\selftools\diary-news"
REMOTE = "origin"
BRANCH = "main"
def run(cmd: str, check: bool = True, capture: bool = True) -> subprocess.CompletedProcess:
"""在仓库目录跑 git 命令。"""
return subprocess.run(
cmd, shell=True, cwd=REPO_DIR, check=check,
capture_output=capture, text=True, encoding="utf-8", errors="replace",
)
def confirm(prompt: str, default_yes: bool = False) -> bool:
suffix = "[Y/n]" if default_yes else "[y/N]"
ans = input(f"{prompt} {suffix}: ").strip().lower()
if not ans:
return default_yes
return ans in ("y", "yes")
def main() -> int:
ap = argparse.ArgumentParser(description="推送 diary-news 到 Gitea")
ap.add_argument("--message", "-m", help="commit message(必填,不然 dry-run 后停)")
ap.add_argument("--files", nargs="*", help="要 add 的文件路径(默认 add 所有 untracked + modified)")
ap.add_argument("--do-push", action="store_true", default=True,
help="默认会真推;传 --no-do-push 只 commit 不 push")
ap.add_argument("--no-do-push", dest="do_push", action="store_false")
ap.add_argument("--allow-apk", action="store_true", help="允许推 APK(默认会拦)")
args = ap.parse_args()
print(f"==== 仓库: {REPO_DIR} ====")
print(f"==== 远程: {REMOTE}/{BRANCH} ====\n")
# 0) 远程是否同步
r = run("git rev-parse --abbrev-ref HEAD")
if r.stdout.strip() != BRANCH:
print(f"✗ 当前在 {r.stdout.strip()},不在 {BRANCH} 分支,拒绝推")
return 2
print(f"✓ 在 {BRANCH} 分支")
r = run("git fetch --quiet " + REMOTE)
if r.returncode != 0:
print(f"✗ fetch 失败: {r.stderr}"); return 2
behind = run(f"git log HEAD..{REMOTE}/{BRANCH} --oneline", capture=True).stdout.strip()
if behind:
print(f"✗ 本地落后于 {REMOTE}/{BRANCH},先 pull:")
print(behind)
return 2
print(f"✓ 本地与 {REMOTE}/{BRANCH} 同步")
# 1) 当前状态
print(f"\n==== 1) git status ====")
print(run("git status --short").stdout.rstrip() or "(无变更)")
# 2) 大文件嗅探(> 5MB) — 用 git status --porcelain + Python 拿 size
# (避开 shell pipe 退出码 255 / xargs 空输入 / Windows 路径带空格)
print(f"\n==== 2) 大文件嗅探(> 5MB) ====")
r = run("git status --porcelain", check=False)
big = [] # [(size_mb, path), ...]
for line in r.stdout.splitlines():
if not line.strip():
continue
# 格式: "XY filename" — XY 是 2 字符,后面可能 1 空格或更复杂(rename)
# 取第三个 token 起为路径
parts = line.split(maxsplit=1)
if len(parts) < 2:
continue
path = parts[1].strip().strip('"')
if not os.path.exists(path):
continue
try:
size_mb = os.path.getsize(path) / 1024 / 1024
except OSError:
continue
if size_mb > 5:
big.append((size_mb, path))
big.sort(key=lambda x: -x[0])
if big:
for size_mb, path in big:
print(f"{size_mb:.1f} MB {path}")
else:
print(" ✓ 无 > 5MB 文件")
if big and not args.allow_apk:
apk = [p for _, p in big if p.lower().endswith(".apk")]
if apk:
print(f"\n ✗ 检测到 APK,默认拒绝推送(传 --allow-apk 强制推)")
print(f" 建议:把 '{apk[0]}' 加进 .gitignore,或上传到 release page")
return 1
# 3) 敏感文件嗅探(.env / secrets / .key / .pem) — 纯 Python
print(f"\n==== 3) 敏感文件嗅探 ====")
sensitive = []
keywords = (".env", "secret", "password", ".key", ".pem", "credentials")
r = run("git status --porcelain", check=False)
for line in r.stdout.splitlines():
if not line.strip():
continue
path = line.split(maxsplit=1)[1].strip().strip('"')
low = path.lower()
if any(k in low for k in keywords):
sensitive.append(path)
if sensitive:
for p in sensitive:
print(f"{p}")
else:
print(" ✓ 未发现敏感文件(.env / secrets / key / pem)")
# 4) 选文件
print(f"\n==== 4) 选择要 add 的文件 ====")
if args.files:
files = args.files
else:
# 默认 add 所有 untracked + modified(不包含被 ignore 的)
r = run("git ls-files --modified --others --exclude-standard")
files = [f for f in r.stdout.splitlines() if f.strip()]
if not files:
print(" (无文件可 add)"); return 1
print(f"{len(files)} 个:")
for f in files:
size_note = ""
if f.lower().endswith(".apk"):
size_note = " ← APK!"
print(f" {f}{size_note}")
if not confirm("确认 add 上面这些?"):
print("已取消"); return 1
# 5) git add
for f in files:
r = run(f'git add -- "{f}"')
if r.returncode != 0:
print(f" ✗ git add {f} 失败: {r.stderr}"); return 2
print(f" ✓ 已 add {len(files)} 个文件")
# 6) 再确认 status
print(f"\n==== 5) git status(已 staged) ====")
print(run("git status --short").stdout.rstrip())
# 7) diff stat
print(f"\n==== 6) diff --stat(将 commit 的内容) ====")
print(run("git diff --cached --stat").stdout.rstrip())
# 8) commit
if not args.message:
msg = input("commit message (直接回车用 'chore: ...'): ").strip()
if not msg:
msg = "chore: push via push_to_gitea.py"
else:
msg = args.message
print(f"\n==== 7) git commit -m \"{msg}\" ====")
r = run(f'git commit -m "{msg}"')
if r.returncode != 0:
# 可能是空 commit 或 hooks 拒
print(f" ! commit 退出码 {r.returncode}")
print(r.stdout)
print(r.stderr)
return 2
print(f" ✓ commit 成功")
print(r.stdout.rstrip())
# 9) push
if not args.push:
print(f"\n==== 8) --no-push 跳过,只 commit ====")
print(" 下次手动:git push origin main")
return 0
print(f"\n==== 8) git push {REMOTE} {BRANCH} ====")
if not confirm(f"确认推 {REMOTE}/{BRANCH}?"):
print("已取消,commit 已留下但未推"); return 1
r = run(f"git push {REMOTE} {BRANCH}")
if r.returncode != 0:
print(f" ✗ push 失败: {r.stderr}"); return 2
print(f" ✓ push 成功")
print(r.stdout.rstrip())
print(f"\n🎉 完成!新 commit 已推 {REMOTE}/{BRANCH}")
return 0
if __name__ == "__main__":
sys.exit(main())