diff --git a/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker.py index 29f69af..ec45c5a 100644 --- a/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker.py @@ -5,16 +5,23 @@ @click.command() +@click.option('--dry-run', is_flag=True) +@click.option('--push', 'pr_remote', metavar='REMOTE', + help='git remote to use for PR branches', default='origin') @click.argument('commit_sha1', 'The commit sha1 to be cherry-picked') @click.argument('branches', 'The branches to backport to', nargs=-1) -def cherry_pick(commit_sha1, branches): +def cherry_pick(dry_run, pr_remote, commit_sha1, branches): if not os.path.exists('./pyconfig.h.in'): os.chdir('./cpython/') - upstream = get_git_upstream_remote() - username = get_forked_repo_name() + upstream = get_git_fetch_remote() + username = get_forked_repo_name(pr_remote) + + if dry_run: + click.echo("Dry run requested, listing expected command sequence") + click.echo("fetching upstream ...") - run_cmd(f"git fetch {upstream}") + run_cmd(f"git fetch {upstream}", dry_run=dry_run) if not branches: raise ValueError("at least one branch is required") @@ -23,53 +30,63 @@ def cherry_pick(commit_sha1, branches): click.echo(f"Now backporting '{commit_sha1}' into '{branch}'") # git checkout -b 61e2bc7-3.5 upstream/3.5 - cherry_pick_branch = f"{commit_sha1[:7]}-{branch}" + cherry_pick_branch = f"backport-{commit_sha1[:7]}-{branch}" cmd = f"git checkout -b {cherry_pick_branch} {upstream}/{branch}" - run_cmd(cmd) + run_cmd(cmd, dry_run=dry_run) cmd = f"git cherry-pick -x {commit_sha1}" - if run_cmd(cmd): - cmd = f"git push origin {cherry_pick_branch}" - if not run_cmd(cmd): - click.echo(f"Failed to push to origin :(") + if run_cmd(cmd, dry_run=dry_run): + cmd = f"git push {pr_remote} {cherry_pick_branch}" + if not run_cmd(cmd, dry_run=dry_run): + click.echo(f"Failed to push to {pr_remote} :(") else: - open_pr(username, branch, cherry_pick_branch) + open_pr(username, branch, cherry_pick_branch, dry_run=dry_run) else: click.echo(f"Failed to cherry-pick {commit_sha1} into {branch} :(") cmd = "git checkout master" - run_cmd(cmd) + run_cmd(cmd, dry_run=dry_run) cmd = f"git branch -D {cherry_pick_branch}" - if run_cmd(cmd): - click.echo(f"branch {cherry_pick_branch} has been deleted.") + if run_cmd(cmd, dry_run=dry_run): + if not dry_run: + click.echo(f"branch {cherry_pick_branch} has been deleted.") else: click.echo(f"branch {cherry_pick_branch} NOT deleted.") -def get_git_upstream_remote(): +def get_git_fetch_remote(): """Get the remote name to use for upstream branches Uses "upstream" if it exists, "origin" otherwise """ cmd = "git remote get-url upstream" - if run_cmd(cmd): - return "upstream" - else: + try: + subprocess.check_output(cmd.split(), stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: return "origin" + return "upstream" -def get_forked_repo_name(): +def get_forked_repo_name(pr_remote): """ Return 'myusername' out of https://github.com/myusername/cpython :return: """ - cmd = "git config --get remote.origin.url" - result = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT).decode('utf-8') - username = result[len("https://github.com/"):result.index('/cpython.git')] + cmd = f"git config --get remote.{pr_remote}.url" + raw_result = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) + result = raw_result.decode('utf-8') + username_end = result.index('/cpython.git') + if result.startswith("https"): + username = result[len("https://github.com/"):username_end] + else: + username = result[len("git@github.com:"):username_end] return username -def run_cmd(cmd): +def run_cmd(cmd, *, dry_run=False): + if dry_run: + click.echo(f" dry-run: {cmd}") + return True try: subprocess.check_output(cmd.split()) except subprocess.CalledProcessError: @@ -77,11 +94,14 @@ def run_cmd(cmd): return True -def open_pr(forked_repo, base_branch, cherry_pick_branch): +def open_pr(forked_repo, base_branch, cherry_pick_branch, *, dry_run=False): """ construct the url for pull request and open it in the web browser """ url = f"https://github.com/python/cpython/compare/{base_branch}...{forked_repo}:{cherry_pick_branch}?expand=1" + if dry_run: + click.echo(f" dry-run: Create new PR: {url}") + return webbrowser.open_new_tab(url) diff --git a/cherry_picker/readme.rst b/cherry_picker/readme.rst index f181622..9e1368c 100644 --- a/cherry_picker/readme.rst +++ b/cherry_picker/readme.rst @@ -1,6 +1,6 @@ Usage:: - python -m cherry_picker + python -m cherry_picker [--push REMOTE] [--dry-run] @@ -34,6 +34,14 @@ Requires Python 3.6. (venv) $ git remote add upstream https://github.com/python/cpython.git (venv) $ cd ../ +The cherry picking script assumes that if an `upstream` remote is defined, then +it should be used as the source of upstream changes and as the base for +cherry-pick branches. Otherwise, `origin` is used for that purpose. + +By default, the PR branches used to submit pull requests back to the main +repository are pushed to `origin`. If this is incorrect, then the correct +remote will need be specified using the ``--push`` option (e.g. +``--push pr`` to use a remote named ``pr``). Cherry-picking :snake: :cherries: :pick: ============== @@ -62,20 +70,41 @@ What this will do: (venv) $ git fetch upstream - (venv) $ git checkout -b 6de2b78-3.5 upstream/3.5 + (venv) $ git checkout -b backport-6de2b78-3.5 upstream/3.5 (venv) $ git cherry-pick -x 6de2b7817f-some-commit-sha1-d064 - (venv) $ git push origin 6de2b78-3.5 + (venv) $ git push origin backport-6de2b78-3.5 (venv) $ git checkout master - (venv) $ git branch -D 6de2b78-3.5 + (venv) $ git branch -D backport-6de2b78-3.5 - (venv) $ git checkout -b 6de2b78-3.6 upstream/3.6 + (venv) $ git checkout -b backport-6de2b78-3.6 upstream/3.6 (venv) $ git cherry-pick -x 6de2b7817f-some-commit-sha1-d064 - (venv) $ git push origin 6de2b78-3.6 + (venv) $ git push origin backport-6de2b78-3.6 (venv) $ git checkout master - (venv) $ git branch -D 6de2b78-3.6 + (venv) $ git branch -D backport-6de2b78-3.6 In case of merge conflicts or errors, then... the script will fail :stuck_out_tongue: +Passing the `--dry-run` option will cause the script to print out all the +steps it would execute without actually executing any of them. For example:: + + $ python -m cherry_picker --dry-run --push pr 1e32a1be4a1705e34011770026cb64ada2d340b5 3.6 3.5 + Dry run requested, listing expected command sequence + fetching upstream ... + dry_run: git fetch origin + Now backporting '1e32a1be4a1705e34011770026cb64ada2d340b5' into '3.6' + dry_run: git checkout -b backport-1e32a1b-3.6 origin/3.6 + dry_run: git cherry-pick -x 1e32a1be4a1705e34011770026cb64ada2d340b5 + dry_run: git push pr backport-1e32a1b-3.6 + dry_run: Create new PR: https://github.com/python/cpython/compare/3.6...ncoghlan:backport-1e32a1b-3.6?expand=1 + dry_run: git checkout master + dry_run: git branch -D backport-1e32a1b-3.6 + Now backporting '1e32a1be4a1705e34011770026cb64ada2d340b5' into '3.5' + dry_run: git checkout -b backport-1e32a1b-3.5 origin/3.5 + dry_run: git cherry-pick -x 1e32a1be4a1705e34011770026cb64ada2d340b5 + dry_run: git push pr backport-1e32a1b-3.5 + dry_run: Create new PR: https://github.com/python/cpython/compare/3.5...ncoghlan:backport-1e32a1b-3.5?expand=1 + dry_run: git checkout master + dry_run: git branch -D backport-1e32a1b-3.5 Creating Pull Requests ====================== @@ -85,8 +114,7 @@ tab that points to the pull request creation page. The url of the pull request page looks similar to the following:: - https://github.com/python/cpython/compare/3.5...:6de2b78-3.5?expand=1 - + https://github.com/python/cpython/compare/3.5...:backport-6de2b78-3.5?expand=1 1. Prefix the pull request description with the branch ``[X.Y]``, e.g.::