class Backport: def __init__(self, token, owner, name, team): self._gh = RemoteRepo(token, owner=owner, name=name, team=team, max_page_size=30, min_page_size=7) self._token = token self.default_branch_name = self._gh.default_branch self.ssh_url = self._gh.ssh_url def getPullRequests(self, from_commit): return self._gh.get_pull_requests(from_commit) def getBranchesWithRelease(self): branches = set() for pull_request in self._gh.find_pull_requests("release"): branches.add(pull_request['headRefName']) return branches def execute(self, repo, upstream, until_commit, run_cherrypick): repo = LocalRepo(repo, upstream, self.default_branch_name) all_branches = repo.get_release_branches( ) # [(branch_name, base_commit)] release_branches = self.getBranchesWithRelease() branches = [] # iterate over all branches to preserve their precedence. for branch in all_branches: if branch[0] in release_branches: branches.append(branch) if not branches: logging.info('No release branches found!') return for branch in branches: logging.info('Found release branch: %s', branch[0]) if not until_commit: until_commit = branches[0][1] pull_requests = self.getPullRequests(until_commit) backport_map = {} RE_MUST_BACKPORT = re.compile(r'^v(\d+\.\d+)-must-backport$') RE_NO_BACKPORT = re.compile(r'^v(\d+\.\d+)-no-backport$') RE_BACKPORTED = re.compile(r'^v(\d+\.\d+)-backported$') # pull-requests are sorted by ancestry from the most recent. for pr in pull_requests: while repo.comparator(branches[-1][1]) >= repo.comparator( pr['mergeCommit']['oid']): logging.info( "PR #{} is already inside {}. Dropping this branch for further PRs" .format(pr['number'], branches[-1][0])) branches.pop() logging.info("Processing PR #{}".format(pr['number'])) assert len(branches) branch_set = set([branch[0] for branch in branches]) # First pass. Find all must-backports for label in pr['labels']['nodes']: if label['name'] == 'pr-bugfix' or label[ 'name'] == 'pr-must-backport': backport_map[pr['number']] = branch_set.copy() continue matched = RE_MUST_BACKPORT.match(label['name']) if matched: if pr['number'] not in backport_map: backport_map[pr['number']] = set() backport_map[pr['number']].add(matched.group(1)) # Second pass. Find all no-backports for label in pr['labels']['nodes']: if label['name'] == 'pr-no-backport' and pr[ 'number'] in backport_map: del backport_map[pr['number']] break matched_no_backport = RE_NO_BACKPORT.match(label['name']) matched_backported = RE_BACKPORTED.match(label['name']) if matched_no_backport and pr[ 'number'] in backport_map and matched_no_backport.group( 1) in backport_map[pr['number']]: backport_map[pr['number']].remove( matched_no_backport.group(1)) logging.info('\tskipping %s because of forced no-backport', matched_no_backport.group(1)) elif matched_backported and pr[ 'number'] in backport_map and matched_backported.group( 1) in backport_map[pr['number']]: backport_map[pr['number']].remove( matched_backported.group(1)) logging.info( '\tskipping %s because it\'s already backported manually', matched_backported.group(1)) for pr, branches in list(backport_map.items()): logging.info('PR #%s needs to be backported to:', pr) for branch in branches: logging.info('\t%s, and the status is: %s', branch, run_cherrypick(self._token, pr, branch)) # print API costs logging.info('\nGitHub API total costs per query:') for name, value in list(self._gh.api_costs.items()): logging.info('%s : %s', name, value)
class Backport: def __init__(self, token, owner, name, team): self._gh = RemoteRepo(token, owner=owner, name=name, team=team, max_page_size=30, min_page_size=7) self._token = token self.default_branch_name = self._gh.default_branch self.ssh_url = self._gh.ssh_url def getPullRequests(self, from_commit): return self._gh.get_pull_requests(from_commit) def execute(self, repo, til, number, run_cherrypick): repo = LocalRepo(repo, 'origin', self.default_branch_name) branches = repo.get_release_branches()[ -number:] # [(branch_name, base_commit)] if not branches: logging.info('No release branches found!') return for branch in branches: logging.info('Found release branch: %s', branch[0]) if not til: til = branches[0][1] prs = self.getPullRequests(til) backport_map = {} RE_MUST_BACKPORT = re.compile(r'^v(\d+\.\d+)-must-backport$') RE_NO_BACKPORT = re.compile(r'^v(\d+\.\d+)-no-backport$') RE_BACKPORTED = re.compile(r'^v(\d+\.\d+)-backported$') # pull-requests are sorted by ancestry from the least recent. for pr in prs: while repo.comparator(branches[-1][1]) >= repo.comparator( pr['mergeCommit']['oid']): logging.info( "PR #{} is already inside {}. Dropping this branch for further PRs" .format(pr['number'], branches[-1][0])) branches.pop() logging.info("Processing PR #{}".format(pr['number'])) assert len(branches) branch_set = set([branch[0] for branch in branches]) # First pass. Find all must-backports for label in pr['labels']['nodes']: if label['name'].startswith( 'pr-') and label['color'] == 'ff0000': backport_map[pr['number']] = branch_set.copy() continue m = RE_MUST_BACKPORT.match(label['name']) if m: if pr['number'] not in backport_map: backport_map[pr['number']] = set() backport_map[pr['number']].add(m.group(1)) # Second pass. Find all no-backports for label in pr['labels']['nodes']: if label['name'] == 'pr-no-backport' and pr[ 'number'] in backport_map: del backport_map[pr['number']] break m1 = RE_NO_BACKPORT.match(label['name']) m2 = RE_BACKPORTED.match(label['name']) if m1 and pr['number'] in backport_map and m1.group( 1) in backport_map[pr['number']]: backport_map[pr['number']].remove(m1.group(1)) logging.info('\tskipping %s because of forced no-backport', m1.group(1)) elif m2 and pr['number'] in backport_map and m2.group( 1) in backport_map[pr['number']]: backport_map[pr['number']].remove(m2.group(1)) logging.info( '\tskipping %s because it\'s already backported manually', m2.group(1)) for pr, branches in backport_map.items(): logging.info('PR #%s needs to be backported to:', pr) for branch in branches: logging.info('\t%s, and the status is: %s', branch, run_cherrypick(self._token, pr, branch)) # print API costs logging.info('\nGitHub API total costs per query:') for name, value in self._gh.api_costs.items(): logging.info('%s : %s', name, value)