1- from typing import TYPE_CHECKING , Any , Mapping , Optional , Sequence , Tuple , Union
1+ from typing import Any , Mapping , Optional , Sequence , Tuple , Union
22
33from django .db import models
44from django .db .models .signals import post_delete , post_save
77from sentry .db .models import Model , region_silo_only_model , sane_repr
88from sentry .db .models .fields import FlexibleForeignKey , JSONField
99from sentry .models import ActorTuple
10- from sentry .models .groupowner import OwnerRuleType
1110from sentry .ownership .grammar import Rule , load_schema , resolve_actors
1211from sentry .utils import metrics
1312from sentry .utils .cache import cache
1413
15- if TYPE_CHECKING :
16- from sentry .models import Team , User
17-
1814READ_CACHE_DURATION = 3600
1915
2016
@@ -131,46 +127,44 @@ def get_owners(
131127 return ordered_actors , rules
132128
133129 @classmethod
134- def _hydrate_rules (cls , project_id , rules , type = OwnerRuleType . OWNERSHIP_RULE . value ):
130+ def _find_actors (cls , project_id , rules , limit ):
135131 """
136132 Get the last matching rule to take the most precedence.
137133 """
138134 owners = [owner for rule in rules for owner in rule .owners ]
135+ owners .reverse ()
139136 actors = {
140137 key : val
141138 for key , val in resolve_actors ({owner for owner in owners }, project_id ).items ()
142139 if val
143140 }
144- result = [
145- (rule , ActorTuple .resolve_many ([actors [owner ] for owner in rule .owners ]), type )
146- for rule in rules
147- ]
148- return result
141+ actors = [actors [owner ] for owner in owners if owner in actors ][:limit ]
142+ return actors
149143
150144 @classmethod
151- def get_issue_owners (
152- cls , project_id , data , limit = 2
153- ) -> Sequence [
154- Tuple [
155- "Rule" ,
156- Sequence [Union ["Team" , "User" ]],
157- Union [OwnerRuleType .OWNERSHIP_RULE .value , OwnerRuleType .CODEOWNERS .value ],
158- ]
159- ]:
145+ def get_autoassign_owners (cls , project_id , data , limit = 2 ):
160146 """
161- Get the issue owners for a project if there are any.
147+ Get the auto-assign owner for a project if there are any.
162148
163149 We combine the schemas from IssueOwners and CodeOwners.
164150
165- Returns list of tuple (rule, owners, rule_type)
151+ Returns a tuple of (
152+ auto_assignment_enabled: boolean,
153+ list_of_owners,
154+ assigned_by_codeowners: boolean,
155+ auto_assigned_rule: Rule | None,
156+ owner_source: List[str]
157+ )
166158 """
167159 from sentry .models import ProjectCodeOwners
160+ from sentry .models .groupowner import OwnerRuleType
168161
169162 with metrics .timer ("projectownership.get_autoassign_owners" ):
170163 ownership = cls .get_ownership_cached (project_id )
171164 codeowners = ProjectCodeOwners .get_codeowners_cached (project_id )
165+ assigned_by_codeowners = False
172166 if not (ownership or codeowners ):
173- return []
167+ return False , [], assigned_by_codeowners , None , []
174168
175169 if not ownership :
176170 ownership = cls (project_id = project_id )
@@ -181,120 +175,43 @@ def get_issue_owners(
181175 )
182176
183177 if not (codeowners_rules or ownership_rules ):
184- return []
185-
186- hydrated_ownership_rules = cls ._hydrate_rules (
187- project_id , ownership_rules , OwnerRuleType .OWNERSHIP_RULE .value
188- )
189- hydrated_codeowners_rules = cls ._hydrate_rules (
190- project_id , codeowners_rules , OwnerRuleType .CODEOWNERS .value
191- )
192-
193- rules_in_evaluation_order = [
194- * hydrated_ownership_rules [::- 1 ],
195- * hydrated_codeowners_rules [::- 1 ],
196- ]
197- rules_with_owners = list (
198- filter (
199- lambda item : len (item [1 ]) > 0 ,
200- rules_in_evaluation_order ,
201- )
178+ return ownership .auto_assignment , [], assigned_by_codeowners , None , []
179+
180+ ownership_actors = cls ._find_actors (project_id , ownership_rules , limit )
181+ codeowners_actors = cls ._find_actors (project_id , codeowners_rules , limit )
182+
183+ # Can happen if the ownership rule references a user/team that no longer
184+ # is assigned to the project or has been removed from the org.
185+ if not (ownership_actors or codeowners_actors ):
186+ return ownership .auto_assignment , [], assigned_by_codeowners , None , []
187+
188+ # Ownership rules take precedence over codeowner rules.
189+ actors = [* ownership_actors , * codeowners_actors ][:limit ]
190+ actor_source = [
191+ * ([OwnerRuleType .OWNERSHIP_RULE .value ] * len (ownership_actors )),
192+ * ([OwnerRuleType .CODEOWNERS .value ] * len (codeowners_actors )),
193+ ][:limit ]
194+
195+ # Only the first item in the list is used for assignment, the rest are just used to suggest suspect owners.
196+ # So if ownership_actors is empty, it will be assigned by codeowners_actors
197+ if len (ownership_actors ) == 0 :
198+ assigned_by_codeowners = True
199+
200+ # The rule that would be used for auto assignment
201+ auto_assignment_rule = (
202+ codeowners_rules [0 ] if assigned_by_codeowners else ownership_rules [0 ]
202203 )
203204
204- return rules_with_owners [:limit ]
205-
206- @classmethod
207- def handle_auto_assignment (cls , project_id , event ):
208- """
209- Get the auto-assign owner for a project if there are any.
205+ from sentry .models import ActorTuple
210206
211- We combine the schemas from IssueOwners and CodeOwners.
212-
213- """
214- from sentry import analytics
215- from sentry .models import ActivityIntegration , GroupAssignee , GroupOwner , GroupOwnerType
216-
217- with metrics .timer ("projectownership.get_autoassign_owners" ):
218- ownership = cls .get_ownership_cached (project_id )
219- queue = []
220-
221- if ownership .suspect_committer_auto_assignment :
222- try :
223- committer = GroupOwner .objects .filter (
224- group = event .group ,
225- type = GroupOwnerType .SUSPECT_COMMIT .value ,
226- project_id = project_id ,
227- )
228- except GroupOwner .DoesNotExist :
229- committer = []
230-
231- if len (committer ) > 0 :
232- queue .append (
233- (
234- committer [0 ].owner (),
235- {
236- "integration" : ActivityIntegration .SUSPECT_COMMITTER .value ,
237- },
238- )
239- )
240-
241- # Skip if we already found a Suspect Committer
242- if ownership .auto_assignment and len (queue ) == 0 :
243- ownership_rules = GroupOwner .objects .filter (
244- group = event .group ,
245- type = GroupOwnerType .OWNERSHIP_RULE .value ,
246- project_id = project_id ,
247- )
248- codeowners = GroupOwner .objects .filter (
249- group = event .group ,
250- type = GroupOwnerType .CODEOWNERS .value ,
251- project_id = project_id ,
252- )
253-
254- for issue_owner in ownership_rules :
255- queue .append (
256- (
257- issue_owner .owner (),
258- {
259- "integration" : ActivityIntegration .PROJECT_OWNERSHIP .value ,
260- "rule" : (issue_owner .context or {}).get ("rule" , "" ),
261- },
262- )
263- )
264-
265- for issue_owner in codeowners :
266- queue .append (
267- (
268- issue_owner .owner (),
269- {
270- "integration" : ActivityIntegration .CODEOWNERS .value ,
271- "rule" : (issue_owner .context or {}).get ("rule" , "" ),
272- },
273- )
274- )
275-
276- try :
277- owner , details = queue .pop (0 )
278- except IndexError :
279- return
280-
281- assignment = GroupAssignee .objects .assign (
282- event .group ,
283- owner .resolve (),
284- create_only = True ,
285- extra = details ,
207+ return (
208+ ownership .auto_assignment ,
209+ ActorTuple .resolve_many (actors ),
210+ assigned_by_codeowners ,
211+ auto_assignment_rule ,
212+ actor_source ,
286213 )
287214
288- if assignment ["new_assignment" ] or assignment ["updated_assignment" ]:
289- analytics .record (
290- "codeowners.assignment"
291- if details .get ("integration" ) == ActivityIntegration .CODEOWNERS .value
292- else "issueowners.assignment" ,
293- organization_id = ownership .project .organization_id ,
294- project_id = project_id ,
295- group_id = event .group .id ,
296- )
297-
298215 @classmethod
299216 def _matching_ownership_rules (
300217 cls , ownership : "ProjectOwnership" , project_id : int , data : Mapping [str , Any ]
0 commit comments