#!/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())