#!/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/create local_branch This script does the following: * cleanups local_branch (warning: drops local changes) * rebases the branch against main * it will attempt to create a PR for you using the GitHub API, but requires the GitHub token, with the public_repo scope, available in `~/.elastic/github.token`. Keep in mind this token has to also be authorized to the Elastic organization as well as to work with SSO. (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 new PR from a branch", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=usage) parser.add_argument("local_branch", help="Branch to Create a PR for") parser.add_argument("--to_branch", default="main", help="Which remote to push the backport branch to") 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("--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") args = parser.parse_args() print(args) create_pr(args) def create_pr(args): if not vars(args)["continue"]: if not args.yes and input("This will destroy all local changes. " + "Continue? [y/n]: ") != "y": return 1 info("Destroying local changess...") check_call("git reset --hard", shell=True) check_call("git clean -df", shell=True) #check_call("git fetch", shell=True) info("Checkout of {} to create a PR....".format(args.local_branch)) check_call("git checkout {}".format(args.local_branch), shell=True) check_call("git rebase {}".format(args.to_branch), shell=True) if args.diff: call("git diff {}".format(args.to_branch), shell=True) if input("Continue? [y/n]: ") != "y": info("Aborting PR creation...") return 1 info("Ready to push branch and create PR...") remote = args.remote if not remote: remote = input("To which remote should I push? (your fork): ") info("Pushing branch {} to remote {}".format(args.local_branch, remote)) call("git push {} :{} > /dev/null".format(remote, args.local_branch), shell=True) check_call("git push --set-upstream {} {}" .format(remote, args.local_branch), shell=True) info("Checking if GitHub API token is available in `~/.elastic/github.token`") try: token = open(expanduser("~/.elastic/github.token"), "r").read().strip() except: token = False if not token: info("GitHub API token not available.\n" + "Manually create a PR by following this URL: \n\t" + "https://github.com/elastic/logstash/compare/{}...{}:{}?expand=1" .format(args.to_branch, remote, args.local_branch)) else: info("Automatically creating a PR for you...") base = "https://api.github.com/repos/elastic/logstash" session = requests.Session() session.headers.update({"Authorization": "token " + token}) # 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[:/](.+)/logstash", str(remote_url)).group(1) ### TODO: title = input("Title: ") body = input("Description: ") # create PR request = session.post(base + "/pulls", json=dict( title=title, head=remote_user + ":" + args.local_branch, base=args.to_branch, body=body )) if request.status_code > 299: info("Creating PR failed: {}".format(request.json())) sys.exit(1) new_pr = request.json() """ # add labels labels = ["backport"] # get the version we are backported to version = get_version(os.getcwd()) if version: labels.append("v" + version) session.post( base + "/issues/{}/labels".format(new_pr["number"]), json=labels) # Set a version label on the original PR if version: session.post( base + "/issues/{}/labels".format(args.pr_number), json=[version]) """ info("Done. PR created: {}".format(new_pr["html_url"])) info("Please go and check it and add the review tags") def get_version(base_dir): #pattern = re.compile(r'(const\s|)\w*(v|V)ersion\s=\s"(?P.*)"') with open(os.path.join(base_dir, "versions.yml"), "r") as f: for line in f: if line.startswith('logstash:'): return line.split(':')[-1].strip() #match = pattern.match(line) #if match: # return match.group('version') def info(msg): print("\nINFO: {}".format(msg)) if __name__ == "__main__": sys.exit(main())