]>
Commit | Line | Data |
---|---|---|
1a3c85fe ML |
1 | #!/usr/bin/env python3 |
2 | ||
21da5261 ML |
3 | # This script is used by maintainers to modify Bugzilla entries in batch |
4 | # mode. | |
5 | # Currently it can remove and add a release from/to PRs that are prefixed | |
6 | # with '[x Regression]'. Apart from that, it can also change target | |
7 | # milestones and optionally enhance the list of known-to-fail versions. | |
8 | # | |
9 | # The script utilizes the Bugzilla API, as documented here: | |
10 | # http://bugzilla.readthedocs.io/en/latest/api/index.html | |
11 | # | |
12 | # It requires the simplejson, requests, semantic_version packages. | |
13 | # In case of openSUSE: | |
14 | # zypper in python3-simplejson python3-requests | |
15 | # pip3 install semantic_version | |
16 | # | |
17 | # Sample usages of the script: | |
18 | # | |
9e07b0bf ML |
19 | # $ ./maintainer-scripts/branch_changer.py api_key --new-target-milestone=6.2:6.3 \ |
20 | # --comment '6.2 has been released....' --add-known-to-fail=6.2 --limit 3 | |
21da5261 ML |
21 | # |
22 | # The invocation will set target milestone to 6.3 for all issues that | |
23 | # have mistone equal to 6.2. Apart from that, a comment is added to these | |
24 | # issues and 6.2 version is added to known-to-fail versions. | |
25 | # At maximum 3 issues will be modified and the script will run | |
26 | # in dry mode (no issues are modified), unless you append --doit option. | |
27 | # | |
9e07b0bf ML |
28 | # $ ./maintainer-scripts/branch_changer.py api_key --new-target-milestone=5.5:6.3 \ |
29 | # --comment 'GCC 5 branch is being closed' --remove 5 --limit 3 | |
21da5261 ML |
30 | # |
31 | # Very similar to previous invocation, but instead of adding to known-to-fail, | |
32 | # '5' release is removed from all issues that have the regression prefix. | |
2b2f687f ML |
33 | # NOTE: If the version 5 is the only one in regression marker ([5 Regression] ...), |
34 | # then the bug summary is not modified. | |
35 | # | |
36 | # NOTE: If we change target milestone in between releases and the PR does not | |
37 | # regress in the new branch, then target milestone change is skipped: | |
38 | # | |
39 | # not changing target milestone: not a regression or does not regress with the new milestone | |
21da5261 ML |
40 | # |
41 | # $ ./maintainer-scripts/branch_changer.py api_key --add=7:8 | |
42 | # | |
43 | # Aforementioned invocation adds '8' release to the regression prefix of all | |
44 | # issues that contain '7' in its regression prefix. | |
45 | # | |
1a3c85fe | 46 | |
1a3c85fe | 47 | import argparse |
9e07b0bf | 48 | import json |
1a3c85fe | 49 | import re |
7d7481ec | 50 | import sys |
1a3c85fe | 51 | |
9e07b0bf ML |
52 | import requests |
53 | ||
a7ce23ce | 54 | from semantic_version import Version |
1a3c85fe ML |
55 | |
56 | base_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/' | |
57 | statuses = ['UNCONFIRMED', 'ASSIGNED', 'SUSPENDED', 'NEW', 'WAITING', 'REOPENED'] | |
58 | search_summary = ' Regression]' | |
9e07b0bf ML |
59 | regex = r'(.*\[)([0-9\./]*)( [rR]egression])(.*)' |
60 | ||
1a3c85fe ML |
61 | |
62 | class Bug: | |
63 | def __init__(self, data): | |
64 | self.data = data | |
65 | self.versions = None | |
66 | self.fail_versions = [] | |
67 | self.is_regression = False | |
68 | ||
69 | self.parse_summary() | |
70 | self.parse_known_to_fail() | |
71 | ||
72 | def parse_summary(self): | |
73 | m = re.match(regex, self.data['summary']) | |
9e07b0bf | 74 | if m: |
1a3c85fe ML |
75 | self.versions = m.group(2).split('/') |
76 | self.is_regression = True | |
77 | self.regex_match = m | |
78 | ||
79 | def parse_known_to_fail(self): | |
80 | v = self.data['cf_known_to_fail'].strip() | |
81 | if v != '': | |
82 | self.fail_versions = [x for x in re.split(' |,', v) if x != ''] | |
83 | ||
84 | def name(self): | |
a0bb9b3b ML |
85 | bugid = self.data['id'] |
86 | url = f'https://gcc.gnu.org/bugzilla/show_bug.cgi?id={bugid}' | |
7d7481ec ML |
87 | if sys.stdout.isatty(): |
88 | return f'\u001b]8;;{url}\u001b\\PR{bugid}\u001b]8;;\u001b\\ ({self.data["summary"]})' | |
89 | else: | |
90 | return f'PR{bugid} ({self.data["summary"]})' | |
1a3c85fe ML |
91 | |
92 | def remove_release(self, release): | |
1a3c85fe ML |
93 | self.versions = list(filter(lambda x: x != release, self.versions)) |
94 | ||
95 | def add_release(self, releases): | |
96 | parts = releases.split(':') | |
97 | assert len(parts) == 2 | |
98 | for i, v in enumerate(self.versions): | |
99 | if v == parts[0]: | |
100 | self.versions.insert(i + 1, parts[1]) | |
101 | break | |
102 | ||
103 | def add_known_to_fail(self, release): | |
104 | if release in self.fail_versions: | |
105 | return False | |
106 | else: | |
107 | self.fail_versions.append(release) | |
108 | return True | |
109 | ||
110 | def update_summary(self, api_key, doit): | |
a0bb9b3b ML |
111 | if not self.versions: |
112 | print(self.name()) | |
113 | print(' not changing summary, candidate for CLOSING') | |
114 | return False | |
115 | ||
1a3c85fe ML |
116 | summary = self.data['summary'] |
117 | new_summary = self.serialize_summary() | |
118 | if new_summary != summary: | |
119 | print(self.name()) | |
2b2f687f | 120 | print(' changing summary to "%s"' % (new_summary)) |
1a3c85fe | 121 | self.modify_bug(api_key, {'summary': new_summary}, doit) |
1a3c85fe ML |
122 | return True |
123 | ||
124 | return False | |
125 | ||
126 | def change_milestone(self, api_key, old_milestone, new_milestone, comment, new_fail_version, doit): | |
127 | old_major = Bug.get_major_version(old_milestone) | |
128 | new_major = Bug.get_major_version(new_milestone) | |
129 | ||
130 | print(self.name()) | |
131 | args = {} | |
132 | if old_major == new_major: | |
133 | args['target_milestone'] = new_milestone | |
134 | print(' changing target milestone: "%s" to "%s" (same branch)' % (old_milestone, new_milestone)) | |
135 | elif self.is_regression and new_major in self.versions: | |
136 | args['target_milestone'] = new_milestone | |
9e07b0bf ML |
137 | print(' changing target milestone: "%s" to "%s" (regresses with the new milestone)' |
138 | % (old_milestone, new_milestone)) | |
1a3c85fe ML |
139 | else: |
140 | print(' not changing target milestone: not a regression or does not regress with the new milestone') | |
141 | ||
9e07b0bf | 142 | if 'target_milestone' in args and comment: |
1a3c85fe | 143 | print(' adding comment: "%s"' % comment) |
9e07b0bf | 144 | args['comment'] = {'comment': comment} |
1a3c85fe | 145 | |
9e07b0bf | 146 | if new_fail_version: |
1a3c85fe ML |
147 | if self.add_known_to_fail(new_fail_version): |
148 | s = self.serialize_known_to_fail() | |
149 | print(' changing known_to_fail: "%s" to "%s"' % (self.data['cf_known_to_fail'], s)) | |
150 | args['cf_known_to_fail'] = s | |
151 | ||
152 | if len(args.keys()) != 0: | |
153 | self.modify_bug(api_key, args, doit) | |
154 | return True | |
155 | else: | |
156 | return False | |
157 | ||
158 | def serialize_summary(self): | |
855ce475 | 159 | assert self.versions |
9e07b0bf | 160 | assert self.is_regression |
1a3c85fe ML |
161 | |
162 | new_version = '/'.join(self.versions) | |
163 | new_summary = self.regex_match.group(1) + new_version + self.regex_match.group(3) + self.regex_match.group(4) | |
164 | return new_summary | |
165 | ||
a7ce23ce ML |
166 | @staticmethod |
167 | def to_version(version): | |
168 | if len(version.split('.')) == 2: | |
169 | version += '.0' | |
170 | return Version(version) | |
171 | ||
1a3c85fe ML |
172 | def serialize_known_to_fail(self): |
173 | assert type(self.fail_versions) is list | |
a7ce23ce | 174 | return ', '.join(sorted(self.fail_versions, key=self.to_version)) |
1a3c85fe ML |
175 | |
176 | def modify_bug(self, api_key, params, doit): | |
177 | u = base_url + 'bug/' + str(self.data['id']) | |
178 | ||
179 | data = { | |
180 | 'ids': [self.data['id']], | |
9e07b0bf | 181 | 'api_key': api_key} |
1a3c85fe ML |
182 | |
183 | data.update(params) | |
184 | ||
185 | if doit: | |
9e07b0bf | 186 | r = requests.put(u, data=json.dumps(data), headers={'content-type': 'text/javascript'}) |
1a3c85fe ML |
187 | print(r) |
188 | ||
189 | @staticmethod | |
190 | def get_major_version(release): | |
191 | parts = release.split('.') | |
192 | assert len(parts) == 2 or len(parts) == 3 | |
193 | return '.'.join(parts[:-1]) | |
194 | ||
195 | @staticmethod | |
196 | def get_bugs(api_key, query): | |
197 | u = base_url + 'bug' | |
9e07b0bf | 198 | r = requests.get(u, params=query) |
1a3c85fe ML |
199 | return [Bug(x) for x in r.json()['bugs']] |
200 | ||
9e07b0bf | 201 | |
1a3c85fe ML |
202 | def search(api_key, remove, add, limit, doit): |
203 | bugs = Bug.get_bugs(api_key, {'api_key': api_key, 'summary': search_summary, 'bug_status': statuses}) | |
204 | bugs = list(filter(lambda x: x.is_regression, bugs)) | |
205 | ||
206 | modified = 0 | |
207 | for bug in bugs: | |
9e07b0bf | 208 | if remove: |
1a3c85fe | 209 | bug.remove_release(remove) |
9e07b0bf | 210 | if add: |
1a3c85fe ML |
211 | bug.add_release(add) |
212 | ||
213 | if bug.update_summary(api_key, doit): | |
214 | modified += 1 | |
215 | if modified == limit: | |
216 | break | |
217 | ||
218 | print('\nModified PRs: %d' % modified) | |
219 | ||
9e07b0bf | 220 | |
1a3c85fe ML |
221 | def replace_milestone(api_key, limit, old_milestone, new_milestone, comment, add_known_to_fail, doit): |
222 | bugs = Bug.get_bugs(api_key, {'api_key': api_key, 'bug_status': statuses, 'target_milestone': old_milestone}) | |
223 | ||
224 | modified = 0 | |
225 | for bug in bugs: | |
226 | if bug.change_milestone(api_key, old_milestone, new_milestone, comment, add_known_to_fail, doit): | |
227 | modified += 1 | |
228 | if modified == limit: | |
229 | break | |
230 | ||
231 | print('\nModified PRs: %d' % modified) | |
232 | ||
9e07b0bf | 233 | |
1a3c85fe | 234 | parser = argparse.ArgumentParser(description='') |
9e07b0bf ML |
235 | parser.add_argument('api_key', help='API key') |
236 | parser.add_argument('--remove', nargs='?', help='Remove a release from summary') | |
237 | parser.add_argument('--add', nargs='?', help='Add a new release to summary, e.g. 6:7 will add 7 where 6 is included') | |
238 | parser.add_argument('--limit', nargs='?', help='Limit number of bugs affected by the script') | |
239 | parser.add_argument('--doit', action='store_true', help='Really modify BUGs in the bugzilla') | |
240 | parser.add_argument('--new-target-milestone', help='Set a new target milestone, ' | |
34bf3250 | 241 | 'e.g. 8.5:9.4 will set milestone to 9.4 for all PRs having milestone set to 8.5') |
9e07b0bf ML |
242 | parser.add_argument('--add-known-to-fail', help='Set a new known to fail ' |
243 | 'for all PRs affected by --new-target-milestone') | |
244 | parser.add_argument('--comment', help='Comment a PR for which we set a new target milestore') | |
1a3c85fe ML |
245 | |
246 | args = parser.parse_args() | |
247 | # Python3 does not have sys.maxint | |
9e07b0bf | 248 | args.limit = int(args.limit) if args.limit else 10**10 |
1a3c85fe | 249 | |
9e07b0bf | 250 | if args.remove or args.add: |
1a3c85fe | 251 | search(args.api_key, args.remove, args.add, args.limit, args.doit) |
9e07b0bf | 252 | if args.new_target_milestone: |
1a3c85fe ML |
253 | t = args.new_target_milestone.split(':') |
254 | assert len(t) == 2 | |
255 | replace_milestone(args.api_key, args.limit, t[0], t[1], args.comment, args.add_known_to_fail, args.doit) |