blob: 4baee778831d229386738d1a42d1a3b2b5ebef88 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
maruel@chromium.org3bbf2942012-01-10 16:52:06 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00009__version__ = '1.8.0'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
enne@chromium.orge72c5f52013-04-16 00:36:40 +000015import cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000016import cPickle # Exposed through the API.
17import cStringIO # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000018import contextlib
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000019import fnmatch
20import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000021import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000022import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000023import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000024import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000025import marshal # Exposed through the API.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000026import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027import optparse
28import os # Somewhat exposed through the API.
29import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000030import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import sys # Parts exposed through API.
33import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000034import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000035import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000037import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000038import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000039from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000040
41# Local imports.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
maruel@chromium.org35625c72011-03-23 17:34:02 +000043import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000044import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000045import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000046import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000047import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000048import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000049import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050
51
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000052# Ask for feedback only once in program lifetime.
53_ASKED_FOR_FEEDBACK = False
54
55
maruel@chromium.org899e1c12011-04-07 17:03:18 +000056class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000057 pass
58
59
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000060class CommandData(object):
61 def __init__(self, name, cmd, kwargs, message):
62 self.name = name
63 self.cmd = cmd
64 self.kwargs = kwargs
65 self.message = message
66 self.info = None
67
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000068
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000069def normpath(path):
70 '''Version of os.path.normpath that also changes backward slashes to
71 forward slashes when not running on Windows.
72 '''
73 # This is safe to always do because the Windows version of os.path.normpath
74 # will replace forward slashes with backward slashes.
75 path = path.replace(os.sep, '/')
76 return os.path.normpath(path)
77
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000079def _RightHandSideLinesImpl(affected_files):
80 """Implements RightHandSideLines for InputApi and GclChange."""
81 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000082 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000083 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000084 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000085
86
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000087class PresubmitOutput(object):
88 def __init__(self, input_stream=None, output_stream=None):
89 self.input_stream = input_stream
90 self.output_stream = output_stream
91 self.reviewers = []
92 self.written_output = []
93 self.error_count = 0
94
95 def prompt_yes_no(self, prompt_string):
96 self.write(prompt_string)
97 if self.input_stream:
98 response = self.input_stream.readline().strip().lower()
99 if response not in ('y', 'yes'):
100 self.fail()
101 else:
102 self.fail()
103
104 def fail(self):
105 self.error_count += 1
106
107 def should_continue(self):
108 return not self.error_count
109
110 def write(self, s):
111 self.written_output.append(s)
112 if self.output_stream:
113 self.output_stream.write(s)
114
115 def getvalue(self):
116 return ''.join(self.written_output)
117
118
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000119# Top level object so multiprocessing can pickle
120# Public access through OutputApi object.
121class _PresubmitResult(object):
122 """Base class for result objects."""
123 fatal = False
124 should_prompt = False
125
126 def __init__(self, message, items=None, long_text=''):
127 """
128 message: A short one-line message to indicate errors.
129 items: A list of short strings to indicate where errors occurred.
130 long_text: multi-line text output, e.g. from another tool
131 """
132 self._message = message
133 self._items = items or []
134 if items:
135 self._items = items
136 self._long_text = long_text.rstrip()
137
138 def handle(self, output):
139 output.write(self._message)
140 output.write('\n')
141 for index, item in enumerate(self._items):
142 output.write(' ')
143 # Write separately in case it's unicode.
144 output.write(str(item))
145 if index < len(self._items) - 1:
146 output.write(' \\')
147 output.write('\n')
148 if self._long_text:
149 output.write('\n***************\n')
150 # Write separately in case it's unicode.
151 output.write(self._long_text)
152 output.write('\n***************\n')
153 if self.fatal:
154 output.fail()
155
156
157# Top level object so multiprocessing can pickle
158# Public access through OutputApi object.
159class _PresubmitAddReviewers(_PresubmitResult):
160 """Add some suggested reviewers to the change."""
161 def __init__(self, reviewers):
162 super(_PresubmitAddReviewers, self).__init__('')
163 self.reviewers = reviewers
164
165 def handle(self, output):
166 output.reviewers.extend(self.reviewers)
167
168
169# Top level object so multiprocessing can pickle
170# Public access through OutputApi object.
171class _PresubmitError(_PresubmitResult):
172 """A hard presubmit error."""
173 fatal = True
174
175
176# Top level object so multiprocessing can pickle
177# Public access through OutputApi object.
178class _PresubmitPromptWarning(_PresubmitResult):
179 """An warning that prompts the user if they want to continue."""
180 should_prompt = True
181
182
183# Top level object so multiprocessing can pickle
184# Public access through OutputApi object.
185class _PresubmitNotifyResult(_PresubmitResult):
186 """Just print something to the screen -- but it's not even a warning."""
187 pass
188
189
190# Top level object so multiprocessing can pickle
191# Public access through OutputApi object.
192class _MailTextResult(_PresubmitResult):
193 """A warning that should be included in the review request email."""
194 def __init__(self, *args, **kwargs):
195 super(_MailTextResult, self).__init__()
196 raise NotImplementedError()
197
198
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000199class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000200 """An instance of OutputApi gets passed to presubmit scripts so that they
201 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000202 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000203 PresubmitResult = _PresubmitResult
204 PresubmitAddReviewers = _PresubmitAddReviewers
205 PresubmitError = _PresubmitError
206 PresubmitPromptWarning = _PresubmitPromptWarning
207 PresubmitNotifyResult = _PresubmitNotifyResult
208 MailTextResult = _MailTextResult
209
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000210 def __init__(self, is_committing):
211 self.is_committing = is_committing
212
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000213 def PresubmitPromptOrNotify(self, *args, **kwargs):
214 """Warn the user when uploading, but only notify if committing."""
215 if self.is_committing:
216 return self.PresubmitNotifyResult(*args, **kwargs)
217 return self.PresubmitPromptWarning(*args, **kwargs)
218
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000219
220class InputApi(object):
221 """An instance of this object is passed to presubmit scripts so they can
222 know stuff about the change they're looking at.
223 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000224 # Method could be a function
225 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226
maruel@chromium.org3410d912009-06-09 20:56:16 +0000227 # File extensions that are considered source files from a style guide
228 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000229 #
230 # Files without an extension aren't included in the list. If you want to
231 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
232 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000233 DEFAULT_WHITE_LIST = (
234 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000235 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
236 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000237 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000238 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000239 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000240 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000241 )
242
243 # Path regexp that should be excluded from being considered containing source
244 # files. Don't modify this list from a presubmit script!
245 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000246 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000247 r".*\bexperimental[\\\/].*",
248 r".*\bthird_party[\\\/].*",
249 # Output directories (just in case)
250 r".*\bDebug[\\\/].*",
251 r".*\bRelease[\\\/].*",
252 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000253 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000254 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000255 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000256 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000257 r"(|.*[\\\/])\.git[\\\/].*",
258 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000259 # There is no point in processing a patch file.
260 r".+\.diff$",
261 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000262 )
263
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000264 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000265 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000266 """Builds an InputApi object.
267
268 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000269 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000270 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000271 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000272 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000274 # Version number of the presubmit_support script.
275 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000277 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000278 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000279 # TBD
280 self.host_url = 'http://codereview.chromium.org'
281 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000282 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283
284 # We expose various modules and functions as attributes of the input_api
285 # so that presubmit scripts don't have to import them.
286 self.basename = os.path.basename
287 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000288 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000290 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000291 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000292 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000293 self.os_listdir = os.listdir
294 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000295 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000296 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000297 self.pickle = pickle
298 self.marshal = marshal
299 self.re = re
300 self.subprocess = subprocess
301 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000302 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000303 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000304 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305 self.urllib2 = urllib2
306
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000307 # To easily fork python.
308 self.python_executable = sys.executable
309 self.environ = os.environ
310
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000311 # InputApi.platform is the platform you're currently running on.
312 self.platform = sys.platform
313
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000314 self.cpu_count = multiprocessing.cpu_count()
315
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000316 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000317 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000318
319 # We carry the canned checks so presubmit scripts can easily use them.
320 self.canned_checks = presubmit_canned_checks
321
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000322 # TODO(dpranke): figure out a list of all approved owners for a repo
323 # in order to be able to handle wildcard OWNERS files?
324 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000325 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000326 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000327 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000328
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000329 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000330 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000331 # Access to a protected member _XX of a client class
332 # pylint: disable=W0212
333 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000334 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000335 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
336 for (a, b, header) in cpplint._re_pattern_templates
337 ]
338
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000339 def PresubmitLocalPath(self):
340 """Returns the local path of the presubmit script currently being run.
341
342 This is useful if you don't want to hard-code absolute paths in the
343 presubmit script. For example, It can be used to find another file
344 relative to the PRESUBMIT.py script, so the whole tree can be branched and
345 the presubmit script still works, without editing its content.
346 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000347 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000348
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000349 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350 """Translate a depot path to a local path (relative to client root).
351
352 Args:
353 Depot path as a string.
354
355 Returns:
356 The local path of the depot path under the user's current client, or None
357 if the file is not mapped.
358
359 Remember to check for the None case and show an appropriate error!
360 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000361 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
362 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000363
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000364 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000365 """Translate a local path to a depot path.
366
367 Args:
368 Local path (relative to current directory, or absolute) as a string.
369
370 Returns:
371 The depot path (SVN URL) of the file if mapped, otherwise None.
372 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000373 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
374 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000375
sail@chromium.org5538e022011-05-12 17:53:16 +0000376 def AffectedFiles(self, include_dirs=False, include_deletes=True,
377 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000378 """Same as input_api.change.AffectedFiles() except only lists files
379 (and optionally directories) in the same directory as the current presubmit
380 script, or subdirectories thereof.
381 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000382 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000383 if len(dir_with_slash) == 1:
384 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000385
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000386 return filter(
387 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000388 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000389
390 def LocalPaths(self, include_dirs=False):
391 """Returns local paths of input_api.AffectedFiles()."""
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000392 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
393 logging.debug("LocalPaths: %s", paths)
394 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000395
396 def AbsoluteLocalPaths(self, include_dirs=False):
397 """Returns absolute local paths of input_api.AffectedFiles()."""
398 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
399
400 def ServerPaths(self, include_dirs=False):
401 """Returns server paths of input_api.AffectedFiles()."""
402 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
403
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000404 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405 """Same as input_api.change.AffectedTextFiles() except only lists files
406 in the same directory as the current presubmit script, or subdirectories
407 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000408 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000409 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000410 warn("AffectedTextFiles(include_deletes=%s)"
411 " is deprecated and ignored" % str(include_deletes),
412 category=DeprecationWarning,
413 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000414 return filter(lambda x: x.IsTextFile(),
415 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000416
maruel@chromium.org3410d912009-06-09 20:56:16 +0000417 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
418 """Filters out files that aren't considered "source file".
419
420 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
421 and InputApi.DEFAULT_BLACK_LIST is used respectively.
422
423 The lists will be compiled as regular expression and
424 AffectedFile.LocalPath() needs to pass both list.
425
426 Note: Copy-paste this function to suit your needs or use a lambda function.
427 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000428 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000429 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000430 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000431 if self.re.match(item, local_path):
432 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000433 return True
434 return False
435 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
436 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
437
438 def AffectedSourceFiles(self, source_file):
439 """Filter the list of AffectedTextFiles by the function source_file.
440
441 If source_file is None, InputApi.FilterSourceFile() is used.
442 """
443 if not source_file:
444 source_file = self.FilterSourceFile
445 return filter(source_file, self.AffectedTextFiles())
446
447 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448 """An iterator over all text lines in "new" version of changed files.
449
450 Only lists lines from new or modified text files in the change that are
451 contained by the directory of the currently executing presubmit script.
452
453 This is useful for doing line-by-line regex checks, like checking for
454 trailing whitespace.
455
456 Yields:
457 a 3 tuple:
458 the AffectedFile instance of the current file;
459 integer line number (1-based); and
460 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000461
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000462 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000463 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000464 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000465 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000466
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000467 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000468 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000469
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000470 Deny reading anything outside the repository.
471 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000472 if isinstance(file_item, AffectedFile):
473 file_item = file_item.AbsoluteLocalPath()
474 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000475 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000476 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000477
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000478 @property
479 def tbr(self):
480 """Returns if a change is TBR'ed."""
481 return 'TBR' in self.change.tags
482
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000483 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000484 tests = []
485 msgs = []
486 for t in tests_mix:
487 if isinstance(t, OutputApi.PresubmitResult):
488 msgs.append(t)
489 else:
490 assert issubclass(t.message, _PresubmitResult)
491 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000492 if self.verbose:
493 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000494 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000495 pool = multiprocessing.Pool()
496 # async recipe works around multiprocessing bug handling Ctrl-C
497 msgs.extend(pool.map_async(CallCommand, tests).get(99999))
498 pool.close()
499 pool.join()
500 else:
501 msgs.extend(map(CallCommand, tests))
502 return [m for m in msgs if m]
503
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000504
nick@chromium.orgff526192013-06-10 19:30:26 +0000505class _DiffCache(object):
506 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000507 def __init__(self, upstream=None):
508 """Stores the upstream revision against which all diffs will be computed."""
509 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000510
511 def GetDiff(self, path, local_root):
512 """Get the diff for a particular path."""
513 raise NotImplementedError()
514
515
516class _SvnDiffCache(_DiffCache):
517 """DiffCache implementation for subversion."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000518 def __init__(self, *args, **kwargs):
519 super(_SvnDiffCache, self).__init__(*args, **kwargs)
nick@chromium.orgff526192013-06-10 19:30:26 +0000520 self._diffs_by_file = {}
521
522 def GetDiff(self, path, local_root):
523 if path not in self._diffs_by_file:
524 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
525 False, None)
526 return self._diffs_by_file[path]
527
528
529class _GitDiffCache(_DiffCache):
530 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000531 def __init__(self, upstream):
532 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000533 self._diffs_by_file = None
534
535 def GetDiff(self, path, local_root):
536 if not self._diffs_by_file:
537 # Compute a single diff for all files and parse the output; should
538 # with git this is much faster than computing one diff for each file.
539 diffs = {}
540
541 # Don't specify any filenames below, because there are command line length
542 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000543 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
544 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000545
546 # This regex matches the path twice, separated by a space. Note that
547 # filename itself may contain spaces.
548 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
549 current_diff = []
550 keep_line_endings = True
551 for x in unified_diff.splitlines(keep_line_endings):
552 match = file_marker.match(x)
553 if match:
554 # Marks the start of a new per-file section.
555 diffs[match.group('filename')] = current_diff = [x]
556 elif x.startswith('diff --git'):
557 raise PresubmitFailure('Unexpected diff line: %s' % x)
558 else:
559 current_diff.append(x)
560
561 self._diffs_by_file = dict(
562 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
563
564 if path not in self._diffs_by_file:
565 raise PresubmitFailure(
566 'Unified diff did not contain entry for file %s' % path)
567
568 return self._diffs_by_file[path]
569
570
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000571class AffectedFile(object):
572 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000573
574 DIFF_CACHE = _DiffCache
575
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000576 # Method could be a function
577 # pylint: disable=R0201
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000578 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000579 self._path = path
580 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000581 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000582 self._is_directory = None
583 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000584 self._cached_changed_contents = None
585 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000586 self._diff_cache = diff_cache
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000587 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000588
589 def ServerPath(self):
590 """Returns a path string that identifies the file in the SCM system.
591
592 Returns the empty string if the file does not exist in SCM.
593 """
nick@chromium.orgff526192013-06-10 19:30:26 +0000594 return ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000595
596 def LocalPath(self):
597 """Returns the path of this file on the local disk relative to client root.
598 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000599 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000600
601 def AbsoluteLocalPath(self):
602 """Returns the absolute path of this file on the local disk.
603 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000604 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000605
606 def IsDirectory(self):
607 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000608 if self._is_directory is None:
609 path = self.AbsoluteLocalPath()
610 self._is_directory = (os.path.exists(path) and
611 os.path.isdir(path))
612 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000613
614 def Action(self):
615 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000616 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
617 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000618 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000620 def Property(self, property_name):
621 """Returns the specified SCM property of this file, or None if no such
622 property.
623 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000624 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000625
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000626 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000627 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000628
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000629 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000630 raise NotImplementedError() # Implement when needed
631
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000632 def NewContents(self):
633 """Returns an iterator over the lines in the new version of file.
634
635 The new version is the file in the user's workspace, i.e. the "right hand
636 side".
637
638 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000639 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000640 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000641 if self._cached_new_contents is None:
642 self._cached_new_contents = []
643 if not self.IsDirectory():
644 try:
645 self._cached_new_contents = gclient_utils.FileRead(
646 self.AbsoluteLocalPath(), 'rU').splitlines()
647 except IOError:
648 pass # File not found? That's fine; maybe it was deleted.
649 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000650
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000651 def ChangedContents(self):
652 """Returns a list of tuples (line number, line text) of all new lines.
653
654 This relies on the scm diff output describing each changed code section
655 with a line of the form
656
657 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
658 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000659 if self._cached_changed_contents is not None:
660 return self._cached_changed_contents[:]
661 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000662 line_num = 0
663
664 if self.IsDirectory():
665 return []
666
667 for line in self.GenerateScmDiff().splitlines():
668 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
669 if m:
670 line_num = int(m.groups(1)[0])
671 continue
672 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000673 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000674 if not line.startswith('-'):
675 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000676 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000677
maruel@chromium.org5de13972009-06-10 18:16:06 +0000678 def __str__(self):
679 return self.LocalPath()
680
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000681 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000682 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683
maruel@chromium.org58407af2011-04-12 23:15:57 +0000684
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000685class SvnAffectedFile(AffectedFile):
686 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000687 # Method 'NNN' is abstract in class 'NNN' but is not overridden
688 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000689
nick@chromium.orgff526192013-06-10 19:30:26 +0000690 DIFF_CACHE = _SvnDiffCache
691
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000692 def __init__(self, *args, **kwargs):
693 AffectedFile.__init__(self, *args, **kwargs)
694 self._server_path = None
695 self._is_text_file = None
696
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000697 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000698 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000699 self._server_path = scm.SVN.CaptureLocalInfo(
700 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000701 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000702
703 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000704 if self._is_directory is None:
705 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000706 if os.path.exists(path):
707 # Retrieve directly from the file system; it is much faster than
708 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000709 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000710 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000711 self._is_directory = scm.SVN.CaptureLocalInfo(
712 [self.LocalPath()], self._local_root
713 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000714 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000715
716 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000717 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000718 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000719 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000720 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000721
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000722 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000723 if self._is_text_file is None:
724 if self.Action() == 'D':
725 # A deleted file is not a text file.
726 self._is_text_file = False
727 elif self.IsDirectory():
728 self._is_text_file = False
729 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000730 mime_type = scm.SVN.GetFileProperty(
731 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000732 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
733 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000734
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000735
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000736class GitAffectedFile(AffectedFile):
737 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000738 # Method 'NNN' is abstract in class 'NNN' but is not overridden
739 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000740
nick@chromium.orgff526192013-06-10 19:30:26 +0000741 DIFF_CACHE = _GitDiffCache
742
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000743 def __init__(self, *args, **kwargs):
744 AffectedFile.__init__(self, *args, **kwargs)
745 self._server_path = None
746 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000747
748 def ServerPath(self):
749 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000750 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000751 return self._server_path
752
753 def IsDirectory(self):
754 if self._is_directory is None:
755 path = self.AbsoluteLocalPath()
756 if os.path.exists(path):
757 # Retrieve directly from the file system; it is much faster than
758 # querying subversion, especially on Windows.
759 self._is_directory = os.path.isdir(path)
760 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000761 self._is_directory = False
762 return self._is_directory
763
764 def Property(self, property_name):
765 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000766 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000767 return self._properties[property_name]
768
769 def IsTextFile(self):
770 if self._is_text_file is None:
771 if self.Action() == 'D':
772 # A deleted file is not a text file.
773 self._is_text_file = False
774 elif self.IsDirectory():
775 self._is_text_file = False
776 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000777 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
778 return self._is_text_file
779
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000780
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000781class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000782 """Describe a change.
783
784 Used directly by the presubmit scripts to query the current change being
785 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000786
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000787 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000788 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000789 self.KEY: equivalent to tags['KEY']
790 """
791
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000792 _AFFECTED_FILES = AffectedFile
793
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000794 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000795 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000796 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000797 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000798
maruel@chromium.org58407af2011-04-12 23:15:57 +0000799 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000800 self, name, description, local_root, files, issue, patchset, author,
801 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000802 if files is None:
803 files = []
804 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000805 # Convert root into an absolute path.
806 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000807 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000808 self.issue = issue
809 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000810 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000811
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000812 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000813 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000814 self._description_without_tags = ''
815 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000816
maruel@chromium.orge085d812011-10-10 19:49:15 +0000817 assert all(
818 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
819
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000820 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000821 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000822 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
823 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000824 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000826 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000828 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000829
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 def DescriptionText(self):
831 """Returns the user-entered changelist description, minus tags.
832
833 Any line in the user-provided description starting with e.g. "FOO="
834 (whitespace permitted before and around) is considered a tag line. Such
835 lines are stripped out of the description this function returns.
836 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000837 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838
839 def FullDescriptionText(self):
840 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000841 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000843 def SetDescriptionText(self, description):
844 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000845
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000846 Also updates the list of tags."""
847 self._full_description = description
848
849 # From the description text, build up a dictionary of key/value pairs
850 # plus the description minus all key/value or "tag" lines.
851 description_without_tags = []
852 self.tags = {}
853 for line in self._full_description.splitlines():
854 m = self.TAG_LINE_RE.match(line)
855 if m:
856 self.tags[m.group('key')] = m.group('value')
857 else:
858 description_without_tags.append(line)
859
860 # Change back to text and remove whitespace at end.
861 self._description_without_tags = (
862 '\n'.join(description_without_tags).rstrip())
863
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000864 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000865 """Returns the repository (checkout) root directory for this change,
866 as an absolute path.
867 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000868 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869
870 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000871 """Return tags directly as attributes on the object."""
872 if not re.match(r"^[A-Z_]*$", attr):
873 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000874 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000875
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000876 def AllFiles(self, root=None):
877 """List all files under source control in the repo."""
878 raise NotImplementedError()
879
sail@chromium.org5538e022011-05-12 17:53:16 +0000880 def AffectedFiles(self, include_dirs=False, include_deletes=True,
881 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000882 """Returns a list of AffectedFile instances for all files in the change.
883
884 Args:
885 include_deletes: If false, deleted files will be filtered out.
886 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000887 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000888
889 Returns:
890 [AffectedFile(path, action), AffectedFile(path, action)]
891 """
892 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000893 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000894 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000895 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000896
sail@chromium.org5538e022011-05-12 17:53:16 +0000897 affected = filter(file_filter, affected)
898
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000899 if include_deletes:
900 return affected
901 else:
902 return filter(lambda x: x.Action() != 'D', affected)
903
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000904 def AffectedTextFiles(self, include_deletes=None):
905 """Return a list of the existing text files in a change."""
906 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000907 warn("AffectedTextFiles(include_deletes=%s)"
908 " is deprecated and ignored" % str(include_deletes),
909 category=DeprecationWarning,
910 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000911 return filter(lambda x: x.IsTextFile(),
912 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000913
914 def LocalPaths(self, include_dirs=False):
915 """Convenience function."""
916 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
917
918 def AbsoluteLocalPaths(self, include_dirs=False):
919 """Convenience function."""
920 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
921
922 def ServerPaths(self, include_dirs=False):
923 """Convenience function."""
924 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
925
926 def RightHandSideLines(self):
927 """An iterator over all text lines in "new" version of changed files.
928
929 Lists lines from new or modified text files in the change.
930
931 This is useful for doing line-by-line regex checks, like checking for
932 trailing whitespace.
933
934 Yields:
935 a 3 tuple:
936 the AffectedFile instance of the current file;
937 integer line number (1-based); and
938 the contents of the line as a string.
939 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000940 return _RightHandSideLinesImpl(
941 x for x in self.AffectedFiles(include_deletes=False)
942 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000943
944
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000945class SvnChange(Change):
946 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000947 scm = 'svn'
948 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000949
950 def _GetChangeLists(self):
951 """Get all change lists."""
952 if self._changelists == None:
953 previous_cwd = os.getcwd()
954 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000955 # Need to import here to avoid circular dependency.
956 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000957 self._changelists = gcl.GetModifiedFiles()
958 os.chdir(previous_cwd)
959 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000960
961 def GetAllModifiedFiles(self):
962 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000963 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000964 all_modified_files = []
965 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000966 all_modified_files.extend(
967 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000968 return all_modified_files
969
970 def GetModifiedFiles(self):
971 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000972 changelists = self._GetChangeLists()
973 return [os.path.join(self.RepositoryRoot(), f[1])
974 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000975
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000976 def AllFiles(self, root=None):
977 """List all files under source control in the repo."""
978 root = root or self.RepositoryRoot()
979 return subprocess.check_output(
980 ['svn', 'ls', '-R', '.'], cwd=root).splitlines()
981
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000982
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000983class GitChange(Change):
984 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000985 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000986
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000987 def AllFiles(self, root=None):
988 """List all files under source control in the repo."""
989 root = root or self.RepositoryRoot()
990 return subprocess.check_output(
991 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
992
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000993
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000994def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995 """Finds all presubmit files that apply to a given set of source files.
996
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000997 If inherit-review-settings-ok is present right under root, looks for
998 PRESUBMIT.py in directories enclosing root.
999
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000 Args:
1001 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001002 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001003
1004 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001005 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001007 files = [normpath(os.path.join(root, f)) for f in files]
1008
1009 # List all the individual directories containing files.
1010 directories = set([os.path.dirname(f) for f in files])
1011
1012 # Ignore root if inherit-review-settings-ok is present.
1013 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1014 root = None
1015
1016 # Collect all unique directories that may contain PRESUBMIT.py.
1017 candidates = set()
1018 for directory in directories:
1019 while True:
1020 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001021 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001022 candidates.add(directory)
1023 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001024 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001025 parent_dir = os.path.dirname(directory)
1026 if parent_dir == directory:
1027 # We hit the system root directory.
1028 break
1029 directory = parent_dir
1030
1031 # Look for PRESUBMIT.py in all candidate directories.
1032 results = []
1033 for directory in sorted(list(candidates)):
1034 p = os.path.join(directory, 'PRESUBMIT.py')
1035 if os.path.isfile(p):
1036 results.append(p)
1037
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001038 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001039 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001040
1041
thestig@chromium.orgde243452009-10-06 21:02:56 +00001042class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001043 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001044 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +00001045 """Executes GetPreferredTrySlaves() from a single presubmit script.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001046
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001047 This will soon be deprecated and replaced by GetPreferredTryMasters().
thestig@chromium.orgde243452009-10-06 21:02:56 +00001048
1049 Args:
1050 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +00001051 presubmit_path: Project script to run.
1052 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001053
1054 Return:
1055 A list of try slaves.
1056 """
1057 context = {}
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001058 main_path = os.getcwd()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001059 try:
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001060 os.chdir(os.path.dirname(presubmit_path))
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001061 exec script_text in context
1062 except Exception, e:
1063 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001064 finally:
1065 os.chdir(main_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001066
1067 function_name = 'GetPreferredTrySlaves'
1068 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001069 get_preferred_try_slaves = context[function_name]
1070 function_info = inspect.getargspec(get_preferred_try_slaves)
1071 if len(function_info[0]) == 1:
1072 result = get_preferred_try_slaves(project)
1073 elif len(function_info[0]) == 2:
1074 result = get_preferred_try_slaves(project, change)
1075 else:
1076 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +00001077 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001078 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +00001079 'Presubmit functions must return a list, got a %s instead: %s' %
1080 (type(result), str(result)))
1081 for item in result:
stip@chromium.org68e04192013-11-04 22:14:38 +00001082 if isinstance(item, basestring):
1083 # Old-style ['bot'] format.
1084 botname = item
1085 elif isinstance(item, tuple):
1086 # New-style [('bot', set(['tests']))] format.
1087 botname = item[0]
1088 else:
1089 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1090 ' format.')
1091
1092 if botname != botname.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001093 raise PresubmitFailure(
1094 'Try slave names cannot start/end with whitespace')
stip@chromium.org68e04192013-11-04 22:14:38 +00001095 if ',' in botname:
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +00001096 raise PresubmitFailure(
stip@chromium.org68e04192013-11-04 22:14:38 +00001097 'Do not use \',\' separated builder or test names: %s' % botname)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001098 else:
1099 result = []
stip@chromium.org5ca27622013-12-18 17:44:58 +00001100
1101 def valid_oldstyle(result):
1102 return all(isinstance(i, basestring) for i in result)
1103
1104 def valid_newstyle(result):
1105 return (all(isinstance(i, tuple) for i in result) and
1106 all(len(i) == 2 for i in result) and
1107 all(isinstance(i[0], basestring) for i in result) and
1108 all(isinstance(i[1], set) for i in result)
1109 )
1110
1111 # Ensure it's either all old-style or all new-style.
1112 if not valid_oldstyle(result) and not valid_newstyle(result):
1113 raise PresubmitFailure(
1114 'PRESUBMIT.py returned invalid trybot specification!')
1115
thestig@chromium.orgde243452009-10-06 21:02:56 +00001116 return result
1117
1118
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001119class GetTryMastersExecuter(object):
1120 @staticmethod
1121 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1122 """Executes GetPreferredTryMasters() from a single presubmit script.
1123
1124 Args:
1125 script_text: The text of the presubmit script.
1126 presubmit_path: Project script to run.
1127 project: Project name to pass to presubmit script for bot selection.
1128
1129 Return:
1130 A map of try masters to map of builders to set of tests.
1131 """
1132 context = {}
1133 try:
1134 exec script_text in context
1135 except Exception, e:
1136 raise PresubmitFailure('"%s" had an exception.\n%s'
1137 % (presubmit_path, e))
1138
1139 function_name = 'GetPreferredTryMasters'
1140 if function_name not in context:
1141 return {}
1142 get_preferred_try_masters = context[function_name]
1143 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1144 raise PresubmitFailure(
1145 'Expected function "GetPreferredTryMasters" to take two arguments.')
1146 return get_preferred_try_masters(project, change)
1147
1148
rmistry@google.com5626a922015-02-26 14:03:30 +00001149class GetPostUploadExecuter(object):
1150 @staticmethod
1151 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1152 """Executes PostUploadHook() from a single presubmit script.
1153
1154 Args:
1155 script_text: The text of the presubmit script.
1156 presubmit_path: Project script to run.
1157 cl: The Changelist object.
1158 change: The Change object.
1159
1160 Return:
1161 A list of results objects.
1162 """
1163 context = {}
1164 try:
1165 exec script_text in context
1166 except Exception, e:
1167 raise PresubmitFailure('"%s" had an exception.\n%s'
1168 % (presubmit_path, e))
1169
1170 function_name = 'PostUploadHook'
1171 if function_name not in context:
1172 return {}
1173 post_upload_hook = context[function_name]
1174 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1175 raise PresubmitFailure(
1176 'Expected function "PostUploadHook" to take three arguments.')
1177 return post_upload_hook(cl, change, OutputApi(False))
1178
1179
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001180def DoGetTrySlaves(change,
1181 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001182 repository_root,
1183 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001184 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001185 verbose,
1186 output_stream):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001187 """Get the list of try servers from the presubmit scripts (deprecated).
thestig@chromium.orgde243452009-10-06 21:02:56 +00001188
1189 Args:
1190 changed_files: List of modified files.
1191 repository_root: The repository root.
1192 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +00001193 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001194 verbose: Prints debug info.
1195 output_stream: A stream to write debug output to.
1196
1197 Return:
1198 List of try slaves
1199 """
1200 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1201 if not presubmit_files and verbose:
mdempsky@chromium.orgd59e7612014-03-05 19:55:56 +00001202 output_stream.write("Warning, no PRESUBMIT.py found.\n")
thestig@chromium.orgde243452009-10-06 21:02:56 +00001203 results = []
1204 executer = GetTrySlavesExecuter()
stip@chromium.org5ca27622013-12-18 17:44:58 +00001205
thestig@chromium.orgde243452009-10-06 21:02:56 +00001206 if default_presubmit:
1207 if verbose:
1208 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001209 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001210 results.extend(executer.ExecPresubmitScript(
1211 default_presubmit, fake_path, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001212 for filename in presubmit_files:
1213 filename = os.path.abspath(filename)
1214 if verbose:
1215 output_stream.write("Running %s\n" % filename)
1216 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001217 presubmit_script = gclient_utils.FileRead(filename, 'rU')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001218 results.extend(executer.ExecPresubmitScript(
1219 presubmit_script, filename, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001220
stip@chromium.org5ca27622013-12-18 17:44:58 +00001221
1222 slave_dict = {}
1223 old_style = filter(lambda x: isinstance(x, basestring), results)
1224 new_style = filter(lambda x: isinstance(x, tuple), results)
1225
1226 for result in new_style:
1227 slave_dict.setdefault(result[0], set()).update(result[1])
1228 slaves = list(slave_dict.items())
1229
1230 slaves.extend(set(old_style))
stip@chromium.org68e04192013-11-04 22:14:38 +00001231
thestig@chromium.orgde243452009-10-06 21:02:56 +00001232 if slaves and verbose:
stip@chromium.org5ca27622013-12-18 17:44:58 +00001233 output_stream.write(', '.join((str(x) for x in slaves)))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001234 output_stream.write('\n')
1235 return slaves
1236
1237
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001238def _MergeMasters(masters1, masters2):
1239 """Merges two master maps. Merges also the tests of each builder."""
1240 result = {}
1241 for (master, builders) in itertools.chain(masters1.iteritems(),
1242 masters2.iteritems()):
1243 new_builders = result.setdefault(master, {})
1244 for (builder, tests) in builders.iteritems():
1245 new_builders.setdefault(builder, set([])).update(tests)
1246 return result
1247
1248
1249def DoGetTryMasters(change,
1250 changed_files,
1251 repository_root,
1252 default_presubmit,
1253 project,
1254 verbose,
1255 output_stream):
1256 """Get the list of try masters from the presubmit scripts.
1257
1258 Args:
1259 changed_files: List of modified files.
1260 repository_root: The repository root.
1261 default_presubmit: A default presubmit script to execute in any case.
1262 project: Optional name of a project used in selecting trybots.
1263 verbose: Prints debug info.
1264 output_stream: A stream to write debug output to.
1265
1266 Return:
1267 Map of try masters to map of builders to set of tests.
1268 """
1269 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1270 if not presubmit_files and verbose:
1271 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1272 results = {}
1273 executer = GetTryMastersExecuter()
1274
1275 if default_presubmit:
1276 if verbose:
1277 output_stream.write("Running default presubmit script.\n")
1278 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1279 results = _MergeMasters(results, executer.ExecPresubmitScript(
1280 default_presubmit, fake_path, project, change))
1281 for filename in presubmit_files:
1282 filename = os.path.abspath(filename)
1283 if verbose:
1284 output_stream.write("Running %s\n" % filename)
1285 # Accept CRLF presubmit script.
1286 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1287 results = _MergeMasters(results, executer.ExecPresubmitScript(
1288 presubmit_script, filename, project, change))
1289
1290 # Make sets to lists again for later JSON serialization.
1291 for builders in results.itervalues():
1292 for builder in builders:
1293 builders[builder] = list(builders[builder])
1294
1295 if results and verbose:
1296 output_stream.write('%s\n' % str(results))
1297 return results
1298
1299
rmistry@google.com5626a922015-02-26 14:03:30 +00001300def DoPostUploadExecuter(change,
1301 cl,
1302 repository_root,
1303 verbose,
1304 output_stream):
1305 """Execute the post upload hook.
1306
1307 Args:
1308 change: The Change object.
1309 cl: The Changelist object.
1310 repository_root: The repository root.
1311 verbose: Prints debug info.
1312 output_stream: A stream to write debug output to.
1313 """
1314 presubmit_files = ListRelevantPresubmitFiles(
1315 change.LocalPaths(), repository_root)
1316 if not presubmit_files and verbose:
1317 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1318 results = []
1319 executer = GetPostUploadExecuter()
1320 # The root presubmit file should be executed after the ones in subdirectories.
1321 # i.e. the specific post upload hooks should run before the general ones.
1322 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1323 presubmit_files.reverse()
1324
1325 for filename in presubmit_files:
1326 filename = os.path.abspath(filename)
1327 if verbose:
1328 output_stream.write("Running %s\n" % filename)
1329 # Accept CRLF presubmit script.
1330 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1331 results.extend(executer.ExecPresubmitScript(
1332 presubmit_script, filename, cl, change))
1333 output_stream.write('\n')
1334 if results:
1335 output_stream.write('** Post Upload Hook Messages **\n')
1336 for result in results:
1337 result.handle(output_stream)
1338 output_stream.write('\n')
1339
1340 return results
1341
1342
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001343class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001344 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001345 """
1346 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001347 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001348 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001349 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001350 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001351 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001352 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001353 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001354 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001355
1356 def ExecPresubmitScript(self, script_text, presubmit_path):
1357 """Executes a single presubmit script.
1358
1359 Args:
1360 script_text: The text of the presubmit script.
1361 presubmit_path: The path to the presubmit file (this will be reported via
1362 input_api.PresubmitLocalPath()).
1363
1364 Return:
1365 A list of result objects, empty if no problems.
1366 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001367
chase@chromium.org8e416c82009-10-06 04:30:44 +00001368 # Change to the presubmit file's directory to support local imports.
1369 main_path = os.getcwd()
1370 os.chdir(os.path.dirname(presubmit_path))
1371
1372 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001373 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001374 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001375 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001376 try:
1377 exec script_text in context
1378 except Exception, e:
1379 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001380
1381 # These function names must change if we make substantial changes to
1382 # the presubmit API that are not backwards compatible.
1383 if self.committing:
1384 function_name = 'CheckChangeOnCommit'
1385 else:
1386 function_name = 'CheckChangeOnUpload'
1387 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001388 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001389 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001390 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001391 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001392 if not (isinstance(result, types.TupleType) or
1393 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001394 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001395 'Presubmit functions must return a tuple or list')
1396 for item in result:
1397 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001398 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001399 'All presubmit results must be of types derived from '
1400 'output_api.PresubmitResult')
1401 else:
1402 result = () # no error since the script doesn't care about current event.
1403
chase@chromium.org8e416c82009-10-06 04:30:44 +00001404 # Return the process to the original working directory.
1405 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001406 return result
1407
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001408
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001409def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001410 committing,
1411 verbose,
1412 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001413 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001414 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001415 may_prompt,
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001416 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001417 """Runs all presubmit checks that apply to the files in the change.
1418
1419 This finds all PRESUBMIT.py files in directories enclosing the files in the
1420 change (up to the repository root) and calls the relevant entrypoint function
1421 depending on whether the change is being committed or uploaded.
1422
1423 Prints errors, warnings and notifications. Prompts the user for warnings
1424 when needed.
1425
1426 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001427 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001428 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1429 verbose: Prints debug info.
1430 output_stream: A stream to write output from presubmit tests to.
1431 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001432 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001433 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001434 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001435
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001436 Warning:
1437 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1438 SHOULD be sys.stdin.
1439
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001440 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001441 A PresubmitOutput object. Use output.should_continue() to figure out
1442 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001443 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001444 old_environ = os.environ
1445 try:
1446 # Make sure python subprocesses won't generate .pyc files.
1447 os.environ = os.environ.copy()
1448 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001449
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001450 output = PresubmitOutput(input_stream, output_stream)
1451 if committing:
1452 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001453 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001454 output.write("Running presubmit upload checks ...\n")
1455 start_time = time.time()
1456 presubmit_files = ListRelevantPresubmitFiles(
1457 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1458 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001459 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001460 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001461 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001462 if default_presubmit:
1463 if verbose:
1464 output.write("Running default presubmit script.\n")
1465 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1466 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1467 for filename in presubmit_files:
1468 filename = os.path.abspath(filename)
1469 if verbose:
1470 output.write("Running %s\n" % filename)
1471 # Accept CRLF presubmit script.
1472 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1473 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001474
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001475 errors = []
1476 notifications = []
1477 warnings = []
1478 for result in results:
1479 if result.fatal:
1480 errors.append(result)
1481 elif result.should_prompt:
1482 warnings.append(result)
1483 else:
1484 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001485
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001486 output.write('\n')
1487 for name, items in (('Messages', notifications),
1488 ('Warnings', warnings),
1489 ('ERRORS', errors)):
1490 if items:
1491 output.write('** Presubmit %s **\n' % name)
1492 for item in items:
1493 item.handle(output)
1494 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001495
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001496 total_time = time.time() - start_time
1497 if total_time > 1.0:
1498 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001499
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001500 if not errors:
1501 if not warnings:
1502 output.write('Presubmit checks passed.\n')
1503 elif may_prompt:
1504 output.prompt_yes_no('There were presubmit warnings. '
1505 'Are you sure you wish to continue? (y/N): ')
1506 else:
1507 output.fail()
1508
1509 global _ASKED_FOR_FEEDBACK
1510 # Ask for feedback one time out of 5.
1511 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001512 output.write(
1513 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1514 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1515 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001516 _ASKED_FOR_FEEDBACK = True
1517 return output
1518 finally:
1519 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001520
1521
1522def ScanSubDirs(mask, recursive):
1523 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001524 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001525 else:
1526 results = []
1527 for root, dirs, files in os.walk('.'):
1528 if '.svn' in dirs:
1529 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001530 if '.git' in dirs:
1531 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001532 for name in files:
1533 if fnmatch.fnmatch(name, mask):
1534 results.append(os.path.join(root, name))
1535 return results
1536
1537
1538def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001539 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001540 files = []
1541 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001542 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001543 return files
1544
1545
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001546def load_files(options, args):
1547 """Tries to determine the SCM."""
1548 change_scm = scm.determine_scm(options.root)
1549 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001550 if args:
1551 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001552 if change_scm == 'svn':
1553 change_class = SvnChange
1554 if not files:
1555 files = scm.SVN.CaptureStatus([], options.root)
1556 elif change_scm == 'git':
1557 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001558 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001559 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001560 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001561 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001562 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1563 if not files:
1564 return None, None
1565 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001566 return change_class, files
1567
1568
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001569class NonexistantCannedCheckFilter(Exception):
1570 pass
1571
1572
1573@contextlib.contextmanager
1574def canned_check_filter(method_names):
1575 filtered = {}
1576 try:
1577 for method_name in method_names:
1578 if not hasattr(presubmit_canned_checks, method_name):
1579 raise NonexistantCannedCheckFilter(method_name)
1580 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1581 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1582 yield
1583 finally:
1584 for name, method in filtered.iteritems():
1585 setattr(presubmit_canned_checks, name, method)
1586
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001587
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001588def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001589 """Runs an external program, potentially from a child process created by the
1590 multiprocessing module.
1591
1592 multiprocessing needs a top level function with a single argument.
1593 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001594 cmd_data.kwargs['stdout'] = subprocess.PIPE
1595 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1596 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001597 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001598 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001599 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001600 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001601 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001602 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001603 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1604 if code != 0:
1605 return cmd_data.message(
1606 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1607 if cmd_data.info:
1608 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001609
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001610
sbc@chromium.org013731e2015-02-26 18:28:43 +00001611def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001612 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001613 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001614 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001615 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001616 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1617 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001618 parser.add_option("-r", "--recursive", action="store_true",
1619 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001620 parser.add_option("-v", "--verbose", action="count", default=0,
1621 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001622 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001623 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001624 parser.add_option("--description", default='')
1625 parser.add_option("--issue", type='int', default=0)
1626 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001627 parser.add_option("--root", default=os.getcwd(),
1628 help="Search for PRESUBMIT.py up to this directory. "
1629 "If inherit-review-settings-ok is present in this "
1630 "directory, parent directories up to the root file "
1631 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001632 parser.add_option("--upstream",
1633 help="Git only: the base ref or upstream branch against "
1634 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001635 parser.add_option("--default_presubmit")
1636 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001637 parser.add_option("--skip_canned", action='append', default=[],
1638 help="A list of checks to skip which appear in "
1639 "presubmit_canned_checks. Can be provided multiple times "
1640 "to skip multiple canned checks.")
maruel@chromium.org239f4112011-06-03 20:08:23 +00001641 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1642 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001643 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1644 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001645 # These are for OAuth2 authentication for bots. See also apply_issue.py
1646 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1647 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1648
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001649 parser.add_option("--trybot-json",
1650 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001651 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001652 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001653 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001654
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001655 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001656 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001657 elif options.verbose:
1658 logging.basicConfig(level=logging.INFO)
1659 else:
1660 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001661
1662 if options.rietveld_email and options.rietveld_email_file:
1663 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1664 "can be passed to this program.")
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001665
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001666 if options.rietveld_email_file:
1667 with open(options.rietveld_email_file, "rb") as f:
1668 options.rietveld_email = f.read().strip()
1669
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001670 change_class, files = load_files(options, args)
1671 if not change_class:
1672 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001673 logging.info('Found %d file(s).' % len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001674
maruel@chromium.org239f4112011-06-03 20:08:23 +00001675 rietveld_obj = None
1676 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001677 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001678 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001679 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1680 options.rietveld_url,
1681 options.rietveld_email,
1682 options.rietveld_private_key_file)
1683 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001684 rietveld_obj = rietveld.CachingRietveld(
1685 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001686 auth_config,
1687 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001688 if options.rietveld_fetch:
1689 assert options.issue
1690 props = rietveld_obj.get_issue_properties(options.issue, False)
1691 options.author = props['owner_email']
1692 options.description = props['description']
1693 logging.info('Got author: "%s"', options.author)
1694 logging.info('Got description: """\n%s\n"""', options.description)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001695 if options.trybot_json:
1696 with open(options.trybot_json, 'w') as f:
1697 # Python's sets aren't JSON-encodable, so we convert them to lists here.
1698 class SetEncoder(json.JSONEncoder):
1699 # pylint: disable=E0202
1700 def default(self, obj):
1701 if isinstance(obj, set):
1702 return sorted(obj)
1703 return json.JSONEncoder.default(self, obj)
1704 change = change_class(options.name,
1705 options.description,
1706 options.root,
1707 files,
1708 options.issue,
1709 options.patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001710 options.author,
1711 upstream=options.upstream)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001712 trybots = DoGetTrySlaves(
1713 change,
1714 change.LocalPaths(),
1715 change.RepositoryRoot(),
1716 None,
1717 None,
1718 options.verbose,
1719 sys.stdout)
1720 json.dump(trybots, f, cls=SetEncoder)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001721 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001722 with canned_check_filter(options.skip_canned):
1723 results = DoPresubmitChecks(
1724 change_class(options.name,
1725 options.description,
1726 options.root,
1727 files,
1728 options.issue,
1729 options.patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001730 options.author,
1731 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001732 options.commit,
1733 options.verbose,
1734 sys.stdout,
1735 sys.stdin,
1736 options.default_presubmit,
1737 options.may_prompt,
1738 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001739 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001740 except NonexistantCannedCheckFilter, e:
1741 print >> sys.stderr, (
1742 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1743 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001744 except PresubmitFailure, e:
1745 print >> sys.stderr, e
1746 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1747 print >> sys.stderr, 'If all fails, contact maruel@'
1748 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001749
1750
1751if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001752 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001753 try:
1754 sys.exit(main())
1755 except KeyboardInterrupt:
1756 sys.stderr.write('interrupted\n')
1757 sys.exit(1)