3 # This script is used by maintainers to modify Bugzilla entries in batch
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.
9 # The script utilizes the Bugzilla API, as documented here:
10 # http://bugzilla.readthedocs.io/en/latest/api/index.html
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
17 # Sample usages of the script:
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
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.
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
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 # NOTE: If the version 5 is the only one in regression marker ([5 Regression] ...),
34 # then the bug summary is not modified.
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:
39 # not changing target milestone: not a regression or does not regress with the new milestone
41 # $ ./maintainer-scripts/branch_changer.py api_key --add=7:8
43 # Aforementioned invocation adds '8' release to the regression prefix of all
44 # issues that contain '7' in its regression prefix.
54 from semantic_version
import Version
56 base_url
= 'https://gcc.gnu.org/bugzilla/rest.cgi/'
57 statuses
= ['UNCONFIRMED', 'ASSIGNED', 'SUSPENDED', 'NEW', 'WAITING', 'REOPENED']
58 search_summary
= ' Regression]'
59 regex
= r
'(.*\[)([0-9\./]*)( [rR]egression])(.*)'
63 def __init__(self
, data
):
66 self
.fail_versions
= []
67 self
.is_regression
= False
70 self
.parse_known_to_fail()
72 def parse_summary(self
):
73 m
= re
.match(regex
, self
.data
['summary'])
75 self
.versions
= m
.group(2).split('/')
76 self
.is_regression
= True
79 def parse_known_to_fail(self
):
80 v
= self
.data
['cf_known_to_fail'].strip()
82 self
.fail_versions
= [x
for x
in re
.split(' |,', v
) if x
!= '']
85 bugid
= self
.data
['id']
86 url
= f
'https://gcc.gnu.org/bugzilla/show_bug.cgi?id={bugid}'
87 if sys
.stdout
.isatty():
88 return f
'\u001b]8;;{url}\u001b\\PR{bugid}\u001b]8;;\u001b\\ ({self.data["summary"]})'
90 return f
'PR{bugid} ({self.data["summary"]})'
92 def remove_release(self
, release
):
93 self
.versions
= list(filter(lambda x
: x
!= release
, self
.versions
))
95 def add_release(self
, releases
):
96 parts
= releases
.split(':')
97 assert len(parts
) == 2
98 for i
, v
in enumerate(self
.versions
):
100 self
.versions
.insert(i
+ 1, parts
[1])
103 def add_known_to_fail(self
, release
):
104 if release
in self
.fail_versions
:
107 self
.fail_versions
.append(release
)
110 def update_summary(self
, api_key
, doit
):
111 if not self
.versions
:
113 print(' not changing summary, candidate for CLOSING')
116 summary
= self
.data
['summary']
117 new_summary
= self
.serialize_summary()
118 if new_summary
!= summary
:
120 print(' changing summary to "%s"' % (new_summary
))
121 self
.modify_bug(api_key
, {'summary': new_summary
}, doit
)
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
)
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
137 print(' changing target milestone: "%s" to "%s" (regresses with the new milestone)'
138 % (old_milestone
, new_milestone
))
140 print(' not changing target milestone: not a regression or does not regress with the new milestone')
142 if 'target_milestone' in args
and comment
:
143 print(' adding comment: "%s"' % comment
)
144 args
['comment'] = {'comment': comment
}
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
152 if len(args
.keys()) != 0:
153 self
.modify_bug(api_key
, args
, doit
)
158 def serialize_summary(self
):
160 assert self
.is_regression
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)
167 def to_version(version
):
168 if len(version
.split('.')) == 2:
170 return Version(version
)
172 def serialize_known_to_fail(self
):
173 assert type(self
.fail_versions
) is list
174 return ', '.join(sorted(self
.fail_versions
, key
=self
.to_version
))
176 def modify_bug(self
, api_key
, params
, doit
):
177 u
= base_url
+ 'bug/' + str(self
.data
['id'])
180 'ids': [self
.data
['id']],
186 r
= requests
.put(u
, data
=json
.dumps(data
), headers
={'content-type': 'text/javascript'})
190 def get_major_version(release
):
191 parts
= release
.split('.')
192 assert len(parts
) == 2 or len(parts
) == 3
193 return '.'.join(parts
[:-1])
196 def get_bugs(api_key
, query
):
198 r
= requests
.get(u
, params
=query
)
199 return [Bug(x
) for x
in r
.json()['bugs']]
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
))
209 bug
.remove_release(remove
)
213 if bug
.update_summary(api_key
, doit
):
215 if modified
== limit
:
218 print('\nModified PRs: %d' % modified
)
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
})
226 if bug
.change_milestone(api_key
, old_milestone
, new_milestone
, comment
, add_known_to_fail
, doit
):
228 if modified
== limit
:
231 print('\nModified PRs: %d' % modified
)
234 parser
= argparse
.ArgumentParser(description
='')
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, '
241 'e.g. 8.5:9.4 will set milestone to 9.4 for all PRs having milestone set to 8.5')
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')
246 args
= parser
.parse_args()
247 # Python3 does not have sys.maxint
248 args
.limit
= int(args
.limit
) if args
.limit
else 10**10
250 if args
.remove
or args
.add
:
251 search(args
.api_key
, args
.remove
, args
.add
, args
.limit
, args
.doit
)
252 if args
.new_target_milestone
:
253 t
= args
.new_target_milestone
.split(':')
255 replace_milestone(args
.api_key
, args
.limit
, t
[0], t
[1], args
.comment
, args
.add_known_to_fail
, args
.doit
)