From ab5bb3046809a321563444f266a3fc58565101e5 Mon Sep 17 00:00:00 2001 From: Mariatta Wijaya Date: Sun, 27 Aug 2017 15:26:38 -0700 Subject: [PATCH 01/11] Automatically create backport PR if oauth token is set - Provide instructions on how to generate the personal access token, and how to set it as an environment variable. - Add gidgethub and requests as dependencies. --- cherry_picker/cherry_picker/cherry_picker.py | 45 ++++++++++++++++++-- cherry_picker/cherry_picker/test.py | 24 ++++++++++- cherry_picker/flit.ini | 2 + cherry_picker/readme.rst | 17 ++++++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/cherry_picker/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker/cherry_picker.py index 01efce3..34b8370 100755 --- a/cherry_picker/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker/cherry_picker.py @@ -6,6 +6,9 @@ import subprocess import webbrowser import sys +import requests + +from gidgethub.sansio import create_headers from . import __version__ @@ -139,9 +142,10 @@ def amend_commit_message(self, cherry_pick_branch): except subprocess.CalledProcessError as cpe: click.echo("Failed to amend the commit message \u2639") click.echo(cpe.output) + return updated_commit_message - def push_to_remote(self, base_branch, head_branch): + def push_to_remote(self, base_branch, head_branch, commit_message=""): """ git push """ cmd = f"git push {self.pr_remote} {head_branch}" @@ -150,7 +154,28 @@ def push_to_remote(self, base_branch, head_branch): except subprocess.CalledProcessError: click.echo(f"Failed to push to {self.pr_remote} \u2639") else: - self.open_pr(self.get_pr_url(base_branch, head_branch)) + if os.getenv("GH_AUTH"): + self.create_gh_pr(base_branch, head_branch, commit_message) + else: + self.open_pr(self.get_pr_url(base_branch, head_branch)) + + def create_gh_pr(self, base_branch, head_branch, commit_message): + request_headers = create_headers(self.username, oauth_token=os.getenv("GH_AUTH")) + url = f"https://api.github.com/repos/python/cpython/pulls" + title, body = normalize_commit_message(commit_message) + data = { + "title": title, + "body": body, + "head": f"{self.username}:{head_branch}", + "base": base_branch, + "maintainer_can_modify": True + } + response = requests.post(url, headers=request_headers, json=data) + if response.status_code == requests.codes.created: + print(f"Backport PR created at {response.json()['_links']['html']}") + else: + print(response.status_code) + print(response.text) def open_pr(self, url): """ @@ -184,16 +209,19 @@ def backport(self): cherry_pick_branch = self.get_cherry_pick_branch(maint_branch) self.checkout_branch(maint_branch) + commit_message = "" try: self.cherry_pick() - self.amend_commit_message(cherry_pick_branch) + commit_message = self.amend_commit_message(cherry_pick_branch) except subprocess.CalledProcessError as cpe: click.echo(cpe.output) click.echo(self.get_exit_message(maint_branch)) sys.exit(-1) else: if self.push: - self.push_to_remote(maint_branch, cherry_pick_branch) + self.push_to_remote(maint_branch, + cherry_pick_branch, + commit_message) self.cleanup_branch(cherry_pick_branch) else: click.echo(\ @@ -331,6 +359,15 @@ def is_cpython_repo(): return False return True +def normalize_commit_message(commit_message): + """ + Return a tuple of title and body from the commit message + """ + split_commit_message = commit_message.split("\n") + title = split_commit_message[0] + body = "\n".join(split_commit_message[1:]) + return title, body.lstrip("\n") + if __name__ == '__main__': cherry_pick_cli() diff --git a/cherry_picker/cherry_picker/test.py b/cherry_picker/cherry_picker/test.py index cae072e..6c50b4f 100644 --- a/cherry_picker/cherry_picker/test.py +++ b/cherry_picker/cherry_picker/test.py @@ -3,7 +3,8 @@ import pytest from .cherry_picker import get_base_branch, get_current_branch, \ - get_full_sha_from_short, is_cpython_repo, CherryPicker + get_full_sha_from_short, is_cpython_repo, CherryPicker, \ + normalize_commit_message def test_get_base_branch(): @@ -112,3 +113,24 @@ def test_is_cpython_repo(subprocess_check_output): def test_is_not_cpython_repo(): assert is_cpython_repo() == False +def test_normalize_long_commit_message(): + commit_message = """[3.6] Fix broken `Show Source` links on documentation pages (GH-3113) + +The `Show Source` was broken because of a change made in sphinx 1.5.1 +In Sphinx 1.4.9, the sourcename was "index.txt". +In Sphinx 1.5.1+, it is now "index.rst.txt". +(cherry picked from commit b9ff498793611d1c6a9b99df464812931a1e2d69)""" + title, body = normalize_commit_message(commit_message) + assert title == "[3.6] Fix broken `Show Source` links on documentation pages (GH-3113)" + assert body == """The `Show Source` was broken because of a change made in sphinx 1.5.1 +In Sphinx 1.4.9, the sourcename was "index.txt". +In Sphinx 1.5.1+, it is now "index.rst.txt". +(cherry picked from commit b9ff498793611d1c6a9b99df464812931a1e2d69)""" + +def test_normalize_short_commit_message(): + commit_message = """[3.6] Fix broken `Show Source` links on documentation pages (GH-3113) + +(cherry picked from commit b9ff498793611d1c6a9b99df464812931a1e2d69)""" + title, body = normalize_commit_message(commit_message) + assert title == "[3.6] Fix broken `Show Source` links on documentation pages (GH-3113)" + assert body == """(cherry picked from commit b9ff498793611d1c6a9b99df464812931a1e2d69)""" \ No newline at end of file diff --git a/cherry_picker/flit.ini b/cherry_picker/flit.ini index 9e1abd1..5539194 100644 --- a/cherry_picker/flit.ini +++ b/cherry_picker/flit.ini @@ -6,6 +6,8 @@ maintainer = Python Core Developers maintainer-email = core-workflow@python.org home-page = https://github.com/python/core-workflow/tree/master/cherry_picker requires = click~=6.7 + gidgethub + requests dev-requires = pytest~=3.0.7 description-file = readme.rst classifiers = Programming Language :: Python :: 3.6 diff --git a/cherry_picker/readme.rst b/cherry_picker/readme.rst index 888bbaf..d26211f 100644 --- a/cherry_picker/readme.rst +++ b/cherry_picker/readme.rst @@ -18,6 +18,9 @@ maintenance branches (``3.6``, ``3.5``, ``2.7``). It will prefix the commit message with the branch, e.g. ``[3.6]``, and then opens up the pull request page. +If a GitHub personal access token was specified as an environment variable, +the pull request will be created automatically. + Tests are to be written using pytest. @@ -32,6 +35,7 @@ Requires Python 3.6. $ source venv/bin/activate (venv) $ python -m pip install cherry_picker + 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. @@ -53,6 +57,19 @@ repository are pushed to ``origin``. If this is incorrect, then the correct remote will need be specified using the ``--pr-remote`` option (e.g. ``--pr-remote pr`` to use a remote named ``pr``). +Optional +-------- + +cherry_picker can automatically create the PR, if the ``GH_AUTH`` token is set +as an environment variable. + +The token can be generated by following `these instructions `_ . + +Once the token has been generated, it can be set as an environment variable. +On the command line:: + + $ export GH_AUTH=token + Cherry-picking 🐍🍒⛏️ ===================== From 0f76f040e474efb73a98bc1b6677b47c3d941d73 Mon Sep 17 00:00:00 2001 From: Mariatta Wijaya Date: Sun, 27 Aug 2017 15:39:05 -0700 Subject: [PATCH 02/11] Updates to the readme. --- cherry_picker/readme.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cherry_picker/readme.rst b/cherry_picker/readme.rst index d26211f..d188ff5 100644 --- a/cherry_picker/readme.rst +++ b/cherry_picker/readme.rst @@ -18,10 +18,11 @@ maintenance branches (``3.6``, ``3.5``, ``2.7``). It will prefix the commit message with the branch, e.g. ``[3.6]``, and then opens up the pull request page. -If a GitHub personal access token was specified as an environment variable, -the pull request will be created automatically. +If a GitHub `personal access token `_ +was specified as an environment variable, the pull request will be created +automatically. -Tests are to be written using pytest. +Tests are to be written using `pytest `_. Setup Info @@ -60,7 +61,7 @@ remote will need be specified using the ``--pr-remote`` option (e.g. Optional -------- -cherry_picker can automatically create the PR, if the ``GH_AUTH`` token is set +``cherry_picker`` can automatically create the PR, if the ``GH_AUTH`` token is set as an environment variable. The token can be generated by following `these instructions `_ . From bea15f72b42269c93ec048f32844b13f891c5d25 Mon Sep 17 00:00:00 2001 From: Mariatta Wijaya Date: Sun, 27 Aug 2017 15:42:52 -0700 Subject: [PATCH 03/11] change the 'optional' heading --- cherry_picker/readme.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cherry_picker/readme.rst b/cherry_picker/readme.rst index d188ff5..dbff1c9 100644 --- a/cherry_picker/readme.rst +++ b/cherry_picker/readme.rst @@ -58,8 +58,8 @@ repository are pushed to ``origin``. If this is incorrect, then the correct remote will need be specified using the ``--pr-remote`` option (e.g. ``--pr-remote pr`` to use a remote named ``pr``). -Optional --------- +Personal Access Token (optional) +-------------------------------- ``cherry_picker`` can automatically create the PR, if the ``GH_AUTH`` token is set as an environment variable. @@ -67,7 +67,7 @@ as an environment variable. The token can be generated by following `these instructions `_ . Once the token has been generated, it can be set as an environment variable. -On the command line:: +On the command line (MacOS and Unix systems):: $ export GH_AUTH=token From 015e961ad0431ad55c43ec84b73a419fb04de8bf Mon Sep 17 00:00:00 2001 From: Mariatta Date: Sun, 27 Aug 2017 15:59:06 -0700 Subject: [PATCH 04/11] Update readme.rst --- cherry_picker/readme.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/cherry_picker/readme.rst b/cherry_picker/readme.rst index dbff1c9..f0d92c6 100644 --- a/cherry_picker/readme.rst +++ b/cherry_picker/readme.rst @@ -36,7 +36,6 @@ Requires Python 3.6. $ source venv/bin/activate (venv) $ python -m pip install cherry_picker - 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. From 3a9d825f70d8ad0b68ccf3bd193b7cdd1fc34e27 Mon Sep 17 00:00:00 2001 From: Mariatta Date: Sun, 27 Aug 2017 16:01:26 -0700 Subject: [PATCH 05/11] url doesn't need to be f-string --- cherry_picker/cherry_picker/cherry_picker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cherry_picker/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker/cherry_picker.py index 34b8370..790301c 100755 --- a/cherry_picker/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker/cherry_picker.py @@ -161,7 +161,7 @@ def push_to_remote(self, base_branch, head_branch, commit_message=""): def create_gh_pr(self, base_branch, head_branch, commit_message): request_headers = create_headers(self.username, oauth_token=os.getenv("GH_AUTH")) - url = f"https://api.github.com/repos/python/cpython/pulls" + url = "https://api.github.com/repos/python/cpython/pulls" title, body = normalize_commit_message(commit_message) data = { "title": title, From 7205d6cea6bd97d81f32044139bccf6dc6090589 Mon Sep 17 00:00:00 2001 From: Mariatta Date: Sun, 27 Aug 2017 16:05:05 -0700 Subject: [PATCH 06/11] update url --- cherry_picker/cherry_picker/cherry_picker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cherry_picker/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker/cherry_picker.py index 790301c..ce3c901 100755 --- a/cherry_picker/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker/cherry_picker.py @@ -12,6 +12,7 @@ from . import __version__ +CPYTHON_CREATE_PR_URL = "https://api.github.com/repos/python/cpython/pulls" class CherryPicker: @@ -161,7 +162,6 @@ def push_to_remote(self, base_branch, head_branch, commit_message=""): def create_gh_pr(self, base_branch, head_branch, commit_message): request_headers = create_headers(self.username, oauth_token=os.getenv("GH_AUTH")) - url = "https://api.github.com/repos/python/cpython/pulls" title, body = normalize_commit_message(commit_message) data = { "title": title, @@ -170,7 +170,7 @@ def create_gh_pr(self, base_branch, head_branch, commit_message): "base": base_branch, "maintainer_can_modify": True } - response = requests.post(url, headers=request_headers, json=data) + response = requests.post(CPYTHON_CREATE_PR_URL, headers=request_headers, json=data) if response.status_code == requests.codes.created: print(f"Backport PR created at {response.json()['_links']['html']}") else: From d4ac13189c06bfe9d5a5be8936a3b85bf46e9b7d Mon Sep 17 00:00:00 2001 From: Mariatta Date: Sun, 27 Aug 2017 22:00:18 -0700 Subject: [PATCH 07/11] use click.echo instead of print --- cherry_picker/cherry_picker/cherry_picker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cherry_picker/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker/cherry_picker.py index ce3c901..69c514d 100755 --- a/cherry_picker/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker/cherry_picker.py @@ -172,10 +172,10 @@ def create_gh_pr(self, base_branch, head_branch, commit_message): } response = requests.post(CPYTHON_CREATE_PR_URL, headers=request_headers, json=data) if response.status_code == requests.codes.created: - print(f"Backport PR created at {response.json()['_links']['html']}") + click.echo(f"Backport PR created at {response.json()['_links']['html']}") else: - print(response.status_code) - print(response.text) + click.echo(response.status_code) + click.echo(response.text) def open_pr(self, url): """ From 2e91907dcce7e7e26bf8a201188230759ae0d190 Mon Sep 17 00:00:00 2001 From: Mariatta Wijaya Date: Mon, 28 Aug 2017 21:37:38 -0700 Subject: [PATCH 08/11] Undocument the personal access token stuff --- cherry_picker/readme.rst | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/cherry_picker/readme.rst b/cherry_picker/readme.rst index f0d92c6..9663460 100644 --- a/cherry_picker/readme.rst +++ b/cherry_picker/readme.rst @@ -18,10 +18,6 @@ maintenance branches (``3.6``, ``3.5``, ``2.7``). It will prefix the commit message with the branch, e.g. ``[3.6]``, and then opens up the pull request page. -If a GitHub `personal access token `_ -was specified as an environment variable, the pull request will be created -automatically. - Tests are to be written using `pytest `_. @@ -57,19 +53,6 @@ repository are pushed to ``origin``. If this is incorrect, then the correct remote will need be specified using the ``--pr-remote`` option (e.g. ``--pr-remote pr`` to use a remote named ``pr``). -Personal Access Token (optional) --------------------------------- - -``cherry_picker`` can automatically create the PR, if the ``GH_AUTH`` token is set -as an environment variable. - -The token can be generated by following `these instructions `_ . - -Once the token has been generated, it can be set as an environment variable. -On the command line (MacOS and Unix systems):: - - $ export GH_AUTH=token - Cherry-picking 🐍🍒⛏️ ===================== From 40a047f3064e1d033697d0413c73aa30ccee4334 Mon Sep 17 00:00:00 2001 From: Mariatta Wijaya Date: Tue, 29 Aug 2017 21:03:44 -0700 Subject: [PATCH 09/11] make gh_auth a required argument when creating GitHub PR --- cherry_picker/cherry_picker/cherry_picker.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cherry_picker/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker/cherry_picker.py index 69c514d..313b18d 100755 --- a/cherry_picker/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker/cherry_picker.py @@ -8,7 +8,7 @@ import sys import requests -from gidgethub.sansio import create_headers +import gidgethub from . import __version__ @@ -155,13 +155,19 @@ def push_to_remote(self, base_branch, head_branch, commit_message=""): except subprocess.CalledProcessError: click.echo(f"Failed to push to {self.pr_remote} \u2639") else: - if os.getenv("GH_AUTH"): - self.create_gh_pr(base_branch, head_branch, commit_message) + gh_auth = os.getenv("GH_AUTH") + if gh_auth: + self.create_gh_pr(base_branch, head_branch, commit_message, + gh_auth) else: self.open_pr(self.get_pr_url(base_branch, head_branch)) - def create_gh_pr(self, base_branch, head_branch, commit_message): - request_headers = create_headers(self.username, oauth_token=os.getenv("GH_AUTH")) + def create_gh_pr(self, base_branch, head_branch, commit_message, gh_auth): + """ + Create PR in GitHub + """ + request_headers = gidgethub.sansio.create_headers( + self.username, oauth_token=gh_auth) title, body = normalize_commit_message(commit_message) data = { "title": title, From 1db71fcf7c5d656ac1f4f9e925f0a24912b43b37 Mon Sep 17 00:00:00 2001 From: Mariatta Wijaya Date: Tue, 29 Aug 2017 21:05:53 -0700 Subject: [PATCH 10/11] from gidgethub import sansio --- cherry_picker/cherry_picker/cherry_picker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cherry_picker/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker/cherry_picker.py index 313b18d..9487563 100755 --- a/cherry_picker/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker/cherry_picker.py @@ -8,7 +8,7 @@ import sys import requests -import gidgethub +from gidgethub import sansio from . import __version__ @@ -166,7 +166,7 @@ def create_gh_pr(self, base_branch, head_branch, commit_message, gh_auth): """ Create PR in GitHub """ - request_headers = gidgethub.sansio.create_headers( + request_headers = sansio.create_headers( self.username, oauth_token=gh_auth) title, body = normalize_commit_message(commit_message) data = { From cbe3841be820cd778bb410e89b37bc6e8ec93f91 Mon Sep 17 00:00:00 2001 From: Mariatta Wijaya Date: Wed, 30 Aug 2017 22:15:03 -0700 Subject: [PATCH 11/11] from gidgethub import sansio made commit_message and gh_auth keyword only args in create_gh_pr --- cherry_picker/cherry_picker/cherry_picker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cherry_picker/cherry_picker/cherry_picker.py b/cherry_picker/cherry_picker/cherry_picker.py index 9487563..c3d29a7 100755 --- a/cherry_picker/cherry_picker/cherry_picker.py +++ b/cherry_picker/cherry_picker/cherry_picker.py @@ -157,12 +157,15 @@ def push_to_remote(self, base_branch, head_branch, commit_message=""): else: gh_auth = os.getenv("GH_AUTH") if gh_auth: - self.create_gh_pr(base_branch, head_branch, commit_message, - gh_auth) + self.create_gh_pr(base_branch, head_branch, + commit_message=commit_message, + gh_auth=gh_auth) else: self.open_pr(self.get_pr_url(base_branch, head_branch)) - def create_gh_pr(self, base_branch, head_branch, commit_message, gh_auth): + def create_gh_pr(self, base_branch, head_branch, *, + commit_message, + gh_auth): """ Create PR in GitHub """