-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnew-contributors.py
executable file
·248 lines (224 loc) · 7.47 KB
/
new-contributors.py
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/env python3
import argparse
import json
import sys
import urllib.error as url_error
import urllib.parse as url_parse
import urllib.request as url_request
from pathlib import Path
from typing import Any
# Generates a list of new contributors to Desktop Firefox for a given version.
PRODUCTS = [
"Core",
"Developer Infrastructure",
"DevTools",
"Firefox",
"Firefox Build System",
"NSPR",
"NSS",
"Remote Protocol",
"Testing",
"Toolkit",
"Web Compatibility",
"WebExtensions",
]
class Error(Exception):
"""throwing this won't generate a stack trace"""
def plural(count: int, item: str, *, suffix: str = "s") -> str:
return f"{count:,d} {item}{'' if count == 1 else suffix}"
def bmo_request(
end_point: str,
query: dict[str, Any],
*,
api_key: str | None = None,
) -> Any:
# dict to query-string
query_args = []
for name, value in query.items():
if isinstance(value, list):
query_args.extend((name, v) for v in value)
else:
query_args.append((name, value))
query_encoded = url_parse.urlencode(query_args)
# build request
req = url_request.Request(
f"https://bugzilla.mozilla.org/rest/{end_point}?{query_encoded}",
headers={
"User-Agent": "new-contributors",
"X-BUGZILLA-API-KEY": api_key if api_key else "",
},
)
# return json response
try:
with url_request.urlopen(req) as r:
res = json.load(r)
return res
except url_error.HTTPError as e:
try:
res = json.load(e.fp)
raise Error(res["message"])
except (OSError, ValueError, KeyError):
raise Error(e)
def main() -> None:
# parse command line arguments
parser = argparse.ArgumentParser()
parser.add_argument(
"version",
type=int,
help="Firefox version",
)
parser.add_argument(
"--api-key",
"--apikey",
required=True,
help="Bugzilla API-Key",
)
args = parser.parse_args()
if args.version < 0:
raise Error(f"Invalid version: {args.version}")
# load cache
# store a list of users against each version that were determined to have patches
# landed _prior_ to the specified version
cache_file = Path(__file__).parent / "new-contributors.cache"
try:
with cache_file.open() as f:
cache = json.load(f)
except (FileNotFoundError, ValueError):
cache = []
current_cache = None
for cache_item in cache:
if cache_item["version"] == args.version:
current_cache = cache_item
break
if not current_cache:
current_cache = {"version": args.version, "skip": []}
cache.append(current_cache)
# find bugs fixed in specified version
print(f"looking for bugs fixed in Firefox {args.version}", file=sys.stderr)
bugs = bmo_request(
"bug",
{
"target_milestone": f"{args.version} Branch",
"status": "RESOLVED",
"product": PRODUCTS,
"include_fields": "id,assigned_to,cf_last_resolved",
"order": "cf_last_resolved",
},
api_key=args.api_key,
)["bugs"]
print(f"found {plural(len(bugs), 'bug')}", file=sys.stderr)
if not bugs:
return
# find new assignees
new = {}
for bug in bugs:
assignee = bug["assigned_to"]
# skip users that are clearly employees or contractors
if assignee.endswith(
(
"@getpocket.com",
"@mozilla.com",
"@mozilla.org",
"@mozillafoundation.org",
"@softvision.com",
"@softvision.ro",
"@softvisioninc.eu",
)
):
continue
# skip users we already know are not new
should_skip = False
for cache_item in cache:
if cache_item["version"] <= args.version and assignee in cache_item["skip"]:
should_skip = True
break
if should_skip:
continue
# handle users that we know are new and fixed more than one bug
if assignee in new:
new[assignee]["bugs"].append(bug["id"])
continue
print(f"checking {assignee}", file=sys.stderr, end="")
# always exclude employees; this is quicker than a bug search, and not
# all bugs have correct metadata
users = bmo_request(
"user",
{"names": assignee},
api_key=args.api_key,
)["users"]
is_employee = False
if users:
for group in users[0]["groups"]:
if group["name"] == "mozilla-employee-confidential":
is_employee = True
break
if is_employee:
print(" employee", file=sys.stderr)
current_cache["skip"].append(assignee)
continue
print(" contributor", file=sys.stderr, end="")
# query for bugs fixed by this user before this one
prior_bugs = bmo_request(
"bug",
{
# resolved bugs in our products
"product": PRODUCTS,
"status": "RESOLVED",
# assigned to our user
"emailassigned_to1": "1",
"emailtype1": "exact",
"email1": assignee,
# where the last resolved is older than this bug's
"f1": "cf_last_resolved",
"o1": "lessthan",
"v1": bug["cf_last_resolved"].replace("T", " ").replace("Z", ""),
# and a target_milestone is set (filter our duplicates, etc)
"f2": "target_milestone",
"o2": "notequals",
"v2": "---",
# don't need the full list or count, just need to know if there's any
"limit": 1,
},
api_key=args.api_key,
)["bugs"]
if prior_bugs:
print(" existing", file=sys.stderr)
current_cache["skip"].append(assignee)
continue
# collate in `new` dict
print(" new", file=sys.stderr)
new.setdefault(
assignee,
{
"name": bug["assigned_to_detail"]["real_name"]
or bug["assigned_to_detail"]["nick"],
"bugs": [],
},
)
new[assignee]["bugs"].append(bug["id"])
print(f"found {plural(len(new), 'new contributor')}", file=sys.stderr)
# update cache
with cache_file.open("w") as f:
json.dump(cache, f, indent=2, sort_keys=True)
# generate nucleus output
print(
f"With the release of Firefox {args.version}, we are pleased to welcome "
"the developers who contributed their first code change to Firefox in "
f"this release, {len(new)} of whom were brand new volunteers! Please "
"join us in thanking each of these diligent and enthusiastic "
"individuals, and take a look at their contributions:\n"
)
for user in sorted(new.values(), key=lambda u: u["name"].lower()):
bug_links = ", ".join(
f'<a href="https://bugzilla.mozilla.org/{b}">{b}</a>'
for b in sorted(user["bugs"])
)
print(f"* {user['name']}: {bug_links}")
if __name__ == "__main__":
try:
main()
except Error as error:
print(error, file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
sys.exit(2)