branch_changer.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. #!/usr/bin/env python3
  2. # This script is used by maintainers to modify Bugzilla entries in batch
  3. # mode.
  4. # Currently it can remove and add a release from/to PRs that are prefixed
  5. # with '[x Regression]'. Apart from that, it can also change target
  6. # milestones and optionally enhance the list of known-to-fail versions.
  7. #
  8. # The script utilizes the Bugzilla API, as documented here:
  9. # http://bugzilla.readthedocs.io/en/latest/api/index.html
  10. #
  11. # It requires the simplejson, requests, semantic_version packages.
  12. # In case of openSUSE:
  13. # zypper in python3-simplejson python3-requests
  14. # pip3 install semantic_version
  15. #
  16. # Sample usages of the script:
  17. #
  18. # $ ./maintainer-scripts/branch_changer.py api_key --new-target-milestone=6.2:6.3 \
  19. # --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 \
  28. # --comment 'GCC 5 branch is being closed' --remove 5 --limit 3
  29. #
  30. # Very similar to previous invocation, but instead of adding to known-to-fail,
  31. # '5' release is removed from all issues that have the regression prefix.
  32. # NOTE: If the version 5 is the only one in regression marker ([5 Regression] ...),
  33. # then the bug summary is not modified.
  34. #
  35. # NOTE: If we change target milestone in between releases and the PR does not
  36. # regress in the new branch, then target milestone change is skipped:
  37. #
  38. # not changing target milestone: not a regression or does not regress with the new milestone
  39. #
  40. # $ ./maintainer-scripts/branch_changer.py api_key --add=7:8
  41. #
  42. # Aforementioned invocation adds '8' release to the regression prefix of all
  43. # issues that contain '7' in its regression prefix.
  44. #
  45. import argparse
  46. import json
  47. import re
  48. import sys
  49. import requests
  50. from semantic_version import Version
  51. base_url = 'https://gcc.gnu.org/bugzilla/rest.cgi/'
  52. statuses = ['UNCONFIRMED', 'ASSIGNED', 'SUSPENDED', 'NEW', 'WAITING', 'REOPENED']
  53. search_summary = ' Regression]'
  54. regex = r'(.*\[)([0-9\./]*)( [rR]egression])(.*)'
  55. class Bug:
  56. def __init__(self, data):
  57. self.data = data
  58. self.versions = None
  59. self.fail_versions = []
  60. self.is_regression = False
  61. self.parse_summary()
  62. self.parse_known_to_fail()
  63. def parse_summary(self):
  64. m = re.match(regex, self.data['summary'])
  65. if m:
  66. self.versions = m.group(2).split('/')
  67. self.is_regression = True
  68. self.regex_match = m
  69. def parse_known_to_fail(self):
  70. v = self.data['cf_known_to_fail'].strip()
  71. if v != '':
  72. self.fail_versions = [x for x in re.split(' |,', v) if x != '']
  73. def name(self):
  74. bugid = self.data['id']
  75. url = f'https://gcc.gnu.org/bugzilla/show_bug.cgi?id={bugid}'
  76. if sys.stdout.isatty():
  77. return f'\u001b]8;;{url}\u001b\\PR{bugid}\u001b]8;;\u001b\\ ({self.data["summary"]})'
  78. else:
  79. return f'PR{bugid} ({self.data["summary"]})'
  80. def remove_release(self, release):
  81. self.versions = list(filter(lambda x: x != release, self.versions))
  82. def add_release(self, releases):
  83. parts = releases.split(':')
  84. assert len(parts) == 2
  85. for i, v in enumerate(self.versions):
  86. if v == parts[0]:
  87. self.versions.insert(i + 1, parts[1])
  88. break
  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. def update_summary(self, api_key, doit):
  96. if not self.versions:
  97. print(self.name())
  98. print(' not changing summary, candidate for CLOSING')
  99. return False
  100. summary = self.data['summary']
  101. new_summary = self.serialize_summary()
  102. if new_summary != summary:
  103. print(self.name())
  104. print(' changing summary to "%s"' % (new_summary))
  105. self.modify_bug(api_key, {'summary': new_summary}, doit)
  106. return True
  107. return False
  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. print(self.name())
  112. args = {}
  113. if old_major == new_major:
  114. args['target_milestone'] = new_milestone
  115. print(' changing target milestone: "%s" to "%s" (same branch)' % (old_milestone, new_milestone))
  116. elif self.is_regression and new_major in self.versions:
  117. args['target_milestone'] = new_milestone
  118. print(' changing target milestone: "%s" to "%s" (regresses with the new milestone)'
  119. % (old_milestone, new_milestone))
  120. else:
  121. print(' not changing target milestone: not a regression or does not regress with the new milestone')
  122. if 'target_milestone' in args and comment:
  123. print(' adding comment: "%s"' % comment)
  124. args['comment'] = {'comment': comment}
  125. if new_fail_version:
  126. if self.add_known_to_fail(new_fail_version):
  127. s = self.serialize_known_to_fail()
  128. print(' changing known_to_fail: "%s" to "%s"' % (self.data['cf_known_to_fail'], s))
  129. args['cf_known_to_fail'] = s
  130. if len(args.keys()) != 0:
  131. self.modify_bug(api_key, args, doit)
  132. return True
  133. else:
  134. return False
  135. def serialize_summary(self):
  136. assert self.versions
  137. assert self.is_regression
  138. new_version = '/'.join(self.versions)
  139. new_summary = self.regex_match.group(1) + new_version + self.regex_match.group(3) + self.regex_match.group(4)
  140. return new_summary
  141. @staticmethod
  142. def to_version(version):
  143. if len(version.split('.')) == 2:
  144. version += '.0'
  145. return Version(version)
  146. def serialize_known_to_fail(self):
  147. assert type(self.fail_versions) is list
  148. return ', '.join(sorted(self.fail_versions, key=self.to_version))
  149. def modify_bug(self, api_key, params, doit):
  150. u = base_url + 'bug/' + str(self.data['id'])
  151. data = {
  152. 'ids': [self.data['id']],
  153. 'api_key': api_key}
  154. data.update(params)
  155. if doit:
  156. r = requests.put(u, data=json.dumps(data), headers={'content-type': 'text/javascript'})
  157. print(r)
  158. @staticmethod
  159. def get_major_version(release):
  160. parts = release.split('.')
  161. assert len(parts) == 2 or len(parts) == 3
  162. return '.'.join(parts[:-1])
  163. @staticmethod
  164. def get_bugs(api_key, query):
  165. u = base_url + 'bug'
  166. r = requests.get(u, params=query)
  167. return [Bug(x) for x in r.json()['bugs']]
  168. def search(api_key, remove, add, limit, doit):
  169. bugs = Bug.get_bugs(api_key, {'api_key': api_key, 'summary': search_summary, 'bug_status': statuses})
  170. bugs = list(filter(lambda x: x.is_regression, bugs))
  171. modified = 0
  172. for bug in bugs:
  173. if remove:
  174. bug.remove_release(remove)
  175. if add:
  176. bug.add_release(add)
  177. if bug.update_summary(api_key, doit):
  178. modified += 1
  179. if modified == limit:
  180. break
  181. print('\nModified PRs: %d' % modified)
  182. def replace_milestone(api_key, limit, old_milestone, new_milestone, comment, add_known_to_fail, doit):
  183. bugs = Bug.get_bugs(api_key, {'api_key': api_key, 'bug_status': statuses, 'target_milestone': old_milestone})
  184. modified = 0
  185. for bug in bugs:
  186. if bug.change_milestone(api_key, old_milestone, new_milestone, comment, add_known_to_fail, doit):
  187. modified += 1
  188. if modified == limit:
  189. break
  190. print('\nModified PRs: %d' % modified)
  191. parser = argparse.ArgumentParser(description='')
  192. parser.add_argument('api_key', help='API key')
  193. parser.add_argument('--remove', nargs='?', help='Remove a release from summary')
  194. parser.add_argument('--add', nargs='?', help='Add a new release to summary, e.g. 6:7 will add 7 where 6 is included')
  195. parser.add_argument('--limit', nargs='?', help='Limit number of bugs affected by the script')
  196. parser.add_argument('--doit', action='store_true', help='Really modify BUGs in the bugzilla')
  197. parser.add_argument('--new-target-milestone', help='Set a new target milestone, '
  198. 'e.g. 8.5:9.4 will set milestone to 9.4 for all PRs having milestone set to 8.5')
  199. parser.add_argument('--add-known-to-fail', help='Set a new known to fail '
  200. 'for all PRs affected by --new-target-milestone')
  201. parser.add_argument('--comment', help='Comment a PR for which we set a new target milestore')
  202. args = parser.parse_args()
  203. # Python3 does not have sys.maxint
  204. args.limit = int(args.limit) if args.limit else 10**10
  205. if args.remove or args.add:
  206. search(args.api_key, args.remove, args.add, args.limit, args.doit)
  207. if args.new_target_milestone:
  208. t = args.new_target_milestone.split(':')
  209. assert len(t) == 2
  210. replace_milestone(args.api_key, args.limit, t[0], t[1], args.comment, args.add_known_to_fail, args.doit)