apply_issue.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. #!/usr/bin/env python
  2. # Copyright (c) 2012 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Applies an issue from Rietveld.
  6. """
  7. import getpass
  8. import json
  9. import logging
  10. import optparse
  11. import os
  12. import subprocess
  13. import sys
  14. import urllib2
  15. import annotated_gclient
  16. import auth
  17. import checkout
  18. import fix_encoding
  19. import gclient_utils
  20. import rietveld
  21. import scm
  22. BASE_DIR = os.path.dirname(os.path.abspath(__file__))
  23. RETURN_CODE_OK = 0
  24. RETURN_CODE_OTHER_FAILURE = 1 # any other failure, likely patch apply one.
  25. RETURN_CODE_ARGPARSE_FAILURE = 2 # default in python.
  26. RETURN_CODE_INFRA_FAILURE = 3 # considered as infra failure.
  27. class Unbuffered(object):
  28. """Disable buffering on a file object."""
  29. def __init__(self, stream):
  30. self.stream = stream
  31. def write(self, data):
  32. self.stream.write(data)
  33. self.stream.flush()
  34. def __getattr__(self, attr):
  35. return getattr(self.stream, attr)
  36. def _get_arg_parser():
  37. parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
  38. parser.add_option(
  39. '-v', '--verbose', action='count', default=0,
  40. help='Prints debugging infos')
  41. parser.add_option(
  42. '-e', '--email',
  43. help='Email address to access rietveld. If not specified, anonymous '
  44. 'access will be used.')
  45. parser.add_option(
  46. '-E', '--email-file',
  47. help='File containing the email address to access rietveld. '
  48. 'If not specified, anonymous access will be used.')
  49. parser.add_option(
  50. '-k', '--private-key-file',
  51. help='Path to file containing a private key in p12 format for OAuth2 '
  52. 'authentication with "notasecret" password (as generated by Google '
  53. 'Cloud Console).')
  54. parser.add_option(
  55. '-i', '--issue', type='int', help='Rietveld issue number')
  56. parser.add_option(
  57. '-p', '--patchset', type='int', help='Rietveld issue\'s patchset number')
  58. parser.add_option(
  59. '-r',
  60. '--root_dir',
  61. default=os.getcwd(),
  62. help='Root directory to apply the patch')
  63. parser.add_option(
  64. '-s',
  65. '--server',
  66. default='http://codereview.chromium.org',
  67. help='Rietveld server')
  68. parser.add_option('--no-auth', action='store_true',
  69. help='Do not attempt authenticated requests.')
  70. parser.add_option('--revision-mapping', default='{}',
  71. help='When running gclient, annotate the got_revisions '
  72. 'using the revision-mapping.')
  73. parser.add_option('-f', '--force', action='store_true',
  74. help='Really run apply_issue, even if .update.flag '
  75. 'is detected.')
  76. parser.add_option('-b', '--base_ref', help='DEPRECATED do not use.')
  77. parser.add_option('--whitelist', action='append', default=[],
  78. help='Patch only specified file(s).')
  79. parser.add_option('--blacklist', action='append', default=[],
  80. help='Don\'t patch specified file(s).')
  81. parser.add_option('-d', '--ignore_deps', action='store_true',
  82. help='Don\'t run gclient sync on DEPS changes.')
  83. parser.add_option('--extra_patchlevel', type='int',
  84. help='Number of directories the patch level number should '
  85. 'be incremented (useful for patches from repos with '
  86. 'different directory hierarchies).')
  87. auth.add_auth_options(parser)
  88. return parser
  89. def main():
  90. # TODO(pgervais,tandrii): split this func, it's still too long.
  91. sys.stdout = Unbuffered(sys.stdout)
  92. parser = _get_arg_parser()
  93. options, args = parser.parse_args()
  94. auth_config = auth.extract_auth_config_from_options(options)
  95. if options.whitelist and options.blacklist:
  96. parser.error('Cannot specify both --whitelist and --blacklist')
  97. if options.email and options.email_file:
  98. parser.error('-e and -E options are incompatible')
  99. if (os.path.isfile(os.path.join(os.getcwd(), 'update.flag'))
  100. and not options.force):
  101. print 'update.flag file found: bot_update has run and checkout is already '
  102. print 'in a consistent state. No actions will be performed in this step.'
  103. return 0
  104. logging.basicConfig(
  105. format='%(levelname)5s %(module)11s(%(lineno)4d): %(message)s',
  106. level=[logging.WARNING, logging.INFO, logging.DEBUG][
  107. min(2, options.verbose)])
  108. if args:
  109. parser.error('Extra argument(s) "%s" not understood' % ' '.join(args))
  110. if not options.issue:
  111. parser.error('Require --issue')
  112. options.server = options.server.rstrip('/')
  113. if not options.server:
  114. parser.error('Require a valid server')
  115. options.revision_mapping = json.loads(options.revision_mapping)
  116. # read email if needed
  117. if options.email_file:
  118. if not os.path.exists(options.email_file):
  119. parser.error('file does not exist: %s' % options.email_file)
  120. with open(options.email_file, 'rb') as f:
  121. options.email = f.read().strip()
  122. print('Connecting to %s' % options.server)
  123. # Always try un-authenticated first, except for OAuth2
  124. if options.private_key_file:
  125. # OAuth2 authentication
  126. rietveld_obj = rietveld.JwtOAuth2Rietveld(options.server,
  127. options.email,
  128. options.private_key_file)
  129. try:
  130. properties = rietveld_obj.get_issue_properties(options.issue, False)
  131. except urllib2.URLError:
  132. logging.exception('failed to fetch issue properties')
  133. sys.exit(RETURN_CODE_INFRA_FAILURE)
  134. else:
  135. # Passing None as auth_config disables authentication.
  136. rietveld_obj = rietveld.Rietveld(options.server, None)
  137. properties = None
  138. # Bad except clauses order (HTTPError is an ancestor class of
  139. # ClientLoginError)
  140. # pylint: disable=bad-except-order
  141. try:
  142. properties = rietveld_obj.get_issue_properties(options.issue, False)
  143. except urllib2.HTTPError as e:
  144. if e.getcode() != 302:
  145. raise
  146. if options.no_auth:
  147. exit('FAIL: Login detected -- is issue private?')
  148. # TODO(maruel): A few 'Invalid username or password.' are printed first,
  149. # we should get rid of those.
  150. except urllib2.URLError:
  151. logging.exception('failed to fetch issue properties')
  152. return RETURN_CODE_INFRA_FAILURE
  153. except rietveld.upload.ClientLoginError as e:
  154. # Fine, we'll do proper authentication.
  155. pass
  156. if properties is None:
  157. rietveld_obj = rietveld.Rietveld(options.server, auth_config,
  158. options.email)
  159. try:
  160. properties = rietveld_obj.get_issue_properties(options.issue, False)
  161. except rietveld.upload.ClientLoginError as e:
  162. print('Accessing the issue requires proper credentials.')
  163. return RETURN_CODE_OTHER_FAILURE
  164. except urllib2.URLError:
  165. logging.exception('failed to fetch issue properties')
  166. return RETURN_CODE_INFRA_FAILURE
  167. if not options.patchset:
  168. options.patchset = properties['patchsets'][-1]
  169. print('No patchset specified. Using patchset %d' % options.patchset)
  170. issues_patchsets_to_apply = [(options.issue, options.patchset)]
  171. try:
  172. depends_on_info = rietveld_obj.get_depends_on_patchset(
  173. options.issue, options.patchset)
  174. except urllib2.URLError:
  175. logging.exception('failed to fetch depends_on_patchset')
  176. return RETURN_CODE_INFRA_FAILURE
  177. while depends_on_info:
  178. depends_on_issue = int(depends_on_info['issue'])
  179. depends_on_patchset = int(depends_on_info['patchset'])
  180. try:
  181. depends_on_info = rietveld_obj.get_depends_on_patchset(depends_on_issue,
  182. depends_on_patchset)
  183. issues_patchsets_to_apply.insert(0, (depends_on_issue,
  184. depends_on_patchset))
  185. except urllib2.HTTPError:
  186. print ('The patchset that was marked as a dependency no longer '
  187. 'exists: %s/%d/#ps%d' % (
  188. options.server, depends_on_issue, depends_on_patchset))
  189. print 'Therefore it is likely that this patch will not apply cleanly.'
  190. print
  191. depends_on_info = None
  192. except urllib2.URLError:
  193. logging.exception('failed to fetch dependency issue')
  194. return RETURN_CODE_INFRA_FAILURE
  195. num_issues_patchsets_to_apply = len(issues_patchsets_to_apply)
  196. if num_issues_patchsets_to_apply > 1:
  197. print
  198. print 'apply_issue.py found %d dependent CLs.' % (
  199. num_issues_patchsets_to_apply - 1)
  200. print 'They will be applied in the following order:'
  201. num = 1
  202. for issue_to_apply, patchset_to_apply in issues_patchsets_to_apply:
  203. print ' #%d %s/%d/#ps%d' % (
  204. num, options.server, issue_to_apply, patchset_to_apply)
  205. num += 1
  206. print
  207. for issue_to_apply, patchset_to_apply in issues_patchsets_to_apply:
  208. issue_url = '%s/%d/#ps%d' % (options.server, issue_to_apply,
  209. patchset_to_apply)
  210. print('Downloading patch from %s' % issue_url)
  211. try:
  212. patchset = rietveld_obj.get_patch(issue_to_apply, patchset_to_apply)
  213. except urllib2.HTTPError:
  214. print(
  215. 'Failed to fetch the patch for issue %d, patchset %d.\n'
  216. 'Try visiting %s/%d') % (
  217. issue_to_apply, patchset_to_apply,
  218. options.server, issue_to_apply)
  219. # If we got this far, then this is likely missing patchset.
  220. # Thus, it's not infra failure.
  221. return RETURN_CODE_OTHER_FAILURE
  222. except urllib2.URLError:
  223. logging.exception(
  224. 'Failed to fetch the patch for issue %d, patchset %d',
  225. issue_to_apply, patchset_to_apply)
  226. return RETURN_CODE_INFRA_FAILURE
  227. if options.whitelist:
  228. patchset.patches = [patch for patch in patchset.patches
  229. if patch.filename in options.whitelist]
  230. if options.blacklist:
  231. patchset.patches = [patch for patch in patchset.patches
  232. if patch.filename not in options.blacklist]
  233. for patch in patchset.patches:
  234. print(patch)
  235. if options.extra_patchlevel:
  236. patch.patchlevel += options.extra_patchlevel
  237. full_dir = os.path.abspath(options.root_dir)
  238. scm_type = scm.determine_scm(full_dir)
  239. if scm_type == 'git':
  240. scm_obj = checkout.GitCheckout(full_dir, None, None, None, None)
  241. else:
  242. parser.error('Couldn\'t determine the scm')
  243. # TODO(maruel): HACK, remove me.
  244. # When run a build slave, make sure buildbot knows that the checkout was
  245. # modified.
  246. if options.root_dir == 'src' and getpass.getuser() == 'chrome-bot':
  247. # See sourcedirIsPatched() in:
  248. # http://src.chromium.org/viewvc/chrome/trunk/tools/build/scripts/slave/
  249. # chromium_commands.py?view=markup
  250. open('.buildbot-patched', 'w').close()
  251. print('\nApplying the patch from %s' % issue_url)
  252. try:
  253. scm_obj.apply_patch(patchset, verbose=True)
  254. except checkout.PatchApplicationFailed as e:
  255. print(str(e))
  256. print('CWD=%s' % os.getcwd())
  257. print('Checkout path=%s' % scm_obj.project_path)
  258. return RETURN_CODE_OTHER_FAILURE
  259. if ('DEPS' in map(os.path.basename, patchset.filenames)
  260. and not options.ignore_deps):
  261. gclient_root = gclient_utils.FindGclientRoot(full_dir)
  262. if gclient_root and scm_type:
  263. print(
  264. 'A DEPS file was updated inside a gclient checkout, running gclient '
  265. 'sync.')
  266. gclient_path = os.path.join(BASE_DIR, 'gclient')
  267. if sys.platform == 'win32':
  268. gclient_path += '.bat'
  269. with annotated_gclient.temp_filename(suffix='gclient') as f:
  270. cmd = [
  271. gclient_path, 'sync',
  272. '--nohooks',
  273. '--delete_unversioned_trees',
  274. ]
  275. if options.revision_mapping:
  276. cmd.extend(['--output-json', f])
  277. retcode = subprocess.call(cmd, cwd=gclient_root)
  278. if retcode == 0 and options.revision_mapping:
  279. revisions = annotated_gclient.parse_got_revision(
  280. f, options.revision_mapping)
  281. annotated_gclient.emit_buildprops(revisions)
  282. return retcode
  283. return RETURN_CODE_OK
  284. if __name__ == "__main__":
  285. fix_encoding.fix_encoding()
  286. try:
  287. sys.exit(main())
  288. except KeyboardInterrupt:
  289. sys.stderr.write('interrupted\n')
  290. sys.exit(RETURN_CODE_OTHER_FAILURE)