update_contributors.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. #!/usr/bin/env python3
  2. import subprocess
  3. from collections import defaultdict
  4. from pathlib import Path
  5. contributors_file = Path(__file__).parent.parent / "CONTRIBUTORS.md"
  6. def run_git(command):
  7. """Run a git command and return the output as a list of lines."""
  8. try:
  9. result = subprocess.run(
  10. ["git"] + command, capture_output=True, text=True, check=True
  11. )
  12. return result.stdout.strip().split("\n")
  13. except subprocess.CalledProcessError as e:
  14. print(f"❌ Error: Git command failed. Make sure you're in a Git repository.")
  15. print(f" Command: git {' '.join(command)}")
  16. print(f" Error: {e.stderr}")
  17. raise SystemExit(1)
  18. except FileNotFoundError:
  19. print("❌ Error: Git is not installed or not found in PATH.")
  20. raise SystemExit(1)
  21. def parse_existing_references():
  22. """Parse existing CONTRIBUTORS.md to extract manual Reference values."""
  23. references = {}
  24. if not contributors_file.exists():
  25. return references
  26. try:
  27. content = contributors_file.read_text(encoding="utf-8")
  28. lines = content.split("\n")
  29. for line in lines:
  30. if (
  31. not line.strip()
  32. or line.startswith("#")
  33. or "---" in line
  34. or line.startswith("This file")
  35. ):
  36. continue
  37. if line.startswith("|") and line.count("|") >= 6:
  38. parts = [p.strip() for p in line.split("|")]
  39. if len(parts) >= 7:
  40. name = parts[1]
  41. reference = parts[6]
  42. if name and reference:
  43. references[name] = reference
  44. except Exception:
  45. pass
  46. return references
  47. def get_contributors():
  48. """Extract contributors from git log."""
  49. log_lines = run_git(["log", "--format=%aN|%aE|%ad", "--date=short"])
  50. contributors = defaultdict(
  51. lambda: {"emails": set(), "first": None, "last": None, "count": 0}
  52. )
  53. for line in log_lines:
  54. if "|" not in line:
  55. continue
  56. name, email, date = line.split("|")
  57. name, email = name.strip(), email.strip()
  58. info = contributors[name]
  59. info["emails"].add(email)
  60. info["count"] += 1
  61. if not info["first"] or date < info["first"]:
  62. info["first"] = date
  63. if not info["last"] or date > info["last"]:
  64. info["last"] = date
  65. return contributors
  66. def generate_table(contributors, existing_references):
  67. """Generate markdown table, preserving manual Reference values."""
  68. header = [
  69. "# 🌍 Project Contributors",
  70. "",
  71. "This file lists all contributors automatically extracted from Git commit history.",
  72. "Do not edit manually — run `python scripts/update_contributors.py` to refresh.",
  73. "",
  74. "| Name | Email | Contributions | First Commit | Last Commit | Reference |",
  75. "|------|--------|----------------|---------------|--------------|-----------|",
  76. ]
  77. rows = []
  78. for name, info in sorted(contributors.items(), key=lambda x: x[0].lower()):
  79. emails = ", ".join(sorted(info["emails"]))
  80. reference = existing_references.get(name, "")
  81. commit_text = (
  82. f"{info['count']} commit"
  83. if info["count"] == 1
  84. else f"{info['count']} commits"
  85. )
  86. rows.append(
  87. f"| {name} | {emails} | {commit_text} | {info['first']} | {info['last']} | {reference} |"
  88. )
  89. return "\n".join(header + rows) + "\n"
  90. def main():
  91. existing_references = parse_existing_references()
  92. contributors = get_contributors()
  93. md = generate_table(contributors, existing_references)
  94. contributors_file.write_text(md, encoding="utf-8")
  95. print(f"✅ Updated {contributors_file} with {len(contributors)} contributors.")
  96. if __name__ == "__main__":
  97. main()