Skip to content

Commit

Permalink
ZOOKEEPER-4756: Merge script should use GitHub api to merge pull requ…
Browse files Browse the repository at this point in the history
…ests

Change-Id: I22ee835617fd96b540edd65191f6c83aae5365a9
  • Loading branch information
Villő Szűcs committed Nov 23, 2023
1 parent 75d0a0a commit 061c10a
Showing 1 changed file with 93 additions and 117 deletions.
210 changes: 93 additions & 117 deletions zk-merge-pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import sys
import urllib.request, urllib.error, urllib.parse
import getpass
import requests

try:
import jira.client
Expand Down Expand Up @@ -123,96 +124,68 @@ def get_current_branch():
return run_cmd("git rev-parse --abbrev-ref HEAD").replace("\n", "")

# 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" % (TEMP_BRANCH_PREFIX, pr_num)
target_branch_name = "%s_MERGE_PR_%s_%s" % (TEMP_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]

reviewers = input(
"Enter reviewers in the format of \"name1 <email1>, name2 <email2>\": ").strip()

commits = run_cmd(['git', 'log', 'HEAD..%s' % pr_branch_name,
'--pretty=format:%h [%an] %s']).split("\n")

if len(commits) > 1:
result = input("List pull request commits in squashed commit message? (y/n): ")
if result.lower().strip() == "y":
should_list_commits = True
else:
should_list_commits = False
def merge_pr(pr_num, title, pr_repo_desc):

# Retrieve the commits separately.
json_commits = get_json(f"https://api.github.com/repos/{PUSH_REMOTE_NAME}/{PROJECT_NAME}/pulls/{pr_num}/commits")
merge_message = []
if json_commits and isinstance(json_commits, list):
for commit in json_commits:
commit_message = commit['commit']['message']
merge_message += [commit_message]

# Check for disapproval reviews.
json_reviewers = get_json(f"https://api.github.com/repos/{PUSH_REMOTE_NAME}/{PROJECT_NAME}/pulls/{pr_num}/reviews")
disapproval_reviews = [review['user']['login'] for review in json_reviewers if review['state'] == 'CHANGES_REQUESTED']
if disapproval_reviews:
continue_maybe("Warning: There are requested changes. Proceed with merging pull request #%s?" % pr_num)
# Verify if there are no approved reviews.
approved_reviewers = [review['user']['login'] for review in json_reviewers if review['state'] == 'APPROVED']
if not approved_reviewers:
continue_maybe("Warning: Pull Request does not have an approved review. Proceed with merging pull request #%s?" % pr_num)
else:
should_list_commits = False

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 the project.
merge_message_flags += ["-m", body.replace("@", "")]

authors = "\n".join(["Author: %s" % a for a in distinct_authors])

merge_message_flags += ["-m", authors]

if (reviewers != ""):
merge_message_flags += ["-m", "Reviewers: %s" % reviewers]

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
reviewers_string = ', '.join(approved_reviewers)
merge_message += [f"Reviewers: {reviewers_string}"]
# Check the author and the closing line.
json_pr = get_json(f"https://api.github.com/repos/{PUSH_REMOTE_NAME}/{PROJECT_NAME}/pulls/{pr_num}")
primary_author = json_pr["user"]["login"]
if primary_author != "":
merge_message += [f"Author: {primary_author}"]
close_line = "Closes #%s from %s" % (pr_num, pr_repo_desc)
if should_list_commits:
close_line += " and squashes the following commits:"
merge_message_flags += ["-m", close_line]

if should_list_commits:
merge_message_flags += ["-m", "\n".join(commits)]

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

merge_message += [close_line]
merged_string = '\n'.join(merge_message)

# Get the latest commit SHA.
latest_commit_sha = json_pr["head"]["sha"]
json_status = get_json(f"https://api.github.com/repos/{PUSH_REMOTE_NAME}/{PROJECT_NAME}/statuses/{latest_commit_sha}")
# Check if all checks have passed on GitHub.
all_checks_passed = all(status["state"] == "success" for status in json_status)
if all_checks_passed:
print("All checks have passed on the github.")
else:
continue_maybe("Warning: Not all checks have passed on the github. Would you like to continue the merge? (y/n): ")

headers = {
"Authorization": f"token {GITHUB_OAUTH_KEY}",
"Accept": "application/vnd.github.v3+json"
}
data = {
"commit_title": title,
"commit_message": merged_string,
"merge_method": "squash"
}

response = requests.put(f"https://api.github.com/repos/{PUSH_REMOTE_NAME}/{PROJECT_NAME}/pulls/{pr_num}/merge", headers=headers, json=data)

if response.status_code == 200:
merge_response_json = response.json()
merge_commit_sha = merge_response_json.get("sha")
print(f"Pull request #{pr_num} merged. Sha: #{merge_commit_sha}")
return merge_commit_sha
else:
print(f"Failed to merge pull request #{pr_num}. Status code: {response.status_code}")
print(response.text)
exit()

def cherry_pick(pr_num, merge_hash, default_branch):
pick_ref = input("Enter a branch name [%s]: " % default_branch)
Expand All @@ -221,8 +194,8 @@ def cherry_pick(pr_num, merge_hash, default_branch):

pick_branch_name = "%s_PICK_PR_%s_%s" % (TEMP_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)
run_cmd("git fetch %s" % PUSH_REMOTE_NAME)
run_cmd("git checkout -b %s %s/%s" % (pick_branch_name, PUSH_REMOTE_NAME, pick_ref))

try:
run_cmd("git cherry-pick -sx %s" % merge_hash)
Expand Down Expand Up @@ -446,7 +419,35 @@ def main():

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))

# Check if the pull request has already been closed or merged.
pull_request_state = pr.get("state", "")
if pull_request_state == "closed":
merge_hash = pr.get("merge_commit_sha", "")
merged = pr.get("merged")
# Verify if the pull request has been merged by the GitHub API.
if merged is True:
print(f"Pull request #{pr['number']} has already been merged, assuming you want to backport")
cherry_pick(pr_num, merge_hash, latest_branch)
sys.exit(0)
# Some merged pull requests may not appear as merged in the GitHub API,
# for example, those closed by an older version of this script.
else:
pr_events = get_json("%s/issues/%s/events" % (GITHUB_API_BASE, pr_num))
for event in pr_events:
if event.get("event") == "closed":
commit_id = event.get("commit_id")
if commit_id is not None:
print(f"Pull request #{pr['number']} has already been merged, assuming you want to backport")
cherry_pick(pr_num, merge_hash, latest_branch)
sys.exit(0)
else:
print(f"Pull request #{pr['number']} has already been closed, but not merged, exiting.")
exit()

if not bool(pr["mergeable"]):
print(f"Pull request %s is not mergeable in its current form.\n" % pr_num)
exit()

url = pr["url"]

Expand All @@ -469,44 +470,19 @@ def main():
print("Using original title:")
print(commit_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 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["actor"]["login"] == "asfgit" and e["event"] == "closed"]

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(("PR title\t%s\nCommit title\t%s\nSource\t\t%s\nTarget\t\t%s\nURL\t\t%s" % (
pr_title, commit_title, pr_repo_desc, target_ref, url)))
continue_maybe("Proceed with merging pull request #%s?" % pr_num)

merged_refs = [target_ref]

merge_hash = merge_pr(pr_num, target_ref, commit_title, body, pr_repo_desc)
merge_hash = merge_pr(pr_num, commit_title, 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().strip() == "y":
Expand Down

0 comments on commit 061c10a

Please sign in to comment.