def find_matching_merge_rule(pr: GitHubPR, repo: GitRepo) -> MergeRule: """Returns merge rule matching to this pr or raises an exception""" changed_files = pr.get_changed_files() approved_by = set(pr.get_approved_by()) rules = read_merge_rules(repo) for rule in rules: rule_name = rule.name rule_approvers_set = set(rule.approved_by) patterns_re = patterns_to_regex(rule.patterns) approvers_intersection = approved_by.intersection(rule_approvers_set) # If rule requires approvers but they aren't the ones that reviewed PR if len(approvers_intersection) == 0 and len(rule_approvers_set) > 0: print(f"Skipping rule {rule_name} due to no approvers overlap") continue if rule.mandatory_app_id is not None: cs_conslusions = pr.get_check_suite_conclusions() mandatory_app_id = rule.mandatory_app_id if mandatory_app_id not in cs_conslusions or cs_conslusions[ mandatory_app_id] != "SUCCESS": print( f"Skipping rule {rule_name} as mandatory app {mandatory_app_id} is not in {cs_conslusions}" ) continue non_matching_files = [] for fname in changed_files: if not patterns_re.match(fname): non_matching_files.append(fname) if len(non_matching_files) > 0: print( f"Skipping rule {rule_name} due to non-matching files: {non_matching_files}" ) continue print(f"Matched rule {rule_name} for {pr.pr_num}") return rule raise RuntimeError(f"PR {pr.pr_num} does not match merge rules")
def find_matching_merge_rule(pr: GitHubPR, repo: GitRepo, force: bool = False) -> MergeRule: """Returns merge rule matching to this pr or raises an exception""" changed_files = pr.get_changed_files() approved_by = set(pr.get_approved_by()) rules = read_merge_rules(repo) reject_reason = f"PR {pr.pr_num} does not match merge rules" # Used to determine best rejection reason # Score 0 to 10K - how many files rule matched # Score 10K - matched all files, but no overlapping approvers # Score 20K - matched all files and approvers, but lacks mandatory checks reject_reason_score = 0 for rule in rules: rule_name = rule.name rule_approvers_set = set() for approver in rule.approved_by: if "/" in approver: org, name = approver.split("/") rule_approvers_set.update(gh_get_team_members(org, name)) else: rule_approvers_set.add(approver) patterns_re = patterns_to_regex(rule.patterns) approvers_intersection = approved_by.intersection(rule_approvers_set) non_matching_files = [] for fname in changed_files: if not patterns_re.match(fname): non_matching_files.append(fname) if len(non_matching_files) > 0: num_matching_files = len(changed_files) - len(non_matching_files) if num_matching_files > reject_reason_score: reject_reason_score = num_matching_files reject_reason = (f"{num_matching_files} files matched rule {rule_name}, but there are still non-matching files: " + f"{','.join(non_matching_files[:5])}{', ...' if len(non_matching_files) > 5 else ''}") continue # If rule requires approvers but they aren't the ones that reviewed PR if len(approvers_intersection) == 0 and len(rule_approvers_set) > 0: if reject_reason_score < 10000: reject_reason_score = 10000 reject_reason = (f"Matched rule {rule_name}, but it was not reviewed yet by any of:" + f"{','.join(list(rule_approvers_set)[:5])}{', ...' if len(rule_approvers_set) > 5 else ''}") continue if rule.mandatory_checks_name is not None: pass_checks = True checks = pr.get_checkrun_conclusions() # HACK: We don't want to skip CLA check, even when forced for checkname in filter(lambda x: force is False or "CLA Check" in x, rule.mandatory_checks_name): if checkname not in checks or checks[checkname] != "SUCCESS": if reject_reason_score < 20000: reject_reason_score = 20000 reject_reason = f"Refusing to merge as mandatory check {checkname} " reject_reason += "has not been run" if checkname not in checks else "failed" reject_reason += f" for rule {rule_name}" pass_checks = False if not pass_checks: continue if pr.has_internal_changes(): raise RuntimeError("This PR has internal changes and must be landed via Phabricator") return rule raise RuntimeError(reject_reason)
def test_double_asterisks(self) -> None: allowed_patterns = [ "aten/src/ATen/native/**LinearAlgebra*", ] patterns_re = patterns_to_regex(allowed_patterns) fnames = [ "aten/src/ATen/native/LinearAlgebra.cpp", "aten/src/ATen/native/cpu/LinearAlgebraKernel.cpp" ] for filename in fnames: self.assertTrue(patterns_re.match(filename))
def find_matching_merge_rule(pr: GitHubPR, repo: GitRepo) -> MergeRule: """Returns merge rule matching to this pr or raises an exception""" changed_files = pr.get_changed_files() approved_by = set(pr.get_approved_by()) rules = read_merge_rules(repo) for rule in rules: rule_name = rule.name rule_approvers_set = set(rule.approved_by) patterns_re = patterns_to_regex(rule.patterns) approvers_intersection = approved_by.intersection(rule_approvers_set) # If rule requires approvers but they aren't the ones that reviewed PR if len(approvers_intersection) == 0 and len(rule_approvers_set) > 0: print(f"Skipping rule {rule_name} due to no approvers overlap") continue if rule.mandatory_checks_name is not None: pass_checks = True checks = pr.get_checkrun_conclusions() for checkname in rule.mandatory_checks_name: if checkname not in checks or checks[checkname] != "SUCCESS": if checkname not in checks: print( f"Skipping rule {rule_name} as mandatory check {checkname} is not in {checks.keys()}" ) else: print( f"Skipping rule {rule_name} as mandatory check {checkname} failed" ) pass_checks = False if not pass_checks: continue non_matching_files = [] for fname in changed_files: if not patterns_re.match(fname): non_matching_files.append(fname) if len(non_matching_files) > 0: print( f"Skipping rule {rule_name} due to non-matching files: {non_matching_files}" ) continue print(f"Matched rule {rule_name} for {pr.pr_num}") if pr.has_internal_changes(): raise RuntimeError( "This PR has internal changes and must be landed via Phabricator" ) return rule raise RuntimeError(f"PR {pr.pr_num} does not match merge rules")
def find_matching_merge_rule(pr: GitHubPR, repo: Optional[GitRepo] = None, force: bool = False, skip_internal_checks: bool = False ) -> MergeRule: """Returns merge rule matching to this pr or raises an exception""" changed_files = pr.get_changed_files() approved_by = set(pr.get_approved_by()) rules = read_merge_rules(repo, pr.org, pr.project) reject_reason = f"PR {pr.pr_num} does not match merge rules" # Used to determine best rejection reason # Score 0 to 10K - how many files rule matched # Score 10K - matched all files, but no overlapping approvers # Score 20K - matched all files and approvers, but mandatory checks are pending # Score 30k - Matched all files and approvers, but mandatory checks failed reject_reason_score = 0 for rule in rules: rule_name = rule.name patterns_re = patterns_to_regex(rule.patterns) non_matching_files = [] for fname in changed_files: if not patterns_re.match(fname): non_matching_files.append(fname) if len(non_matching_files) > 0: num_matching_files = len(changed_files) - len(non_matching_files) if num_matching_files > reject_reason_score: reject_reason_score = num_matching_files reject_reason = (f"{num_matching_files} files matched rule {rule_name}, but there are still non-matching files: " + f"{','.join(non_matching_files[:5])}{', ...' if len(non_matching_files) > 5 else ''}") continue # If rule needs approvers but PR has not been reviewed, skip it if len(rule.approved_by) > 0 and len(approved_by) == 0: if reject_reason_score < 10000: reject_reason_score = 10000 reject_reason = f"Matched rule {rule_name}, but PR #{pr.pr_num} has not been reviewed yet" continue rule_approvers_set = set() for approver in rule.approved_by: if "/" in approver: org, name = approver.split("/") rule_approvers_set.update(gh_get_team_members(org, name)) else: rule_approvers_set.add(approver) approvers_intersection = approved_by.intersection(rule_approvers_set) # If rule requires approvers but they aren't the ones that reviewed PR if len(approvers_intersection) == 0 and len(rule_approvers_set) > 0: if reject_reason_score < 10000: reject_reason_score = 10000 reject_reason = (f"Matched rule {rule_name}, but PR #{pr.pr_num} was not reviewed yet by any of: " + f"{', '.join(list(rule_approvers_set)[:5])}{', ...' if len(rule_approvers_set) > 5 else ''}") continue if rule.mandatory_checks_name is not None: pending_checks: List[Tuple[str, Optional[str]]] = [] failed_checks: List[Tuple[str, Optional[str]]] = [] checks = pr.get_checkrun_conclusions() # HACK: We don't want to skip CLA check, even when forced for checkname in filter(lambda x: force is False or "CLA Check" in x, rule.mandatory_checks_name): if checkname not in checks: pending_checks.append((checkname, None)) elif checks[checkname][0] is None: pending_checks.append((checkname, checks[checkname][1])) elif checks[checkname][0] != 'SUCCESS': failed_checks.append((checkname, checks[checkname][1])) def checks_to_str(checks: List[Tuple[str, Optional[str]]]) -> str: return ", ".join(f"[{c[0]}]({c[1]})" if c[1] is not None else c[0] for c in checks) if len(failed_checks) > 0: if reject_reason_score < 30000: reject_reason_score = 30000 reject_reason = ("Refusing to merge as mandatory check(s) " + checks_to_str(failed_checks) + f" failed for rule {rule_name}") continue elif len(pending_checks) > 0: if reject_reason_score < 20000: reject_reason_score = 20000 reject_reason = f"Refusing to merge as mandatory check(s) {checks_to_str(pending_checks)}" reject_reason += f" are pending/not yet run for rule {rule_name}" continue if not skip_internal_checks and pr.has_internal_changes(): raise RuntimeError("This PR has internal changes and must be landed via Phabricator") return rule if reject_reason_score == 20000: raise MandatoryChecksMissingError(reject_reason) raise RuntimeError(reject_reason)