#!/usr/bin/python3 import sys import subprocess from typing import List, Tuple, Set, Optional, Dict, Any import os import json import requests from pathlib import Path def die(*args: Any, **kwargs: Any) -> None: """ Function to print to stderr and then exit with status code 1. Parameters: *args (Any): Variable length argument list. **kwargs (Any): Arbitrary keyword arguments. """ print(*args, file=sys.stderr, **kwargs) exit(1) def get_git_config(key: str) -> str: """ Retrieve the git configuration value for a given key. Parameters: key (str): The git configuration key to retrieve. Returns: str: The value of the git configuration key. """ try: value = subprocess.check_output(['git', 'config', '--get', key], text=True).strip() return value except subprocess.CalledProcessError: return '' def read_token_from_file(file_path: str) -> str: """ Read a token string from a file. Parameters: file_path (str): The path to the file containing the token. Returns: str: The token string read from the file. """ try: with open(file_path, 'r') as f: return f.read().strip() except FileNotFoundError: die(f"Token file {file_path} not found.") def create_github_repo(token: str, repo_name: str, description: str, homepage: str, private: bool) -> Optional[str]: """ Create a GitHub repository using the provided details. Parameters: token (str): The GitHub access token. repo_name (str): The name of the repository to create. description (str): The description of the repository. homepage (str): The homepage URL for the repository. private (bool): Whether the repository should be private. Returns: Optional[str]: The SSH URL of the created repository, or None if creation fails. """ headers = { 'Accept': 'application/vnd.github+json', 'Authorization': f'Bearer {token}', 'X-GitHub-Api-Version': '2022-11-28' } payload = { 'name': repo_name, 'description': description, 'homepage': homepage, 'private': private, 'is_template': False # Setting this to false as it's a mirror repo } url = 'https://api.github.com/user/repos' response = requests.post(url, headers=headers, json=payload) response_data: Dict[str, Any] = json.loads(response.text) if response.status_code != 201: error_message = response_data.get('message', 'Unknown error') die(f"Failed to create GitHub repo: {error_message}") return response_data.get('ssh_url') def configure_github_repo() -> str: """ Configure a GitHub repository by retrieving token and other configuration settings from git config. Returns: str: The SSH URL of the configured repository. """ token_file = get_git_config('automirror.gh-token-file') if not token_file: die("Token file not specified for GitHub integration.") token = read_token_from_file(token_file) repo_name = os.environ.get('GL_REPO', '') # Retrieve the repository name from the environment variable if not repo_name: die("GL_REPO environment variable not set.") description_template = get_git_config('automirror.gh-desc') homepage_template = get_git_config('automirror.gh-homepage') is_private_str = get_git_config('automirror.private') is_private = is_private_str.lower() == 'true' if is_private_str else False description = description_template.format(repo_name=repo_name) homepage = homepage_template.format(repo_name=repo_name) ssh_url = create_github_repo(token, repo_name, description, homepage, is_private) if ssh_url: subprocess.run(['git', 'config', '--local', '--add', 'automirror.remote', ssh_url]) return ssh_url def resolve_refs(ref_patterns: List[str]) -> Set[str]: """ Resolve ref patterns into actual git refs. Parameters: ref_patterns (List[str]): The ref patterns to resolve. Returns: Set[str]: The set of resolved git refs. """ # Use git for-each-ref to expand the ref pattern resolve_refs = subprocess.check_output( ['git', 'for-each-ref', "--format=%(refname)"] + ref_patterns, text=True ).strip().split('\n') return set(resolve_refs) def head_ref() -> str: """ Retrieve the symbolic reference for the HEAD in the git repository. Returns: str: The HEAD ref in the repository. """ ref = subprocess.check_output(['git', 'symbolic-ref', 'HEAD'], text=True) return ref.strip() def git_push(remote_url: str, ref_spec: List[str]) -> None: """ Push to the remote git repository. Parameters: remote_url (str): The URL of the remote repository. ref_spec (List[str]): The ref specs to push. """ ref_spec_args = [f'+{ref}:{ref}' for ref in ref_spec] subprocess.run(['git', 'push'] + [remote_url] + ref_spec_args, check=True) def main() -> None: """ Main function to execute the git automirror process. """ remote_url = get_git_config('automirror.remote') gh_create = get_git_config('automirror.gh-create') == 'true' if gh_create and not remote_url: remote_url = configure_github_repo() elif not remote_url: exit(0) ref_spec_config = get_git_config('automirror.refs') ref_spec_patterns = ref_spec_config.split(',') if ref_spec_config else [ head_ref() ] resolved_ref_spec = resolve_refs(ref_spec_patterns) # Collect updated refs from stdin updated_refs = [] for line in sys.stdin: old_sha1, new_sha1, refname = line.strip().split() updated_refs.append(refname) # Check if any of the updated refs are in the configured ref_spec refs_to_push = [ref for ref in updated_refs if ref in resolved_ref_spec] print('[post-receive] pushing refs to remote:', refs_to_push) if refs_to_push: git_push(remote_url, refs_to_push) if __name__ == '__main__': main()