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:
commit
3b4e698b2e
182
dist/tools/backport_pr/backport_pr.py
vendored
182
dist/tools/backport_pr/backport_pr.py
vendored
@ -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
12
dist/tools/backport_pr/setup.cfg
vendored
Normal 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
|
||||||
10
dist/tools/backport_pr/tox.ini
vendored
10
dist/tools/backport_pr/tox.ini
vendored
@ -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}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user