You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by we...@apache.org on 2018/09/22 15:39:37 UTC
[arrow] branch master updated: ARROW-3196: Add support for merging
both ARROW and PARQUET patches
This is an automated email from the ASF dual-hosted git repository.
wesm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/master by this push:
new 3160cf1 ARROW-3196: Add support for merging both ARROW and PARQUET patches
3160cf1 is described below
commit 3160cf182f528c997da33d7ab76755c26ba63c37
Author: Wes McKinney <we...@apache.org>
AuthorDate: Sat Sep 22 11:39:27 2018 -0400
ARROW-3196: Add support for merging both ARROW and PARQUET patches
I will test this out on the PARQUET-1398 patch. We should make sure we can merge both ARROW and PARQUET patches before merging this
Author: Wes McKinney <we...@apache.org>
Closes #2578 from wesm/ARROW-3196 and squashes the following commits:
2389c45c3 <Wes McKinney> Actually merge PR
9140b33b9 <Wes McKinney> Fix typo
6555909fe <Wes McKinney> Add test for case where JIRA already resolved
78014ed0c <Wes McKinney> Fix case where JIRA issue already resolved
9af731d33 <Wes McKinney> Fixes
c7e30967e <Wes McKinney> small fix
12aed4cbd <Wes McKinney> Allow user to choose a non-mainline fix version
34b0d2e1e <Wes McKinney> Pass through cli
d78c92dfd <Wes McKinney> More refactoring, some unit tests
30f47c8af <Wes McKinney> More refactoring
af253083b <Wes McKinney> More refactoring
3bade010f <Wes McKinney> Start refactoring merge pr script
f26e013aa <Wes McKinney> Compile regex
9dc3309ef <Wes McKinney> Add support for both ARROW and PARQUET patches
---
dev/merge_arrow_pr.py | 564 +++++++++++++++++++++++++--------------------
dev/test_merge_arrow_pr.py | 164 +++++++++++++
2 files changed, 475 insertions(+), 253 deletions(-)
diff --git a/dev/merge_arrow_pr.py b/dev/merge_arrow_pr.py
index 2bfee7e..8539d5d 100755
--- a/dev/merge_arrow_pr.py
+++ b/dev/merge_arrow_pr.py
@@ -19,7 +19,7 @@
# Utility for creating well-formed pull request merges and pushing them to
# Apache.
-# usage: ./apache-pr-merge.py (see config env vars below)
+# usage: ./merge_arrow_pr.py (see config env vars below)
#
# This utility assumes you already have a local Arrow git clone and that you
# have added remotes corresponding to both (i) the Github Apache Arrow mirror
@@ -37,49 +37,16 @@ import six
try:
import jira.client
- JIRA_IMPORTED = True
except ImportError:
- JIRA_IMPORTED = False
print("Could not find jira-python library. "
"Run 'sudo pip install jira-python' to install.")
print("Exiting without trying to close the associated JIRA.")
+ sys.exit(1)
+
-# Location of your Arrow git clone
-SEP = os.path.sep
-ARROW_HOME = os.path.abspath(__file__).rsplit(SEP, 2)[0]
-PROJECT_NAME = ARROW_HOME.rsplit(SEP, 1)[1]
-print("ARROW_HOME = " + ARROW_HOME)
-print("PROJECT_NAME = " + PROJECT_NAME)
-
-# Remote name which points to the Gihub site
-PR_REMOTE_NAME = os.environ.get("PR_REMOTE_NAME", "apache-github")
-# Remote name which points to Apache git
-PUSH_REMOTE_NAME = os.environ.get("PUSH_REMOTE_NAME", "apache")
-# ASF JIRA username
-JIRA_USERNAME = os.environ.get("JIRA_USERNAME")
-# ASF JIRA password
-JIRA_PASSWORD = os.environ.get("JIRA_PASSWORD")
-
-GITHUB_BASE = "https://github.com/apache/" + PROJECT_NAME + "/pull"
-GITHUB_API_BASE = "https://api.github.com/repos/apache/" + PROJECT_NAME
-JIRA_BASE = "https://issues.apache.org/jira/browse"
-JIRA_API_BASE = "https://issues.apache.org/jira"
# Prefix added to temporary branches
BRANCH_PREFIX = "PR_TOOL"
-
-os.chdir(ARROW_HOME)
-
-if not JIRA_USERNAME:
- JIRA_USERNAME = input("Env JIRA_USERNAME not set, "
- "please enter your JIRA username:")
-
-if not JIRA_PASSWORD:
- JIRA_PASSWORD = getpass.getpass("Env JIRA_PASSWORD not set, please enter "
- "your JIRA password:")
-
-
-ASF_JIRA = jira.client.JIRA({'server': JIRA_API_BASE},
- basic_auth=(JIRA_USERNAME, JIRA_PASSWORD))
+JIRA_API_BASE = "https://issues.apache.org/jira"
def get_json(url):
@@ -87,12 +54,6 @@ def get_json(url):
return req.json()
-def fail(msg):
- print(msg)
- clean_up()
- sys.exit(-1)
-
-
def run_cmd(cmd):
if isinstance(cmd, six.string_types):
cmd = cmd.split(' ')
@@ -113,12 +74,6 @@ def run_cmd(cmd):
return output
-def continue_maybe(prompt):
- result = input("\n%s (y/n): " % prompt)
- if result.lower() != "y":
- fail("Okay, exiting")
-
-
original_head = run_cmd("git rev-parse HEAD")[:8]
@@ -128,93 +83,12 @@ def clean_up():
branches = run_cmd("git branch").replace(" ", "").split("\n")
- for branch in [x for x in branches if x.startswith(BRANCH_PREFIX)]:
+ for branch in [x for x in branches
+ if x.startswith(BRANCH_PREFIX)]:
print("Deleting local branch %s" % branch)
run_cmd("git branch -D %s" % branch)
-# merge the requested PR and return the merge hash
-def merge_pr(pr_num, target_ref):
- pr_branch_name = "%s_MERGE_PR_%s" % (BRANCH_PREFIX, pr_num)
- target_branch_name = "%s_MERGE_PR_%s_%s" % (BRANCH_PREFIX, pr_num,
- target_ref.upper())
- run_cmd("git fetch %s pull/%s/head:%s" % (PR_REMOTE_NAME, pr_num,
- pr_branch_name))
- run_cmd("git fetch %s %s:%s" % (PUSH_REMOTE_NAME, target_ref,
- target_branch_name))
- run_cmd("git checkout %s" % target_branch_name)
-
- had_conflicts = False
- try:
- run_cmd(['git', 'merge', pr_branch_name, '--squash'])
- except Exception as e:
- msg = ("Error merging: %s\nWould you like to "
- "manually fix-up this merge?" % e)
- continue_maybe(msg)
- msg = ("Okay, please fix any conflicts and 'git add' "
- "conflicting files... Finished?")
- continue_maybe(msg)
- had_conflicts = True
-
- commit_authors = run_cmd(['git', 'log', 'HEAD..%s' % pr_branch_name,
- '--pretty=format:%an <%ae>']).split("\n")
- distinct_authors = sorted(set(commit_authors),
- key=lambda x: commit_authors.count(x),
- reverse=True)
- primary_author = distinct_authors[0]
- commits = run_cmd(['git', 'log', 'HEAD..%s' % pr_branch_name,
- '--pretty=format:%h <%an> %s']).split("\n\n")
-
- merge_message_flags = []
-
- merge_message_flags += ["-m", title]
- if body is not None:
- merge_message_flags += ["-m", body]
-
- authors = "\n".join(["Author: %s" % a for a in distinct_authors])
-
- merge_message_flags += ["-m", authors]
-
- if had_conflicts:
- committer_name = run_cmd("git config --get user.name").strip()
- committer_email = run_cmd("git config --get user.email").strip()
- message = ("This patch had conflicts when merged, "
- "resolved by\nCommitter: %s <%s>" %
- (committer_name, committer_email))
- merge_message_flags += ["-m", message]
-
- # The string "Closes #%s" string is required for GitHub to correctly close
- # the PR
- merge_message_flags += [
- "-m",
- "Closes #%s from %s and squashes the following commits:"
- % (pr_num, pr_repo_desc)]
- for c in commits:
- stripped_message = strip_ci_directives(c).strip()
- merge_message_flags += ["-m", stripped_message]
-
- run_cmd(['git', 'commit',
- '--no-verify', # do not run commit hooks
- '--author="%s"' % primary_author] +
- merge_message_flags)
-
- continue_maybe("Merge complete (local ref %s). Push to %s?" % (
- target_branch_name, PUSH_REMOTE_NAME))
-
- try:
- run_cmd('git push %s %s:%s' % (PUSH_REMOTE_NAME, target_branch_name,
- target_ref))
- except Exception as e:
- clean_up()
- fail("Exception while pushing: %s" % e)
-
- merge_hash = run_cmd("git rev-parse %s" % target_branch_name)[:8]
- clean_up()
- print("Pull request #%s merged!" % pr_num)
- print("Merge hash: %s" % merge_hash)
- return merge_hash
-
-
_REGEX_CI_DIRECTIVE = re.compile('\[[^\]]*\]')
@@ -234,147 +108,331 @@ def fix_version_from_branch(branch, versions):
return [x for x in versions if x.name.startswith(branch_ver)][-1]
-def extract_jira_id(title):
- m = re.search(r'^(ARROW-[0-9]+)\b.*$', title)
- if m:
- return m.group(1)
- else:
- fail("PR title should be prefixed by a jira id "
- "\"ARROW-XXX: ...\", found: \"%s\"" % title)
+# We can merge both ARROW and PARQUET patchesa
+SUPPORTED_PROJECTS = ['ARROW', 'PARQUET']
+PR_TITLE_REGEXEN = [(project, re.compile(r'^(' + project + r'-[0-9]+)\b.*$'))
+ for project in SUPPORTED_PROJECTS]
+
+
+class JiraIssue(object):
+
+ def __init__(self, jira_con, jira_id, project, cmd):
+ self.jira_con = jira_con
+ self.jira_id = jira_id
+ self.project = project
+ self.cmd = cmd
+
+ try:
+ self.issue = jira_con.issue(jira_id)
+ except Exception as e:
+ self.cmd.fail("ASF JIRA could not find %s\n%s" % (jira_id, e))
+
+ def get_candidate_fix_versions(self, merge_branches=('master',)):
+ # Only suggest versions starting with a number, like 0.x but not JS-0.x
+ all_versions = self.jira_con.project_versions(self.project)
+ unreleased_versions = [x for x in all_versions
+ if not x.raw['released']]
+
+ unreleased_versions = sorted(unreleased_versions,
+ key=lambda x: x.name, reverse=True)
+
+ mainline_version_regex = re.compile('\d.*')
+ mainline_versions = [x for x in unreleased_versions
+ if mainline_version_regex.match(x.name)]
+
+ default_fix_versions = [
+ fix_version_from_branch(x, mainline_versions).name
+ for x in merge_branches]
+
+ for v in default_fix_versions:
+ # Handles the case where we have forked a release branch but not
+ # yet made the release. In this case, if the PR is committed to
+ # the master branch and the release branch, we only consider the
+ # release branch to be the fix version. E.g. it is not valid to
+ # have both 1.1.0 and 1.0.0 as fix versions.
+ (major, minor, patch) = v.split(".")
+ if patch == "0":
+ previous = "%s.%s.%s" % (major, int(minor) - 1, 0)
+ if previous in default_fix_versions:
+ default_fix_versions = [x for x in default_fix_versions
+ if x != v]
+
+ return unreleased_versions, default_fix_versions
+
+ def resolve(self, fix_versions, comment):
+ cur_status = self.issue.fields.status.name
+ cur_summary = self.issue.fields.summary
+ cur_assignee = self.issue.fields.assignee
+ if cur_assignee is None:
+ cur_assignee = "NOT ASSIGNED!!!"
+ else:
+ cur_assignee = cur_assignee.displayName
+
+ if cur_status == "Resolved" or cur_status == "Closed":
+ self.cmd.fail("JIRA issue %s already has status '%s'"
+ % (self.jira_id, cur_status))
+ print("=== JIRA %s ===" % self.jira_id)
+ print("summary\t\t%s\nassignee\t%s\nstatus\t\t%s\nurl\t\t%s/%s\n"
+ % (cur_summary, cur_assignee, cur_status,
+ '/'.join((JIRA_API_BASE, 'browse')),
+ self.jira_id))
+
+ resolve = [x for x in self.jira_con.transitions(self.jira_id)
+ if x['name'] == "Resolve Issue"][0]
+ self.jira_con.transition_issue(self.jira_id, resolve["id"],
+ comment=comment,
+ fixVersions=fix_versions)
+
+ print("Successfully resolved %s!" % (self.jira_id))
+
+
+class GitHubAPI(object):
+
+ def __init__(self, project_name):
+ self.github_api = ("https://api.github.com/repos/apache/{0}"
+ .format(project_name))
+
+ def get_pr_data(self, number):
+ return get_json("%s/pulls/%s" % (self.github_api, number))
+
+
+class CommandInput(object):
+ """
+ Interface to input(...) to enable unit test mocks to be created
+ """
+
+ def fail(self, msg):
+ clean_up()
+ raise Exception(msg)
+
+ def prompt(self, prompt):
+ return input(prompt)
+
+ def getpass(self, prompt):
+ return getpass.getpass(prompt)
+
+ def continue_maybe(self, prompt):
+ result = input("\n%s (y/n): " % prompt)
+ if result.lower() != "y":
+ self.fail("Okay, exiting")
+
+
+class PullRequest(object):
+
+ def __init__(self, cmd, github_api, 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.url = self._pr_data["url"]
+ self.title = self._pr_data["title"]
+
+ self.body = self._pr_data["body"]
+ self.target_ref = self._pr_data["base"]["ref"]
+ self.user_login = self._pr_data["user"]["login"]
+ self.base_ref = self._pr_data["head"]["ref"]
+ self.description = "%s/%s" % (self.user_login, self.base_ref)
+
+ self.jira_issue = self._get_jira()
+
+ def show(self):
+ print("\n=== Pull Request #%s ===" % self.number)
+ print("title\t%s\nsource\t%s\ntarget\t%s\nurl\t%s"
+ % (self.title, self.description, self.target_ref, self.url))
+
+ @property
+ def is_merged(self):
+ return bool(self._pr_data["merged"])
+
+ @property
+ 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
+
+ if jira_id is None:
+ options = ' or '.join('{0}-XXX'.format(project)
+ for project in SUPPORTED_PROJECTS)
+ self.cmd.fail("PR title should be prefixed by a jira id "
+ "{0}, but found {1}".format(options, self.title))
+
+ return JiraIssue(self.con, jira_id, project, self.cmd)
+
+ def merge(self, target_ref='master'):
+ """
+ merge the requested PR and return the merge hash
+ """
+ pr_branch_name = "%s_MERGE_PR_%s" % (BRANCH_PREFIX, self.number)
+ target_branch_name = "%s_MERGE_PR_%s_%s" % (BRANCH_PREFIX,
+ self.number,
+ target_ref.upper())
+ run_cmd("git fetch %s pull/%s/head:%s" % (self.git_remote,
+ self.number,
+ pr_branch_name))
+ run_cmd("git fetch %s %s:%s" % (self.git_remote, target_ref,
+ target_branch_name))
+ run_cmd("git checkout %s" % target_branch_name)
+
+ had_conflicts = False
+ try:
+ run_cmd(['git', 'merge', pr_branch_name, '--squash'])
+ except Exception as e:
+ msg = ("Error merging: %s\nWould you like to "
+ "manually fix-up this merge?" % e)
+ self.cmd.continue_maybe(msg)
+ msg = ("Okay, please fix any conflicts and 'git add' "
+ "conflicting files... Finished?")
+ self.cmd.continue_maybe(msg)
+ had_conflicts = True
+
+ commit_authors = run_cmd(['git', 'log', 'HEAD..%s' % pr_branch_name,
+ '--pretty=format:%an <%ae>']).split("\n")
+ distinct_authors = sorted(set(commit_authors),
+ key=lambda x: commit_authors.count(x),
+ reverse=True)
+ primary_author = distinct_authors[0]
+ commits = run_cmd(['git', 'log', 'HEAD..%s' % pr_branch_name,
+ '--pretty=format:%h <%an> %s']).split("\n\n")
+
+ merge_message_flags = []
+
+ merge_message_flags += ["-m", self.title]
+ if self.body is not None:
+ merge_message_flags += ["-m", self.body]
+
+ authors = "\n".join(["Author: %s" % a for a in distinct_authors])
+
+ merge_message_flags += ["-m", authors]
+
+ if had_conflicts:
+ committer_name = run_cmd("git config --get user.name").strip()
+ committer_email = run_cmd("git config --get user.email").strip()
+ message = ("This patch had conflicts when merged, "
+ "resolved by\nCommitter: %s <%s>" %
+ (committer_name, committer_email))
+ merge_message_flags += ["-m", message]
+
+ # The string "Closes #%s" string is required for GitHub to correctly
+ # close the PR
+ merge_message_flags += [
+ "-m",
+ "Closes #%s from %s and squashes the following commits:"
+ % (self.number, self.description)]
+ for c in commits:
+ stripped_message = strip_ci_directives(c).strip()
+ merge_message_flags += ["-m", stripped_message]
+
+ run_cmd(['git', 'commit',
+ '--no-verify', # do not run commit hooks
+ '--author="%s"' % primary_author] +
+ merge_message_flags)
+
+ self.cmd.continue_maybe("Merge complete (local ref %s). Push to %s?"
+ % (target_branch_name, self.git_remote))
+
+ try:
+ run_cmd('git push %s %s:%s' % (self.git_remote,
+ target_branch_name,
+ target_ref))
+ except Exception as e:
+ clean_up()
+ self.cmd.fail("Exception while pushing: %s" % e)
+
+ merge_hash = run_cmd("git rev-parse %s" % target_branch_name)[:8]
+ clean_up()
+ print("Pull request #%s merged!" % self.number)
+ print("Merge hash: %s" % merge_hash)
+ return merge_hash
-def check_jira(title):
- jira_id = extract_jira_id(title)
- try:
- ASF_JIRA.issue(jira_id)
- except Exception as e:
- fail("ASF JIRA could not find %s\n%s" % (jira_id, e))
+def cli():
+ # Location of your Arrow git clone
+ SEP = os.path.sep
+ ARROW_HOME = os.path.abspath(__file__).rsplit(SEP, 2)[0]
+ PROJECT_NAME = ARROW_HOME.rsplit(SEP, 1)[1]
+ print("ARROW_HOME = " + ARROW_HOME)
+ print("PROJECT_NAME = " + PROJECT_NAME)
+ cmd = CommandInput()
-def resolve_jira(title, merge_branches, comment):
- default_jira_id = extract_jira_id(title)
+ # ASF JIRA username
+ jira_username = os.environ.get("JIRA_USERNAME")
- jira_id = input("Enter a JIRA id [%s]: " % default_jira_id)
- if jira_id == "":
- jira_id = default_jira_id
+ # ASF JIRA password
+ jira_password = os.environ.get("JIRA_PASSWORD")
- try:
- issue = ASF_JIRA.issue(jira_id)
- except Exception as e:
- fail("ASF JIRA could not find %s\n%s" % (jira_id, e))
+ if not jira_username:
+ jira_username = cmd.prompt("Env JIRA_USERNAME not set, "
+ "please enter your JIRA username:")
- cur_status = issue.fields.status.name
- cur_summary = issue.fields.summary
- cur_assignee = issue.fields.assignee
- if cur_assignee is None:
- cur_assignee = "NOT ASSIGNED!!!"
- else:
- cur_assignee = cur_assignee.displayName
-
- if cur_status == "Resolved" or cur_status == "Closed":
- fail("JIRA issue %s already has status '%s'" % (jira_id, cur_status))
- print("=== JIRA %s ===" % jira_id)
- print("summary\t\t%s\nassignee\t%s\nstatus\t\t%s\nurl\t\t%s/%s\n"
- % (cur_summary, cur_assignee, cur_status, JIRA_BASE, jira_id))
-
- jira_fix_versions = _get_fix_version(merge_branches)
-
- resolve = [x for x in ASF_JIRA.transitions(jira_id)
- if x['name'] == "Resolve Issue"][0]
- ASF_JIRA.transition_issue(jira_id, resolve["id"], comment=comment,
- fixVersions=jira_fix_versions)
-
- print("Successfully resolved %s!" % (jira_id))
-
-
-def _get_fix_version(merge_branches):
- # Only suggest versions starting with a number, like 0.x but not JS-0.x
- mainline_version_regex = re.compile('\d.*')
- versions = [x for x in ASF_JIRA.project_versions("ARROW")
- if not x.raw['released'] and
- mainline_version_regex.match(x.name)]
-
- versions = sorted(versions, key=lambda x: x.name, reverse=True)
-
- default_fix_versions = [fix_version_from_branch(x, versions).name
- for x in merge_branches]
-
- for v in default_fix_versions:
- # Handles the case where we have forked a release branch but not yet
- # made the release. In this case, if the PR is committed to the master
- # branch and the release branch, we only consider the release branch to
- # be the fix version. E.g. it is not valid to have both 1.1.0 and 1.0.0
- # as fix versions.
- (major, minor, patch) = v.split(".")
- if patch == "0":
- previous = "%s.%s.%s" % (major, int(minor) - 1, 0)
- if previous in default_fix_versions:
- default_fix_versions = [x for x in default_fix_versions
- if x != v]
- default_fix_versions = ",".join(default_fix_versions)
+ if not jira_password:
+ jira_password = cmd.getpass("Env JIRA_PASSWORD not set, "
+ "please enter "
+ "your JIRA password:")
- fix_versions = input("Enter comma-separated fix version(s) [%s]: "
- % default_fix_versions)
- if fix_versions == "":
- fix_versions = default_fix_versions
- fix_versions = fix_versions.replace(" ", "").split(",")
+ pr_num = input("Which pull request would you like to merge? (e.g. 34): ")
- def get_version_json(version_str):
- return [x for x in versions if x.name == version_str][0].raw
+ # Remote name which points to the GitHub site
+ git_remote = os.environ.get("PR_REMOTE_NAME", "apache")
+
+ os.chdir(ARROW_HOME)
+
+ jira_con = jira.client.JIRA({'server': JIRA_API_BASE},
+ basic_auth=(jira_username, jira_password))
+ github_api = GitHubAPI(PROJECT_NAME)
- return [get_version_json(v) for v in fix_versions]
+ pr = PullRequest(cmd, github_api, git_remote, jira_con, pr_num)
+ if pr.is_merged:
+ print("Pull request %s has already been merged")
+ sys.exit(0)
-branches = get_json("%s/branches" % GITHUB_API_BASE)
-branch_names = [x['name'] for x in branches if x['name'].startswith('branch-')]
+ if not pr.is_mergeable:
+ msg = ("Pull request %s is not mergeable in its current form.\n"
+ % pr_num + "Continue? (experts only!)")
+ cmd.continue_maybe(msg)
-# Assumes branch names can be sorted lexicographically
-# Julien: I commented this out as we don't have any "branch-*" branch yet
-# latest_branch = sorted(branch_names, reverse=True)[0]
+ pr.show()
-pr_num = input("Which pull request would you like to merge? (e.g. 34): ")
-pr = get_json("%s/pulls/%s" % (GITHUB_API_BASE, pr_num))
+ cmd.continue_maybe("Proceed with merging pull request #%s?" % pr_num)
-url = pr["url"]
-title = pr["title"]
-check_jira(title)
-body = pr["body"]
-target_ref = pr["base"]["ref"]
-user_login = pr["user"]["login"]
-base_ref = pr["head"]["ref"]
-pr_repo_desc = "%s/%s" % (user_login, base_ref)
+ # merged hash not used
+ pr.merge()
-if pr["merged"] is True:
- print("Pull request %s has already been merged, "
- "assuming you want to backport" % pr_num)
- merge_commit_desc = run_cmd([
- 'git', 'log', '--merges', '--first-parent',
- '--grep=pull request #%s' % pr_num, '--oneline']).split("\n")[0]
- if merge_commit_desc == "":
- fail("Couldn't find any merge commit for #%s, "
- "you may need to update HEAD." % pr_num)
+ cmd.continue_maybe("Would you like to update the associated JIRA?")
+ jira_comment = (
+ "Issue resolved by pull request %s\n[%s/%s]"
+ % (pr_num,
+ "https://github.com/apache/" + PROJECT_NAME + "/pull",
+ pr_num))
- merge_hash = merge_commit_desc[:7]
- message = merge_commit_desc[8:]
+ versions, default_fix_versions = pr.jira_issue.get_candidate_fix_versions()
- print("Found: %s" % message)
- sys.exit(0)
+ default_fix_versions = ",".join(default_fix_versions)
-if not bool(pr["mergeable"]):
- msg = ("Pull request %s is not mergeable in its current form.\n"
- % pr_num + "Continue? (experts only!)")
- continue_maybe(msg)
+ issue_fix_versions = cmd.prompt("Enter comma-separated "
+ "fix version(s) [%s]: "
+ % default_fix_versions)
+ if issue_fix_versions == "":
+ issue_fix_versions = default_fix_versions
+ issue_fix_versions = issue_fix_versions.replace(" ", "").split(",")
-print("\n=== Pull Request #%s ===" % pr_num)
-print("title\t%s\nsource\t%s\ntarget\t%s\nurl\t%s"
- % (title, pr_repo_desc, target_ref, url))
-continue_maybe("Proceed with merging pull request #%s?" % pr_num)
+ def get_version_json(version_str):
+ return [x for x in versions if x.name == version_str][0].raw
-merged_refs = [target_ref]
+ fix_versions_json = [get_version_json(v) for v in issue_fix_versions]
+ pr.jira_issue.resolve(fix_versions_json, jira_comment)
-merge_hash = merge_pr(pr_num, target_ref)
-continue_maybe("Would you like to update the associated JIRA?")
-jira_comment = ("Issue resolved by pull request %s\n[%s/%s]"
- % (pr_num, GITHUB_BASE, pr_num))
-resolve_jira(title, merged_refs, jira_comment)
+if __name__ == '__main__':
+ try:
+ cli()
+ except Exception as e:
+ print(e.args[0])
diff --git a/dev/test_merge_arrow_pr.py b/dev/test_merge_arrow_pr.py
new file mode 100644
index 0000000..f69cafe
--- /dev/null
+++ b/dev/test_merge_arrow_pr.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from collections import namedtuple
+
+import pytest
+
+import merge_arrow_pr
+
+
+FakeIssue = namedtuple('issue', ['fields'])
+FakeFields = namedtuple('fields', ['status', 'summary', 'assignee'])
+FakeAssignee = namedtuple('assignee', ['displayName'])
+FakeStatus = namedtuple('status', ['name'])
+FakeProjectVersion = namedtuple('version', ['name', 'raw'])
+
+SOURCE_VERSIONS = [FakeProjectVersion('JS-0.4.0', {'released': False}),
+ FakeProjectVersion('0.11.0', {'released': False}),
+ FakeProjectVersion('0.12.0', {'released': False}),
+ FakeProjectVersion('0.10.0', {'released': True}),
+ FakeProjectVersion('0.9.0', {'released': True})]
+
+TRANSITIONS = [{'name': 'Resolve Issue', 'id': 1}]
+
+jira_id = 'ARROW-1234'
+status = FakeStatus('In Progress')
+fields = FakeFields(status, 'issue summary', FakeAssignee('groundhog'))
+FAKE_ISSUE_1 = FakeIssue(fields)
+
+
+class FakeJIRA:
+
+ def __init__(self, issue=None, project_versions=None, transitions=None):
+ self._issue = issue
+ self._project_versions = project_versions
+ self._transitions = transitions
+
+ def issue(self, jira_id):
+ return self._issue
+
+ def transitions(self, jira_id):
+ return self._transitions
+
+ def transition_issue(self, jira_id, transition_id, comment=None,
+ fixVersions=None):
+ self.captured_transition = {
+ 'jira_id': jira_id,
+ 'transition_id': transition_id,
+ 'comment': comment,
+ 'fixVersions': fixVersions
+ }
+
+ def project_versions(self, project):
+ return self._project_versions
+
+
+class FakeCLI:
+
+ def __init__(self, responses=()):
+ self.responses = responses
+ self.position = 0
+
+ def prompt(self, prompt):
+ response = self.responses[self.position]
+ self.position += 1
+ return response
+
+ def fail(self, msg):
+ raise Exception(msg)
+
+
+def test_jira_fix_versions():
+ jira = FakeJIRA(project_versions=SOURCE_VERSIONS,
+ transitions=TRANSITIONS)
+
+ issue = merge_arrow_pr.JiraIssue(jira, 'ARROW-1234', 'ARROW', FakeCLI())
+ all_versions, default_versions = issue.get_candidate_fix_versions()
+
+ expected = sorted([x for x in SOURCE_VERSIONS
+ if not x.raw['released']],
+ key=lambda x: x.name, reverse=True)
+ assert all_versions == expected
+ assert default_versions == ['0.11.0']
+
+
+def test_jira_invalid_issue():
+ class Mock:
+
+ def issue(self, jira_id):
+ raise Exception("not found")
+
+ with pytest.raises(Exception):
+ merge_arrow_pr.JiraIssue(Mock(), 'ARROW-1234', 'ARROW', FakeCLI())
+
+
+def test_jira_resolve():
+ jira = FakeJIRA(issue=FAKE_ISSUE_1,
+ project_versions=SOURCE_VERSIONS,
+ transitions=TRANSITIONS)
+
+ my_comment = 'my comment'
+ fix_versions = [SOURCE_VERSIONS[1].raw]
+
+ issue = merge_arrow_pr.JiraIssue(jira, 'ARROW-1234', 'ARROW', FakeCLI())
+ issue.resolve(fix_versions, my_comment)
+
+ assert jira.captured_transition == {
+ 'jira_id': 'ARROW-1234',
+ 'transition_id': 1,
+ 'comment': my_comment,
+ 'fixVersions': fix_versions
+ }
+
+
+def test_jira_resolve_non_mainline():
+ jira = FakeJIRA(issue=FAKE_ISSUE_1,
+ project_versions=SOURCE_VERSIONS,
+ transitions=TRANSITIONS)
+
+ my_comment = 'my comment'
+ fix_versions = [SOURCE_VERSIONS[0].raw]
+
+ issue = merge_arrow_pr.JiraIssue(jira, 'ARROW-1234', 'ARROW', FakeCLI())
+ issue.resolve(fix_versions, my_comment)
+
+ assert jira.captured_transition == {
+ 'jira_id': 'ARROW-1234',
+ 'transition_id': 1,
+ 'comment': my_comment,
+ 'fixVersions': fix_versions
+ }
+
+
+def test_jira_already_resolved():
+ status = FakeStatus('Resolved')
+ fields = FakeFields(status, 'issue summary', FakeAssignee('groundhog'))
+ issue = FakeIssue(fields)
+
+ jira = FakeJIRA(issue=issue,
+ project_versions=SOURCE_VERSIONS,
+ transitions=TRANSITIONS)
+
+ fix_versions = [SOURCE_VERSIONS[0].raw]
+ issue = merge_arrow_pr.JiraIssue(jira, 'ARROW-1234', 'ARROW', FakeCLI())
+
+ with pytest.raises(Exception,
+ match="ARROW-1234 already has status 'Resolved'"):
+ issue.resolve(fix_versions, "")