44# SPDX-License-Identifier: Apache-2.0
55
66import argparse
7- import sys
7+ import datetime
88import os
9+ import sys
910import time
10- import datetime
11+ from collections import defaultdict
12+
1113from github import Github , GithubException
1214from github .GithubException import UnknownObjectException
13- from collections import defaultdict
14- from west .manifest import Manifest
15- from west .manifest import ManifestProject
15+ from west .manifest import Manifest , ManifestProject
1616
1717TOP_DIR = os .path .join (os .path .dirname (__file__ ))
1818sys .path .insert (0 , os .path .join (TOP_DIR , "scripts" ))
1919from get_maintainer import Maintainers
2020
21+ zephyr_base = os .getenv ('ZEPHYR_BASE' , os .path .join (TOP_DIR , '..' ))
22+
2123def log (s ):
2224 if args .verbose > 0 :
2325 print (s , file = sys .stdout )
@@ -50,11 +52,45 @@ def parse_args():
5052 parser .add_argument ("-r" , "--repo" , default = "zephyr" ,
5153 help = "Github repository" )
5254
55+ parser .add_argument ( "--updated-manifest" , default = None ,
56+ help = "Updated manifest file to compare against current west.yml" )
57+
5358 parser .add_argument ("-v" , "--verbose" , action = "count" , default = 0 ,
5459 help = "Verbose Output" )
5560
5661 args = parser .parse_args ()
5762
63+
64+ def process_manifest (old_manifest_file ):
65+ log ("Processing manifest changes" )
66+ if not os .path .isfile ("west.yml" ) or not os .path .isfile (old_manifest_file ):
67+ log ("No west.yml found, skipping..." )
68+ return []
69+ old_manifest = Manifest .from_file (old_manifest_file )
70+ new_manifest = Manifest .from_file ("west.yml" )
71+ old_projs = set ((p .name , p .revision ) for p in old_manifest .projects )
72+ new_projs = set ((p .name , p .revision ) for p in new_manifest .projects )
73+ # Removed projects
74+ rprojs = set (filter (lambda p : p [0 ] not in list (p [0 ] for p in new_projs ),
75+ old_projs - new_projs ))
76+ # Updated projects
77+ uprojs = set (filter (lambda p : p [0 ] in list (p [0 ] for p in old_projs ),
78+ new_projs - old_projs ))
79+ # Added projects
80+ aprojs = new_projs - old_projs - uprojs
81+
82+ # All projs
83+ projs = rprojs | uprojs | aprojs
84+ projs_names = [name for name , rev in projs ]
85+
86+ log (f"found modified projects: { projs_names } " )
87+ areas = []
88+ for p in projs_names :
89+ areas .append (f'West project: { p } ' )
90+
91+ log (f'manifest areas: { areas } ' )
92+ return areas
93+
5894def process_pr (gh , maintainer_file , number ):
5995
6096 gh_repo = gh .get_repo (f"{ args .org } /{ args .repo } " )
@@ -67,35 +103,59 @@ def process_pr(gh, maintainer_file, number):
67103 found_maintainers = defaultdict (int )
68104
69105 num_files = 0
70- all_areas = set ()
71106 fn = list (pr .get_files ())
72107
73- for changed_file in fn :
74- if changed_file .filename in ['west.yml' ,'submanifests/optional.yaml' ]:
75- break
76-
77108 if pr .commits == 1 and (pr .additions <= 1 and pr .deletions <= 1 ):
78109 labels = {'size: XS' }
79110
80111 if len (fn ) > 500 :
81112 log (f"Too many files changed ({ len (fn )} ), skipping...." )
82113 return
83114
115+ # areas where assignment happens if only area is affected
116+ meta_areas = [
117+ 'Release Notes' ,
118+ 'Documentation' ,
119+ 'Samples'
120+ ]
121+
84122 for changed_file in fn :
123+
85124 num_files += 1
86125 log (f"file: { changed_file .filename } " )
87- areas = maintainer_file .path2areas (changed_file .filename )
126+
127+ areas = []
128+ if changed_file .filename in ['west.yml' ,'submanifests/optional.yaml' ]:
129+ if not args .updated_manifest :
130+ log ("No updated manifest file provided, cannot process west.yml changes, skipping..." )
131+ continue
132+ parsed_areas = process_manifest (old_manifest_file = args .updated_manifest )
133+ for _area in parsed_areas :
134+ area_match = maintainer_file .name2areas (_area )
135+ if area_match :
136+ areas .extend (area_match )
137+ else :
138+ areas = maintainer_file .path2areas (changed_file .filename )
139+
140+ log (f"areas for { changed_file } : { areas } " )
88141
89142 if not areas :
90143 continue
91144
92- all_areas . update ( areas )
145+ # instance of an area, for example a driver or a board, not APIs or subsys code.
93146 is_instance = False
94147 sorted_areas = sorted (areas , key = lambda x : 'Platform' in x .name , reverse = True )
95148 for area in sorted_areas :
96- c = 1 if not is_instance else 0
149+ # do not count cmake file changes, i.e. when there are changes to
150+ # instances of an area listed in both the subsystem and the
151+ # platform implementing it
152+ if 'CMakeLists.txt' in changed_file .filename or area .name in meta_areas :
153+ c = 0
154+ else :
155+ c = 1 if not is_instance else 0
97156
98157 area_counter [area ] += c
158+ log (f"area counter: { area_counter } " )
99159 labels .update (area .labels )
100160 # FIXME: Here we count the same file multiple times if it exists in
101161 # multiple areas with same maintainer
@@ -122,22 +182,26 @@ def process_pr(gh, maintainer_file, number):
122182 log (f"Submitted by: { pr .user .login } " )
123183 log (f"candidate maintainers: { _all_maintainers } " )
124184
125- assignees = []
126- tmp_assignees = []
185+ ranked_assignees = []
186+ assignees = None
127187
128188 # we start with areas with most files changed and pick the maintainer from the first one.
129189 # if the first area is an implementation, i.e. driver or platform, we
130190 # continue searching for any other areas involved
131191 for area , count in area_counter .items ():
132- if count == 0 :
192+ # if only meta area is affected, assign one of the maintainers of that area
193+ if area .name in meta_areas and len (area_counter ) == 1 :
194+ assignees = area .maintainers
195+ break
196+ # if no maintainers, skip
197+ if count == 0 or len (area .maintainers ) == 0 :
133198 continue
199+ # if there are maintainers, but no assignees yet, set them
134200 if len (area .maintainers ) > 0 :
135- tmp_assignees = area .maintainers
136201 if pr .user .login in area .maintainers :
137- # submitter = assignee, try to pick next area and
138- # assign someone else other than the submitter
139- # when there also other maintainers for the area
140- # assign them
202+ # If submitter = assignee, try to pick next area and assign
203+ # someone else other than the submitter, otherwise when there
204+ # are other maintainers for the area, assign them.
141205 if len (area .maintainers ) > 1 :
142206 assignees = area .maintainers .copy ()
143207 assignees .remove (pr .user .login )
@@ -146,16 +210,25 @@ def process_pr(gh, maintainer_file, number):
146210 else :
147211 assignees = area .maintainers
148212
149- if 'Platform' not in area .name :
150- break
213+ # found a non-platform area that was changed, pick assignee from this
214+ # area and put them on top of the list, otherwise just append.
215+ if 'Platform' not in area .name :
216+ ranked_assignees .insert (0 , area .maintainers )
217+ break
218+ else :
219+ ranked_assignees .append (area .maintainers )
151220
152- if tmp_assignees and not assignees :
153- assignees = tmp_assignees
221+ if ranked_assignees :
222+ assignees = ranked_assignees [ 0 ]
154223
155224 if assignees :
156225 prop = (found_maintainers [assignees [0 ]] / num_files ) * 100
157226 log (f"Picked assignees: { assignees } ({ prop :.2f} % ownership)" )
158227 log ("+++++++++++++++++++++++++" )
228+ elif len (_all_maintainers ) > 0 :
229+ # if we have maintainers found, but could not pick one based on area,
230+ # then pick the one with most changes
231+ assignees = [next (iter (_all_maintainers ))]
159232
160233 # Set labels
161234 if labels :
@@ -165,7 +238,7 @@ def process_pr(gh, maintainer_file, number):
165238 if not args .dry_run :
166239 pr .add_to_labels (l )
167240 else :
168- log (f "Too many labels to be applied" )
241+ log ("Too many labels to be applied" )
169242
170243 if collab :
171244 reviewers = []
@@ -206,27 +279,30 @@ def process_pr(gh, maintainer_file, number):
206279 if len (existing_reviewers ) < 15 :
207280 reviewer_vacancy = 15 - len (existing_reviewers )
208281 reviewers = reviewers [:reviewer_vacancy ]
209-
210- if reviewers :
211- try :
212- log (f"adding reviewers { reviewers } ..." )
213- if not args .dry_run :
214- pr .create_review_request (reviewers = reviewers )
215- except GithubException :
216- log ("cant add reviewer" )
217282 else :
218283 log ("not adding reviewers because the existing reviewer count is greater than or "
219- "equal to 15" )
284+ "equal to 15. Adding maintainers of all areas as reviewers instead." )
285+ # FIXME: Here we could also add collaborators of the areas most
286+ # affected, i.e. the one with the final assigne.
287+ reviewers = list (_all_maintainers .keys ())
288+
289+ if reviewers :
290+ try :
291+ log (f"adding reviewers { reviewers } ..." )
292+ if not args .dry_run :
293+ pr .create_review_request (reviewers = reviewers )
294+ except GithubException :
295+ log ("can't add reviewer" )
220296
221297 ms = []
222298 # assignees
223- if assignees and not pr .assignee :
299+ if assignees and ( not pr .assignee or args . dry_run ) :
224300 try :
225301 for assignee in assignees :
226302 u = gh .get_user (assignee )
227303 ms .append (u )
228304 except GithubException :
229- log (f "Error: Unknown user" )
305+ log ("Error: Unknown user" )
230306
231307 for mm in ms :
232308 log (f"Adding assignee { mm } ..." )
@@ -280,7 +356,7 @@ def process_issue(gh, maintainer_file, number):
280356 print (f"Using labels: { issue_labels } " )
281357
282358 if issue_labels not in label_to_maintainer :
283- print (f "no match for the label set, not assigning" )
359+ print ("no match for the label set, not assigning" )
284360 return
285361
286362 for maintainer in label_to_maintainer [issue_labels ]:
@@ -317,7 +393,7 @@ def process_modules(gh, maintainers_file):
317393 repo_name = f"{ args .org } /{ project .name } "
318394 repos [repo_name ] = maintainers_file .areas [area ]
319395
320- query = f "is:open is:pr no:assignee"
396+ query = "is:open is:pr no:assignee"
321397 for repo in repos :
322398 query += f" repo:{ repo } "
323399
0 commit comments