# 🔍 Description ## Issue References 🔗 This pull request fixes # ## Describe Your Solution 🔧 Preparing v1.11.0-SNAPSHOT after branch-1.10 cut ```shell build/mvn versions:set -DgenerateBackupPoms=false -DnewVersion="1.11.0-SNAPSHOT" (cd kyuubi-server/web-ui && npm version "1.11.0-SNAPSHOT") ``` ## Types of changes 🔖 - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Test Plan 🧪 #### Behavior Without This Pull Request ⚰️ #### Behavior With This Pull Request 🎉 #### Related Unit Tests --- # Checklist 📝 - [ ] This patch was not authored or co-authored using [Generative Tooling](https://www.apache.org/legal/generative-tooling.html) **Be nice. Be informative.** Closes #6769 from bowenliang123/bump-1.11. Closes #6769 6db219d28 [Bowen Liang] get latest_branch by sorting version in branch name 465276204 [Bowen Liang] update package.json 81f2865e5 [Bowen Liang] bump Authored-by: Bowen Liang <liangbowen@gf.com.cn> Signed-off-by: Bowen Liang <liangbowen@gf.com.cn>
314 lines
12 KiB
Python
Executable File
314 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
#
|
|
# 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.
|
|
#
|
|
|
|
# Utility for creating well-formed pull request merges and pushing them to Kyuubi
|
|
# For committers:
|
|
# Please check your local git envs via `git remote -v` which should
|
|
# apache git@github.com:apache/kyuubi.git (fetch)
|
|
# apache git@github.com:apache/kyuubi.git (push)
|
|
# origin git@github.com:[ YOUR GITHUB USER NAME ]/kyuubi.git (fetch)
|
|
# origin git@github.com:[ YOUR GITHUB USER NAME ]/kyuubi.git (push)
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from urllib.error import HTTPError
|
|
from urllib.request import Request
|
|
from urllib.request import urlopen
|
|
|
|
KYUUBI_HOME = os.environ.get("KYUUBI_HOME", os.getcwd())
|
|
PR_REMOTE_NAME = os.environ.get("PR_REMOTE_NAME", "apache")
|
|
PUSH_REMOTE_NAME = os.environ.get("PUSH_REMOTE_NAME", "apache")
|
|
GITHUB_OAUTH_KEY = os.environ.get("GITHUB_OAUTH_KEY")
|
|
GITHUB_API_BASE = "https://api.github.com/repos/apache/kyuubi"
|
|
BRANCH_PREFIX = "PR_TOOL"
|
|
|
|
|
|
def get_json(url):
|
|
try:
|
|
request = Request(url)
|
|
if GITHUB_OAUTH_KEY:
|
|
request.add_header('Authorization', 'token %s' % GITHUB_OAUTH_KEY)
|
|
return json.load(urlopen(request))
|
|
except HTTPError as e:
|
|
if "X-RateLimit-Remaining" in e.headers and e.headers["X-RateLimit-Remaining"] == '0':
|
|
print("Exceeded the GitHub API rate limit; see the instructions in " +
|
|
"dev/merge_kyuubi_pr.py to configure an OAuth token for making authenticated " +
|
|
"GitHub requests.")
|
|
else:
|
|
print("Unable to fetch URL, exiting: %s" % url, e)
|
|
sys.exit(-1)
|
|
|
|
|
|
def fail(msg):
|
|
print(msg)
|
|
clean_up()
|
|
sys.exit(-1)
|
|
|
|
|
|
def run_cmd(cmd):
|
|
print(cmd)
|
|
if isinstance(cmd, list):
|
|
return subprocess.check_output(cmd).decode('utf-8')
|
|
else:
|
|
return subprocess.check_output(cmd.split(" ")).decode('utf-8')
|
|
|
|
|
|
def continue_maybe(prompt):
|
|
result = input("\n%s (y/n): " % prompt)
|
|
if result.lower() != "y":
|
|
fail("Okay, exiting")
|
|
|
|
|
|
def clean_up():
|
|
if 'original_head' in globals():
|
|
print("Restoring head pointer to %s" % original_head)
|
|
run_cmd("git checkout %s" % original_head)
|
|
|
|
branches = run_cmd("git branch").replace(" ", "").split("\n")
|
|
|
|
for branch in list(filter(lambda x: x.startswith(BRANCH_PREFIX), branches)):
|
|
print("Deleting local branch %s" % branch)
|
|
run_cmd("git branch -D %s" % branch)
|
|
|
|
def fix_title(text, num):
|
|
if (re.search(r'^\[KYUUBI\s#[0-9]{3,6}\].*', text)):
|
|
return text
|
|
|
|
return '[KYUUBI #%s] %s' % (num, text)
|
|
|
|
# merge the requested PR and return the merge hash
|
|
def merge_pr(pr_num, target_ref, title, body, pr_repo_desc):
|
|
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 = input(
|
|
"Enter primary author in the format of \"name <email>\" [%s]: " %
|
|
distinct_authors[0])
|
|
if primary_author == "":
|
|
primary_author = distinct_authors[0]
|
|
else:
|
|
# When primary author is specified manually, de-dup it from author list and
|
|
# put it at the head of author list.
|
|
distinct_authors = list(filter(lambda x: x != primary_author, distinct_authors))
|
|
distinct_authors.insert(0, primary_author)
|
|
|
|
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:
|
|
# We remove @ symbols from the body to avoid triggering e-mails
|
|
# to people every time someone creates a public fork of Kyuubi.
|
|
merge_message_flags += ["-m", body.replace("@", "")]
|
|
|
|
committer_name = run_cmd("git config --get user.name").strip()
|
|
committer_email = run_cmd("git config --get user.email").strip()
|
|
|
|
if had_conflicts:
|
|
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." % (pr_num, pr_repo_desc)]
|
|
|
|
for issueId in re.findall("KYUUBI #[0-9]{3,5}", title):
|
|
merge_message_flags += ["-m", issueId.replace("KYUUBI", "Closes")]
|
|
|
|
for c in commits:
|
|
merge_message_flags += ["-m", c]
|
|
|
|
authors = "Authored-by:" if len(distinct_authors) == 1 else "Lead-authored-by:"
|
|
authors += " %s" % (distinct_authors.pop(0))
|
|
if len(distinct_authors) > 0:
|
|
authors += "\n" + "\n".join(["Co-authored-by: %s" % a for a in distinct_authors])
|
|
authors += "\n" + "Signed-off-by: %s <%s>" % (committer_name, committer_email)
|
|
|
|
merge_message_flags += ["-m", authors]
|
|
|
|
run_cmd(['git', 'commit', '--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
|
|
|
|
|
|
def cherry_pick(pr_num, merge_hash, default_branch):
|
|
pick_ref = input("Enter a branch name [%s]: " % default_branch)
|
|
if pick_ref == "":
|
|
pick_ref = default_branch
|
|
|
|
pick_branch_name = "%s_PICK_PR_%s_%s" % (BRANCH_PREFIX, pr_num, pick_ref.upper())
|
|
|
|
run_cmd("git fetch %s %s:%s" % (PUSH_REMOTE_NAME, pick_ref, pick_branch_name))
|
|
run_cmd("git checkout %s" % pick_branch_name)
|
|
|
|
try:
|
|
run_cmd("git cherry-pick -sx %s" % merge_hash)
|
|
except Exception as e:
|
|
msg = "Error cherry-picking: %s\nWould you like to manually fix-up this merge?" % e
|
|
continue_maybe(msg)
|
|
msg = "Okay, please fix any conflicts and finish the cherry-pick. Finished?"
|
|
continue_maybe(msg)
|
|
|
|
continue_maybe("Pick complete (local ref %s). Push to %s?" % (
|
|
pick_branch_name, PUSH_REMOTE_NAME))
|
|
|
|
try:
|
|
run_cmd('git push %s %s:%s' % (PUSH_REMOTE_NAME, pick_branch_name, pick_ref))
|
|
except Exception as e:
|
|
clean_up()
|
|
fail("Exception while pushing: %s" % e)
|
|
|
|
pick_hash = run_cmd("git rev-parse %s" % pick_branch_name)[:8]
|
|
clean_up()
|
|
|
|
print("Pull request #%s picked into %s!" % (pr_num, pick_ref))
|
|
print("Pick hash: %s" % pick_hash)
|
|
return pick_ref
|
|
|
|
def get_current_ref():
|
|
ref = run_cmd("git rev-parse --abbrev-ref HEAD").strip()
|
|
if ref == 'HEAD':
|
|
# The current ref is a detached HEAD, so grab its SHA.
|
|
return run_cmd("git rev-parse HEAD").strip()
|
|
else:
|
|
return ref
|
|
|
|
|
|
def main():
|
|
global original_head
|
|
|
|
os.chdir(KYUUBI_HOME)
|
|
original_head = get_current_ref()
|
|
|
|
branches = get_json("%s/branches" % GITHUB_API_BASE)
|
|
branch_names = list(filter(lambda x: x.startswith("branch-"), [x['name'] for x in branches]))
|
|
# Assumes branch names can be sorted lexicographically
|
|
def sort_by_version(branch_name):
|
|
return tuple(map(int, branch_name.split('-')[1].split('.')))
|
|
latest_branch = sorted(branch_names, key=sort_by_version, reverse=True)[0]
|
|
|
|
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))
|
|
pr_events = get_json("%s/issues/%s/events" % (GITHUB_API_BASE, pr_num))
|
|
|
|
url = pr["url"]
|
|
title = pr["title"]
|
|
title = fix_title(title, pr_num)
|
|
body = re.sub(re.compile(r"<!--[^>]*-->\n?", re.DOTALL), "", pr["body"]).lstrip()
|
|
target_ref = pr["base"]["ref"]
|
|
user_login = pr["user"]["login"]
|
|
base_ref = pr["head"]["ref"]
|
|
pr_repo_desc = "%s/%s" % (user_login, base_ref)
|
|
assignees = pr["assignees"]
|
|
milestone = pr["milestone"]
|
|
|
|
# Merged pull requests don't appear as merged in the GitHub API;
|
|
# Instead, they're closed by asfgit.
|
|
merge_commits = \
|
|
[e for e in pr_events if e["event"] == "closed" and e["commit_id"]]
|
|
|
|
if merge_commits:
|
|
merge_hash = merge_commits[0]["commit_id"]
|
|
message = get_json("%s/commits/%s" % (GITHUB_API_BASE, merge_hash))["commit"]["message"]
|
|
|
|
print("Pull request %s has already been merged, assuming you want to backport" % pr_num)
|
|
commit_is_downloaded = run_cmd(['git', 'rev-parse', '--quiet', '--verify',
|
|
"%s^{commit}" % merge_hash]).strip() != ""
|
|
if not commit_is_downloaded:
|
|
fail("Couldn't find any merge commit for #%s, you may need to update HEAD." % pr_num)
|
|
|
|
print("Found commit %s:\n%s" % (merge_hash, message))
|
|
cherry_pick(pr_num, merge_hash, latest_branch)
|
|
sys.exit(0)
|
|
|
|
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)
|
|
|
|
print("\n=== Pull Request #%s ===" % pr_num)
|
|
print("title:\t%s\nsource:\t%s\ntarget:\t%s\nurl:\t%s\nbody:\n\n%s" %
|
|
(title, pr_repo_desc, target_ref, url, body))
|
|
|
|
if assignees is None or len(assignees)==0:
|
|
continue_maybe("Assignees have NOT been set. Continue?")
|
|
else:
|
|
print("assignees: %s" % [assignee["login"] for assignee in assignees])
|
|
|
|
if milestone is None:
|
|
continue_maybe("Milestone has NOT been set. Continue?")
|
|
else:
|
|
print("milestone: %s" % milestone["title"])
|
|
|
|
continue_maybe("Proceed with merging pull request #%s?" % pr_num)
|
|
|
|
merged_refs = [target_ref]
|
|
|
|
merge_hash = merge_pr(pr_num, target_ref, title, body, pr_repo_desc)
|
|
|
|
pick_prompt = "Would you like to pick %s into another branch?" % merge_hash
|
|
while input("\n%s (y/n): " % pick_prompt).lower() == "y":
|
|
merged_refs = merged_refs + [cherry_pick(pr_num, merge_hash, latest_branch)]
|
|
|
|
if __name__ == "__main__":
|
|
import doctest
|
|
(failure_count, test_count) = doctest.testmod()
|
|
if failure_count:
|
|
sys.exit(-1)
|
|
try:
|
|
main()
|
|
except:
|
|
clean_up()
|
|
raise
|