mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			264 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			264 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/env python
 | |
| """
 | |
| 
 | |
| This script squashes a PR tagged with the "typo" label into a single, dedicated
 | |
| "squash PR".
 | |
| 
 | |
| """
 | |
| 
 | |
| import subprocess
 | |
| import sys
 | |
| import os
 | |
| 
 | |
| 
 | |
| def get_authors_and_emails_from_pr():
 | |
|     """
 | |
| 
 | |
|     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.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     # Get a list of all authors involved in the pull request (including co-authors).
 | |
|     authors = subprocess.check_output(
 | |
|         [
 | |
|             "gh",
 | |
|             "pr",
 | |
|             "view",
 | |
|             os.environ["PR_NUMBER"],
 | |
|             "--json",
 | |
|             "commits",
 | |
|             "--jq",
 | |
|             ".[][].authors.[].name",
 | |
|         ],
 | |
|         text=True,
 | |
|     ).splitlines()
 | |
| 
 | |
|     # Get a list of emails of the aforementioned authors.
 | |
|     emails = subprocess.check_output(
 | |
|         [
 | |
|             "gh",
 | |
|             "pr",
 | |
|             "view",
 | |
|             os.environ["PR_NUMBER"],
 | |
|             "--json",
 | |
|             "commits",
 | |
|             "--jq",
 | |
|             ".[][].authors.[].email",
 | |
|         ],
 | |
|         text=True,
 | |
|     ).splitlines()
 | |
| 
 | |
|     authors_and_emails = [(author, mail) for author, mail in zip(authors, emails)]
 | |
| 
 | |
|     return authors_and_emails
 | |
| 
 | |
| 
 | |
| def rebase_onto_pr():
 | |
|     """
 | |
| 
 | |
|     Rebase current branch onto the PR.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     # Check out the pull request.
 | |
|     subprocess.call(["gh", "pr", "checkout", os.environ["PR_NUMBER"]])
 | |
| 
 | |
|     rebase_onto_master()
 | |
| 
 | |
|     # Change back to the original branch.
 | |
|     subprocess.call(["git", "switch", "-"])
 | |
| 
 | |
|     # 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",
 | |
|                 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.",
 | |
|             ]
 | |
|         )
 | |
| 
 | |
|         sys.exit(
 | |
|             f"\n\nERROR: Your edit conflicts with an already scheduled fix \
 | |
| {squash_url} \n\n"
 | |
|         )
 | |
| 
 | |
| 
 | |
| def rebase_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])
 | |
| 
 | |
| 
 | |
| def squash_all_commits(message_body_before):
 | |
|     """
 | |
| 
 | |
|     Squash all commits on the PR into a single commit. Credit all authors by
 | |
|     name and email.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     default_branch = f"{os.environ['GITHUB_BASE_REF']}"
 | |
|     subprocess.call(["git", "reset", "--soft", default_branch])
 | |
| 
 | |
|     authors_and_emails = get_authors_and_emails_from_pr()
 | |
|     commit_message_coauthors = (
 | |
|         "\n"
 | |
|         + "\n".join([f"Co-authored-by: {i[0]} <{i[1]}>" for i in authors_and_emails])
 | |
|         + "\n"
 | |
|         + message_body_before
 | |
|     )
 | |
|     subprocess.call(
 | |
|         ["git", "commit", "-m", "chore: typo fixes", "-m", commit_message_coauthors]
 | |
|     )
 | |
| 
 | |
| 
 | |
| def force_push(branch):
 | |
|     """
 | |
| 
 | |
|     Like the name implies, force push <branch>.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     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,
 | |
|         ]
 | |
|     )
 | |
| 
 | |
| 
 | |
| def checkout_branch(branch):
 | |
|     """
 | |
| 
 | |
|     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
 | |
| 
 | |
|     show_ref_output = subprocess.check_output(["git", "show-ref"], text=True).strip()
 | |
| 
 | |
|     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(pr_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.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     all_pr_urls = ""
 | |
|     if pr_branch_exists:
 | |
|         all_pr_urls += subprocess.check_output(
 | |
|             ["gh", "pr", "view", "--json", "body", "--jq", ".body"], text=True
 | |
|         )
 | |
| 
 | |
|     all_pr_urls += subprocess.check_output(
 | |
|         ["gh", "pr", "view", os.environ["PR_NUMBER"], "--json", "url", "--jq", ".url"],
 | |
|         text=True,
 | |
|     ).strip()
 | |
| 
 | |
|     return all_pr_urls
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     pr_branch = "marvim/squash-typos"
 | |
| 
 | |
|     pr_branch_exists = checkout_branch(pr_branch)
 | |
| 
 | |
|     rebase_onto_master()
 | |
|     force_push(pr_branch)
 | |
| 
 | |
|     message_body_before = "\n".join(
 | |
|         subprocess.check_output(
 | |
|             ["git", "log", "--format=%B", "-n1", pr_branch], text=True
 | |
|         ).splitlines()[2:]
 | |
|     )
 | |
| 
 | |
|     rebase_onto_pr()
 | |
|     force_push(pr_branch)
 | |
| 
 | |
|     subprocess.call(
 | |
|         [
 | |
|             "gh",
 | |
|             "pr",
 | |
|             "create",
 | |
|             "--fill",
 | |
|             "--head",
 | |
|             pr_branch,
 | |
|             "--title",
 | |
|             "chore: typo fixes (automated)",
 | |
|         ],
 | |
|         text=True,
 | |
|     )
 | |
| 
 | |
|     squash_all_commits(message_body_before)
 | |
|     force_push(pr_branch)
 | |
| 
 | |
|     all_pr_urls = get_all_pr_urls(pr_branch_exists)
 | |
|     subprocess.call(["gh", "pr", "edit", "--add-label", "typo", "--body", all_pr_urls])
 | |
| 
 | |
|     subprocess.call(["gh", "pr", "close", os.environ["PR_NUMBER"]])
 | |
| 
 | |
|     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",
 | |
|             f"Thank you for your contribution! We collect all typo fixes \
 | |
|                 into a single pull request and merge it once it gets big enough: \
 | |
|                 {squash_url}",
 | |
|         ]
 | |
|     )
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     main()
 | 
