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