88import os
99import time
1010import datetime
11+ import json
1112from github import Github , GithubException
1213from github .GithubException import UnknownObjectException
1314from collections import defaultdict
1415from west .manifest import Manifest
1516from west .manifest import ManifestProject
17+ from git import Repo
18+ from pathlib import Path
1619
1720TOP_DIR = os .path .join (os .path .dirname (__file__ ))
1821sys .path .insert (0 , os .path .join (TOP_DIR , "scripts" ))
1922from get_maintainer import Maintainers
2023
24+ zephyr_base = os .getenv ('ZEPHYR_BASE' , os .path .join (TOP_DIR , '..' ))
25+
2126def log (s ):
2227 if args .verbose > 0 :
2328 print (s , file = sys .stdout )
@@ -50,11 +55,45 @@ def parse_args():
5055 parser .add_argument ("-r" , "--repo" , default = "zephyr" ,
5156 help = "Github repository" )
5257
58+ parser .add_argument ( "--updated-manifest" , default = None ,
59+ help = "Updated manifest file to compare against current west.yml" )
60+
5361 parser .add_argument ("-v" , "--verbose" , action = "count" , default = 0 ,
5462 help = "Verbose Output" )
5563
5664 args = parser .parse_args ()
5765
66+
67+ def process_manifest (old_manifest_file ):
68+ log ("Processing manifest changes" )
69+ if not os .path .isfile ("west.yml" ) or not os .path .isfile (old_manifest_file ):
70+ log ("No west.yml found, skipping..." )
71+ return []
72+ old_manifest = Manifest .from_file (old_manifest_file )
73+ new_manifest = Manifest .from_file ("west.yml" )
74+ old_projs = set ((p .name , p .revision ) for p in old_manifest .projects )
75+ new_projs = set ((p .name , p .revision ) for p in new_manifest .projects )
76+ # Removed projects
77+ rprojs = set (filter (lambda p : p [0 ] not in list (p [0 ] for p in new_projs ),
78+ old_projs - new_projs ))
79+ # Updated projects
80+ uprojs = set (filter (lambda p : p [0 ] in list (p [0 ] for p in old_projs ),
81+ new_projs - old_projs ))
82+ # Added projects
83+ aprojs = new_projs - old_projs - uprojs
84+
85+ # All projs
86+ projs = rprojs | uprojs | aprojs
87+ projs_names = [name for name , rev in projs ]
88+
89+ log (f"found modified projects: { projs_names } " )
90+ areas = []
91+ for p in projs_names :
92+ areas .append (f'West project: { p } ' )
93+
94+ log (f'manifest areas: { areas } ' )
95+ return areas
96+
5897def process_pr (gh , maintainer_file , number ):
5998
6099 gh_repo = gh .get_repo (f"{ args .org } /{ args .repo } " )
@@ -67,35 +106,59 @@ def process_pr(gh, maintainer_file, number):
67106 found_maintainers = defaultdict (int )
68107
69108 num_files = 0
70- all_areas = set ()
71109 fn = list (pr .get_files ())
72110
73- for changed_file in fn :
74- if changed_file .filename in ['west.yml' ,'submanifests/optional.yaml' ]:
75- break
76-
77111 if pr .commits == 1 and (pr .additions <= 1 and pr .deletions <= 1 ):
78112 labels = {'size: XS' }
79113
80114 if len (fn ) > 500 :
81115 log (f"Too many files changed ({ len (fn )} ), skipping...." )
82116 return
83117
118+ # areas where assignment happens if only area is affected
119+ meta_areas = [
120+ 'Release Notes' ,
121+ 'Documentation' ,
122+ 'Samples'
123+ ]
124+
84125 for changed_file in fn :
126+
85127 num_files += 1
86128 log (f"file: { changed_file .filename } " )
87- areas = maintainer_file .path2areas (changed_file .filename )
129+
130+ areas = []
131+ if changed_file .filename in ['west.yml' ,'submanifests/optional.yaml' ]:
132+ if not args .updated_manifest :
133+ log ("No updated manifest file provided, cannot process west.yml changes, skipping..." )
134+ continue
135+ parsed_areas = process_manifest (old_manifest_file = args .updated_manifest )
136+ for _area in parsed_areas :
137+ area_match = maintainer_file .name2areas (_area )
138+ if area_match :
139+ areas .extend (area_match )
140+ else :
141+ areas = maintainer_file .path2areas (changed_file .filename )
142+
143+ print (f"areas for { changed_file } : { areas } " )
88144
89145 if not areas :
90146 continue
91147
92- all_areas . update ( areas )
148+ # instance of an area, for example a driver or a board, not APIs or subsys code.
93149 is_instance = False
94150 sorted_areas = sorted (areas , key = lambda x : 'Platform' in x .name , reverse = True )
95151 for area in sorted_areas :
96- c = 1 if not is_instance else 0
152+ # do not count cmake file changes, i.e. when there are changes to
153+ # instances of an area listed in both the subsystem and the
154+ # platform implementing it
155+ if 'CMakeLists.txt' in changed_file .filename or area .name in meta_areas :
156+ c = 0
157+ else :
158+ c = 1 if not is_instance else 0
97159
98160 area_counter [area ] += c
161+ print (f"area counter: { area_counter } " )
99162 labels .update (area .labels )
100163 # FIXME: Here we count the same file multiple times if it exists in
101164 # multiple areas with same maintainer
@@ -122,22 +185,26 @@ def process_pr(gh, maintainer_file, number):
122185 log (f"Submitted by: { pr .user .login } " )
123186 log (f"candidate maintainers: { _all_maintainers } " )
124187
125- assignees = []
126- tmp_assignees = []
188+ ranked_assignees = []
189+ assignees = None
127190
128191 # we start with areas with most files changed and pick the maintainer from the first one.
129192 # if the first area is an implementation, i.e. driver or platform, we
130193 # continue searching for any other areas involved
131194 for area , count in area_counter .items ():
132- if count == 0 :
195+ # if only meta area is affected, assign one of the maintainers of that area
196+ if area .name in meta_areas and len (area_counter ) == 1 :
197+ assignees = area .maintainers
198+ break
199+ # if no maintainers, skip
200+ if count == 0 or len (area .maintainers ) == 0 :
133201 continue
202+ # if there are maintainers, but no assignees yet, set them
134203 if len (area .maintainers ) > 0 :
135- tmp_assignees = area .maintainers
136204 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
205+ # If submitter = assignee, try to pick next area and assign
206+ # someone else other than the submitter, otherwise when there
207+ # are other maintainers for the area, assign them.
141208 if len (area .maintainers ) > 1 :
142209 assignees = area .maintainers .copy ()
143210 assignees .remove (pr .user .login )
@@ -146,16 +213,25 @@ def process_pr(gh, maintainer_file, number):
146213 else :
147214 assignees = area .maintainers
148215
149- if 'Platform' not in area .name :
150- break
216+ # found a non-platform area that was changed, pick assignee from this
217+ # area and put them on top of the list, otherwise just append.
218+ if 'Platform' not in area .name :
219+ ranked_assignees .insert (0 , area .maintainers )
220+ break
221+ else :
222+ ranked_assignees .append (area .maintainers )
151223
152- if tmp_assignees and not assignees :
153- assignees = tmp_assignees
224+ if ranked_assignees :
225+ assignees = ranked_assignees [ 0 ]
154226
155227 if assignees :
156228 prop = (found_maintainers [assignees [0 ]] / num_files ) * 100
157229 log (f"Picked assignees: { assignees } ({ prop :.2f} % ownership)" )
158230 log ("+++++++++++++++++++++++++" )
231+ elif len (_all_maintainers ) > 0 :
232+ # if we have maintainers found, but could not pick one based on area,
233+ # then pick the one with most changes
234+ assignees = [next (iter (_all_maintainers ))]
159235
160236 # Set labels
161237 if labels :
@@ -206,21 +282,24 @@ def process_pr(gh, maintainer_file, number):
206282 if len (existing_reviewers ) < 15 :
207283 reviewer_vacancy = 15 - len (existing_reviewers )
208284 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" )
217285 else :
218286 log ("not adding reviewers because the existing reviewer count is greater than or "
219- "equal to 15" )
287+ "equal to 15. Adding maintainers of all areas as reviewers instead." )
288+ # FIXME: Here we could also add collaborators of the areas most
289+ # affected, i.e. the one with the final assigne.
290+ reviewers = list (_all_maintainers .keys ())
291+
292+ if reviewers :
293+ try :
294+ log (f"adding reviewers { reviewers } ..." )
295+ if not args .dry_run :
296+ pr .create_review_request (reviewers = reviewers )
297+ except GithubException :
298+ log ("can't add reviewer" )
220299
221300 ms = []
222301 # assignees
223- if assignees and not pr .assignee :
302+ if assignees and ( not pr .assignee or args . dry_run ) :
224303 try :
225304 for assignee in assignees :
226305 u = gh .get_user (assignee )
0 commit comments