(root)/
gcc-13.2.0/
maintainer-scripts/
branch_changer.py
       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 \
      20  #       --comment '6.2 has been released....' --add-known-to-fail=6.2 --limit 3
      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  #
      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
      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  # NOTE: If the version 5 is the only one in regression marker ([5 Regression] ...),
      34  # then the bug summary is not modified.
      35  #
      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:
      38  #
      39  #  not changing target milestone: not a regression or does not regress with the new milestone
      40  #
      41  # $ ./maintainer-scripts/branch_changer.py api_key --add=7:8
      42  #
      43  # Aforementioned invocation adds '8' release to the regression prefix of all
      44  # issues that contain '7' in its regression prefix.
      45  #
      46  
      47  import argparse
      48  import json
      49  import re
      50  import sys
      51  
      52  import requests
      53  
      54  from semantic_version import Version
      55  
      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])(.*)'
      60  
      61  
      62  class ESC[4;38;5;81mBug:
      63      def __init__(self, data):
      64          self.data = data
      65          self.versions = None
      66          self.fail_versions = []
      67          self.is_regression = False
      68  
      69          self.parse_summary()
      70          self.parse_known_to_fail()
      71  
      72      def parse_summary(self):
      73          m = re.match(regex, self.data['summary'])
      74          if m:
      75              self.versions = m.group(2).split('/')
      76              self.is_regression = True
      77              self.regex_match = m
      78  
      79      def parse_known_to_fail(self):
      80          v = self.data['cf_known_to_fail'].strip()
      81          if v != '':
      82              self.fail_versions = [x for x in re.split(' |,', v) if x != '']
      83  
      84      def name(self):
      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"]})'
      89          else:
      90              return f'PR{bugid} ({self.data["summary"]})'
      91  
      92      def remove_release(self, release):
      93          self.versions = list(filter(lambda x: x != release, self.versions))
      94  
      95      def add_release(self, releases):
      96          parts = releases.split(':')
      97          assert len(parts) == 2
      98          for i, v in enumerate(self.versions):
      99              if v == parts[0]:
     100                  self.versions.insert(i + 1, parts[1])
     101                  break
     102  
     103      def add_known_to_fail(self, release):
     104          if release in self.fail_versions:
     105              return False
     106          else:
     107              self.fail_versions.append(release)
     108              return True
     109  
     110      def update_summary(self, api_key, doit):
     111          if not self.versions:
     112              print(self.name())
     113              print('  not changing summary, candidate for CLOSING')
     114              return False
     115  
     116          summary = self.data['summary']
     117          new_summary = self.serialize_summary()
     118          if new_summary != summary:
     119              print(self.name())
     120              print('  changing summary to "%s"' % (new_summary))
     121              self.modify_bug(api_key, {'summary': new_summary}, doit)
     122              return True
     123  
     124          return False
     125  
     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)
     129  
     130          print(self.name())
     131          args = {}
     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))
     139          else:
     140              print('  not changing target milestone: not a regression or does not regress with the new milestone')
     141  
     142          if 'target_milestone' in args and comment:
     143              print('  adding comment: "%s"' % comment)
     144              args['comment'] = {'comment': comment}
     145  
     146          if new_fail_version:
     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
     151  
     152          if len(args.keys()) != 0:
     153              self.modify_bug(api_key, args, doit)
     154              return True
     155          else:
     156              return False
     157  
     158      def serialize_summary(self):
     159          assert self.versions
     160          assert self.is_regression
     161  
     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)
     164          return new_summary
     165  
     166      @staticmethod
     167      def to_version(version):
     168          if len(version.split('.')) == 2:
     169              version += '.0'
     170          return Version(version)
     171  
     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))
     175  
     176      def modify_bug(self, api_key, params, doit):
     177          u = base_url + 'bug/' + str(self.data['id'])
     178  
     179          data = {
     180              'ids': [self.data['id']],
     181              'api_key': api_key}
     182  
     183          data.update(params)
     184  
     185          if doit:
     186              r = requests.put(u, data=json.dumps(data), headers={'content-type': 'text/javascript'})
     187              print(r)
     188  
     189      @staticmethod
     190      def get_major_version(release):
     191          parts = release.split('.')
     192          assert len(parts) == 2 or len(parts) == 3
     193          return '.'.join(parts[:-1])
     194  
     195      @staticmethod
     196      def get_bugs(api_key, query):
     197          u = base_url + 'bug'
     198          r = requests.get(u, params=query)
     199          return [Bug(x) for x in r.json()['bugs']]
     200  
     201  
     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))
     205  
     206      modified = 0
     207      for bug in bugs:
     208          if remove:
     209              bug.remove_release(remove)
     210          if add:
     211              bug.add_release(add)
     212  
     213          if bug.update_summary(api_key, doit):
     214              modified += 1
     215              if modified == limit:
     216                  break
     217  
     218      print('\nModified PRs: %d' % modified)
     219  
     220  
     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})
     223  
     224      modified = 0
     225      for bug in bugs:
     226          if bug.change_milestone(api_key, old_milestone, new_milestone, comment, add_known_to_fail, doit):
     227              modified += 1
     228              if modified == limit:
     229                  break
     230  
     231      print('\nModified PRs: %d' % modified)
     232  
     233  
     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')
     245  
     246  args = parser.parse_args()
     247  # Python3 does not have sys.maxint
     248  args.limit = int(args.limit) if args.limit else 10**10
     249  
     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(':')
     254      assert len(t) == 2
     255      replace_milestone(args.api_key, args.limit, t[0], t[1], args.comment, args.add_known_to_fail, args.doit)