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 --comment '6.2 has been released....' --add-known-to-fail=6.2 --limit 3
21 # The invocation will set target milestone to 6.3 for all issues that
22 # have mistone equal to 6.2. Apart from that, a comment is added to these
23 # issues and 6.2 version is added to known-to-fail versions.
24 # At maximum 3 issues will be modified and the script will run
25 # in dry mode (no issues are modified), unless you append --doit option.
27 # $ ./maintainer-scripts/branch_changer.py api_key --new-target-milestone=5.5:6.3 --comment 'GCC 5 branch is being closed' --remove 5 --limit 3
29 # Very similar to previous invocation, but instead of adding to known-to-fail,
30 # '5' release is removed from all issues that have the regression prefix.
32 # $ ./maintainer-scripts/branch_changer.py api_key --add=7:8
34 # Aforementioned invocation adds '8' release to the regression prefix of all
35 # issues that contain '7' in its regression prefix.
43 from semantic_version
import Version
45 base_url
= 'https://gcc.gnu.org/bugzilla/rest.cgi/'
46 statuses
= ['UNCONFIRMED', 'ASSIGNED', 'SUSPENDED', 'NEW', 'WAITING', 'REOPENED']
47 search_summary
= ' Regression]'
48 regex
= '(.*\[)([0-9\./]*)( [rR]egression])(.*)'
51 def __init__(self
, data
):
54 self
.fail_versions
= []
55 self
.is_regression
= False
58 self
.parse_known_to_fail()
60 def parse_summary(self
):
61 m
= re
.match(regex
, self
.data
['summary'])
63 self
.versions
= m
.group(2).split('/')
64 self
.is_regression
= True
67 def parse_known_to_fail(self
):
68 v
= self
.data
['cf_known_to_fail'].strip()
70 self
.fail_versions
= [x
for x
in re
.split(' |,', v
) if x
!= '']
73 return 'PR%d (%s)' % (self
.data
['id'], self
.data
['summary'])
75 def remove_release(self
, release
):
76 # Do not remove last value of [x Regression]
77 if len(self
.versions
) == 1:
79 self
.versions
= list(filter(lambda x
: x
!= release
, self
.versions
))
81 def add_release(self
, releases
):
82 parts
= releases
.split(':')
83 assert len(parts
) == 2
84 for i
, v
in enumerate(self
.versions
):
86 self
.versions
.insert(i
+ 1, parts
[1])
89 def add_known_to_fail(self
, release
):
90 if release
in self
.fail_versions
:
93 self
.fail_versions
.append(release
)
96 def update_summary(self
, api_key
, doit
):
97 summary
= self
.data
['summary']
98 new_summary
= self
.serialize_summary()
99 if new_summary
!= summary
:
101 print(' changing summary: "%s" to "%s"' % (summary
, new_summary
))
102 self
.modify_bug(api_key
, {'summary': new_summary
}, doit
)
108 def change_milestone(self
, api_key
, old_milestone
, new_milestone
, comment
, new_fail_version
, doit
):
109 old_major
= Bug
.get_major_version(old_milestone
)
110 new_major
= Bug
.get_major_version(new_milestone
)
114 if old_major
== new_major
:
115 args
['target_milestone'] = new_milestone
116 print(' changing target milestone: "%s" to "%s" (same branch)' % (old_milestone
, new_milestone
))
117 elif self
.is_regression
and new_major
in self
.versions
:
118 args
['target_milestone'] = new_milestone
119 print(' changing target milestone: "%s" to "%s" (regresses with the new milestone)' % (old_milestone
, new_milestone
))
121 print(' not changing target milestone: not a regression or does not regress with the new milestone')
123 if 'target_milestone' in args
and comment
!= None:
124 print(' adding comment: "%s"' % comment
)
125 args
['comment'] = {'comment': comment
}
127 if new_fail_version
!= None:
128 if self
.add_known_to_fail(new_fail_version
):
129 s
= self
.serialize_known_to_fail()
130 print(' changing known_to_fail: "%s" to "%s"' % (self
.data
['cf_known_to_fail'], s
))
131 args
['cf_known_to_fail'] = s
133 if len(args
.keys()) != 0:
134 self
.modify_bug(api_key
, args
, doit
)
139 def serialize_summary(self
):
140 assert self
.versions
!= None
141 assert self
.is_regression
== True
143 new_version
= '/'.join(self
.versions
)
144 new_summary
= self
.regex_match
.group(1) + new_version
+ self
.regex_match
.group(3) + self
.regex_match
.group(4)
147 def serialize_known_to_fail(self
):
148 assert type(self
.fail_versions
) is list
149 return ', '.join(sorted(self
.fail_versions
, key
= lambda x
: Version(x
, partial
= True)))
151 def modify_bug(self
, api_key
, params
, doit
):
152 u
= base_url
+ 'bug/' + str(self
.data
['id'])
155 'ids': [self
.data
['id']],
161 r
= requests
.put(u
, data
= json
.dumps(data
), headers
= {"content-type": "text/javascript"})
165 def get_major_version(release
):
166 parts
= release
.split('.')
167 assert len(parts
) == 2 or len(parts
) == 3
168 return '.'.join(parts
[:-1])
171 def get_bugs(api_key
, query
):
173 r
= requests
.get(u
, params
= query
)
174 return [Bug(x
) for x
in r
.json()['bugs']]
176 def search(api_key
, remove
, add
, limit
, doit
):
177 bugs
= Bug
.get_bugs(api_key
, {'api_key': api_key
, 'summary': search_summary
, 'bug_status': statuses
})
178 bugs
= list(filter(lambda x
: x
.is_regression
, bugs
))
183 bug
.remove_release(remove
)
187 if bug
.update_summary(api_key
, doit
):
189 if modified
== limit
:
192 print('\nModified PRs: %d' % modified
)
194 def replace_milestone(api_key
, limit
, old_milestone
, new_milestone
, comment
, add_known_to_fail
, doit
):
195 bugs
= Bug
.get_bugs(api_key
, {'api_key': api_key
, 'bug_status': statuses
, 'target_milestone': old_milestone
})
199 if bug
.change_milestone(api_key
, old_milestone
, new_milestone
, comment
, add_known_to_fail
, doit
):
201 if modified
== limit
:
204 print('\nModified PRs: %d' % modified
)
206 parser
= argparse
.ArgumentParser(description
='')
207 parser
.add_argument('api_key', help = 'API key')
208 parser
.add_argument('--remove', nargs
= '?', help = 'Remove a release from summary')
209 parser
.add_argument('--add', nargs
= '?', help = 'Add a new release to summary, e.g. 6:7 will add 7 where 6 is included')
210 parser
.add_argument('--limit', nargs
= '?', help = 'Limit number of bugs affected by the script')
211 parser
.add_argument('--doit', action
= 'store_true', help = 'Really modify BUGs in the bugzilla')
212 parser
.add_argument('--new-target-milestone', help = 'Set a new target milestone, e.g. 4.9.3:4.9.4 will set milestone to 4.9.4 for all PRs having milestone set to 4.9.3')
213 parser
.add_argument('--add-known-to-fail', help = 'Set a new known to fail for all PRs affected by --new-target-milestone')
214 parser
.add_argument('--comment', help = 'Comment a PR for which we set a new target milestore')
216 args
= parser
.parse_args()
217 # Python3 does not have sys.maxint
218 args
.limit
= int(args
.limit
) if args
.limit
!= None else 10**10
220 if args
.remove
!= None or args
.add
!= None:
221 search(args
.api_key
, args
.remove
, args
.add
, args
.limit
, args
.doit
)
222 if args
.new_target_milestone
!= None:
223 t
= args
.new_target_milestone
.split(':')
225 replace_milestone(args
.api_key
, args
.limit
, t
[0], t
[1], args
.comment
, args
.add_known_to_fail
, args
.doit
)