Files
publish-to-gitea/gitea_push.py
T

264 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""推送到 Gitea 仓库脚本
用法:
python3 gitea_push.py <repo_name> <description> <files_dir...>
环境变量:
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 <repo_name> <description> <files_dir...>", 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()