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