|  | #!/usr/bin/env python3 | 
|  |  | 
|  | # This script is used by maintainers to modify Bugzilla entries in batch | 
|  | # mode. | 
|  | # Currently it can remove and add a release from/to PRs that are prefixed | 
|  | # with '[x Regression]'. Apart from that, it can also change target | 
|  | # milestones and optionally enhance the list of known-to-fail versions. | 
|  | # | 
|  | # The script utilizes the Bugzilla API, as documented here: | 
|  | # http://bugzilla.readthedocs.io/en/latest/api/index.html | 
|  | # | 
|  | # It requires the simplejson, requests, semantic_version packages. | 
|  | # In case of openSUSE: | 
|  | #   zypper in python3-simplejson python3-requests | 
|  | #   pip3 install semantic_version | 
|  | # | 
|  | # Sample usages of the script: | 
|  | # | 
|  | # $ ./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 | 
|  | # | 
|  | # The invocation will set target milestone to 6.3 for all issues that | 
|  | # have mistone equal to 6.2. Apart from that, a comment is added to these | 
|  | # issues and 6.2 version is added to known-to-fail versions. | 
|  | # At maximum 3 issues will be modified and the script will run | 
|  | # in dry mode (no issues are modified), unless you append --doit option. | 
|  | # | 
|  | # $ ./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 | 
|  | # | 
|  | # Very similar to previous invocation, but instead of adding to known-to-fail, | 
|  | # '5' release is removed from all issues that have the regression prefix. | 
|  | # NOTE: If the version 5 is the only one in regression marker ([5 Regression] ...), | 
|  | # then the bug summary is not modified. | 
|  | # | 
|  | # NOTE: If we change target milestone in between releases and the PR does not | 
|  | # regress in the new branch, then target milestone change is skipped: | 
|  | # | 
|  | #  not changing target milestone: not a regression or does not regress with the new milestone | 
|  | # | 
|  | # $ ./maintainer-scripts/branch_changer.py api_key --add=7:8 | 
|  | # | 
|  | # Aforementioned invocation adds '8' release to the regression prefix of all | 
|  | # issues that contain '7' in its regression prefix. | 
|  | # | 
|  |  | 
|  | import argparse | 
|  | import json | 
|  | import re | 
|  | import sys | 
|  |  | 
|  | import requests | 
|  |  | 
|  | from semantic_version import Version | 
|  |  | 
|  | base_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/' | 
|  | statuses = ['UNCONFIRMED', 'ASSIGNED', 'SUSPENDED', 'NEW', 'WAITING', 'REOPENED'] | 
|  | search_summary = ' Regression]' | 
|  | regex = r'(.*\[)([0-9\./]*)( [rR]egression])(.*)' | 
|  |  | 
|  |  | 
|  | class Bug: | 
|  | def __init__(self, data): | 
|  | self.data = data | 
|  | self.versions = None | 
|  | self.fail_versions = [] | 
|  | self.is_regression = False | 
|  |  | 
|  | self.parse_summary() | 
|  | self.parse_known_to_fail() | 
|  |  | 
|  | def parse_summary(self): | 
|  | m = re.match(regex, self.data['summary']) | 
|  | if m: | 
|  | self.versions = m.group(2).split('/') | 
|  | self.is_regression = True | 
|  | self.regex_match = m | 
|  |  | 
|  | def parse_known_to_fail(self): | 
|  | v = self.data['cf_known_to_fail'].strip() | 
|  | if v != '': | 
|  | self.fail_versions = [x for x in re.split(' |,', v) if x != ''] | 
|  |  | 
|  | def name(self): | 
|  | bugid = self.data['id'] | 
|  | url = f'https://gcc.gnu.org/bugzilla/show_bug.cgi?id={bugid}' | 
|  | if sys.stdout.isatty(): | 
|  | return f'\u001b]8;;{url}\u001b\\PR{bugid}\u001b]8;;\u001b\\ ({self.data["summary"]})' | 
|  | else: | 
|  | return f'PR{bugid} ({self.data["summary"]})' | 
|  |  | 
|  | def remove_release(self, release): | 
|  | self.versions = list(filter(lambda x: x != release, self.versions)) | 
|  |  | 
|  | def add_release(self, releases): | 
|  | parts = releases.split(':') | 
|  | assert len(parts) == 2 | 
|  | for i, v in enumerate(self.versions): | 
|  | if v == parts[0]: | 
|  | self.versions.insert(i + 1, parts[1]) | 
|  | break | 
|  |  | 
|  | def add_known_to_fail(self, release): | 
|  | if release in self.fail_versions: | 
|  | return False | 
|  | else: | 
|  | self.fail_versions.append(release) | 
|  | return True | 
|  |  | 
|  | def update_summary(self, api_key, doit): | 
|  | if not self.versions: | 
|  | print(self.name()) | 
|  | print('  not changing summary, candidate for CLOSING') | 
|  | return False | 
|  |  | 
|  | summary = self.data['summary'] | 
|  | new_summary = self.serialize_summary() | 
|  | if new_summary != summary: | 
|  | print(self.name()) | 
|  | print('  changing summary to "%s"' % (new_summary)) | 
|  | self.modify_bug(api_key, {'summary': new_summary}, doit) | 
|  | return True | 
|  |  | 
|  | return False | 
|  |  | 
|  | def change_milestone(self, api_key, old_milestone, new_milestone, comment, new_fail_version, doit): | 
|  | old_major = Bug.get_major_version(old_milestone) | 
|  | new_major = Bug.get_major_version(new_milestone) | 
|  |  | 
|  | print(self.name()) | 
|  | args = {} | 
|  | if old_major == new_major: | 
|  | args['target_milestone'] = new_milestone | 
|  | print('  changing target milestone: "%s" to "%s" (same branch)' % (old_milestone, new_milestone)) | 
|  | elif self.is_regression and new_major in self.versions: | 
|  | args['target_milestone'] = new_milestone | 
|  | print('  changing target milestone: "%s" to "%s" (regresses with the new milestone)' | 
|  | % (old_milestone, new_milestone)) | 
|  | else: | 
|  | print('  not changing target milestone: not a regression or does not regress with the new milestone') | 
|  |  | 
|  | if 'target_milestone' in args and comment: | 
|  | print('  adding comment: "%s"' % comment) | 
|  | args['comment'] = {'comment': comment} | 
|  |  | 
|  | if new_fail_version: | 
|  | if self.add_known_to_fail(new_fail_version): | 
|  | s = self.serialize_known_to_fail() | 
|  | print('  changing known_to_fail: "%s" to "%s"' % (self.data['cf_known_to_fail'], s)) | 
|  | args['cf_known_to_fail'] = s | 
|  |  | 
|  | if len(args.keys()) != 0: | 
|  | self.modify_bug(api_key, args, doit) | 
|  | return True | 
|  | else: | 
|  | return False | 
|  |  | 
|  | def serialize_summary(self): | 
|  | assert self.versions | 
|  | assert self.is_regression | 
|  |  | 
|  | new_version = '/'.join(self.versions) | 
|  | new_summary = self.regex_match.group(1) + new_version + self.regex_match.group(3) + self.regex_match.group(4) | 
|  | return new_summary | 
|  |  | 
|  | @staticmethod | 
|  | def to_version(version): | 
|  | if len(version.split('.')) == 2: | 
|  | version += '.0' | 
|  | return Version(version) | 
|  |  | 
|  | def serialize_known_to_fail(self): | 
|  | assert type(self.fail_versions) is list | 
|  | return ', '.join(sorted(self.fail_versions, key=self.to_version)) | 
|  |  | 
|  | def modify_bug(self, api_key, params, doit): | 
|  | u = base_url + 'bug/' + str(self.data['id']) | 
|  |  | 
|  | data = { | 
|  | 'ids': [self.data['id']], | 
|  | 'api_key': api_key} | 
|  |  | 
|  | data.update(params) | 
|  |  | 
|  | if doit: | 
|  | r = requests.put(u, data=json.dumps(data), headers={'content-type': 'text/javascript'}) | 
|  | print(r) | 
|  |  | 
|  | @staticmethod | 
|  | def get_major_version(release): | 
|  | parts = release.split('.') | 
|  | assert len(parts) == 2 or len(parts) == 3 | 
|  | return '.'.join(parts[:-1]) | 
|  |  | 
|  | @staticmethod | 
|  | def get_bugs(api_key, query): | 
|  | u = base_url + 'bug' | 
|  | r = requests.get(u, params=query) | 
|  | return [Bug(x) for x in r.json()['bugs']] | 
|  |  | 
|  |  | 
|  | def search(api_key, remove, add, limit, doit): | 
|  | bugs = Bug.get_bugs(api_key, {'api_key': api_key, 'summary': search_summary, 'bug_status': statuses}) | 
|  | bugs = list(filter(lambda x: x.is_regression, bugs)) | 
|  |  | 
|  | modified = 0 | 
|  | for bug in bugs: | 
|  | if remove: | 
|  | bug.remove_release(remove) | 
|  | if add: | 
|  | bug.add_release(add) | 
|  |  | 
|  | if bug.update_summary(api_key, doit): | 
|  | modified += 1 | 
|  | if modified == limit: | 
|  | break | 
|  |  | 
|  | print('\nModified PRs: %d' % modified) | 
|  |  | 
|  |  | 
|  | def replace_milestone(api_key, limit, old_milestone, new_milestone, comment, add_known_to_fail, doit): | 
|  | bugs = Bug.get_bugs(api_key, {'api_key': api_key, 'bug_status': statuses, 'target_milestone': old_milestone}) | 
|  |  | 
|  | modified = 0 | 
|  | for bug in bugs: | 
|  | if bug.change_milestone(api_key, old_milestone, new_milestone, comment, add_known_to_fail, doit): | 
|  | modified += 1 | 
|  | if modified == limit: | 
|  | break | 
|  |  | 
|  | print('\nModified PRs: %d' % modified) | 
|  |  | 
|  |  | 
|  | parser = argparse.ArgumentParser(description='') | 
|  | parser.add_argument('api_key', help='API key') | 
|  | parser.add_argument('--remove', nargs='?', help='Remove a release from summary') | 
|  | parser.add_argument('--add', nargs='?', help='Add a new release to summary, e.g. 6:7 will add 7 where 6 is included') | 
|  | parser.add_argument('--limit', nargs='?', help='Limit number of bugs affected by the script') | 
|  | parser.add_argument('--doit', action='store_true', help='Really modify BUGs in the bugzilla') | 
|  | parser.add_argument('--new-target-milestone', help='Set a new target milestone, ' | 
|  | 'e.g. 8.5:9.4 will set milestone to 9.4 for all PRs having milestone set to 8.5') | 
|  | parser.add_argument('--add-known-to-fail', help='Set a new known to fail ' | 
|  | 'for all PRs affected by --new-target-milestone') | 
|  | parser.add_argument('--comment', help='Comment a PR for which we set a new target milestore') | 
|  |  | 
|  | args = parser.parse_args() | 
|  | # Python3 does not have sys.maxint | 
|  | args.limit = int(args.limit) if args.limit else 10**10 | 
|  |  | 
|  | if args.remove or args.add: | 
|  | search(args.api_key, args.remove, args.add, args.limit, args.doit) | 
|  | if args.new_target_milestone: | 
|  | t = args.new_target_milestone.split(':') | 
|  | assert len(t) == 2 | 
|  | replace_milestone(args.api_key, args.limit, t[0], t[1], args.comment, args.add_known_to_fail, args.doit) |