1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
#!/usr/bin/env python
"""
This script squashes a PR tagged with the "typo" label into a single, dedicated
"squash PR".
"""
import subprocess
import os
import re
def get_authors_and_emails_from_pr():
"""
For a given PR number, returns all contributing authors and their emails
for that PR. 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", "--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()
return [(author, mail) for author, mail in zip(authors, emails)]
def rebase_onto_pr(pr, squash_branch):
"""
Add all commits from PR into current branch. This is done by rebasing
current branch onto the PR.
"""
# Check out the pull request.
subprocess.call(["gh", "pr", "checkout", pr])
pr_branch_name = subprocess.check_output(
["git", "branch", "--show-current"], text=True
).strip()
# Change back to the original branch.
subprocess.call(["git", "switch", squash_branch])
# Rebase onto the pull request, aka include the commits in the pull
# request in the current branch.
subprocess.call(["git", "rebase", pr_branch_name])
def squash_all_commits():
"""
Squash all commits into a single commit. Credit all authors by name and
email.
"""
authors_and_emails = get_authors_and_emails_from_pr()
subprocess.call(["git", "reset", "--soft", f"{os.environ['GITHUB_BASE_REF']}"])
authors_and_emails = sorted(set(authors_and_emails))
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):
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 main():
squash_branch = "marvim/squash-typos"
all_pr_urls = ""
pr_number = re.sub(r"\D", "", os.environ["GITHUB_REF"])
show_ref_output = subprocess.check_output(["git", "show-ref"], text=True).strip()
if squash_branch in show_ref_output:
subprocess.call(
["git", "checkout", "-b", squash_branch, f"origin/{squash_branch}"]
)
squash_branch_exists = True
all_pr_urls += subprocess.check_output(
["gh", "pr", "view", "--json", "body", "--jq", ".body"], text=True
)
else:
subprocess.call(["git", "checkout", "-b", squash_branch])
squash_branch_exists = False
all_pr_urls += subprocess.check_output(
["gh", "pr", "view", pr_number, "--json", "url", "--jq", ".url"], text=True
).strip()
rebase_onto_pr(pr_number, squash_branch)
force_push(squash_branch)
subprocess.call(["gh", "pr", "close", pr_number])
squash_all_commits()
force_push(squash_branch)
if not squash_branch_exists:
subprocess.call(
[
"gh",
"pr",
"create",
"--fill",
"--head",
squash_branch,
"--title",
"Dedicated PR for all typo fixes.",
]
)
subprocess.call(["gh", "pr", "edit", "--add-label", "typo", "--body", all_pr_urls])
if __name__ == "__main__":
main()
|