]> git.ipfire.org Git - thirdparty/gcc.git/blame - maintainer-scripts/branch_changer.py
branch_changer.py: use new versioning scheme.
[thirdparty/gcc.git] / maintainer-scripts / branch_changer.py
CommitLineData
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.
33#
34# $ ./maintainer-scripts/branch_changer.py api_key --add=7:8
35#
36# Aforementioned invocation adds '8' release to the regression prefix of all
37# issues that contain '7' in its regression prefix.
38#
1a3c85fe 39
1a3c85fe 40import argparse
9e07b0bf 41import json
1a3c85fe
ML
42import re
43
9e07b0bf
ML
44import requests
45
46from semantic_version import SimpleSpec
1a3c85fe
ML
47
48base_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/'
49statuses = ['UNCONFIRMED', 'ASSIGNED', 'SUSPENDED', 'NEW', 'WAITING', 'REOPENED']
50search_summary = ' Regression]'
9e07b0bf
ML
51regex = r'(.*\[)([0-9\./]*)( [rR]egression])(.*)'
52
1a3c85fe
ML
53
54class Bug:
55 def __init__(self, data):
56 self.data = data
57 self.versions = None
58 self.fail_versions = []
59 self.is_regression = False
60
61 self.parse_summary()
62 self.parse_known_to_fail()
63
64 def parse_summary(self):
65 m = re.match(regex, self.data['summary'])
9e07b0bf 66 if m:
1a3c85fe
ML
67 self.versions = m.group(2).split('/')
68 self.is_regression = True
69 self.regex_match = m
70
71 def parse_known_to_fail(self):
72 v = self.data['cf_known_to_fail'].strip()
73 if v != '':
74 self.fail_versions = [x for x in re.split(' |,', v) if x != '']
75
76 def name(self):
77 return 'PR%d (%s)' % (self.data['id'], self.data['summary'])
78
79 def remove_release(self, release):
80 # Do not remove last value of [x Regression]
81 if len(self.versions) == 1:
82 return
83 self.versions = list(filter(lambda x: x != release, self.versions))
84
85 def add_release(self, releases):
86 parts = releases.split(':')
87 assert len(parts) == 2
88 for i, v in enumerate(self.versions):
89 if v == parts[0]:
90 self.versions.insert(i + 1, parts[1])
91 break
92
93 def add_known_to_fail(self, release):
94 if release in self.fail_versions:
95 return False
96 else:
97 self.fail_versions.append(release)
98 return True
99
100 def update_summary(self, api_key, doit):
101 summary = self.data['summary']
102 new_summary = self.serialize_summary()
103 if new_summary != summary:
104 print(self.name())
105 print(' changing summary: "%s" to "%s"' % (summary, new_summary))
106 self.modify_bug(api_key, {'summary': new_summary}, doit)
107
108 return True
109
110 return False
111
112 def change_milestone(self, api_key, old_milestone, new_milestone, comment, new_fail_version, doit):
113 old_major = Bug.get_major_version(old_milestone)
114 new_major = Bug.get_major_version(new_milestone)
115
116 print(self.name())
117 args = {}
118 if old_major == new_major:
119 args['target_milestone'] = new_milestone
120 print(' changing target milestone: "%s" to "%s" (same branch)' % (old_milestone, new_milestone))
121 elif self.is_regression and new_major in self.versions:
122 args['target_milestone'] = new_milestone
9e07b0bf
ML
123 print(' changing target milestone: "%s" to "%s" (regresses with the new milestone)'
124 % (old_milestone, new_milestone))
1a3c85fe
ML
125 else:
126 print(' not changing target milestone: not a regression or does not regress with the new milestone')
127
9e07b0bf 128 if 'target_milestone' in args and comment:
1a3c85fe 129 print(' adding comment: "%s"' % comment)
9e07b0bf 130 args['comment'] = {'comment': comment}
1a3c85fe 131
9e07b0bf 132 if new_fail_version:
1a3c85fe
ML
133 if self.add_known_to_fail(new_fail_version):
134 s = self.serialize_known_to_fail()
135 print(' changing known_to_fail: "%s" to "%s"' % (self.data['cf_known_to_fail'], s))
136 args['cf_known_to_fail'] = s
137
138 if len(args.keys()) != 0:
139 self.modify_bug(api_key, args, doit)
140 return True
141 else:
142 return False
143
144 def serialize_summary(self):
855ce475 145 assert self.versions
9e07b0bf 146 assert self.is_regression
1a3c85fe
ML
147
148 new_version = '/'.join(self.versions)
149 new_summary = self.regex_match.group(1) + new_version + self.regex_match.group(3) + self.regex_match.group(4)
150 return new_summary
151
152 def serialize_known_to_fail(self):
153 assert type(self.fail_versions) is list
9e07b0bf 154 return ', '.join(sorted(self.fail_versions, key=lambda x: SimpleSpec(x)))
1a3c85fe
ML
155
156 def modify_bug(self, api_key, params, doit):
157 u = base_url + 'bug/' + str(self.data['id'])
158
159 data = {
160 'ids': [self.data['id']],
9e07b0bf 161 'api_key': api_key}
1a3c85fe
ML
162
163 data.update(params)
164
165 if doit:
9e07b0bf 166 r = requests.put(u, data=json.dumps(data), headers={'content-type': 'text/javascript'})
1a3c85fe
ML
167 print(r)
168
169 @staticmethod
170 def get_major_version(release):
171 parts = release.split('.')
172 assert len(parts) == 2 or len(parts) == 3
173 return '.'.join(parts[:-1])
174
175 @staticmethod
176 def get_bugs(api_key, query):
177 u = base_url + 'bug'
9e07b0bf 178 r = requests.get(u, params=query)
1a3c85fe
ML
179 return [Bug(x) for x in r.json()['bugs']]
180
9e07b0bf 181
1a3c85fe
ML
182def search(api_key, remove, add, limit, doit):
183 bugs = Bug.get_bugs(api_key, {'api_key': api_key, 'summary': search_summary, 'bug_status': statuses})
184 bugs = list(filter(lambda x: x.is_regression, bugs))
185
186 modified = 0
187 for bug in bugs:
9e07b0bf 188 if remove:
1a3c85fe 189 bug.remove_release(remove)
9e07b0bf 190 if add:
1a3c85fe
ML
191 bug.add_release(add)
192
193 if bug.update_summary(api_key, doit):
194 modified += 1
195 if modified == limit:
196 break
197
198 print('\nModified PRs: %d' % modified)
199
9e07b0bf 200
1a3c85fe
ML
201def replace_milestone(api_key, limit, old_milestone, new_milestone, comment, add_known_to_fail, doit):
202 bugs = Bug.get_bugs(api_key, {'api_key': api_key, 'bug_status': statuses, 'target_milestone': old_milestone})
203
204 modified = 0
205 for bug in bugs:
206 if bug.change_milestone(api_key, old_milestone, new_milestone, comment, add_known_to_fail, doit):
207 modified += 1
208 if modified == limit:
209 break
210
211 print('\nModified PRs: %d' % modified)
212
9e07b0bf 213
1a3c85fe 214parser = argparse.ArgumentParser(description='')
9e07b0bf
ML
215parser.add_argument('api_key', help='API key')
216parser.add_argument('--remove', nargs='?', help='Remove a release from summary')
217parser.add_argument('--add', nargs='?', help='Add a new release to summary, e.g. 6:7 will add 7 where 6 is included')
218parser.add_argument('--limit', nargs='?', help='Limit number of bugs affected by the script')
219parser.add_argument('--doit', action='store_true', help='Really modify BUGs in the bugzilla')
220parser.add_argument('--new-target-milestone', help='Set a new target milestone, '
34bf3250 221 'e.g. 8.5:9.4 will set milestone to 9.4 for all PRs having milestone set to 8.5')
9e07b0bf
ML
222parser.add_argument('--add-known-to-fail', help='Set a new known to fail '
223 'for all PRs affected by --new-target-milestone')
224parser.add_argument('--comment', help='Comment a PR for which we set a new target milestore')
1a3c85fe
ML
225
226args = parser.parse_args()
227# Python3 does not have sys.maxint
9e07b0bf 228args.limit = int(args.limit) if args.limit else 10**10
1a3c85fe 229
9e07b0bf 230if args.remove or args.add:
1a3c85fe 231 search(args.api_key, args.remove, args.add, args.limit, args.doit)
9e07b0bf 232if args.new_target_milestone:
1a3c85fe
ML
233 t = args.new_target_milestone.split(':')
234 assert len(t) == 2
235 replace_milestone(args.api_key, args.limit, t[0], t[1], args.comment, args.add_known_to_fail, args.doit)