#!/usr/bin/env python3 """推送到 Gitea 仓库脚本 用法: python3 gitea_push.py 环境变量: GITEA_URL Gitea 实例地址(默认 https://git.yoresee.cc) GITEA_USER Gitea 用户名(默认 NightStar) GITEA_EMAIL Git 提交邮箱(默认 nightstar@yoresee.cc) 示例: python3 gitea_push.py remind-me "飞书加急提醒" ./skills/remind-me/ python3 gitea_push.py my-tool "一个工具" ./src/ scripts/ """ import json import sys import subprocess import os import re import tempfile from pathlib import Path GITEA_URL = os.environ.get("GITEA_URL", "https://git.yoresee.cc") GITEA_USER = os.environ.get("GITEA_USER", "NightStar") GITEA_EMAIL = os.environ.get("GITEA_EMAIL", "nightstar@yoresee.cc") DEFAULT_BRANCH = "master" GIT_HOST = "git.yoresee.cc" SANITIZE_PATTERNS = [ (r'ou_[a-f0-9]{20,}', 'ou_xxxxxx'), # 飞书 open_id (r'sk-[a-zA-Z0-9]{20,}', 'sk-xxxxxx'), # API key (r't-[a-zA-Z0-9]{20,}', 't-xxxxxx'), # token (r'cli_[a-zA-Z0-9]{10,}', 'cli_xxxxxx'), # 飞书 app_id (r'Bearer [a-zA-Z0-9\-_\.]{20,}', 'Bearer xxxxxx'), (r'Authorization: [a-zA-Z0-9\-_\.]{20,}', 'Authorization: xxxxxx'), (r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}', 'x.x.x.x'), # IP ] ALWAYS_EXCLUDE = {".git", "node_modules", "__pycache__", ".DS_Store", "*.pyc", "*.pyo"} def run_tea(*args): cmd = ["tea"] + list(args) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"tea 命令失败: {' '.join(cmd)}\n{result.stderr}") return result.stdout def load_gitignore_rules(dirs): """读取所有相关目录下的 .gitignore 并返回 pathspec""" patterns = [] seen = set() for d in dirs: p = Path(d).resolve() while True: gitignore = p / ".gitignore" if str(gitignore) not in seen and gitignore.exists(): seen.add(str(gitignore)) try: with open(gitignore) as f: for line in f: line = line.strip() if line and not line.startswith("#"): patterns.append(line) except Exception: pass if p.parent == p: break p = p.parent try: import pathspec return pathspec.PathSpec.from_lines("gitwildmatch", patterns) except ImportError: # fallback: 简单 glob 匹配 print(" (pathspec 未安装,使用简单 .gitignore 匹配)", file=sys.stderr) patterns_set = set(patterns) return patterns_set, None def is_ignored(filepath, spec, src_root): """判断文件是否应被忽略""" name = filepath.name # 内置排除 if name in ALWAYS_EXCLUDE: return True if name.endswith(".pyc") or name.endswith(".pyo"): return True if name == ".gitignore": return True # .gitignore 规则 try: rel = filepath.relative_to(src_root) rel_str = str(rel) except ValueError: return False if spec is None: return False try: return spec.match_file(rel_str) except Exception: return False def sanitize_file(path): """检查文件中的隐私信息""" try: content = Path(path).read_text() except Exception: return False issues = [] for pattern, replacement in SANITIZE_PATTERNS: matches = re.findall(pattern, content) if matches: issues.append(f" {pattern}: 发现 {len(matches)} 处疑似隐私数据") if issues: print(f"\n⚠️ 隐私检查 - {path}:", file=sys.stderr) for issue in issues: print(issue, file=sys.stderr) print("", file=sys.stderr) return True return False def collect_files(dirs, work_dir): """收集需要推送的文件,应用 .gitignore 过滤""" print("[3/5] 准备推送内容(应用 .gitignore 过滤)...", file=sys.stderr) spec = load_gitignore_rules(dirs) count = 0 for d in dirs: src = Path(d).resolve() if src.is_file(): dest = work_dir / src.name dest.write_bytes(src.read_bytes()) print(f" + {src.name}", file=sys.stderr) count += 1 elif src.is_dir(): for f in sorted(src.rglob("*")): if not f.is_file(): continue if ".git" in f.parts: continue if is_ignored(f, spec, src): continue rel = f.relative_to(src) dest = work_dir / rel dest.parent.mkdir(parents=True, exist_ok=True) dest.write_bytes(f.read_bytes()) print(f" + {rel}", file=sys.stderr) count += 1 print(f" 共收集 {count} 个文件", file=sys.stderr) def main(): if len(sys.argv) < 3: print("用法: gitea_push.py ", file=sys.stderr) print("示例: gitea_push.py my-tool '一个工具' ./src/", file=sys.stderr) sys.exit(1) repo_name = sys.argv[1] description = sys.argv[2] dirs = [d for d in sys.argv[3:] if d.strip()] if not dirs: print("❌ 请至少指定一个文件或目录", file=sys.stderr) sys.exit(1) # 1. 隐私检查 print("[1/5] 隐私检查...", file=sys.stderr) has_issues = False for d in dirs: p = Path(d) if p.is_file(): has_issues |= sanitize_file(p) elif p.is_dir(): for f in p.rglob("*"): if f.is_file() and f.suffix in ('.py', '.md', '.json', '.yaml', '.yml', '.sh', '.txt', '.toml', '.js', '.ts'): has_issues |= sanitize_file(f) if has_issues: resp = input("⚠️ 发现疑似隐私数据,继续推送?(yes/no): ").strip().lower() if resp not in ('yes', 'y'): print("已取消推送", file=sys.stderr) sys.exit(1) # 2. 创建 Gitea 仓库(默认公开,默认分支 master) print(f"[2/5] 创建公开仓库 {repo_name}(分支: {DEFAULT_BRANCH})...", file=sys.stderr) try: output = run_tea("repo", "create", "--name", repo_name, "--description", description, "--branch", DEFAULT_BRANCH, "--init") print(output.strip(), file=sys.stderr) except RuntimeError as e: if "already exists" in str(e).lower() or "409" in str(e): print(f" 仓库 {repo_name} 已存在,使用现有仓库", file=sys.stderr) else: raise # 3. 收集文件(使用 .gitignore) work_dir = Path(tempfile.mkdtemp(prefix="gitea_")) collect_files(dirs, work_dir) # 4. Git 初始化并推送到 master 分支 print(f"[4/5] 推送到 Gitea (分支: {DEFAULT_BRANCH})...", file=sys.stderr) subprocess.run(["git", "init", "-b", DEFAULT_BRANCH], cwd=work_dir, capture_output=True) subprocess.run(["git", "config", "user.name", GITEA_USER], cwd=work_dir, capture_output=True) subprocess.run(["git", "config", "user.email", GITEA_EMAIL], cwd=work_dir, capture_output=True) subprocess.run(["git", "add", "-A"], cwd=work_dir, capture_output=True) commit_res = subprocess.run( ["git", "commit", "-m", f"feat: {description}"], cwd=work_dir, capture_output=True, text=True ) if commit_res.returncode != 0: # 可能是空提交(所有文件都被过滤了) print(f" commit 可能为空: {commit_res.stderr.strip()}", file=sys.stderr) repo_url = f"git@{GIT_HOST}:{GITEA_USER}/{repo_name}.git" remote_res = subprocess.run( ["git", "remote", "add", "origin", repo_url], cwd=work_dir, capture_output=True, text=True ) if remote_res.returncode != 0: subprocess.run(["git", "remote", "set-url", "origin", repo_url], cwd=work_dir, capture_output=True) push_result = subprocess.run( ["git", "push", "-u", "origin", DEFAULT_BRANCH, "--force"], cwd=work_dir, capture_output=True, text=True, env={**os.environ, "GIT_SSH_COMMAND": "ssh -o StrictHostKeyChecking=accept-new"} ) if push_result.returncode != 0: print(f"❌ 推送失败:\n{push_result.stderr[:500]}", file=sys.stderr) sys.exit(1) # 5. 完成 browse_url = f"{GITEA_URL}/{GITEA_USER}/{repo_name}" clone_url = f"git@{GIT_HOST}:{GITEA_USER}/{repo_name}.git" print(f"\n[5/5] ✅ 推送成功!", file=sys.stderr) print(f" 浏览: {browse_url}", file=sys.stderr) print(f" 克隆: {clone_url}", file=sys.stderr) print(json.dumps({ "ok": True, "repo": repo_name, "url": browse_url, "clone": clone_url, "branch": DEFAULT_BRANCH })) if __name__ == "__main__": main()