Exemplo n.º 1
0
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")
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
 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))
Exemplo n.º 4
0
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")
Exemplo n.º 5
0
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)