| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 |
- #!/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()
|