Files
2026-03-08 23:03:19 +07:00

334 lines
9.3 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 Sync - Automatically sync repositories to Gitea
Creates/updates repositories and pushes code automatically.
Auto-detects new vs existing repositories.
Usage:
python3 sync.py --repo my-website --path ./my-website
"""
import os
import sys
import json
import argparse
import requests
import subprocess
from pathlib import Path
def load_env():
"""Load environment from .env file."""
env_path = Path(__file__).parent / ".env"
if env_path.exists():
for line in env_path.read_text().splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip().strip("\"'"))
load_env()
GITEA_URL = os.environ.get("GITEA_URL", "https://git.moreminimore.com")
GITEA_API_TOKEN = os.environ.get("GITEA_API_TOKEN")
GITEA_USERNAME = os.environ.get("GITEA_USERNAME")
def check_auth():
"""Verify Gitea authentication."""
if not GITEA_API_TOKEN:
print("Error: GITEA_API_TOKEN not set", file=sys.stderr)
sys.exit(1)
response = requests.get(
f"{GITEA_URL}/api/v1/user",
headers={"Authorization": f"token {GITEA_API_TOKEN}"}
)
if response.status_code != 200:
print(f"Error: Gitea authentication failed ({response.status_code})", file=sys.stderr)
print(f"Check your API token at: {GITEA_URL}/user/settings/applications", file=sys.stderr)
sys.exit(1)
user = response.json()
return user.get("login", GITEA_USERNAME)
def repo_exists(username, repo_name):
"""Check if repository exists on Gitea."""
response = requests.get(
f"{GITEA_URL}/api/v1/repos/{username}/{repo_name}",
headers={"Authorization": f"token {GITEA_API_TOKEN}"}
)
return response.status_code == 200
def create_repo(repo_name, description="", private=False):
"""Create new repository on Gitea."""
print(f"📦 Creating repository: {repo_name}")
data = {
"name": repo_name,
"description": description,
"private": private,
"auto_init": True,
"readme": "Default",
"default_branch": "main"
}
response = requests.post(
f"{GITEA_URL}/api/v1/user/repos",
headers={"Authorization": f"token {GITEA_API_TOKEN}"},
json=data
)
if response.status_code == 201:
print(f"✅ Repository created: {repo_name}")
return response.json()
elif response.status_code == 409:
print(f"⚠️ Repository already exists: {repo_name}")
return None
else:
print(f"❌ Failed to create repository: {response.text}", file=sys.stderr)
sys.exit(1)
def update_repo(repo_name, description=""):
"""Update existing repository."""
print(f"🔄 Updating repository: {repo_name}")
data = {
"description": description,
"website": "",
"has_issues": True,
"has_pull_requests": True,
"has_wiki": False
}
response = requests.patch(
f"{GITEA_URL}/api/v1/repos/{GITEA_USERNAME}/{repo_name}",
headers={"Authorization": f"token {GITEA_API_TOKEN}"},
json=data
)
if response.status_code == 200:
print(f"✅ Repository updated: {repo_name}")
return response.json()
else:
print(f"⚠️ Could not update repository: {response.text}")
return None
def get_repo_url(username, repo_name):
"""Get HTTPS URL for repository."""
return f"{GITEA_URL}/{username}/{repo_name}.git"
def is_git_repo(path):
"""Check if directory is a git repository."""
git_dir = Path(path) / ".git"
return git_dir.exists()
def push_code(repo_path, git_url, branch="main"):
"""Push code to Gitea repository."""
repo_path = Path(repo_path)
if not repo_path.exists():
print(f"Error: Path does not exist: {repo_path}", file=sys.stderr)
sys.exit(1)
print(f"🚀 Pushing code to Gitea...")
# Initialize git if needed
if not is_git_repo(repo_path):
print(" → Initializing git repository")
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
# Configure git to use token for authentication
# This avoids interactive password prompts
subprocess.run(
["git", "config", "credential.helper", "store"],
cwd=repo_path,
check=True,
capture_output=True
)
# Add .gitignore if not exists
gitignore = repo_path / ".gitignore"
if not gitignore.exists():
with open(gitignore, "w") as f:
f.write("""node_modules
dist
.env
.astro
*.db
*.log
.DS_Store
""")
# Add remote if not exists
result = subprocess.run(
["git", "remote", "get-url", "origin"],
cwd=repo_path,
capture_output=True
)
if result.returncode != 0:
print(f" → Adding remote: {git_url}")
# Use token in URL for authentication
auth_url = git_url.replace(
f"{GITEA_URL}/",
f"{GITEA_URL}/{GITEA_API_TOKEN}:@"
)
subprocess.run(
["git", "remote", "add", "origin", auth_url],
cwd=repo_path,
check=True,
capture_output=True
)
else:
# Update existing remote with auth
auth_url = git_url.replace(
f"{GITEA_URL}/",
f"{GITEA_URL}/{GITEA_API_TOKEN}:@"
)
subprocess.run(
["git", "remote", "set-url", "origin", auth_url],
cwd=repo_path,
check=True,
capture_output=True
)
# Add all files
print(" → Adding files")
subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True)
# Check if there are changes to commit
result = subprocess.run(
["git", "status", "--porcelain"],
cwd=repo_path,
capture_output=True,
text=True
)
if result.stdout.strip():
# Commit changes
print(" → Committing changes")
subprocess.run(
["git", "commit", "-m", "Auto-sync from website-creator"],
cwd=repo_path,
check=True,
capture_output=True
)
# Set main as default branch
subprocess.run(
["git", "branch", "-M", branch],
cwd=repo_path,
check=True,
capture_output=True
)
# Push with force to handle initial push
print(" → Pushing to Gitea")
result = subprocess.run(
["git", "push", "-u", "-f", "origin", branch],
cwd=repo_path,
capture_output=True,
text=True
)
if result.returncode == 0:
print(f"✅ Code pushed successfully")
return True
else:
print(f"⚠️ Push output: {result.stderr}")
# Try without -f if it fails
subprocess.run(
["git", "push", "-u", "origin", branch],
cwd=repo_path,
capture_output=True
)
print(f"✅ Code pushed (without force)")
return True
else:
print(f" No changes to push")
return True
def sync_repo(repo_name, repo_path, description="", auto_push=True):
"""Complete sync workflow."""
# Step 1: Check auth
username = check_auth()
print(f"🔐 Authenticated as: {username}")
print("")
# Step 2: Check if repo exists
exists = repo_exists(username, repo_name)
if exists:
update_repo(repo_name, description)
else:
create_repo(repo_name, description)
print("")
# Step 3: Push code
if auto_push:
git_url = get_repo_url(username, repo_name)
push_code(repo_path, git_url)
print("")
print(f"🌐 Repository URL: {git_url.replace('.git', '')}")
return {
"username": username,
"repo_name": repo_name,
"git_url": get_repo_url(username, repo_name),
"created": not exists
}
def main():
parser = argparse.ArgumentParser(description="Sync repository to Gitea")
parser.add_argument("--repo", required=True, help="Repository name")
parser.add_argument("--path", required=True, help="Path to repository")
parser.add_argument("--description", default="", help="Repository description")
parser.add_argument("--no-push", action="store_true", help="Don't push code")
parser.add_argument("--private", action="store_true", help="Make repository private")
args = parser.parse_args()
print("🔄 Gitea Sync")
print("=" * 50)
print(f"Repository: {args.repo}")
print(f"Path: {args.path}")
print(f"Description: {args.description or '(none)'}")
print("=" * 50)
print("")
result = sync_repo(
args.repo,
args.path,
args.description,
auto_push=not args.no_push
)
print("")
print("=" * 50)
print("✅ Sync complete!")
print(f"Repository: {result['repo_name']}")
print(f"URL: {result['git_url'].replace('.git', '')}")
if result['created']:
print("Status: Created new repository")
else:
print("Status: Updated existing repository")
print("=" * 50)
if __name__ == "__main__":
main()