From 98fa15124cbe845974a9a4afce190c56aa2f439f Mon Sep 17 00:00:00 2001 From: Patrick Donnelly Date: Fri, 22 Sep 2017 13:41:55 -0700 Subject: [PATCH] scripts: add ptl-tool for scripting merges Signed-off-by: Patrick Donnelly --- src/script/ptl-tool.py | 168 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/script/ptl-tool.py diff --git a/src/script/ptl-tool.py b/src/script/ptl-tool.py new file mode 100644 index 000000000000..1284956ef2d7 --- /dev/null +++ b/src/script/ptl-tool.py @@ -0,0 +1,168 @@ +# TODO +# Look for check failures? +# redmine issue update: http://www.redmine.org/projects/redmine/wiki/Rest_Issues + +import datetime +import getpass +import git +import json +import logging +import re +import requests +import sys +from os.path import expanduser + +log = logging.getLogger(__name__) +log.addHandler(logging.StreamHandler()) +log.setLevel(logging.INFO) + +BASE = "refs/remotes/upstream/heads/%s" +USER = getpass.getuser() +with open(expanduser("~/.github.key")) as f: + PASSWORD = f.read().strip() +BRANCH_PREFIX = "wip-%s-testing-" % USER +TESTING_LABEL = ["wip-%s-testing" % USER] + +SPECIAL_BRANCHES = ('master', 'luminous', 'jewel', 'HEAD') + +INDICATIONS = [ + re.compile("(Reviewed-by: .+ <[\w@.-]+>)", re.IGNORECASE), + re.compile("(Acked-by: .+ <[\w@.-]+>)", re.IGNORECASE), + re.compile("(Tested-by: .+ <[\w@.-]+>)", re.IGNORECASE), +] + +CONTRIBUTORS = {} +NEW_CONTRIBUTORS = {} +with open(".githubmap") as f: + comment = re.compile("\s*#") + patt = re.compile("([\w-]+)\s+(.*)") + for line in f: + if comment.match(line): + continue + m = patt.match(line) + CONTRIBUTORS[m.group(1)] = m.group(2) + +def build_branch(branch_name, pull_requests): + repo = git.Repo(".") + + repo.remotes.upstream.fetch() + + # First get the latest base branch from upstream + if branch_name == 'HEAD': + log.info("Branch base is HEAD; not checking out!") + else: + if branch_name in SPECIAL_BRANCHES: + base = BASE % branch_name + else: + base = BASE % "master" + log.info("Branch base on {}".format(base)) + base = filter(lambda r: r.path == base, repo.refs)[0] + + # So we know that we're not on an old test branch, detach HEAD onto ref: + base.checkout() + + for pr in pull_requests: + log.info("Merging PR {pr}".format(pr=pr)) + pr = int(pr) + r = filter(lambda r: r.path == "refs/remotes/upstream/pull/%d/head" % pr, repo.refs)[0] + + message = "Merge PR #%d into %s\n\n* %s:\n" % (pr, branch_name, r.path) + + for commit in repo.iter_commits(rev="HEAD.."+r.path): + message = message + ("\t%s\n" % commit.message.split('\n', 1)[0]) + + message = message + "\n" + + comments = requests.get("https://api.github.com/repos/ceph/ceph/issues/{pr}/comments".format(pr=pr), auth=(USER, PASSWORD)) + if comments.status_code != 200: + log.error("PR '{pr}' not found: {c}".format(pr=pr,c=comments)) + return + + reviews = requests.get("https://api.github.com/repos/ceph/ceph/pulls/{pr}/reviews".format(pr=pr), auth=(USER, PASSWORD)) + if reviews.status_code != 200: + log.error("PR '{pr}' not found: {c}".format(pr=pr,c=comments)) + return + + review_comments = requests.get("https://api.github.com/repos/ceph/ceph/pulls/{pr}/comments".format(pr=pr), auth=(USER, PASSWORD)) + if review_comments.status_code != 200: + log.error("PR '{pr}' not found: {c}".format(pr=pr,c=comments)) + return + + indications = set() + for comment in comments.json()+review_comments.json(): + for indication in INDICATIONS: + for cap in indication.findall(comment["body"]): + indications.add(cap) + + new_new_contributors = {} + for review in reviews.json(): + if review["state"] == "APPROVED": + user = review["user"]["login"] + try: + indications.add("Reviewed-by: "+CONTRIBUTORS[user]) + except KeyError as e: + try: + indications.add("Reviewed-by: "+NEW_CONTRIBUTORS[user]) + except KeyError as e: + try: + name = raw_input("Need name for contributor \"%s\"; Reviewed-by: " % user) + name = name.strip() + if len(name) == 0: + continue + NEW_CONTRIBUTORS[user] = name + new_new_contributors[user] = name + indications.add("Reviewed-by: "+name) + except EOFError as e: + continue + + for indication in indications: + message = message + indication + "\n" + + if new_new_contributors: + # Check out the PR, add a commit adding to .githubmap + HEAD = repo.head.commit + log.info("adding new contributors to githubmap on top of PR #%s" % pr) + r.checkout() + with open(".githubmap", "a") as f: + for c in new_new_contributors: + f.write("%s %s\n" % (c, new_new_contributors[c])) + repo.index.add([".githubmap"]) + repo.git.commit("-s", "-m", "githubmap: update contributors") + c = repo.head.commit + repo.head.reset(HEAD, index=True, working_tree=True) + else: + c = r.commit + + repo.git.merge(c.hexsha, '--no-ff', m=message) + + if branch_name not in SPECIAL_BRANCHES: + req = requests.post("https://api.github.com/repos/ceph/ceph/issues/{pr}/labels".format(pr=pr), data=json.dumps(TESTING_LABEL), auth=(USER, PASSWORD)) + if req.status_code != 200: + log.error("PR #%d could not be labeled %s: %s" % (pr, wip, req)) + return + + # If the branch is master, leave HEAD detached (but use "master" for commit message) + if branch_name not in SPECIAL_BRANCHES: + # Delete test branch if it already existed + try: + getattr(repo.branches, branch_name).delete( + repo, getattr(repo.branches, branch_name), force=True) + log.info("Deleted old test branch %s" % branch_name) + except AttributeError: + pass + + log.info("Creating branch {branch_name}".format(branch_name=branch_name)) + repo.create_head(branch_name) + # tag it for future reference. + name = "testing/%s" % branch_name + log.info("Creating tag %s" % name) + git.refs.tag.Tag.create(repo, name, force=True) + +if __name__ == "__main__": + if sys.argv[1] in SPECIAL_BRANCHES: + branch_name = sys.argv[1] + pull_requests = sys.argv[2:] + else: + branch_name = BRANCH_PREFIX + datetime.datetime.now().strftime("%Y%m%d") + pull_requests = sys.argv[1:] + build_branch(branch_name, pull_requests) -- 2.47.3