#!/usr/bin/env python3 import subprocess from collections import defaultdict from pathlib import Path # Path to CONTRIBUTORS.md (in repository root) contributors_file = Path(__file__).parent.parent / "CONTRIBUTORS.md" def run_git(command): """Run a git command and return the output as a list of lines.""" try: result = subprocess.run( ["git"] + command, capture_output=True, text=True, check=True ) return result.stdout.strip().split("\n") except subprocess.CalledProcessError as e: print(f"❌ Error: Git command failed. Make sure you're in a Git repository.") print(f" Command: git {' '.join(command)}") print(f" Error: {e.stderr}") raise SystemExit(1) except FileNotFoundError: print("❌ Error: Git is not installed or not found in PATH.") raise SystemExit(1) def parse_existing_references(): """Parse existing CONTRIBUTORS.md to extract manual Reference values.""" references = {} if not contributors_file.exists(): return references try: content = contributors_file.read_text(encoding="utf-8") lines = content.split("\n") for line in lines: # Skip header, separator, and empty lines if not line.strip() or line.startswith("#") or "---" in line or line.startswith("This file"): continue # Check if it's a table row (starts with |) if line.startswith("|") and line.count("|") >= 6: parts = [p.strip() for p in line.split("|")] # parts[0] is empty (before first |), parts[1] is Name, parts[6] is Reference if len(parts) >= 7: name = parts[1] reference = parts[6] # Only store non-empty references if name and reference: references[name] = reference except Exception: # If parsing fails, just return empty dict pass return references def get_contributors(): """Extract contributors from git log.""" log_lines = run_git(["log", "--format=%aN|%aE|%ad", "--date=short"]) contributors = defaultdict(lambda: {"emails": set(), "first": None, "last": None, "count": 0}) for line in log_lines: if "|" not in line: continue name, email, date = line.split("|") name, email = name.strip(), email.strip() info = contributors[name] info["emails"].add(email) info["count"] += 1 if not info["first"] or date < info["first"]: info["first"] = date if not info["last"] or date > info["last"]: info["last"] = date return contributors def generate_table(contributors, existing_references): """Generate markdown table, preserving manual Reference values.""" header = [ "# 🌍 Project Contributors", "", "This file lists all contributors automatically extracted from Git commit history.", "Do not edit manually — run `python scripts/update_contributors.py` to refresh.", "", "| Name | Email | Contributions | First Commit | Last Commit | Reference |", "|------|--------|----------------|---------------|--------------|-----------|", ] rows = [] for name, info in sorted(contributors.items(), key=lambda x: x[0].lower()): emails = ", ".join(sorted(info["emails"])) # Preserve existing reference if it exists, otherwise leave empty reference = existing_references.get(name, "") # Use proper pluralization for commits commit_text = f"{info['count']} commit" if info['count'] == 1 else f"{info['count']} commits" rows.append(f"| {name} | {emails} | {commit_text} | {info['first']} | {info['last']} | {reference} |") return "\n".join(header + rows) + "\n" def main(): existing_references = parse_existing_references() contributors = get_contributors() md = generate_table(contributors, existing_references) contributors_file.write_text(md, encoding="utf-8") print(f"✅ Updated {contributors_file} with {len(contributors)} contributors.") if __name__ == "__main__": main()