-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathorphaned_backports.py
More file actions
172 lines (137 loc) · 4.38 KB
/
orphaned_backports.py
File metadata and controls
172 lines (137 loc) · 4.38 KB
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""
Find closed CPython issues with open backports.
They may have been forgotten and are candidates
for merging or closing.
"""
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "ghapi",
# "rich",
# "stamina",
# ]
# ///
from __future__ import annotations
import argparse
import os
import urllib
from typing import Any, TypeAlias
import stamina
from fastcore.xtras import obj2dict
from ghapi.all import GhApi, paged # pip install ghapi
from rich import print # pip install rich
from potential_closeable_issues import save_json, sort_by_to_sort_and_direction
PR: TypeAlias = dict[str, Any]
GITHUB_TOKEN = os.environ["GITHUB_TOOLS_TOKEN"]
def is_linked_issue_closed(api: GhApi, pr: PR) -> bool:
"""
Look for a chunk like this, collect the issue:
<!-- gh-issue-number: gh-79846 -->
* Issue: gh-79846
<!-- /gh-issue-number -->
"""
if pr["base"]["ref"] == "main":
# We only want backports
print(" [yellow]Not a backport[/yellow]")
return False
if not (pr.body and "gh-issue-number" in pr.body):
print(" [yellow]No body or linked issue[/yellow]")
return False
linked_issue = None
for line in pr.body.splitlines():
if line.startswith("<!-- gh-issue-number: gh-"):
linked_issue = line.removeprefix("<!-- gh-issue-number: gh-").removesuffix(
" -->"
)
break
if not linked_issue:
print(" [yellow]No linked issue found[/yellow]")
return False
issue = api.issues.get(linked_issue)
colour_state = (
"[green]open[/green]" if issue["state"] == "open" else "[red]closed[/red]"
)
print(" gh-" + linked_issue, colour_state, issue.html_url)
return issue["state"] == "closed"
@stamina.retry(on=urllib.error.HTTPError)
def stamina_paged(*args, **kwargs):
return paged(*args, **kwargs)
def check_prs(
start: int = 1,
number: int = 100,
author: str | None = None,
sort_by: str = "newest",
) -> list[PR]:
api = GhApi(owner="python", repo="cpython", token=GITHUB_TOKEN)
sort, direction = sort_by_to_sort_and_direction(sort_by)
candidates = []
pr_count = 0
for page in stamina_paged(
api.pulls.list,
state="open",
creator=author,
sort=sort,
direction=direction,
per_page=100,
):
for pr in page:
pr_count += 1
print(pr_count, start, number, pr.html_url)
if pr_count < start:
continue
if is_linked_issue_closed(api, pr):
candidates.append(pr)
if pr_count >= start + number - 1:
return candidates
return candidates
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-s", "--start", default=1, type=int, help="start at this PR number"
)
parser.add_argument(
"-n", "--number", default=100, type=int, help="number of PRs to check"
)
parser.add_argument("-a", "--author", help="PR author, blank for any")
parser.add_argument(
"--sort",
default="newest",
choices=(
"newest",
"oldest",
"most-commented",
"least-commented",
"recently-updated",
"least-recently-updated",
),
help="Sort by",
)
parser.add_argument("-j", "--json", action="store_true", help="output to JSON file")
parser.add_argument(
"-x", "--dry-run", action="store_true", help="show but don't open PRs"
)
args = parser.parse_args()
# Find
candidates = check_prs(args.start, args.number, args.author, args.sort)
# Report
print("\n[bold]Summary")
print(f"Found {len(candidates)} orphaned backports (parent issue closed)")
if candidates:
cmd = "open "
for pr in candidates:
print(f'\\[#{pr["number"]}]({pr["html_url"]}) {pr["title"]}')
cmd += f"{pr['html_url']} "
print()
print(cmd)
if not args.dry_run:
os.system(cmd)
if args.json:
data = {"candidates": [obj2dict(c) for c in candidates]}
# Use same name as this .py but with .json
filename = os.path.splitext(__file__)[0] + ".json"
save_json(data, filename)
if __name__ == "__main__":
main()