From 05e1d95ff6b3de0009fcb01ff6d1c17366c0f23f Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 27 Feb 2021 06:00:17 -0500 Subject: [PATCH 1/4] ARROW-11804: [Developer] Offer to create JIRA issue --- dev/merge_arrow_pr.py | 116 ++++++++++++++++++++++++++++++++++--- dev/test_merge_arrow_pr.py | 11 ++++ 2 files changed, 118 insertions(+), 9 deletions(-) diff --git a/dev/merge_arrow_pr.py b/dev/merge_arrow_pr.py index 9036c012313..030d3defe45 100755 --- a/dev/merge_arrow_pr.py +++ b/dev/merge_arrow_pr.py @@ -41,6 +41,7 @@ import sys import requests import getpass +import json from six.moves import input import six @@ -135,6 +136,15 @@ def fix_version_from_branch(branch, versions): for project in SUPPORTED_PROJECTS] +def get_jira_id(title): + jira_id = None + for project, regex in PR_TITLE_REGEXEN: + m = regex.search(title) + if m: + return (m.group(1), project) + return (None, None) + + class JiraIssue(object): def __init__(self, jira_con, jira_id, project, cmd): @@ -263,6 +273,21 @@ def get_pr_data(self, number): return get_json("%s/pulls/%s" % (self.github_api, number), headers=self.headers) + def update_pr_title(self, number, title): + if not self.headers: + msg = "Can not update PR {} title to '{}'. ARROW_GITHUB_API_TOKEN environment not set".format( + number, title + ) + raise Exception(msg) + + # note need to use the issues URL to update + url = "%s/issues/%s" % (self.github_api, number) + data = json.dumps({'title': title}) + resp = requests.patch(url, data = data, headers=self.headers) + resp.raise_for_status() + return resp.json() + + class CommandInput(object): """ @@ -292,12 +317,12 @@ def continue_maybe(self, prompt): class PullRequest(object): - def __init__(self, cmd, github_api, git_remote, jira_con, number): + def __init__(self, cmd, pr_data, git_remote, jira_con, number): self.cmd = cmd self.git_remote = git_remote self.con = jira_con self.number = number - self._pr_data = github_api.get_pr_data(number) + self._pr_data = pr_data try: self.url = self._pr_data["url"] self.title = self._pr_data["title"] @@ -327,12 +352,7 @@ def is_mergeable(self): return bool(self._pr_data["mergeable"]) def _get_jira(self): - jira_id = None - for project, regex in PR_TITLE_REGEXEN: - m = regex.search(self.title) - if m: - jira_id = m.group(1) - break + (jira_id, project) = get_jira_id(self.title) if jira_id is None: options = ' or '.join('{0}-XXX'.format(project) @@ -550,6 +570,83 @@ def get_pr_num(): return input("Which pull request would you like to merge? (e.g. 34): ") +_JIRA_COMPONENT_REGEX = re.compile(r'(\[[^\]]*\])+') + +# Maps PR title prefixes to JIRA components +PR_COMPONENTS_TO_JIRA_COMPONENTS = { + '[Rust]': 'Rust', + '[Rust][DataFusion]': 'Rust - DataFusion', + '[C++]': 'C++', + '[R]': 'R', +} + +# Return the best matching JIRA component from a PR title, if any +# "[Rust] Fix something" --> Rust +# "[Rust][DataFusion] Fix" --> Rust - DataFusion +# "[CPP] Fix " --> "C++" +def jira_component_name_from_title(title): + match = _JIRA_COMPONENT_REGEX.match(title.strip()) + if match: + pr_component = str(match.group(0)) + return PR_COMPONENTS_TO_JIRA_COMPONENTS.get(pr_component) + + +# If no jira ID can be found for the PR, offer to create one +# returns returns the github pr_data +def make_auto_jira(github_api, jira_con, cmd, pr_num): + pr_data = github_api.get_pr_data(pr_num) + + try: + title = pr_data["title"] + html_link = pr_data["_links"]["html"]["href"] + except KeyError: + pprint.pprint(pr_data) + raise + + # had valid JIRA already + if get_jira_id(title)[0]: + return pr_data + + + print("No JIRA link found for PR", pr_num) + options = ' or '.join('{0}-XXX'.format(project) + for project in SUPPORTED_PROJECTS) + print(" Looked for PR title prefixed by {}".format(options)) + + # try to make a JIRA issue in the ARROW project with a + # component extracted from the PR title + component = jira_component_name_from_title(title) + + if not component: + print(" Could not determine component from title") + print(" Expected '[Component] description', found '{}'".format(title)) + print(" Known components: {}".format(", ".join(PR_COMPONENTS_TO_JIRA_COMPONENTS))) + return pr_data + + components=[{"name": component}] + summary="{}".format(title) + + print("=== NEW JIRA ===") + print("Summary\t\t{}".format(summary)) + print("Assignee\tNONE") + print("Components\t{}".format(component)) + print("Status\t\tNew") + description = "Issue automatically created from Pull Request [{}|{}]\n{}".format( + pr_num, html_link, pr_data.get("body") + ) + + cmd.continue_maybe("Create JIRA and link to PR?") + issue = jira_con.create_issue(project='ARROW', + summary=summary, + description=description, + issuetype={'name': 'Bug'}, + components=components, + ) + key = issue.key + print(" Created", key) + github_api.update_pr_title(pr_num, "{}: {}".format(key, title)) + return github_api.get_pr_data(pr_num) + def cli(): # Location of your Arrow git clone @@ -567,7 +664,8 @@ def cli(): github_api = GitHubAPI(PROJECT_NAME) jira_con = connect_jira(cmd) - pr = PullRequest(cmd, github_api, PR_REMOTE_NAME, jira_con, pr_num) + pr_data = make_auto_jira(github_api, jira_con, cmd, pr_num) + pr = PullRequest(cmd, pr_data, PR_REMOTE_NAME, jira_con, pr_num) if pr.is_merged: print("Pull request %s has already been merged") diff --git a/dev/test_merge_arrow_pr.py b/dev/test_merge_arrow_pr.py index 8fe18835082..830dfe65b9a 100644 --- a/dev/test_merge_arrow_pr.py +++ b/dev/test_merge_arrow_pr.py @@ -315,3 +315,14 @@ def test_jira_output_no_components(): Components\tC++, Python Status\t\tResolved URL\t\thttps://issues.apache.org/jira/browse/ARROW-1234""" + + +def test_jira_component_name_from_title(): + assert merge_arrow_pr.jira_component_name_from_title('[Rust] Example PR') == 'Rust' + assert merge_arrow_pr.jira_component_name_from_title('[Rust ] Example PR') == None + assert merge_arrow_pr.jira_component_name_from_title(' [Rust] Example PR') == 'Rust' + assert merge_arrow_pr.jira_component_name_from_title('[Rust][DataFusion] Example PR') == 'Rust - DataFusion' + assert merge_arrow_pr.jira_component_name_from_title('[DataFusion][Rust] Example PR') == None + assert merge_arrow_pr.jira_component_name_from_title('[Rust][ddFusion] Example PR') == None + assert merge_arrow_pr.jira_component_name_from_title('[C++] Example PR') == 'C++' + assert merge_arrow_pr.jira_component_name_from_title('[R] Example PR') == 'R' From 2fdf17dd8a695c9f020e253d3248dcfa57bf838e Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 27 Feb 2021 15:26:14 -0500 Subject: [PATCH 2/4] flake8 --- dev/merge_arrow_pr.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/dev/merge_arrow_pr.py b/dev/merge_arrow_pr.py index 030d3defe45..a24800267df 100755 --- a/dev/merge_arrow_pr.py +++ b/dev/merge_arrow_pr.py @@ -137,7 +137,6 @@ def fix_version_from_branch(branch, versions): def get_jira_id(title): - jira_id = None for project, regex in PR_TITLE_REGEXEN: m = regex.search(title) if m: @@ -275,7 +274,8 @@ def get_pr_data(self, number): def update_pr_title(self, number, title): if not self.headers: - msg = "Can not update PR {} title to '{}'. ARROW_GITHUB_API_TOKEN environment not set".format( + msg = ("Can not update PR {} title to '{}'. " + + "ARROW_GITHUB_API_TOKEN environment not set").format( number, title ) raise Exception(msg) @@ -283,12 +283,11 @@ def update_pr_title(self, number, title): # note need to use the issues URL to update url = "%s/issues/%s" % (self.github_api, number) data = json.dumps({'title': title}) - resp = requests.patch(url, data = data, headers=self.headers) + resp = requests.patch(url, data=data, headers=self.headers) resp.raise_for_status() return resp.json() - class CommandInput(object): """ Interface to input(...) to enable unit test mocks to be created @@ -570,6 +569,7 @@ def get_pr_num(): return input("Which pull request would you like to merge? (e.g. 34): ") + _JIRA_COMPONENT_REGEX = re.compile(r'(\[[^\]]*\])+') # Maps PR title prefixes to JIRA components @@ -580,6 +580,7 @@ def get_pr_num(): '[R]': 'R', } + # Return the best matching JIRA component from a PR title, if any # "[Rust] Fix something" --> Rust # "[Rust][DataFusion] Fix" --> Rust - DataFusion @@ -607,7 +608,6 @@ def make_auto_jira(github_api, jira_con, cmd, pr_num): if get_jira_id(title)[0]: return pr_data - print("No JIRA link found for PR", pr_num) options = ' or '.join('{0}-XXX'.format(project) for project in SUPPORTED_PROJECTS) @@ -620,18 +620,20 @@ def make_auto_jira(github_api, jira_con, cmd, pr_num): if not component: print(" Could not determine component from title") print(" Expected '[Component] description', found '{}'".format(title)) - print(" Known components: {}".format(", ".join(PR_COMPONENTS_TO_JIRA_COMPONENTS))) + print(" Known components: {}".format( + ", ".join(PR_COMPONENTS_TO_JIRA_COMPONENTS))) return pr_data - components=[{"name": component}] - summary="{}".format(title) + components = [{"name": component}] + summary = "{}".format(title) print("=== NEW JIRA ===") print("Summary\t\t{}".format(summary)) print("Assignee\tNONE") print("Components\t{}".format(component)) print("Status\t\tNew") - description = "Issue automatically created from Pull Request [{}|{}]\n{}".format( + description = ("Issue automatically created " + + "from Pull Request [{}|{}]\n{}").format( pr_num, html_link, pr_data.get("body") ) From d727a7079c6a1ca012c9058b5036139b376c3b7f Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 27 Feb 2021 15:27:13 -0500 Subject: [PATCH 3/4] pep8 --- dev/merge_arrow_pr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/merge_arrow_pr.py b/dev/merge_arrow_pr.py index a24800267df..7e2883df50e 100755 --- a/dev/merge_arrow_pr.py +++ b/dev/merge_arrow_pr.py @@ -389,7 +389,7 @@ def merge(self): had_conflicts = True commit_authors = run_cmd(['git', 'log', 'HEAD..%s' % pr_branch_name, - '--pretty=format:%an <%ae>']).split("\n") + '--pretty=format:%an <%ae>']).split("\n") distinct_authors = sorted(set(commit_authors), key=lambda x: commit_authors.count(x), reverse=True) From 1ca402eff241906acc3e6ec5d0577ee785297b54 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 27 Feb 2021 15:38:06 -0500 Subject: [PATCH 4/4] pep8 for test_merge_arrow.py --- dev/test_merge_arrow_pr.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/dev/test_merge_arrow_pr.py b/dev/test_merge_arrow_pr.py index 830dfe65b9a..6d0eca7a084 100644 --- a/dev/test_merge_arrow_pr.py +++ b/dev/test_merge_arrow_pr.py @@ -318,11 +318,19 @@ def test_jira_output_no_components(): def test_jira_component_name_from_title(): - assert merge_arrow_pr.jira_component_name_from_title('[Rust] Example PR') == 'Rust' - assert merge_arrow_pr.jira_component_name_from_title('[Rust ] Example PR') == None - assert merge_arrow_pr.jira_component_name_from_title(' [Rust] Example PR') == 'Rust' - assert merge_arrow_pr.jira_component_name_from_title('[Rust][DataFusion] Example PR') == 'Rust - DataFusion' - assert merge_arrow_pr.jira_component_name_from_title('[DataFusion][Rust] Example PR') == None - assert merge_arrow_pr.jira_component_name_from_title('[Rust][ddFusion] Example PR') == None - assert merge_arrow_pr.jira_component_name_from_title('[C++] Example PR') == 'C++' - assert merge_arrow_pr.jira_component_name_from_title('[R] Example PR') == 'R' + assert merge_arrow_pr.jira_component_name_from_title( + '[Rust] Example PR') == 'Rust' + assert merge_arrow_pr.jira_component_name_from_title( + '[Rust ] Example PR') is None + assert merge_arrow_pr.jira_component_name_from_title( + ' [Rust] Example PR') == 'Rust' + assert merge_arrow_pr.jira_component_name_from_title( + '[Rust][DataFusion] Example PR') == 'Rust - DataFusion' + assert merge_arrow_pr.jira_component_name_from_title( + '[DataFusion][Rust] Example PR') is None + assert merge_arrow_pr.jira_component_name_from_title( + '[Rust][ddFusion] Example PR') is None + assert merge_arrow_pr.jira_component_name_from_title( + '[C++] Example PR') == 'C++' + assert merge_arrow_pr.jira_component_name_from_title( + '[R] Example PR') == 'R'