1+ #!/usr/bin/env python3
2+
3+ import sys
4+ import os
5+ import yaml
6+ import argparse
7+ from github import Github
8+
9+ def load_areas (filename : str ):
10+ with open (filename , "r" ) as f :
11+ doc = yaml .safe_load (f )
12+ return {k : v for k , v in doc .items () if isinstance (v , dict ) and ("files" in v or "files-regex" in v )}
13+
14+ def set_or_empty (d , key ):
15+ return set (d .get (key , []) or [])
16+
17+ def check_github_access (usernames , repo_fullname , token ):
18+ """Check if each username has at least Triage access to the repo."""
19+ gh = Github (token )
20+ repo = gh .get_repo (repo_fullname )
21+ missing_access = set ()
22+ for username in usernames :
23+ try :
24+ collab = repo .get_collaborator_permission (username )
25+ # Permissions: admin, maintain, write, triage, read
26+ if collab not in ("admin" , "maintain" , "write" , "triage" ):
27+ missing_access .add (username )
28+ except Exception :
29+ missing_access .add (username )
30+ return missing_access
31+
32+ def compare_areas (old , new , repo_fullname = None , token = None ):
33+ old_areas = set (old .keys ())
34+ new_areas = set (new .keys ())
35+
36+ added_areas = new_areas - old_areas
37+ removed_areas = old_areas - new_areas
38+ common_areas = old_areas & new_areas
39+
40+ all_added_maintainers = set ()
41+ all_added_collaborators = set ()
42+
43+ print ("=== Areas Added ===" )
44+ for area in sorted (added_areas ):
45+ print (f"+ { area } " )
46+ entry = new [area ]
47+ all_added_maintainers .update (set_or_empty (entry , "maintainers" ))
48+ all_added_collaborators .update (set_or_empty (entry , "collaborators" ))
49+
50+ print ("\n === Areas Removed ===" )
51+ for area in sorted (removed_areas ):
52+ print (f"- { area } " )
53+
54+ print ("\n === Area Changes ===" )
55+ for area in sorted (common_areas ):
56+ changes = []
57+ old_entry = old [area ]
58+ new_entry = new [area ]
59+
60+ # Compare maintainers
61+ old_maint = set_or_empty (old_entry , "maintainers" )
62+ new_maint = set_or_empty (new_entry , "maintainers" )
63+ added_maint = new_maint - old_maint
64+ removed_maint = old_maint - new_maint
65+ if added_maint :
66+ changes .append (f" Maintainers added: { ', ' .join (sorted (added_maint ))} " )
67+ all_added_maintainers .update (added_maint )
68+ if removed_maint :
69+ changes .append (f" Maintainers removed: { ', ' .join (sorted (removed_maint ))} " )
70+
71+ # Compare collaborators
72+ old_collab = set_or_empty (old_entry , "collaborators" )
73+ new_collab = set_or_empty (new_entry , "collaborators" )
74+ added_collab = new_collab - old_collab
75+ removed_collab = old_collab - new_collab
76+ if added_collab :
77+ changes .append (f" Collaborators added: { ', ' .join (sorted (added_collab ))} " )
78+ all_added_collaborators .update (added_collab )
79+ if removed_collab :
80+ changes .append (f" Collaborators removed: { ', ' .join (sorted (removed_collab ))} " )
81+
82+ # Compare status
83+ old_status = old_entry .get ("status" )
84+ new_status = new_entry .get ("status" )
85+ if old_status != new_status :
86+ changes .append (f" Status changed: { old_status } -> { new_status } " )
87+
88+ # Compare labels
89+ old_labels = set_or_empty (old_entry , "labels" )
90+ new_labels = set_or_empty (new_entry , "labels" )
91+ added_labels = new_labels - old_labels
92+ removed_labels = old_labels - new_labels
93+ if added_labels :
94+ changes .append (f" Labels added: { ', ' .join (sorted (added_labels ))} " )
95+ if removed_labels :
96+ changes .append (f" Labels removed: { ', ' .join (sorted (removed_labels ))} " )
97+
98+ # Compare files
99+ old_files = set_or_empty (old_entry , "files" )
100+ new_files = set_or_empty (new_entry , "files" )
101+ added_files = new_files - old_files
102+ removed_files = old_files - new_files
103+ if added_files :
104+ changes .append (f" Files added: { ', ' .join (sorted (added_files ))} " )
105+ if removed_files :
106+ changes .append (f" Files removed: { ', ' .join (sorted (removed_files ))} " )
107+
108+ # Compare files-regex
109+ old_regex = set_or_empty (old_entry , "files-regex" )
110+ new_regex = set_or_empty (new_entry , "files-regex" )
111+ added_regex = new_regex - old_regex
112+ removed_regex = old_regex - new_regex
113+ if added_regex :
114+ changes .append (f" files-regex added: { ', ' .join (sorted (added_regex ))} " )
115+ if removed_regex :
116+ changes .append (f" files-regex removed: { ', ' .join (sorted (removed_regex ))} " )
117+
118+ if changes :
119+ print (f"* { area } " )
120+ for c in changes :
121+ print (c )
122+
123+ print ("\n === Summary ===" )
124+ print (f"Total areas added: { len (added_areas )} " )
125+ print (f"Total maintainers added: { len (all_added_maintainers )} " )
126+ if all_added_maintainers :
127+ print (" Added maintainers: " + ", " .join (sorted (all_added_maintainers )))
128+ print (f"Total collaborators added: { len (all_added_collaborators )} " )
129+ if all_added_collaborators :
130+ print (" Added collaborators: " + ", " .join (sorted (all_added_collaborators )))
131+
132+ # Check GitHub access if repo and token are provided
133+ if repo_fullname and token :
134+ print ("\n === GitHub Access Check ===" )
135+ missing_maint = check_github_access (all_added_maintainers , repo_fullname , token )
136+ missing_collab = check_github_access (all_added_collaborators , repo_fullname , token )
137+ if missing_maint :
138+ print ("Maintainers without at least triage access:" )
139+ for u in sorted (missing_maint ):
140+ print (f" - { u } " )
141+ if missing_collab :
142+ print ("Collaborators without at least triage access:" )
143+ for u in sorted (missing_collab ):
144+ print (f" - { u } " )
145+ if not missing_maint and not missing_collab :
146+ print ("All added maintainers and collaborators have at least triage access." )
147+
148+ def main ():
149+ parser = argparse .ArgumentParser (
150+ description = "Compare two MAINTAINERS.yml files and show changes in areas, maintainers, collaborators, etc."
151+ )
152+ parser .add_argument ("old" , help = "Old MAINTAINERS.yml file" )
153+ parser .add_argument ("new" , help = "New MAINTAINERS.yml file" )
154+ parser .add_argument ("--repo" , help = "GitHub repository in org/repo format for access check" )
155+ parser .add_argument ("--token" , help = "GitHub token for API access (required for access check)" )
156+ args = parser .parse_args ()
157+
158+ old_areas = load_areas (args .old )
159+ new_areas = load_areas (args .new )
160+ token = os .environ .get ("GITHUB_TOKEN" ) or args .token
161+ compare_areas (old_areas , new_areas , repo_fullname = args .repo , token = token )
162+
163+ if __name__ == "__main__" :
164+ main ()
0 commit comments