Merge pull request #16907 from aabadie/pr/tools/backport_pr_with_black

tools/backport_pr: check code format with black
This commit is contained in:
Alexandre Abadie 2021-10-22 12:58:01 +02:00 committed by GitHub
commit 3b4e698b2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 127 additions and 77 deletions

View File

@ -28,10 +28,10 @@ WORKTREE_SUBDIR = "backport_temp"
RELEASE_PREFIX = "" RELEASE_PREFIX = ""
RELEASE_SUFFIX = "-branch" RELEASE_SUFFIX = "-branch"
LABELS_REMOVE = ['Process: needs backport', 'Reviewed: '] LABELS_REMOVE = ["Process: needs backport", "Reviewed: "]
LABELS_ADD = ['Process: release backport'] LABELS_ADD = ["Process: release backport"]
BACKPORT_BRANCH = 'backport/{release}/{origbranch}' BACKPORT_BRANCH = "backport/{release}/{origbranch}"
def _get_labels(pull_request): def _get_labels(pull_request):
@ -46,22 +46,22 @@ def _get_labels(pull_request):
>>> _get_labels({'labels': [{'name': 'Process: needs backport'}]}) >>> _get_labels({'labels': [{'name': 'Process: needs backport'}]})
['Process: release backport'] ['Process: release backport']
""" """
labels = set(label['name'] for label in pull_request['labels'] labels = set(
if all(not label['name'].startswith(remove) label["name"]
for remove in LABELS_REMOVE)) for label in pull_request["labels"]
if all(not label["name"].startswith(remove) for remove in LABELS_REMOVE)
)
labels.update(LABELS_ADD) labels.update(LABELS_ADD)
return sorted(list(labels)) return sorted(list(labels))
def _branch_name_strip(branch_name, prefix=RELEASE_PREFIX, def _branch_name_strip(branch_name, prefix=RELEASE_PREFIX, suffix=RELEASE_SUFFIX):
suffix=RELEASE_SUFFIX):
"""Strip suffix and prefix. """Strip suffix and prefix.
>>> _branch_name_strip('2018.10-branch') >>> _branch_name_strip('2018.10-branch')
'2018.10' '2018.10'
""" """
if (branch_name.startswith(prefix) and if branch_name.startswith(prefix) and branch_name.endswith(suffix):
branch_name.endswith(suffix)):
if prefix: if prefix:
branch_name = branch_name.split(prefix, maxsplit=1)[0] branch_name = branch_name.split(prefix, maxsplit=1)[0]
if suffix: if suffix:
@ -83,29 +83,29 @@ def _get_latest_release(branches):
('2020.04', '2020.04-branch') ('2020.04', '2020.04-branch')
""" """
version_latest = 0 version_latest = 0
release_fullname = '' release_fullname = ""
release_short = '' release_short = ""
for branch in branches: for branch in branches:
branch_name = _branch_name_strip(branch['name']) branch_name = _branch_name_strip(branch["name"])
branch_num = 0 branch_num = 0
try: try:
branch_num = int(''.join(branch_name.split('.'))) branch_num = int("".join(branch_name.split(".")))
except ValueError: except ValueError:
pass pass
if branch_num > version_latest: if branch_num > version_latest:
version_latest = branch_num version_latest = branch_num
release_short = branch_name release_short = branch_name
release_fullname = branch['name'] release_fullname = branch["name"]
return (release_short, release_fullname) return (release_short, release_fullname)
def _find_remote(repo, user, repo_name): def _find_remote(repo, user, repo_name):
for remote in repo.remotes: for remote in repo.remotes:
if (remote.url.endswith(f"{user}/{repo_name}.git") or if remote.url.endswith(f"{user}/{repo_name}.git") or remote.url.endswith(
remote.url.endswith(f"{user}/{repo_name}")): f"{user}/{repo_name}"
):
return remote return remote
raise ValueError("Could not find remote with URL ending in " raise ValueError(f"Could not find remote with URL ending in {user}/{repo_name}.git")
f"{user}/{repo_name}.git")
def _get_upstream(repo): def _get_upstream(repo):
@ -114,34 +114,55 @@ def _get_upstream(repo):
def _delete_worktree(repo, workdir): def _delete_worktree(repo, workdir):
shutil.rmtree(workdir) shutil.rmtree(workdir)
repo.git.worktree('prune') repo.git.worktree("prune")
def main(): def main():
# pylint:disable=too-many-locals,too-many-branches,too-many-statements # pylint:disable=too-many-locals,too-many-branches,too-many-statements
"""Main function of this script.""" """Main function of this script."""
keyfile = os.path.join(os.environ['HOME'], GITHUBTOKEN_FILE) keyfile = os.path.join(os.environ["HOME"], GITHUBTOKEN_FILE)
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-k", "--keyfile", type=argparse.FileType('r'), parser.add_argument(
default=keyfile, "-k",
help="File containing github token") "--keyfile",
parser.add_argument("-c", "--comment", action="store_true", type=argparse.FileType("r"),
help="Put a comment with a reference under" default=keyfile,
"the original PR") help="File containing github token",
parser.add_argument("-n", "--noop", action="store_true", )
help="Limited noop mode, creates branch, but doesn't" parser.add_argument(
"push and create the PR") "-c",
parser.add_argument("-r", "--release-branch", type=str, "--comment",
help="Base the backport on this branch, " action="store_true",
"default is the latest") help="Put a comment with a reference under" "the original PR",
parser.add_argument("--backport-branch-fmt", type=str, )
default=BACKPORT_BRANCH, parser.add_argument(
help="Backport branch format. " "-n",
"Fields '{release}' and '{origbranch} will be " "--noop",
"replaced by the release name and remote branch " action="store_true",
"name.") help="Limited noop mode, creates branch, but doesn't" "push and create the PR",
parser.add_argument('-d', '--gitdir', type=str, default=os.getcwd(), )
help="Base git repo to work from") parser.add_argument(
"-r",
"--release-branch",
type=str,
help="Base the backport on this branch, " "default is the latest",
)
parser.add_argument(
"--backport-branch-fmt",
type=str,
default=BACKPORT_BRANCH,
help="Backport branch format. "
"Fields '{release}' and '{origbranch} will be "
"replaced by the release name and remote branch "
"name.",
)
parser.add_argument(
"-d",
"--gitdir",
type=str,
default=os.getcwd(),
help="Base git repo to work from",
)
parser.add_argument("PR", type=int, help="Pull request number to backport") parser.add_argument("PR", type=int, help="Pull request number to backport")
args = parser.parse_args() args = parser.parse_args()
@ -157,25 +178,27 @@ def main():
response_headers = dict(github_api.getheaders()) response_headers = dict(github_api.getheaders())
# agithub documentation says it's lower case header field-names but # agithub documentation says it's lower case header field-names but
# at this moment it's not # at this moment it's not
if 'X-OAuth-Scopes' in response_headers: if "X-OAuth-Scopes" in response_headers:
scopes = response_headers['X-OAuth-Scopes'] scopes = response_headers["X-OAuth-Scopes"]
else: else:
scopes = response_headers['x-oauth-scopes'] scopes = response_headers["x-oauth-scopes"]
scopes_list = [x.strip() for x in scopes.split(',')] scopes_list = [x.strip() for x in scopes.split(",")]
if not ('public_repo' in scopes_list or 'repo' in scopes_list): if not ("public_repo" in scopes_list or "repo" in scopes_list):
print("missing public_repo scope from token settings." print(
" Please add it on the GitHub webinterface") "missing public_repo scope from token settings."
" Please add it on the GitHub webinterface"
)
sys.exit(1) sys.exit(1)
username = user['login'] username = user["login"]
status, pulldata = github_api.repos[ORG][REPO].pulls[args.PR].get() status, pulldata = github_api.repos[ORG][REPO].pulls[args.PR].get()
if status != 200: if status != 200:
print(f'Commit #{args.PR} not found: {pulldata["message"]}') print(f'Commit #{args.PR} not found: {pulldata["message"]}')
sys.exit(2) sys.exit(2)
if not pulldata['merged']: if not pulldata["merged"]:
print("Original PR not yet merged") print("Original PR not yet merged")
sys.exit(0) sys.exit(0)
print(f'Fetching for commit: #{args.PR}: {pulldata["title"]}') print(f'Fetching for commit: #{args.PR}: {pulldata["title"]}')
orig_branch = pulldata['head']['ref'] orig_branch = pulldata["head"]["ref"]
status, commits = github_api.repos[ORG][REPO].pulls[args.PR].commits.get() status, commits = github_api.repos[ORG][REPO].pulls[args.PR].commits.get()
if status != 200: if status != 200:
print(f'No commits found for #{args.PR}: {commits["message"]}') print(f'No commits found for #{args.PR}: {commits["message"]}')
@ -190,8 +213,10 @@ def main():
else: else:
status, branches = github_api.repos[ORG][REPO].branches.get() status, branches = github_api.repos[ORG][REPO].branches.get()
if status != 200: if status != 200:
print(f'Could not retrieve branches for {ORG}/{REPO}: ' print(
f'{branches["message"]}') f"Could not retrieve branches for {ORG}/{REPO}: "
f'{branches["message"]}'
)
sys.exit(4) sys.exit(4)
release_shortname, release_fullname = _get_latest_release(branches) release_shortname, release_fullname = _get_latest_release(branches)
if not release_fullname: if not release_fullname:
@ -209,23 +234,27 @@ def main():
upstream_remote.fetch() upstream_remote.fetch()
# Build topic branch in temp dir # Build topic branch in temp dir
new_branch = args.backport_branch_fmt.format(release=release_shortname, new_branch = args.backport_branch_fmt.format(
origbranch=orig_branch) release=release_shortname, origbranch=orig_branch
)
if new_branch in repo.branches: if new_branch in repo.branches:
print(f"ERROR: Branch {new_branch} already exists") print(f"ERROR: Branch {new_branch} already exists")
sys.exit(1) sys.exit(1)
worktree_dir = os.path.join(args.gitdir, WORKTREE_SUBDIR) worktree_dir = os.path.join(args.gitdir, WORKTREE_SUBDIR)
repo.git.worktree("add", "-b", repo.git.worktree(
new_branch, "add",
WORKTREE_SUBDIR, "-b",
f"{upstream_remote}/{release_fullname}") new_branch,
WORKTREE_SUBDIR,
f"{upstream_remote}/{release_fullname}",
)
# transform branch name into Head object for later configuring # transform branch name into Head object for later configuring
new_branch = repo.branches[new_branch] new_branch = repo.branches[new_branch]
try: try:
bp_repo = git.Repo(worktree_dir) bp_repo = git.Repo(worktree_dir)
# Apply commits # Apply commits
for commit in commits: for commit in commits:
bp_repo.git.cherry_pick('-x', commit['sha']) bp_repo.git.cherry_pick("-x", commit["sha"])
# Push to github # Push to github
origin = _find_remote(repo, username, REPO) origin = _find_remote(repo, username, REPO)
print(f"Pushing branch {new_branch} to {origin}") print(f"Pushing branch {new_branch} to {origin}")
@ -246,33 +275,36 @@ def main():
_delete_worktree(repo, worktree_dir) _delete_worktree(repo, worktree_dir)
labels = _get_labels(pulldata) labels = _get_labels(pulldata)
merger = pulldata['merged_by']['login'] merger = pulldata["merged_by"]["login"]
if not args.noop: if not args.noop:
# Open new PR on github # Open new PR on github
pull_request = { pull_request = {
'title': f'{pulldata["title"]} [backport {release_shortname}]', "title": f'{pulldata["title"]} [backport {release_shortname}]',
'head': f'{username}:{new_branch}', "head": f"{username}:{new_branch}",
'base': release_fullname, "base": release_fullname,
'body': f'# Backport of #{args.PR}\n\n{pulldata["body"]}', "body": f'# Backport of #{args.PR}\n\n{pulldata["body"]}',
'maintainer_can_modify': True, "maintainer_can_modify": True,
} }
status, new_pr = github_api.repos[ORG][REPO].pulls.post( status, new_pr = github_api.repos[ORG][REPO].pulls.post(body=pull_request)
body=pull_request)
if status != 201: if status != 201:
print(f'Error creating the new pr: "{new_pr["message"]}". ' print(
'Is "Public Repo" access enabled for the token?') f'Error creating the new pr: "{new_pr["message"]}". '
pr_number = new_pr['number'] 'Is "Public Repo" access enabled for the token?'
)
pr_number = new_pr["number"]
print(f"Create PR number #{pr_number} for backport") print(f"Create PR number #{pr_number} for backport")
github_api.repos[ORG][REPO].issues[pr_number].labels.post(body=labels) github_api.repos[ORG][REPO].issues[pr_number].labels.post(body=labels)
review_request = {"reviewers": [merger]} review_request = {"reviewers": [merger]}
github_api.repos[ORG][REPO].pulls[pr_number].\ github_api.repos[ORG][REPO].pulls[pr_number].requested_reviewers.post(
requested_reviewers.post(body=review_request) body=review_request
)
# Put commit under old PR # Put commit under old PR
if args.comment and not args.noop: if args.comment and not args.noop:
comment = {"body": f"Backport provided in #{pr_number}"} comment = {"body": f"Backport provided in #{pr_number}"}
status, res = github_api.repos[ORG][REPO].\ status, res = (
issues[args.PR].comments.post(body=comment) github_api.repos[ORG][REPO].issues[args.PR].comments.post(body=comment)
)
if status != 201: if status != 201:
print(f'Something went wrong adding the comment: {res["message"]}') print(f'Something went wrong adding the comment: {res["message"]}')
print(f"Added comment to #{args.PR}") print(f"Added comment to #{args.PR}")

