blob: dabfbf0bac2e7bbf253aaa29245fca5378ed58eb [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
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000022import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000023import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000024import marshal # Exposed through the API.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000025import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import optparse
27import os # Somewhat exposed through the API.
28import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000029import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000030import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031import sys # Parts exposed through API.
32import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000033import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000034import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000035import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000036import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000037import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000038from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000039
40# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000041import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000042import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000043import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000044import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000045import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000046import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000047import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000048
49
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000050# Ask for feedback only once in program lifetime.
51_ASKED_FOR_FEEDBACK = False
52
53
maruel@chromium.org899e1c12011-04-07 17:03:18 +000054class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055 pass
56
57
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000058class CommandData(object):
59 def __init__(self, name, cmd, kwargs, message):
60 self.name = name
61 self.cmd = cmd
62 self.kwargs = kwargs
63 self.message = message
64 self.info = None
65
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000066
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000067def normpath(path):
68 '''Version of os.path.normpath that also changes backward slashes to
69 forward slashes when not running on Windows.
70 '''
71 # This is safe to always do because the Windows version of os.path.normpath
72 # will replace forward slashes with backward slashes.
73 path = path.replace(os.sep, '/')
74 return os.path.normpath(path)
75
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000076
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000077def _RightHandSideLinesImpl(affected_files):
78 """Implements RightHandSideLines for InputApi and GclChange."""
79 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000080 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000081 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000082 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000083
84
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000085class PresubmitOutput(object):
86 def __init__(self, input_stream=None, output_stream=None):
87 self.input_stream = input_stream
88 self.output_stream = output_stream
89 self.reviewers = []
90 self.written_output = []
91 self.error_count = 0
92
93 def prompt_yes_no(self, prompt_string):
94 self.write(prompt_string)
95 if self.input_stream:
96 response = self.input_stream.readline().strip().lower()
97 if response not in ('y', 'yes'):
98 self.fail()
99 else:
100 self.fail()
101
102 def fail(self):
103 self.error_count += 1
104
105 def should_continue(self):
106 return not self.error_count
107
108 def write(self, s):
109 self.written_output.append(s)
110 if self.output_stream:
111 self.output_stream.write(s)
112
113 def getvalue(self):
114 return ''.join(self.written_output)
115
116
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000117# Top level object so multiprocessing can pickle
118# Public access through OutputApi object.
119class _PresubmitResult(object):
120 """Base class for result objects."""
121 fatal = False
122 should_prompt = False
123
124 def __init__(self, message, items=None, long_text=''):
125 """
126 message: A short one-line message to indicate errors.
127 items: A list of short strings to indicate where errors occurred.
128 long_text: multi-line text output, e.g. from another tool
129 """
130 self._message = message
131 self._items = items or []
132 if items:
133 self._items = items
134 self._long_text = long_text.rstrip()
135
136 def handle(self, output):
137 output.write(self._message)
138 output.write('\n')
139 for index, item in enumerate(self._items):
140 output.write(' ')
141 # Write separately in case it's unicode.
142 output.write(str(item))
143 if index < len(self._items) - 1:
144 output.write(' \\')
145 output.write('\n')
146 if self._long_text:
147 output.write('\n***************\n')
148 # Write separately in case it's unicode.
149 output.write(self._long_text)
150 output.write('\n***************\n')
151 if self.fatal:
152 output.fail()
153
154
155# Top level object so multiprocessing can pickle
156# Public access through OutputApi object.
157class _PresubmitAddReviewers(_PresubmitResult):
158 """Add some suggested reviewers to the change."""
159 def __init__(self, reviewers):
160 super(_PresubmitAddReviewers, self).__init__('')
161 self.reviewers = reviewers
162
163 def handle(self, output):
164 output.reviewers.extend(self.reviewers)
165
166
167# Top level object so multiprocessing can pickle
168# Public access through OutputApi object.
169class _PresubmitError(_PresubmitResult):
170 """A hard presubmit error."""
171 fatal = True
172
173
174# Top level object so multiprocessing can pickle
175# Public access through OutputApi object.
176class _PresubmitPromptWarning(_PresubmitResult):
177 """An warning that prompts the user if they want to continue."""
178 should_prompt = True
179
180
181# Top level object so multiprocessing can pickle
182# Public access through OutputApi object.
183class _PresubmitNotifyResult(_PresubmitResult):
184 """Just print something to the screen -- but it's not even a warning."""
185 pass
186
187
188# Top level object so multiprocessing can pickle
189# Public access through OutputApi object.
190class _MailTextResult(_PresubmitResult):
191 """A warning that should be included in the review request email."""
192 def __init__(self, *args, **kwargs):
193 super(_MailTextResult, self).__init__()
194 raise NotImplementedError()
195
196
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000197class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000198 """An instance of OutputApi gets passed to presubmit scripts so that they
199 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000200 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000201 PresubmitResult = _PresubmitResult
202 PresubmitAddReviewers = _PresubmitAddReviewers
203 PresubmitError = _PresubmitError
204 PresubmitPromptWarning = _PresubmitPromptWarning
205 PresubmitNotifyResult = _PresubmitNotifyResult
206 MailTextResult = _MailTextResult
207
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000208 def __init__(self, is_committing):
209 self.is_committing = is_committing
210
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000211 def PresubmitPromptOrNotify(self, *args, **kwargs):
212 """Warn the user when uploading, but only notify if committing."""
213 if self.is_committing:
214 return self.PresubmitNotifyResult(*args, **kwargs)
215 return self.PresubmitPromptWarning(*args, **kwargs)
216
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000217
218class InputApi(object):
219 """An instance of this object is passed to presubmit scripts so they can
220 know stuff about the change they're looking at.
221 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000222 # Method could be a function
223 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000224
maruel@chromium.org3410d912009-06-09 20:56:16 +0000225 # File extensions that are considered source files from a style guide
226 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000227 #
228 # Files without an extension aren't included in the list. If you want to
229 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
230 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000231 DEFAULT_WHITE_LIST = (
232 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000233 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
234 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000235 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000236 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000237 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000238 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000239 )
240
241 # Path regexp that should be excluded from being considered containing source
242 # files. Don't modify this list from a presubmit script!
243 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000244 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000245 r".*\bexperimental[\\\/].*",
246 r".*\bthird_party[\\\/].*",
247 # Output directories (just in case)
248 r".*\bDebug[\\\/].*",
249 r".*\bRelease[\\\/].*",
250 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000251 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000252 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000253 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000254 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000255 r"(|.*[\\\/])\.git[\\\/].*",
256 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000257 # There is no point in processing a patch file.
258 r".+\.diff$",
259 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000260 )
261
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000262 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000263 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000264 """Builds an InputApi object.
265
266 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000267 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000269 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000270 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000272 # Version number of the presubmit_support script.
273 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000274 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000275 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000276 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000277 # TBD
278 self.host_url = 'http://codereview.chromium.org'
279 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000280 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000281
282 # We expose various modules and functions as attributes of the input_api
283 # so that presubmit scripts don't have to import them.
284 self.basename = os.path.basename
285 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000286 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000287 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000288 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000289 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000290 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000291 self.os_listdir = os.listdir
292 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000293 self.os_path = os.path
294 self.pickle = pickle
295 self.marshal = marshal
296 self.re = re
297 self.subprocess = subprocess
298 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000299 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000300 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000301 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000302 self.urllib2 = urllib2
303
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000304 # To easily fork python.
305 self.python_executable = sys.executable
306 self.environ = os.environ
307
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000308 # InputApi.platform is the platform you're currently running on.
309 self.platform = sys.platform
310
311 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000312 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000313
314 # We carry the canned checks so presubmit scripts can easily use them.
315 self.canned_checks = presubmit_canned_checks
316
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000317 # TODO(dpranke): figure out a list of all approved owners for a repo
318 # in order to be able to handle wildcard OWNERS files?
319 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000320 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000321 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000322 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000323
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000324 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000325 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000326 # Access to a protected member _XX of a client class
327 # pylint: disable=W0212
328 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000329 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000330 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
331 for (a, b, header) in cpplint._re_pattern_templates
332 ]
333
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000334 def PresubmitLocalPath(self):
335 """Returns the local path of the presubmit script currently being run.
336
337 This is useful if you don't want to hard-code absolute paths in the
338 presubmit script. For example, It can be used to find another file
339 relative to the PRESUBMIT.py script, so the whole tree can be branched and
340 the presubmit script still works, without editing its content.
341 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000342 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000343
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000344 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345 """Translate a depot path to a local path (relative to client root).
346
347 Args:
348 Depot path as a string.
349
350 Returns:
351 The local path of the depot path under the user's current client, or None
352 if the file is not mapped.
353
354 Remember to check for the None case and show an appropriate error!
355 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000356 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
357 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000358
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000359 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000360 """Translate a local path to a depot path.
361
362 Args:
363 Local path (relative to current directory, or absolute) as a string.
364
365 Returns:
366 The depot path (SVN URL) of the file if mapped, otherwise None.
367 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000368 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
369 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000370
sail@chromium.org5538e022011-05-12 17:53:16 +0000371 def AffectedFiles(self, include_dirs=False, include_deletes=True,
372 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000373 """Same as input_api.change.AffectedFiles() except only lists files
374 (and optionally directories) in the same directory as the current presubmit
375 script, or subdirectories thereof.
376 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000377 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000378 if len(dir_with_slash) == 1:
379 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000380
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000381 return filter(
382 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000383 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000384
385 def LocalPaths(self, include_dirs=False):
386 """Returns local paths of input_api.AffectedFiles()."""
387 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
388
389 def AbsoluteLocalPaths(self, include_dirs=False):
390 """Returns absolute local paths of input_api.AffectedFiles()."""
391 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
392
393 def ServerPaths(self, include_dirs=False):
394 """Returns server paths of input_api.AffectedFiles()."""
395 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
396
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000397 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000398 """Same as input_api.change.AffectedTextFiles() except only lists files
399 in the same directory as the current presubmit script, or subdirectories
400 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000401 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000402 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000403 warn("AffectedTextFiles(include_deletes=%s)"
404 " is deprecated and ignored" % str(include_deletes),
405 category=DeprecationWarning,
406 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000407 return filter(lambda x: x.IsTextFile(),
408 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000409
maruel@chromium.org3410d912009-06-09 20:56:16 +0000410 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
411 """Filters out files that aren't considered "source file".
412
413 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
414 and InputApi.DEFAULT_BLACK_LIST is used respectively.
415
416 The lists will be compiled as regular expression and
417 AffectedFile.LocalPath() needs to pass both list.
418
419 Note: Copy-paste this function to suit your needs or use a lambda function.
420 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000421 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000422 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000423 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000424 if self.re.match(item, local_path):
425 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000426 return True
427 return False
428 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
429 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
430
431 def AffectedSourceFiles(self, source_file):
432 """Filter the list of AffectedTextFiles by the function source_file.
433
434 If source_file is None, InputApi.FilterSourceFile() is used.
435 """
436 if not source_file:
437 source_file = self.FilterSourceFile
438 return filter(source_file, self.AffectedTextFiles())
439
440 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000441 """An iterator over all text lines in "new" version of changed files.
442
443 Only lists lines from new or modified text files in the change that are
444 contained by the directory of the currently executing presubmit script.
445
446 This is useful for doing line-by-line regex checks, like checking for
447 trailing whitespace.
448
449 Yields:
450 a 3 tuple:
451 the AffectedFile instance of the current file;
452 integer line number (1-based); and
453 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000454
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000455 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000456 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000457 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000458 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000459
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000460 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000461 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000462
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000463 Deny reading anything outside the repository.
464 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000465 if isinstance(file_item, AffectedFile):
466 file_item = file_item.AbsoluteLocalPath()
467 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000468 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000469 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000470
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000471 @property
472 def tbr(self):
473 """Returns if a change is TBR'ed."""
474 return 'TBR' in self.change.tags
475
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000476 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000477 tests = []
478 msgs = []
479 for t in tests_mix:
480 if isinstance(t, OutputApi.PresubmitResult):
481 msgs.append(t)
482 else:
483 assert issubclass(t.message, _PresubmitResult)
484 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000485 if self.verbose:
486 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000487 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000488 pool = multiprocessing.Pool()
489 # async recipe works around multiprocessing bug handling Ctrl-C
490 msgs.extend(pool.map_async(CallCommand, tests).get(99999))
491 pool.close()
492 pool.join()
493 else:
494 msgs.extend(map(CallCommand, tests))
495 return [m for m in msgs if m]
496
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000497
nick@chromium.orgff526192013-06-10 19:30:26 +0000498class _DiffCache(object):
499 """Caches diffs retrieved from a particular SCM."""
500
501 def GetDiff(self, path, local_root):
502 """Get the diff for a particular path."""
503 raise NotImplementedError()
504
505
506class _SvnDiffCache(_DiffCache):
507 """DiffCache implementation for subversion."""
508 def __init__(self):
509 super(_SvnDiffCache, self).__init__()
510 self._diffs_by_file = {}
511
512 def GetDiff(self, path, local_root):
513 if path not in self._diffs_by_file:
514 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
515 False, None)
516 return self._diffs_by_file[path]
517
518
519class _GitDiffCache(_DiffCache):
520 """DiffCache implementation for git; gets all file diffs at once."""
521 def __init__(self):
522 super(_GitDiffCache, self).__init__()
523 self._diffs_by_file = None
524
525 def GetDiff(self, path, local_root):
526 if not self._diffs_by_file:
527 # Compute a single diff for all files and parse the output; should
528 # with git this is much faster than computing one diff for each file.
529 diffs = {}
530
531 # Don't specify any filenames below, because there are command line length
532 # limits on some platforms and GenerateDiff would fail.
533 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True)
534
535 # This regex matches the path twice, separated by a space. Note that
536 # filename itself may contain spaces.
537 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
538 current_diff = []
539 keep_line_endings = True
540 for x in unified_diff.splitlines(keep_line_endings):
541 match = file_marker.match(x)
542 if match:
543 # Marks the start of a new per-file section.
544 diffs[match.group('filename')] = current_diff = [x]
545 elif x.startswith('diff --git'):
546 raise PresubmitFailure('Unexpected diff line: %s' % x)
547 else:
548 current_diff.append(x)
549
550 self._diffs_by_file = dict(
551 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
552
553 if path not in self._diffs_by_file:
554 raise PresubmitFailure(
555 'Unified diff did not contain entry for file %s' % path)
556
557 return self._diffs_by_file[path]
558
559
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000560class AffectedFile(object):
561 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000562
563 DIFF_CACHE = _DiffCache
564
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000565 # Method could be a function
566 # pylint: disable=R0201
nick@chromium.orgff526192013-06-10 19:30:26 +0000567 def __init__(self, path, action, repository_root, diff_cache=None):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000568 self._path = path
569 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000570 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000571 self._is_directory = None
572 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000573 self._cached_changed_contents = None
574 self._cached_new_contents = None
nick@chromium.orgff526192013-06-10 19:30:26 +0000575 if diff_cache:
576 self._diff_cache = diff_cache
577 else:
578 self._diff_cache = self.DIFF_CACHE()
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000579 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000580
581 def ServerPath(self):
582 """Returns a path string that identifies the file in the SCM system.
583
584 Returns the empty string if the file does not exist in SCM.
585 """
nick@chromium.orgff526192013-06-10 19:30:26 +0000586 return ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000587
588 def LocalPath(self):
589 """Returns the path of this file on the local disk relative to client root.
590 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000591 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000592
593 def AbsoluteLocalPath(self):
594 """Returns the absolute path of this file on the local disk.
595 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000596 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597
598 def IsDirectory(self):
599 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000600 if self._is_directory is None:
601 path = self.AbsoluteLocalPath()
602 self._is_directory = (os.path.exists(path) and
603 os.path.isdir(path))
604 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000605
606 def Action(self):
607 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000608 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
609 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000610 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000611
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000612 def Property(self, property_name):
613 """Returns the specified SCM property of this file, or None if no such
614 property.
615 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000616 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000617
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000618 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000619 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000620
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000621 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000622 raise NotImplementedError() # Implement when needed
623
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000624 def NewContents(self):
625 """Returns an iterator over the lines in the new version of file.
626
627 The new version is the file in the user's workspace, i.e. the "right hand
628 side".
629
630 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000631 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000632 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000633 if self._cached_new_contents is None:
634 self._cached_new_contents = []
635 if not self.IsDirectory():
636 try:
637 self._cached_new_contents = gclient_utils.FileRead(
638 self.AbsoluteLocalPath(), 'rU').splitlines()
639 except IOError:
640 pass # File not found? That's fine; maybe it was deleted.
641 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000642
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000643 def ChangedContents(self):
644 """Returns a list of tuples (line number, line text) of all new lines.
645
646 This relies on the scm diff output describing each changed code section
647 with a line of the form
648
649 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
650 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000651 if self._cached_changed_contents is not None:
652 return self._cached_changed_contents[:]
653 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000654 line_num = 0
655
656 if self.IsDirectory():
657 return []
658
659 for line in self.GenerateScmDiff().splitlines():
660 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
661 if m:
662 line_num = int(m.groups(1)[0])
663 continue
664 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000665 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000666 if not line.startswith('-'):
667 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000668 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000669
maruel@chromium.org5de13972009-06-10 18:16:06 +0000670 def __str__(self):
671 return self.LocalPath()
672
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000673 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000674 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000675
maruel@chromium.org58407af2011-04-12 23:15:57 +0000676
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000677class SvnAffectedFile(AffectedFile):
678 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000679 # Method 'NNN' is abstract in class 'NNN' but is not overridden
680 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000681
nick@chromium.orgff526192013-06-10 19:30:26 +0000682 DIFF_CACHE = _SvnDiffCache
683
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000684 def __init__(self, *args, **kwargs):
685 AffectedFile.__init__(self, *args, **kwargs)
686 self._server_path = None
687 self._is_text_file = None
688
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000689 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000690 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000691 self._server_path = scm.SVN.CaptureLocalInfo(
692 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000693 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000694
695 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000696 if self._is_directory is None:
697 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000698 if os.path.exists(path):
699 # Retrieve directly from the file system; it is much faster than
700 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000701 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000702 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000703 self._is_directory = scm.SVN.CaptureLocalInfo(
704 [self.LocalPath()], self._local_root
705 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000706 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000707
708 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000709 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000710 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000711 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000712 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000713
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000714 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000715 if self._is_text_file is None:
716 if self.Action() == 'D':
717 # A deleted file is not a text file.
718 self._is_text_file = False
719 elif self.IsDirectory():
720 self._is_text_file = False
721 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000722 mime_type = scm.SVN.GetFileProperty(
723 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000724 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
725 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000726
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000727
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000728class GitAffectedFile(AffectedFile):
729 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000730 # Method 'NNN' is abstract in class 'NNN' but is not overridden
731 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000732
nick@chromium.orgff526192013-06-10 19:30:26 +0000733 DIFF_CACHE = _GitDiffCache
734
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000735 def __init__(self, *args, **kwargs):
736 AffectedFile.__init__(self, *args, **kwargs)
737 self._server_path = None
738 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000739
740 def ServerPath(self):
741 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000742 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000743 return self._server_path
744
745 def IsDirectory(self):
746 if self._is_directory is None:
747 path = self.AbsoluteLocalPath()
748 if os.path.exists(path):
749 # Retrieve directly from the file system; it is much faster than
750 # querying subversion, especially on Windows.
751 self._is_directory = os.path.isdir(path)
752 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000753 self._is_directory = False
754 return self._is_directory
755
756 def Property(self, property_name):
757 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000758 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000759 return self._properties[property_name]
760
761 def IsTextFile(self):
762 if self._is_text_file is None:
763 if self.Action() == 'D':
764 # A deleted file is not a text file.
765 self._is_text_file = False
766 elif self.IsDirectory():
767 self._is_text_file = False
768 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000769 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
770 return self._is_text_file
771
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000772
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000773class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000774 """Describe a change.
775
776 Used directly by the presubmit scripts to query the current change being
777 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000778
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000779 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000780 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000781 self.KEY: equivalent to tags['KEY']
782 """
783
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000784 _AFFECTED_FILES = AffectedFile
785
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000786 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000787 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000788 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000789 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000790
maruel@chromium.org58407af2011-04-12 23:15:57 +0000791 def __init__(
792 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000793 if files is None:
794 files = []
795 self._name = name
796 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000797 # Convert root into an absolute path.
798 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000799 self.issue = issue
800 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000801 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000802
803 # From the description text, build up a dictionary of key/value pairs
804 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000805 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000806 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000807 for line in self._full_description.splitlines():
maruel@chromium.org428342a2011-11-10 15:46:33 +0000808 m = self.TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000809 if m:
810 self.tags[m.group('key')] = m.group('value')
811 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000812 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000813
814 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000815 self._description_without_tags = (
816 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817
maruel@chromium.orge085d812011-10-10 19:49:15 +0000818 assert all(
819 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
820
nick@chromium.orgff526192013-06-10 19:30:26 +0000821 diff_cache = self._AFFECTED_FILES.DIFF_CACHE()
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000822 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000823 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
824 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000825 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000826
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000827 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000828 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000829 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000831 def DescriptionText(self):
832 """Returns the user-entered changelist description, minus tags.
833
834 Any line in the user-provided description starting with e.g. "FOO="
835 (whitespace permitted before and around) is considered a tag line. Such
836 lines are stripped out of the description this function returns.
837 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000838 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839
840 def FullDescriptionText(self):
841 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000842 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843
844 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000845 """Returns the repository (checkout) root directory for this change,
846 as an absolute path.
847 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000848 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000849
850 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000851 """Return tags directly as attributes on the object."""
852 if not re.match(r"^[A-Z_]*$", attr):
853 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000854 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000855
sail@chromium.org5538e022011-05-12 17:53:16 +0000856 def AffectedFiles(self, include_dirs=False, include_deletes=True,
857 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000858 """Returns a list of AffectedFile instances for all files in the change.
859
860 Args:
861 include_deletes: If false, deleted files will be filtered out.
862 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000863 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000864
865 Returns:
866 [AffectedFile(path, action), AffectedFile(path, action)]
867 """
868 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000869 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000870 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000871 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000872
sail@chromium.org5538e022011-05-12 17:53:16 +0000873 affected = filter(file_filter, affected)
874
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000875 if include_deletes:
876 return affected
877 else:
878 return filter(lambda x: x.Action() != 'D', affected)
879
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000880 def AffectedTextFiles(self, include_deletes=None):
881 """Return a list of the existing text files in a change."""
882 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000883 warn("AffectedTextFiles(include_deletes=%s)"
884 " is deprecated and ignored" % str(include_deletes),
885 category=DeprecationWarning,
886 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000887 return filter(lambda x: x.IsTextFile(),
888 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889
890 def LocalPaths(self, include_dirs=False):
891 """Convenience function."""
892 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
893
894 def AbsoluteLocalPaths(self, include_dirs=False):
895 """Convenience function."""
896 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
897
898 def ServerPaths(self, include_dirs=False):
899 """Convenience function."""
900 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
901
902 def RightHandSideLines(self):
903 """An iterator over all text lines in "new" version of changed files.
904
905 Lists lines from new or modified text files in the change.
906
907 This is useful for doing line-by-line regex checks, like checking for
908 trailing whitespace.
909
910 Yields:
911 a 3 tuple:
912 the AffectedFile instance of the current file;
913 integer line number (1-based); and
914 the contents of the line as a string.
915 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000916 return _RightHandSideLinesImpl(
917 x for x in self.AffectedFiles(include_deletes=False)
918 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000919
920
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000921class SvnChange(Change):
922 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000923 scm = 'svn'
924 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000925
926 def _GetChangeLists(self):
927 """Get all change lists."""
928 if self._changelists == None:
929 previous_cwd = os.getcwd()
930 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000931 # Need to import here to avoid circular dependency.
932 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000933 self._changelists = gcl.GetModifiedFiles()
934 os.chdir(previous_cwd)
935 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000936
937 def GetAllModifiedFiles(self):
938 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000939 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000940 all_modified_files = []
941 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000942 all_modified_files.extend(
943 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000944 return all_modified_files
945
946 def GetModifiedFiles(self):
947 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000948 changelists = self._GetChangeLists()
949 return [os.path.join(self.RepositoryRoot(), f[1])
950 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000951
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000952
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000953class GitChange(Change):
954 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000955 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000956
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000957
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000958def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000959 """Finds all presubmit files that apply to a given set of source files.
960
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000961 If inherit-review-settings-ok is present right under root, looks for
962 PRESUBMIT.py in directories enclosing root.
963
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964 Args:
965 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000966 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000967
968 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000969 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000970 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000971 files = [normpath(os.path.join(root, f)) for f in files]
972
973 # List all the individual directories containing files.
974 directories = set([os.path.dirname(f) for f in files])
975
976 # Ignore root if inherit-review-settings-ok is present.
977 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
978 root = None
979
980 # Collect all unique directories that may contain PRESUBMIT.py.
981 candidates = set()
982 for directory in directories:
983 while True:
984 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000985 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000986 candidates.add(directory)
987 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000988 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000989 parent_dir = os.path.dirname(directory)
990 if parent_dir == directory:
991 # We hit the system root directory.
992 break
993 directory = parent_dir
994
995 # Look for PRESUBMIT.py in all candidate directories.
996 results = []
997 for directory in sorted(list(candidates)):
998 p = os.path.join(directory, 'PRESUBMIT.py')
999 if os.path.isfile(p):
1000 results.append(p)
1001
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001002 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001003 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001004
1005
thestig@chromium.orgde243452009-10-06 21:02:56 +00001006class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001007 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001008 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +00001009 """Executes GetPreferredTrySlaves() from a single presubmit script.
1010
1011 Args:
1012 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +00001013 presubmit_path: Project script to run.
1014 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001015
1016 Return:
1017 A list of try slaves.
1018 """
1019 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001020 try:
1021 exec script_text in context
1022 except Exception, e:
1023 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001024
1025 function_name = 'GetPreferredTrySlaves'
1026 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001027 get_preferred_try_slaves = context[function_name]
1028 function_info = inspect.getargspec(get_preferred_try_slaves)
1029 if len(function_info[0]) == 1:
1030 result = get_preferred_try_slaves(project)
1031 elif len(function_info[0]) == 2:
1032 result = get_preferred_try_slaves(project, change)
1033 else:
1034 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +00001035 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001036 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +00001037 'Presubmit functions must return a list, got a %s instead: %s' %
1038 (type(result), str(result)))
1039 for item in result:
stip@chromium.org68e04192013-11-04 22:14:38 +00001040 if isinstance(item, basestring):
1041 # Old-style ['bot'] format.
1042 botname = item
1043 elif isinstance(item, tuple):
1044 # New-style [('bot', set(['tests']))] format.
1045 botname = item[0]
1046 else:
1047 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1048 ' format.')
1049
1050 if botname != botname.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001051 raise PresubmitFailure(
1052 'Try slave names cannot start/end with whitespace')
stip@chromium.org68e04192013-11-04 22:14:38 +00001053 if ',' in botname:
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +00001054 raise PresubmitFailure(
stip@chromium.org68e04192013-11-04 22:14:38 +00001055 'Do not use \',\' separated builder or test names: %s' % botname)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001056 else:
1057 result = []
stip@chromium.org5ca27622013-12-18 17:44:58 +00001058
1059 def valid_oldstyle(result):
1060 return all(isinstance(i, basestring) for i in result)
1061
1062 def valid_newstyle(result):
1063 return (all(isinstance(i, tuple) for i in result) and
1064 all(len(i) == 2 for i in result) and
1065 all(isinstance(i[0], basestring) for i in result) and
1066 all(isinstance(i[1], set) for i in result)
1067 )
1068
1069 # Ensure it's either all old-style or all new-style.
1070 if not valid_oldstyle(result) and not valid_newstyle(result):
1071 raise PresubmitFailure(
1072 'PRESUBMIT.py returned invalid trybot specification!')
1073
thestig@chromium.orgde243452009-10-06 21:02:56 +00001074 return result
1075
1076
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001077def DoGetTrySlaves(change,
1078 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001079 repository_root,
1080 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001081 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001082 verbose,
1083 output_stream):
1084 """Get the list of try servers from the presubmit scripts.
1085
1086 Args:
1087 changed_files: List of modified files.
1088 repository_root: The repository root.
1089 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +00001090 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001091 verbose: Prints debug info.
1092 output_stream: A stream to write debug output to.
1093
1094 Return:
1095 List of try slaves
1096 """
1097 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1098 if not presubmit_files and verbose:
1099 output_stream.write("Warning, no presubmit.py found.\n")
1100 results = []
1101 executer = GetTrySlavesExecuter()
stip@chromium.org5ca27622013-12-18 17:44:58 +00001102
thestig@chromium.orgde243452009-10-06 21:02:56 +00001103 if default_presubmit:
1104 if verbose:
1105 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001106 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001107 results.extend(executer.ExecPresubmitScript(
1108 default_presubmit, fake_path, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001109 for filename in presubmit_files:
1110 filename = os.path.abspath(filename)
1111 if verbose:
1112 output_stream.write("Running %s\n" % filename)
1113 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001114 presubmit_script = gclient_utils.FileRead(filename, 'rU')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001115 results.extend(executer.ExecPresubmitScript(
1116 presubmit_script, filename, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001117
stip@chromium.org5ca27622013-12-18 17:44:58 +00001118
1119 slave_dict = {}
1120 old_style = filter(lambda x: isinstance(x, basestring), results)
1121 new_style = filter(lambda x: isinstance(x, tuple), results)
1122
1123 for result in new_style:
1124 slave_dict.setdefault(result[0], set()).update(result[1])
1125 slaves = list(slave_dict.items())
1126
1127 slaves.extend(set(old_style))
stip@chromium.org68e04192013-11-04 22:14:38 +00001128
thestig@chromium.orgde243452009-10-06 21:02:56 +00001129 if slaves and verbose:
stip@chromium.org5ca27622013-12-18 17:44:58 +00001130 output_stream.write(', '.join((str(x) for x in slaves)))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001131 output_stream.write('\n')
1132 return slaves
1133
1134
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001135class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001136 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001137 """
1138 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001139 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001140 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001141 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001142 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001143 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001144 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001145 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001146 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001147
1148 def ExecPresubmitScript(self, script_text, presubmit_path):
1149 """Executes a single presubmit script.
1150
1151 Args:
1152 script_text: The text of the presubmit script.
1153 presubmit_path: The path to the presubmit file (this will be reported via
1154 input_api.PresubmitLocalPath()).
1155
1156 Return:
1157 A list of result objects, empty if no problems.
1158 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001159
1160 # Change to the presubmit file's directory to support local imports.
1161 main_path = os.getcwd()
1162 os.chdir(os.path.dirname(presubmit_path))
1163
1164 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001165 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001166 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001167 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001168 try:
1169 exec script_text in context
1170 except Exception, e:
1171 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001172
1173 # These function names must change if we make substantial changes to
1174 # the presubmit API that are not backwards compatible.
1175 if self.committing:
1176 function_name = 'CheckChangeOnCommit'
1177 else:
1178 function_name = 'CheckChangeOnUpload'
1179 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001180 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001181 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001182 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001183 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001184 if not (isinstance(result, types.TupleType) or
1185 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001186 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001187 'Presubmit functions must return a tuple or list')
1188 for item in result:
1189 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001190 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001191 'All presubmit results must be of types derived from '
1192 'output_api.PresubmitResult')
1193 else:
1194 result = () # no error since the script doesn't care about current event.
1195
chase@chromium.org8e416c82009-10-06 04:30:44 +00001196 # Return the process to the original working directory.
1197 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001198 return result
1199
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001200
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001201def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001202 committing,
1203 verbose,
1204 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001205 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001206 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001207 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001208 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001209 """Runs all presubmit checks that apply to the files in the change.
1210
1211 This finds all PRESUBMIT.py files in directories enclosing the files in the
1212 change (up to the repository root) and calls the relevant entrypoint function
1213 depending on whether the change is being committed or uploaded.
1214
1215 Prints errors, warnings and notifications. Prompts the user for warnings
1216 when needed.
1217
1218 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001219 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001220 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1221 verbose: Prints debug info.
1222 output_stream: A stream to write output from presubmit tests to.
1223 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001224 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001225 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001226 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001227
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001228 Warning:
1229 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1230 SHOULD be sys.stdin.
1231
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001232 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001233 A PresubmitOutput object. Use output.should_continue() to figure out
1234 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001235 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001236 old_environ = os.environ
1237 try:
1238 # Make sure python subprocesses won't generate .pyc files.
1239 os.environ = os.environ.copy()
1240 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001241
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001242 output = PresubmitOutput(input_stream, output_stream)
1243 if committing:
1244 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001245 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001246 output.write("Running presubmit upload checks ...\n")
1247 start_time = time.time()
1248 presubmit_files = ListRelevantPresubmitFiles(
1249 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1250 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001251 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001252 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001253 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001254 if default_presubmit:
1255 if verbose:
1256 output.write("Running default presubmit script.\n")
1257 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1258 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1259 for filename in presubmit_files:
1260 filename = os.path.abspath(filename)
1261 if verbose:
1262 output.write("Running %s\n" % filename)
1263 # Accept CRLF presubmit script.
1264 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1265 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001266
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001267 errors = []
1268 notifications = []
1269 warnings = []
1270 for result in results:
1271 if result.fatal:
1272 errors.append(result)
1273 elif result.should_prompt:
1274 warnings.append(result)
1275 else:
1276 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001277
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001278 output.write('\n')
1279 for name, items in (('Messages', notifications),
1280 ('Warnings', warnings),
1281 ('ERRORS', errors)):
1282 if items:
1283 output.write('** Presubmit %s **\n' % name)
1284 for item in items:
1285 item.handle(output)
1286 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001287
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001288 total_time = time.time() - start_time
1289 if total_time > 1.0:
1290 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001291
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001292 if not errors:
1293 if not warnings:
1294 output.write('Presubmit checks passed.\n')
1295 elif may_prompt:
1296 output.prompt_yes_no('There were presubmit warnings. '
1297 'Are you sure you wish to continue? (y/N): ')
1298 else:
1299 output.fail()
1300
1301 global _ASKED_FOR_FEEDBACK
1302 # Ask for feedback one time out of 5.
1303 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001304 output.write(
1305 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1306 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1307 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001308 _ASKED_FOR_FEEDBACK = True
1309 return output
1310 finally:
1311 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001312
1313
1314def ScanSubDirs(mask, recursive):
1315 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001316 return [x for x in glob.glob(mask) if '.svn' not in x and '.git' not in x]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001317 else:
1318 results = []
1319 for root, dirs, files in os.walk('.'):
1320 if '.svn' in dirs:
1321 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001322 if '.git' in dirs:
1323 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001324 for name in files:
1325 if fnmatch.fnmatch(name, mask):
1326 results.append(os.path.join(root, name))
1327 return results
1328
1329
1330def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001331 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001332 files = []
1333 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001334 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001335 return files
1336
1337
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001338def load_files(options, args):
1339 """Tries to determine the SCM."""
1340 change_scm = scm.determine_scm(options.root)
1341 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001342 if args:
1343 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001344 if change_scm == 'svn':
1345 change_class = SvnChange
1346 if not files:
1347 files = scm.SVN.CaptureStatus([], options.root)
1348 elif change_scm == 'git':
1349 change_class = GitChange
1350 # TODO(maruel): Get upstream.
1351 if not files:
1352 files = scm.GIT.CaptureStatus([], options.root, None)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001353 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001354 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1355 if not files:
1356 return None, None
1357 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001358 return change_class, files
1359
1360
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001361class NonexistantCannedCheckFilter(Exception):
1362 pass
1363
1364
1365@contextlib.contextmanager
1366def canned_check_filter(method_names):
1367 filtered = {}
1368 try:
1369 for method_name in method_names:
1370 if not hasattr(presubmit_canned_checks, method_name):
1371 raise NonexistantCannedCheckFilter(method_name)
1372 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1373 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1374 yield
1375 finally:
1376 for name, method in filtered.iteritems():
1377 setattr(presubmit_canned_checks, name, method)
1378
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001379
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001380def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001381 """Runs an external program, potentially from a child process created by the
1382 multiprocessing module.
1383
1384 multiprocessing needs a top level function with a single argument.
1385 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001386 cmd_data.kwargs['stdout'] = subprocess.PIPE
1387 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1388 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001389 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001390 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001391 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001392 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001393 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001394 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001395 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1396 if code != 0:
1397 return cmd_data.message(
1398 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1399 if cmd_data.info:
1400 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001401
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001402
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001403def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001404 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001405 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001406 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001407 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001408 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1409 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001410 parser.add_option("-r", "--recursive", action="store_true",
1411 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001412 parser.add_option("-v", "--verbose", action="count", default=0,
1413 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001414 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001415 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001416 parser.add_option("--description", default='')
1417 parser.add_option("--issue", type='int', default=0)
1418 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001419 parser.add_option("--root", default=os.getcwd(),
1420 help="Search for PRESUBMIT.py up to this directory. "
1421 "If inherit-review-settings-ok is present in this "
1422 "directory, parent directories up to the root file "
1423 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001424 parser.add_option("--default_presubmit")
1425 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001426 parser.add_option("--skip_canned", action='append', default=[],
1427 help="A list of checks to skip which appear in "
1428 "presubmit_canned_checks. Can be provided multiple times "
1429 "to skip multiple canned checks.")
maruel@chromium.org239f4112011-06-03 20:08:23 +00001430 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1431 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1432 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001433 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1434 help=optparse.SUPPRESS_HELP)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001435 parser.add_option("--trybot-json",
1436 help="Output trybot information to the file specified.")
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001437 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001438 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001439 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001440 elif options.verbose:
1441 logging.basicConfig(level=logging.INFO)
1442 else:
1443 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001444 change_class, files = load_files(options, args)
1445 if not change_class:
1446 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001447 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001448 rietveld_obj = None
1449 if options.rietveld_url:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001450 rietveld_obj = rietveld.CachingRietveld(
maruel@chromium.org239f4112011-06-03 20:08:23 +00001451 options.rietveld_url,
1452 options.rietveld_email,
1453 options.rietveld_password)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001454 if options.rietveld_fetch:
1455 assert options.issue
1456 props = rietveld_obj.get_issue_properties(options.issue, False)
1457 options.author = props['owner_email']
1458 options.description = props['description']
1459 logging.info('Got author: "%s"', options.author)
1460 logging.info('Got description: """\n%s\n"""', options.description)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001461 if options.trybot_json:
1462 with open(options.trybot_json, 'w') as f:
1463 # Python's sets aren't JSON-encodable, so we convert them to lists here.
1464 class SetEncoder(json.JSONEncoder):
1465 # pylint: disable=E0202
1466 def default(self, obj):
1467 if isinstance(obj, set):
1468 return sorted(obj)
1469 return json.JSONEncoder.default(self, obj)
1470 change = change_class(options.name,
1471 options.description,
1472 options.root,
1473 files,
1474 options.issue,
1475 options.patchset,
1476 options.author)
1477 trybots = DoGetTrySlaves(
1478 change,
1479 change.LocalPaths(),
1480 change.RepositoryRoot(),
1481 None,
1482 None,
1483 options.verbose,
1484 sys.stdout)
1485 json.dump(trybots, f, cls=SetEncoder)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001486 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001487 with canned_check_filter(options.skip_canned):
1488 results = DoPresubmitChecks(
1489 change_class(options.name,
1490 options.description,
1491 options.root,
1492 files,
1493 options.issue,
1494 options.patchset,
1495 options.author),
1496 options.commit,
1497 options.verbose,
1498 sys.stdout,
1499 sys.stdin,
1500 options.default_presubmit,
1501 options.may_prompt,
1502 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001503 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001504 except NonexistantCannedCheckFilter, e:
1505 print >> sys.stderr, (
1506 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1507 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001508 except PresubmitFailure, e:
1509 print >> sys.stderr, e
1510 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1511 print >> sys.stderr, 'If all fails, contact maruel@'
1512 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001513
1514
1515if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001516 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001517 sys.exit(Main(None))