Skip to content

Commit

Permalink
Add cherrypick_pr dev-tools helper.
Browse files Browse the repository at this point in the history
(cherry picked from commit 50affd5)
  • Loading branch information
blakerouse committed Jan 20, 2021
1 parent 308ddda commit 397ce3b
Showing 1 changed file with 209 additions and 0 deletions.
209 changes: 209 additions & 0 deletions dev-tools/cherrypick_pr
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
#!/usr/bin/env python3
"""Cherry pick and backport a PR"""
from __future__ import print_function

from builtins import input
import sys
import os
import argparse
from os.path import expanduser
import re
from subprocess import check_call, call, check_output
import requests

usage = """
Example usage:
./dev-tools/cherrypick_pr --create_pr 5.0 2565 6490604aa0cf7fa61932a90700e6ca988fc8a527
In case of backporting errors, fix them, then run:
git cherry-pick --continue
./dev-tools/cherrypick_pr --create_pr 5.0 2565 6490604aa0cf7fa61932a90700e6ca988fc8a527 --continue
This script does the following:
* cleanups both from_branch and to_branch (warning: drops local changes)
* creates a temporary branch named something like "branch_2565"
* calls the git cherry-pick command in this branch
* after fixing the merge errors (if needed), pushes the branch to your
remote
* if the --create_pr flag is used, it uses the GitHub API to create the PR
for you. Note that this requires you to have a Github token with the
public_repo scope in the `~/.elastic/github.token` file. This token
should be also authorized to Elastic organization so as to work with single-sign-on.
(see https://help.github.com/en/articles/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on)
Note that you need to take the commit hashes from `git log` on the
from_branch, copying the IDs from Github doesn't work in case we squashed the
PR.
"""


def main():
"""Main"""
parser = argparse.ArgumentParser(
description="Creates a PR for cherry-picking commits",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=usage)
parser.add_argument("to_branch",
help="To branch (e.g 7.x)")
parser.add_argument("pr_number",
help="The PR number being merged (e.g. 2345)")
parser.add_argument("commit_hashes", metavar="hash", nargs="+",
help="The commit hashes to cherry pick." +
" You can specify multiple.")
parser.add_argument("--yes", action="store_true",
help="Assume yes. Warning: discards local changes.")
parser.add_argument("--continue", action="store_true",
help="Continue after fixing merging errors.")
parser.add_argument("--from_branch", default="master",
help="From branch")
parser.add_argument("--create_pr", action="store_true",
help="Create a PR using the Github API " +
"(requires token in ~/.elastic/github.token)")
parser.add_argument("--diff", action="store_true",
help="Display the diff before pushing the PR")
parser.add_argument("--remote", default="",
help="Which remote to push the backport branch to")
parser.add_argument("--zube-team", default="",
help="Team the PR belongs to")
parser.add_argument("--keep-backport-label", action="store_true",
help="Preserve label needs_backport in original PR")
args = parser.parse_args()

print(args)

tmp_branch = "backport_{}_{}".format(args.pr_number, args.to_branch)

if not vars(args)["continue"]:
if not args.yes and input("This will destroy all local changes. " +
"Continue? [y/n]: ") != "y":
return 1
check_call("git reset --hard", shell=True)
check_call("git clean -df", shell=True)
check_call("git fetch", shell=True)

check_call("git checkout {}".format(args.from_branch), shell=True)
check_call("git pull", shell=True)

check_call("git checkout {}".format(args.to_branch), shell=True)
check_call("git pull", shell=True)

call("git branch -D {} > /dev/null".format(tmp_branch), shell=True)
check_call("git checkout -b {}".format(tmp_branch), shell=True)
if call("git cherry-pick -x {}".format(" ".join(args.commit_hashes)),
shell=True) != 0:
print("Looks like you have cherry-pick errors.")
print("Fix them, then run: ")
print(" git cherry-pick --continue")
print(" {} --continue".format(" ".join(sys.argv)))
return 1

if len(check_output("git status -s", shell=True).strip()) > 0:
print("Looks like you have uncommitted changes." +
" Please execute first: git cherry-pick --continue")
return 1

if len(check_output("git log HEAD...{}".format(args.to_branch),
shell=True).strip()) == 0:
print("No commit to push")
return 1

if args.diff:
call("git diff {}".format(args.to_branch), shell=True)
if input("Continue? [y/n]: ") != "y":
print("Aborting cherry-pick.")
return 1

print("Ready to push branch.")

remote = args.remote
if not remote:
remote = input("To which remote should I push? (your fork): ")

call("git push {} :{} > /dev/null".format(remote, tmp_branch),
shell=True)
check_call("git push --set-upstream {} {}"
.format(remote, tmp_branch), shell=True)
if not args.create_pr:
print("Done. Open PR by following this URL: \n\t" +
"https://github.com/elastic/fleet-server/compare/{}...{}:{}?expand=1"
.format(args.to_branch, remote, tmp_branch))
else:
token = open(expanduser("~/.elastic/github.token"), "r").read().strip()
base = "https://api.github.com/repos/elastic/fleet-server"
session = requests.Session()
session.headers.update({"Authorization": "token " + token})

original_pr = session.get(base + "/pulls/" + args.pr_number).json()

# get the github username from the remote where we pushed
remote_url = check_output("git remote get-url {}".format(remote),
shell=True)
remote_user = re.search("github.com[:/](.+)/fleet-server", str(remote_url)).group(1)

# create PR
request = session.post(base + "/pulls", json=dict(
title="Cherry-pick #{} to {}: {}".format(args.pr_number, args.to_branch, original_pr["title"]),
head=remote_user + ":" + tmp_branch,
base=args.to_branch,
body="Cherry-pick of PR #{} to {} branch. Original message: \n\n{}"
.format(args.pr_number, args.to_branch, original_pr["body"])
))
if request.status_code > 299:
print("Creating PR failed: {}".format(request.json()))
sys.exit(1)
new_pr = request.json()

# add labels
labels = ["backport"]

zube_teams = zube_team_labels(original_pr)
if args.zube_team:
resp = session.get(base + "/labels/Team:"+args.zube_team)
if resp.status_code != 200:
print("Cannot find team label", resp.text)
sys.exit(1)
zube_teams = ["Team:" + args.zube_team]

if len(zube_teams) > 0:
labels += zube_teams
labels.append("[zube]: In Review")
else:
labels.append("review")

session.post(
base + "/issues/{}/labels".format(new_pr["number"]), json=labels)

if not args.keep_backport_label:
# remove needs backport label from the original PR
session.delete(base + "/issues/{}/labels/needs_backport".format(args.pr_number))

# get version and set a version label on the original PR
version = get_version(os.getcwd())
if version:
session.post(
base + "/issues/{}/labels".format(args.pr_number), json=["v" + version])

print("\nDone. PR created: {}".format(new_pr["html_url"]))
print("Please go and check it and add the review tags")

def get_version(repo_dir):
pattern = re.compile(r'(const\s|)\w*(v|V)ersion\s=\s"(?P<version>.*)"')
with open(os.path.join(repo_dir, "main.go"), "r") as f:
for line in f:
match = pattern.match(line)
if match:
return match.group('version')

def zube_team_labels(pr):
teams = []
for label in pr.get('labels', []):
name = label.get('name', '')
if name.startswith('Team:'):
teams.append(name)
return teams

if __name__ == "__main__":
sys.exit(main())

0 comments on commit 397ce3b

Please sign in to comment.