12
dist/tools/backport_pr/setup.cfg vendored Normal file
View File

@ -0,0 +1,12 @@
# Use black compatible configuration for flake8 and pylint
# flake8: https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8
# pylint: https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#pylint
[flake8]
max-line-length = 88
extend-ignore = E203
[pylint]
max-line-length = 88
[pylint.messages_control]
disable = C0330, C0326

View File

@ -1,5 +1,5 @@
[tox] [tox]
envlist = test,lint,flake8 envlist = test,lint,flake8,black
skipsdist = True skipsdist = True
[testenv] [testenv]
@ -11,6 +11,7 @@ commands =
test: {[testenv:test]commands} test: {[testenv:test]commands}
lint: {[testenv:lint]commands} lint: {[testenv:lint]commands}
flake8: {[testenv:flake8]commands} flake8: {[testenv:flake8]commands}
black: {[testenv:black]commands}
[testenv:test] [testenv:test]
deps = deps =
@ -25,10 +26,15 @@ deps =
{[testenv]deps} {[testenv]deps}
commands = commands =
# Suppress warning about TODO in code # Suppress warning about TODO in code
pylint --disable=fixme {env:script} pylint --rcfile=setup.cfg --disable=fixme {env:script}
[testenv:flake8] [testenv:flake8]
deps = flake8 deps = flake8
commands = commands =
# main() is quite complex, provide enough margin # main() is quite complex, provide enough margin
flake8 --max-complexity=25 {env:script} flake8 --max-complexity=25 {env:script}
[testenv:black]
deps = black
commands =
black --check --diff {env:script}