2021-08-09 06:21:50 -07:00
|
|
|
#!/usr/bin/env python
|
|
|
|
"""
|
|
|
|
|
|
|
|
This script squashes a PR tagged with the "typo" label into a single, dedicated
|
|
|
|
"squash PR".
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
import subprocess
|
2021-08-13 06:18:15 -07:00
|
|
|
import sys
|
2021-08-09 06:21:50 -07:00
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
|
|
def get_authors_and_emails_from_pr():
|
|
|
|
"""
|
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
Return all contributing authors and their emails for the PR on current branch.
|
|
|
|
This includes co-authors, meaning that if two authors are credited for a
|
|
|
|
single commit, which is possible with GitHub, then both will get credited.
|
2021-08-09 06:21:50 -07:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Get a list of all authors involved in the pull request (including co-authors).
|
|
|
|
authors = subprocess.check_output(
|
|
|
|
["gh", "pr", "view", "--json", "commits", "--jq", ".[][].authors.[].name"],
|
|
|
|
text=True,
|
|
|
|
).splitlines()
|
|
|
|
|
|
|
|
# Get a list of emails of the aforementioned authors.
|
|
|
|
emails = subprocess.check_output(
|
|
|
|
["gh", "pr", "view", "--json", "commits", "--jq", ".[][].authors.[].email"],
|
|
|
|
text=True,
|
|
|
|
).splitlines()
|
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
authors_and_emails_unique = {
|
|
|
|
(author, mail) for author, mail in zip(authors, emails)
|
|
|
|
}
|
2021-08-09 06:21:50 -07:00
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
return sorted(authors_and_emails_unique)
|
2021-08-09 06:21:50 -07:00
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
|
2021-08-13 10:17:24 -07:00
|
|
|
def rebase_squash_branch_onto_pr():
|
2021-08-09 06:21:50 -07:00
|
|
|
"""
|
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
Rebase current branch onto the PR.
|
2021-08-09 06:21:50 -07:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Check out the pull request.
|
2021-08-13 06:18:15 -07:00
|
|
|
subprocess.call(["gh", "pr", "checkout", os.environ["PR_NUMBER"]])
|
2021-08-09 06:21:50 -07:00
|
|
|
|
2021-08-13 10:17:24 -07:00
|
|
|
# Rebase onto master
|
|
|
|
default_branch = f"{os.environ['GITHUB_BASE_REF']}"
|
|
|
|
subprocess.check_call(["git", "rebase", default_branch])
|
|
|
|
|
2021-08-09 06:21:50 -07:00
|
|
|
# Change back to the original branch.
|
2021-08-13 06:18:15 -07:00
|
|
|
subprocess.call(["git", "switch", "-"])
|
2021-08-09 06:21:50 -07:00
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
# Rebase onto the pull request, aka include the commits in the pull request
|
|
|
|
# in the current branch. Abort with error message if rebase fails.
|
|
|
|
|
|
|
|
try:
|
|
|
|
subprocess.check_call(["git", "rebase", "-"])
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
subprocess.call(["git", "rebase", "--abort"])
|
|
|
|
squash_url = subprocess.check_output(
|
|
|
|
["gh", "pr", "view", "--json", "url", "--jq", ".url"], text=True
|
|
|
|
).strip()
|
|
|
|
|
|
|
|
subprocess.call(
|
|
|
|
[
|
|
|
|
"gh",
|
|
|
|
"pr",
|
|
|
|
"comment",
|
|
|
|
os.environ["PR_NUMBER"],
|
|
|
|
"--body",
|
2021-08-13 06:58:31 -07:00
|
|
|
f"Your edit conflicts with an already scheduled fix \
|
|
|
|
({squash_url}). Please check that batch PR whether your fix is \
|
|
|
|
already included; if not, then please wait until the batch PR \
|
|
|
|
is merged and then rebase your PR on top of master.",
|
2021-08-13 06:18:15 -07:00
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
sys.exit(
|
|
|
|
f"\n\nERROR: Your edit conflicts with an already scheduled fix \
|
|
|
|
{squash_url} \n\n"
|
|
|
|
)
|
2021-08-09 06:21:50 -07:00
|
|
|
|
|
|
|
|
2021-08-13 10:17:24 -07:00
|
|
|
def rebase_squash_branch_onto_master():
|
|
|
|
"""
|
|
|
|
|
|
|
|
Rebase current branch onto the master i.e. make sure current branch is up
|
|
|
|
to date. Abort on error.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
default_branch = f"{os.environ['GITHUB_BASE_REF']}"
|
|
|
|
subprocess.check_call(["git", "rebase", default_branch])
|
|
|
|
|
|
|
|
|
2021-08-09 06:21:50 -07:00
|
|
|
def squash_all_commits():
|
|
|
|
"""
|
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
Squash all commits on the PR into a single commit. Credit all authors by
|
|
|
|
name and email.
|
2021-08-09 06:21:50 -07:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
default_branch = f"{os.environ['GITHUB_BASE_REF']}"
|
|
|
|
subprocess.call(["git", "reset", "--soft", default_branch])
|
2021-08-09 06:21:50 -07:00
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
authors_and_emails = get_authors_and_emails_from_pr()
|
2021-08-09 06:21:50 -07:00
|
|
|
commit_message_coauthors = "\n" + "\n".join(
|
|
|
|
[f"Co-authored-by: {i[0]} <{i[1]}>" for i in authors_and_emails]
|
|
|
|
)
|
|
|
|
subprocess.call(
|
|
|
|
["git", "commit", "-m", "chore: typo fixes", "-m", commit_message_coauthors]
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def force_push(branch):
|
2021-08-13 06:18:15 -07:00
|
|
|
"""
|
|
|
|
|
|
|
|
Like the name implies, force push <branch>.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2021-08-09 06:21:50 -07:00
|
|
|
gh_actor = os.environ["GITHUB_ACTOR"]
|
|
|
|
gh_token = os.environ["GITHUB_TOKEN"]
|
|
|
|
gh_repo = os.environ["GITHUB_REPOSITORY"]
|
|
|
|
subprocess.call(
|
|
|
|
[
|
|
|
|
"git",
|
|
|
|
"push",
|
|
|
|
"--force",
|
|
|
|
f"https://{gh_actor}:{gh_token}@github.com/{gh_repo}",
|
|
|
|
branch,
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
def checkout_branch(branch):
|
|
|
|
"""
|
2021-08-09 06:21:50 -07:00
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
Create and checkout <branch>. Check if branch exists on remote, if so then
|
|
|
|
sync local branch to remote.
|
|
|
|
|
|
|
|
Return True if remote branch exists, else False.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
# FIXME I'm not sure why the local branch isn't tracking the remote branch
|
|
|
|
# automatically. This works but I'm pretty sure it can be done in a more
|
|
|
|
# "elegant" fashion
|
2021-08-09 06:21:50 -07:00
|
|
|
|
|
|
|
show_ref_output = subprocess.check_output(["git", "show-ref"], text=True).strip()
|
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
if branch in show_ref_output:
|
|
|
|
subprocess.call(["git", "checkout", "-b", branch, f"origin/{branch}"])
|
|
|
|
return True
|
|
|
|
|
|
|
|
subprocess.call(["git", "checkout", "-b", branch])
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def get_all_pr_urls(squash_branch_exists):
|
|
|
|
"""
|
|
|
|
|
|
|
|
Return a list of URLs for the pull requests with the typo fixes. If a
|
|
|
|
squash branch exists then extract the URLs from the body text.
|
|
|
|
|
|
|
|
"""
|
2021-08-09 06:21:50 -07:00
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
all_pr_urls = ""
|
|
|
|
if squash_branch_exists:
|
2021-08-09 06:21:50 -07:00
|
|
|
all_pr_urls += subprocess.check_output(
|
|
|
|
["gh", "pr", "view", "--json", "body", "--jq", ".body"], text=True
|
|
|
|
)
|
|
|
|
|
|
|
|
all_pr_urls += subprocess.check_output(
|
2021-08-13 06:18:15 -07:00
|
|
|
["gh", "pr", "view", os.environ["PR_NUMBER"], "--json", "url", "--jq", ".url"],
|
|
|
|
text=True,
|
2021-08-09 06:21:50 -07:00
|
|
|
).strip()
|
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
return all_pr_urls
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
squash_branch = "marvim/squash-typos"
|
|
|
|
|
|
|
|
squash_branch_exists = checkout_branch(squash_branch)
|
|
|
|
|
2021-08-13 10:17:24 -07:00
|
|
|
rebase_squash_branch_onto_master()
|
|
|
|
force_push(squash_branch)
|
|
|
|
|
|
|
|
rebase_squash_branch_onto_pr()
|
2021-08-09 06:21:50 -07:00
|
|
|
force_push(squash_branch)
|
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
subprocess.call(
|
|
|
|
[
|
|
|
|
"gh",
|
|
|
|
"pr",
|
|
|
|
"create",
|
|
|
|
"--fill",
|
|
|
|
"--head",
|
|
|
|
squash_branch,
|
|
|
|
"--title",
|
2021-08-13 06:58:31 -07:00
|
|
|
"chore: typo fixes (automated)",
|
2021-08-13 06:18:15 -07:00
|
|
|
]
|
|
|
|
)
|
2021-08-09 06:21:50 -07:00
|
|
|
|
|
|
|
squash_all_commits()
|
|
|
|
force_push(squash_branch)
|
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
all_pr_urls = get_all_pr_urls(squash_branch_exists)
|
2021-08-09 06:21:50 -07:00
|
|
|
subprocess.call(["gh", "pr", "edit", "--add-label", "typo", "--body", all_pr_urls])
|
|
|
|
|
2021-08-13 06:18:15 -07:00
|
|
|
subprocess.call(["gh", "pr", "close", os.environ["PR_NUMBER"]])
|
|
|
|
|
2021-08-09 06:21:50 -07:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|