|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Update CONTRIBUTORS.md with current GitHub contributors |
| 4 | +This script fetches contributor information from GitHub API and updates the contributors list |
| 5 | +""" |
| 6 | + |
| 7 | +import subprocess |
| 8 | +import sys |
| 9 | +from pathlib import Path |
| 10 | +from typing import Dict, List, Optional |
| 11 | + |
| 12 | +try: |
| 13 | + import requests |
| 14 | +except ImportError: |
| 15 | + print("Error: requests library not found. Install with: pip install requests") |
| 16 | + sys.exit(1) |
| 17 | + |
| 18 | + |
| 19 | +class ContributorUpdater: |
| 20 | + """Manages updating the contributors list""" |
| 21 | + |
| 22 | + def __init__(self, repo_owner: str, repo_name: str, token: Optional[str] = None): |
| 23 | + self.repo_owner = repo_owner |
| 24 | + self.repo_name = repo_name |
| 25 | + self.token = token |
| 26 | + self.headers = {} |
| 27 | + if token: |
| 28 | + self.headers["Authorization"] = f"token {token}" |
| 29 | + |
| 30 | + def fetch_contributors(self) -> List[Dict]: |
| 31 | + """Fetch contributors from GitHub API""" |
| 32 | + url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}/contributors" |
| 33 | + |
| 34 | + all_contributors = [] |
| 35 | + page = 1 |
| 36 | + |
| 37 | + while True: |
| 38 | + params = {"page": page, "per_page": 100} |
| 39 | + response = requests.get(url, headers=self.headers, params=params) |
| 40 | + |
| 41 | + if response.status_code != 200: |
| 42 | + print(f"Error fetching contributors: {response.status_code}") |
| 43 | + print(response.text) |
| 44 | + break |
| 45 | + |
| 46 | + contributors = response.json() |
| 47 | + if not contributors: |
| 48 | + break |
| 49 | + |
| 50 | + all_contributors.extend(contributors) |
| 51 | + page += 1 |
| 52 | + |
| 53 | + return all_contributors |
| 54 | + |
| 55 | + def get_contributor_details(self, username: str) -> Dict: |
| 56 | + """Get detailed information about a contributor""" |
| 57 | + url = f"https://api.github.com/users/{username}" |
| 58 | + response = requests.get(url, headers=self.headers) |
| 59 | + |
| 60 | + if response.status_code == 200: |
| 61 | + return response.json() |
| 62 | + return {} |
| 63 | + |
| 64 | + def categorize_contributors( |
| 65 | + self, contributors: List[Dict] |
| 66 | + ) -> Dict[str, List[Dict]]: |
| 67 | + """Categorize contributors by contribution level""" |
| 68 | + # Core team members (manually maintained) |
| 69 | + core_team = {"mpusz", "JohelEGP", "chiphogg"} |
| 70 | + core_team_lower = {name.lower() for name in core_team} |
| 71 | + |
| 72 | + # Categorize by contribution count |
| 73 | + major_contributors = [] # 50+ contributions |
| 74 | + regular_contributors = [] # 10-49 contributions |
| 75 | + occasional_contributors = [] # 1-9 contributions |
| 76 | + |
| 77 | + for contributor in contributors: |
| 78 | + username = contributor["login"] |
| 79 | + contributions = contributor["contributions"] |
| 80 | + |
| 81 | + # Skip core team members (handled separately) |
| 82 | + if username.lower() in core_team_lower: |
| 83 | + continue |
| 84 | + |
| 85 | + # Skip bots |
| 86 | + if contributor.get("type") == "Bot": |
| 87 | + continue |
| 88 | + |
| 89 | + if contributions >= 50: |
| 90 | + major_contributors.append(contributor) |
| 91 | + elif contributions >= 10: |
| 92 | + regular_contributors.append(contributor) |
| 93 | + else: |
| 94 | + occasional_contributors.append(contributor) |
| 95 | + |
| 96 | + return { |
| 97 | + "major": major_contributors, |
| 98 | + "regular": regular_contributors, |
| 99 | + "occasional": occasional_contributors, |
| 100 | + } |
| 101 | + |
| 102 | + def generate_contributor_section( |
| 103 | + self, contributors: List[Dict], include_contributions: bool = True |
| 104 | + ) -> str: |
| 105 | + """Generate markdown for a list of contributors""" |
| 106 | + if not contributors: |
| 107 | + return "*No contributors in this category yet.*\n" |
| 108 | + |
| 109 | + lines = [] |
| 110 | + for contributor in contributors: |
| 111 | + username = contributor["login"] |
| 112 | + profile_url = contributor["html_url"] |
| 113 | + contributions = contributor["contributions"] |
| 114 | + |
| 115 | + if include_contributions: |
| 116 | + line = ( |
| 117 | + f"- **[{username}]({profile_url})** ({contributions} contributions)" |
| 118 | + ) |
| 119 | + else: |
| 120 | + line = f"- **[{username}]({profile_url})**" |
| 121 | + |
| 122 | + lines.append(line) |
| 123 | + |
| 124 | + return "\n".join(lines) + "\n" |
| 125 | + |
| 126 | + def update_contributors_file(self, contributors: List[Dict]): |
| 127 | + """Update the CONTRIBUTORS.md file""" |
| 128 | + contributors_file = Path("CONTRIBUTORS.md") |
| 129 | + |
| 130 | + if not contributors_file.exists(): |
| 131 | + print("CONTRIBUTORS.md not found!") |
| 132 | + return |
| 133 | + |
| 134 | + # Read current content |
| 135 | + content = contributors_file.read_text() |
| 136 | + |
| 137 | + # Categorize contributors |
| 138 | + categorized = self.categorize_contributors(contributors) |
| 139 | + |
| 140 | + # Generate new contributor sections |
| 141 | + major_section = self.generate_contributor_section(categorized["major"]) |
| 142 | + regular_section = self.generate_contributor_section(categorized["regular"]) |
| 143 | + occasional_section = self.generate_contributor_section( |
| 144 | + categorized["occasional"], include_contributions=False |
| 145 | + ) |
| 146 | + |
| 147 | + # Generate statistics (excluding core team) |
| 148 | + core_team = {"mpusz", "JohelEGP", "chiphogg"} |
| 149 | + core_team_lower = {name.lower() for name in core_team} |
| 150 | + non_core_contributors = [ |
| 151 | + c |
| 152 | + for c in contributors |
| 153 | + if c["login"].lower() not in core_team_lower and c.get("type") != "Bot" |
| 154 | + ] |
| 155 | + total_contributors = len(non_core_contributors) |
| 156 | + total_contributions = sum(c["contributions"] for c in non_core_contributors) |
| 157 | + |
| 158 | + stats_section = f"""## Statistics |
| 159 | +
|
| 160 | +- **Total Contributors**: {total_contributors} |
| 161 | +- **Total Contributions**: {total_contributions} |
| 162 | +- **Major Contributors** (50+ contributions): {len(categorized['major'])} |
| 163 | +- **Regular Contributors** (10-49 contributions): {len(categorized['regular'])} |
| 164 | +- **Occasional Contributors** (1-9 contributions): {len(categorized['occasional'])} |
| 165 | +
|
| 166 | +_Last updated: {self.get_current_date()}_ |
| 167 | +""" |
| 168 | + |
| 169 | + # Update the contributors section |
| 170 | + # Look for the CONTRIBUTORS_START/END markers |
| 171 | + start_marker = "<!-- CONTRIBUTORS_START -->" |
| 172 | + end_marker = "<!-- CONTRIBUTORS_END -->" |
| 173 | + |
| 174 | + if start_marker in content and end_marker in content: |
| 175 | + # Replace the content between markers |
| 176 | + before = content.split(start_marker)[0] |
| 177 | + after = content.split(end_marker)[1] |
| 178 | + |
| 179 | + new_content = f"""{before}{start_marker} |
| 180 | +
|
| 181 | +{stats_section} |
| 182 | +
|
| 183 | +### Major Contributors |
| 184 | +
|
| 185 | +_50+ contributions_ |
| 186 | +
|
| 187 | +{major_section} |
| 188 | +
|
| 189 | +### Regular Contributors |
| 190 | +
|
| 191 | +_10-49 contributions_ |
| 192 | +
|
| 193 | +{regular_section} |
| 194 | +
|
| 195 | +### All Contributors |
| 196 | +
|
| 197 | +_Everyone who has contributed to mp-units_ |
| 198 | +
|
| 199 | +{occasional_section} |
| 200 | +
|
| 201 | +{end_marker}{after}""" |
| 202 | + |
| 203 | + contributors_file.write_text(new_content) |
| 204 | + print(f"Updated CONTRIBUTORS.md with {total_contributors} contributors") |
| 205 | + else: |
| 206 | + print("Could not find contributor markers in CONTRIBUTORS.md") |
| 207 | + |
| 208 | + def get_current_date(self) -> str: |
| 209 | + """Get current date in a readable format""" |
| 210 | + from datetime import datetime |
| 211 | + |
| 212 | + return datetime.now().strftime("%Y-%m-%d") |
| 213 | + |
| 214 | + |
| 215 | +def get_github_token() -> Optional[str]: |
| 216 | + """Try to get GitHub token from various sources""" |
| 217 | + import os |
| 218 | + |
| 219 | + # Try environment variable |
| 220 | + token = os.getenv("GITHUB_TOKEN") |
| 221 | + if token: |
| 222 | + return token |
| 223 | + |
| 224 | + # Try git config |
| 225 | + try: |
| 226 | + result = subprocess.run( |
| 227 | + ["git", "config", "--get", "github.token"], capture_output=True, text=True |
| 228 | + ) |
| 229 | + if result.returncode == 0: |
| 230 | + return result.stdout.strip() |
| 231 | + except Exception: |
| 232 | + pass |
| 233 | + |
| 234 | + return None |
| 235 | + |
| 236 | + |
| 237 | +def main(): |
| 238 | + """Main function""" |
| 239 | + # Get GitHub token (optional but recommended to avoid rate limits) |
| 240 | + token = get_github_token() |
| 241 | + if not token: |
| 242 | + print("Warning: No GitHub token found. You may hit API rate limits.") |
| 243 | + print("Set GITHUB_TOKEN environment variable for better performance.") |
| 244 | + |
| 245 | + # Initialize updater |
| 246 | + updater = ContributorUpdater("mpusz", "units", token) |
| 247 | + |
| 248 | + try: |
| 249 | + # Fetch contributors |
| 250 | + print("Fetching contributors from GitHub...") |
| 251 | + contributors = updater.fetch_contributors() |
| 252 | + print(f"Found {len(contributors)} contributors") |
| 253 | + |
| 254 | + # Update contributors file |
| 255 | + updater.update_contributors_file(contributors) |
| 256 | + |
| 257 | + except Exception as e: |
| 258 | + print(f"Error updating contributors: {e}") |
| 259 | + sys.exit(1) |
| 260 | + |
| 261 | + |
| 262 | +if __name__ == "__main__": |
| 263 | + main() |
0 commit comments