def execute_review_protection( change: Change[str], branch: Branch, existing_protection: Optional[BranchProtection], review_count: int) -> Change[str]: try: if branch.protected and existing_protection and existing_protection.required_pull_request_reviews: if review_count > 0: print_debug( "Replacing review protection on branch %s (%s reviews)" % (highlight(branch.name), str(review_count))) branch.edit_required_pull_request_reviews( required_approving_review_count=review_count) else: print_debug("Removing review protection on branch: %s" % highlight(branch.name)) branch.remove_required_pull_request_reviews() elif review_count > 0: print_debug( "Adding review protection on branch: %s (%s reviews)" % (highlight(branch.name), str(review_count))) safe_branch_edit_protection( branch, required_approving_review_count=review_count) except GithubException as e: print_error("Can't set review protection on branch %s to %s: %s" % (highlight(branch.name), str(review_count), str(e))) return change.failure() return change.success()
def execute_dismiss_reviews( change: Change[str], branch: Branch, required_reviews: Optional[RequiredPullRequestReviews], dismiss_approvals: bool) -> Change[str]: try: if branch.protected and required_reviews: print_debug( "Setting already protected branch %s to %s stale reviews" % (highlight(branch.name), highlight("dismiss" if dismiss_approvals else "allow"))) branch.edit_required_pull_request_reviews( dismiss_stale_reviews=dismiss_approvals) else: print_debug( "Changing branch %s to %s stale reviews" % (highlight(branch.name), highlight("dismiss" if dismiss_approvals else "allow"))) safe_branch_edit_protection( branch, dismiss_stale_reviews=dismiss_approvals) except GithubException as e: print_error( "Can't set review dismissal on branch %s to %s: %s" % (highlight(branch.name), str(dismiss_approvals), str(e))) return change.failure() return change.success()
def safe_branch_edit_protection( branch: Branch, strict: _GithubOptional[bool] = NotSet, contexts: _GithubOptional[List[str]] = NotSet, enforce_admins: _GithubOptional[bool] = NotSet, dismissal_users: _GithubOptional[List[str]] = NotSet, dismissal_teams: _GithubOptional[List[str]] = NotSet, dismiss_stale_reviews: _GithubOptional[bool] = NotSet, require_code_owner_reviews: _GithubOptional[bool] = NotSet, required_approving_review_count: _GithubOptional[int] = NotSet, user_push_restrictions: _GithubOptional[List[str]] = NotSet, team_push_restrictions: _GithubOptional[List[str]] = NotSet) -> None: try: prot = branch.get_protection() except GithubException as e: prot = None rsc = prot.required_status_checks if prot else None # type: RequiredStatusChecks rpr = prot.required_pull_request_reviews if prot else None # type: RequiredPullRequestReviews protupr = prot.get_user_push_restrictions() if prot else None if protupr is None: upr = NotSet else: upr = [u.login for u in protupr] prottpr = prot.get_team_push_restrictions() if prot else None if prottpr is None: tpr = NotSet else: tpr = [t.name for t in prottpr] kw = { 'strict': strict if strict != NotSet else (rsc.strict if rsc else NotSet), 'contexts': contexts if contexts != NotSet else (rsc.contexts if rsc else NotSet), 'enforce_admins': enforce_admins if enforce_admins != NotSet else (prot.enforce_admins if prot else NotSet), 'dismissal_users': dismissal_users if dismissal_users != NotSet else [], 'dismissal_teams': dismissal_teams if dismissal_teams != NotSet else [], 'dismiss_stale_reviews': dismiss_stale_reviews if dismiss_stale_reviews != NotSet else (rpr.dismiss_stale_reviews if rpr is not None else NotSet), 'require_code_owner_reviews': require_code_owner_reviews if require_code_owner_reviews != NotSet else (rpr.require_code_owner_reviews if rpr is not None else NotSet), 'required_approving_review_count': required_approving_review_count if required_approving_review_count != NotSet else (rpr.required_approving_review_count if rpr is not None else NotSet), 'user_push_restrictions': user_push_restrictions if user_push_restrictions != NotSet else upr, 'team_push_restrictions': team_push_restrictions if team_push_restrictions != NotSet else tpr, } branch.edit_protection(**kw)
def execute_remove_all_status_checks( change: Change[str], branch: Branch, existing_checks: Set[str]) -> Change[str]: print_debug("Removing all status checks from branch %s" % highlight(branch.name)) try: if existing_checks: branch.remove_required_status_checks() except GithubException as e: print_error(str(e)) return change.failure() else: return change.success()
def _set_dismiss_stale_approvals(branch: Branch, dismiss_approvals: bool = True ) -> List[Change[str]]: def execute_dismiss_reviews( change: Change[str], branch: Branch, required_reviews: Optional[RequiredPullRequestReviews], dismiss_approvals: bool) -> Change[str]: try: if branch.protected and required_reviews: print_debug( "Setting already protected branch %s to %s stale reviews" % (highlight(branch.name), highlight("dismiss" if dismiss_approvals else "allow"))) branch.edit_required_pull_request_reviews( dismiss_stale_reviews=dismiss_approvals) else: print_debug( "Changing branch %s to %s stale reviews" % (highlight(branch.name), highlight("dismiss" if dismiss_approvals else "allow"))) safe_branch_edit_protection( branch, dismiss_stale_reviews=dismiss_approvals) except GithubException as e: print_error( "Can't set review dismissal on branch %s to %s: %s" % (highlight(branch.name), str(dismiss_approvals), str(e))) return change.failure() return change.success() change_needed = False rpr = None # type: Optional[RequiredPullRequestReviews] if branch.protected: prot = branch.get_protection() rpr = prot.required_pull_request_reviews if rpr.dismiss_stale_reviews == dismiss_approvals: print_debug( "Branch %s already %s stale reviews" % (highlight(branch.name), highlight("dismisses" if dismiss_approvals else "allows"))) change_needed = False else: change_needed = True else: change_needed = True if change_needed: change = Change(meta=ChangeMetadata( executor=execute_dismiss_reviews, params=[branch, rpr, dismiss_approvals]), action=ChangeActions.REPLACE if branch.protected else ChangeActions.ADD, before="%s stale reviews" % ("Allow" if dismiss_approvals else "Dismiss"), after="%s stale reviews" % ("Dismiss" if dismiss_approvals else "Allow"), cosmetic_prefix="Protect branch<%s>" % branch.name) return [change] return []
def execute_test_protection(change: Change[str], branch: Branch, existing_checks: Set[str], known_checks: Set[str]) -> Change[str]: print_debug("[%s] Changing status checks on branch '%s' to [%s]" % (highlight(repo.name), highlight(branch.name), highlight(", ".join(list(known_checks))))) try: if existing_checks: branch.edit_required_status_checks(strict=True, contexts=list(known_checks)) else: safe_branch_edit_protection( branch, strict=True, contexts=list(known_checks), ) except GithubException as e: print_error( "Can't edit required status checks on repo %s branch %s: %s" % (repo.name, branch.name, str(e))) return change.failure() return change.success()
def create_plans( stash_api, github_api, stash_projects, github_organizations, branch_text, *, exactly_branch_name=False, assure_has_prs=True, ): """ Go over all the branches in all Stash and GitHub repositories searching for branches and PRs that match the given branch text. :rtype: List[MergePlan] """ # Plans for Stash repos: stash_repos = [] for project in stash_projects: stash_repos += stash_api.fetch_repos(project) plans = [] has_prs = False for repo in stash_repos: slug = repo["slug"] project = repo["project"]["key"] branches = list( Branch(b["id"], b["displayId"], b["latestCommit"]) for b in stash_api.fetch_branches(project, slug, branch_text) ) if exactly_branch_name: branches = [ branch for branch in branches if branch.display_id == branch_text ] if branches: plan = MergePlan(project, slug) plans.append(plan) plan.branches = branches branch_ids = [x.branch_id for x in plan.branches] prs = list(stash_api.fetch_pull_requests(project, slug)) for pr in prs: if pr["fromRef"]["id"] in branch_ids: has_prs = True plan.pull_requests.append(pr) if plan.pull_requests: plan.to_branch = plan.pull_requests[0]["toRef"]["id"] # Plans for Github repos: github_branch_text = branch_text if len(plans) > 0 and len(plans[0].branches) > 0: # if we already found the branch name on Stash, we can use its name here github_branch_text = plans[0].branches[0].display_id organization_to_repos = defaultdict(list) for organization in github_organizations: organization_to_repos[organization] += github_api.fetch_repos(organization) futures = dict() for organization in organization_to_repos.keys(): with ThreadPoolExecutor(max_workers=16) as executor: for repo in organization_to_repos[organization]: repo_name = repo.name f = executor.submit( github_api.fetch_branches, organization, repo_name, branch_name=github_branch_text, ) futures[f] = repo_name for f in as_completed(futures.keys()): repo_name = futures[f] branches = f.result() if not branches: continue plan = MergePlan(organization, repo_name, comes_from_github=True) plans.append(plan) # b.name is passed twice because github uses the same `id` and `display_id` plan.branches = [Branch(b.name, b.name, b.commit.sha) for b in branches] prs = github_api.fetch_pull_requests(organization, repo_name) for pr in prs: if pr.head.ref == github_branch_text: has_prs = True plan.pull_requests.append(pr) if plan.pull_requests: plan.to_branch = "refs/heads/{}".format( plan.pull_requests[0].base.ref ) if not plans: raise CheckError( 'Could not find any branch with text `"{}"` in any repositories of Stash projects: {} nor ' "Github organizations: {}.".format( branch_text, ", ".join("`{}`".format(x) for x in stash_projects), ", ".join("`{}`".format(x) for x in github_organizations), ) ) if assure_has_prs and not has_prs: raise CheckError('No PRs are open with text `"{}"`'.format(branch_text)) return plans
def _protect_branch(branch: Branch, required_review_count: int) -> List[Change[str]]: def execute_review_protection( change: Change[str], branch: Branch, existing_protection: Optional[BranchProtection], review_count: int) -> Change[str]: try: if branch.protected and existing_protection and existing_protection.required_pull_request_reviews: if review_count > 0: print_debug( "Replacing review protection on branch %s (%s reviews)" % (highlight(branch.name), str(review_count))) branch.edit_required_pull_request_reviews( required_approving_review_count=review_count) else: print_debug("Removing review protection on branch: %s" % highlight(branch.name)) branch.remove_required_pull_request_reviews() elif review_count > 0: print_debug( "Adding review protection on branch: %s (%s reviews)" % (highlight(branch.name), str(review_count))) safe_branch_edit_protection( branch, required_approving_review_count=review_count) except GithubException as e: print_error("Can't set review protection on branch %s to %s: %s" % (highlight(branch.name), str(review_count), str(e))) return change.failure() return change.success() change_needed = False prot = None current_reqcount = 0 # The Github API will gladly return a required review count > 0 for a branch that had a required review # count previously, but it has now been turned off. So we need to correlate a bunch of information to find # out whether the branch actually requires reviews or not. if branch.protected: prot = branch.get_protection() if prot and prot.required_pull_request_reviews: rpr = prot.required_pull_request_reviews # type: RequiredPullRequestReviews if rpr.required_approving_review_count == required_review_count: print_debug( "Branch %s already requires %s reviews" % (highlight( branch.name), highlight(str(required_review_count)))) change_needed = False else: current_reqcount = rpr.required_approving_review_count change_needed = True else: if required_review_count == 0 and ( prot is None or prot.required_pull_request_reviews is None): print_debug( "Branch %s required no review and requested count is %s" % (highlight(branch.name), highlight("zero"))) change_needed = False else: change_needed = True else: change_needed = True if change_needed: change = Change( meta=ChangeMetadata(executor=execute_review_protection, params=[branch, prot, required_review_count]), action=ChangeActions.REPLACE if branch.protected else ChangeActions.ADD, before="Require %s reviews" % current_reqcount if branch.protected else "No protection", after="Require %s reviews" % required_review_count, cosmetic_prefix="Protect branch<%s>:" % branch.name) return [change] return []