blob: 23cc5e427ab9cfe101e5cdc0ad65da14e0312807 [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
enne@chromium.orge72c5f52013-04-16 00:36:40 +00009__version__ = '1.6.2'
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.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000018import collections
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000019import contextlib
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000020import fnmatch
21import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000022import inspect
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.
maruel@chromium.org35625c72011-03-23 17:34:02 +000042import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000043import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000044import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000045import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000046import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000047import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000048import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049
50
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000051# Ask for feedback only once in program lifetime.
52_ASKED_FOR_FEEDBACK = False
53
54
maruel@chromium.org899e1c12011-04-07 17:03:18 +000055class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000056 pass
57
58
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000059CommandData = collections.namedtuple('CommandData',
60 ['name', 'cmd', 'kwargs', 'message'])
61
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000062def normpath(path):
63 '''Version of os.path.normpath that also changes backward slashes to
64 forward slashes when not running on Windows.
65 '''
66 # This is safe to always do because the Windows version of os.path.normpath
67 # will replace forward slashes with backward slashes.
68 path = path.replace(os.sep, '/')
69 return os.path.normpath(path)
70
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000071
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000072def _RightHandSideLinesImpl(affected_files):
73 """Implements RightHandSideLines for InputApi and GclChange."""
74 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000075 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000076 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000077 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078
79
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000080class PresubmitOutput(object):
81 def __init__(self, input_stream=None, output_stream=None):
82 self.input_stream = input_stream
83 self.output_stream = output_stream
84 self.reviewers = []
85 self.written_output = []
86 self.error_count = 0
87
88 def prompt_yes_no(self, prompt_string):
89 self.write(prompt_string)
90 if self.input_stream:
91 response = self.input_stream.readline().strip().lower()
92 if response not in ('y', 'yes'):
93 self.fail()
94 else:
95 self.fail()
96
97 def fail(self):
98 self.error_count += 1
99
100 def should_continue(self):
101 return not self.error_count
102
103 def write(self, s):
104 self.written_output.append(s)
105 if self.output_stream:
106 self.output_stream.write(s)
107
108 def getvalue(self):
109 return ''.join(self.written_output)
110
111
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000112# Top level object so multiprocessing can pickle
113# Public access through OutputApi object.
114class _PresubmitResult(object):
115 """Base class for result objects."""
116 fatal = False
117 should_prompt = False
118
119 def __init__(self, message, items=None, long_text=''):
120 """
121 message: A short one-line message to indicate errors.
122 items: A list of short strings to indicate where errors occurred.
123 long_text: multi-line text output, e.g. from another tool
124 """
125 self._message = message
126 self._items = items or []
127 if items:
128 self._items = items
129 self._long_text = long_text.rstrip()
130
131 def handle(self, output):
132 output.write(self._message)
133 output.write('\n')
134 for index, item in enumerate(self._items):
135 output.write(' ')
136 # Write separately in case it's unicode.
137 output.write(str(item))
138 if index < len(self._items) - 1:
139 output.write(' \\')
140 output.write('\n')
141 if self._long_text:
142 output.write('\n***************\n')
143 # Write separately in case it's unicode.
144 output.write(self._long_text)
145 output.write('\n***************\n')
146 if self.fatal:
147 output.fail()
148
149
150# Top level object so multiprocessing can pickle
151# Public access through OutputApi object.
152class _PresubmitAddReviewers(_PresubmitResult):
153 """Add some suggested reviewers to the change."""
154 def __init__(self, reviewers):
155 super(_PresubmitAddReviewers, self).__init__('')
156 self.reviewers = reviewers
157
158 def handle(self, output):
159 output.reviewers.extend(self.reviewers)
160
161
162# Top level object so multiprocessing can pickle
163# Public access through OutputApi object.
164class _PresubmitError(_PresubmitResult):
165 """A hard presubmit error."""
166 fatal = True
167
168
169# Top level object so multiprocessing can pickle
170# Public access through OutputApi object.
171class _PresubmitPromptWarning(_PresubmitResult):
172 """An warning that prompts the user if they want to continue."""
173 should_prompt = True
174
175
176# Top level object so multiprocessing can pickle
177# Public access through OutputApi object.
178class _PresubmitNotifyResult(_PresubmitResult):
179 """Just print something to the screen -- but it's not even a warning."""
180 pass
181
182
183# Top level object so multiprocessing can pickle
184# Public access through OutputApi object.
185class _MailTextResult(_PresubmitResult):
186 """A warning that should be included in the review request email."""
187 def __init__(self, *args, **kwargs):
188 super(_MailTextResult, self).__init__()
189 raise NotImplementedError()
190
191
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000192class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000193 """An instance of OutputApi gets passed to presubmit scripts so that they
194 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000195 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000196 PresubmitResult = _PresubmitResult
197 PresubmitAddReviewers = _PresubmitAddReviewers
198 PresubmitError = _PresubmitError
199 PresubmitPromptWarning = _PresubmitPromptWarning
200 PresubmitNotifyResult = _PresubmitNotifyResult
201 MailTextResult = _MailTextResult
202
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000203 def __init__(self, is_committing):
204 self.is_committing = is_committing
205
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000206 def PresubmitPromptOrNotify(self, *args, **kwargs):
207 """Warn the user when uploading, but only notify if committing."""
208 if self.is_committing:
209 return self.PresubmitNotifyResult(*args, **kwargs)
210 return self.PresubmitPromptWarning(*args, **kwargs)
211
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000212
213class InputApi(object):
214 """An instance of this object is passed to presubmit scripts so they can
215 know stuff about the change they're looking at.
216 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000217 # Method could be a function
218 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000219
maruel@chromium.org3410d912009-06-09 20:56:16 +0000220 # File extensions that are considered source files from a style guide
221 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000222 #
223 # Files without an extension aren't included in the list. If you want to
224 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
225 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000226 DEFAULT_WHITE_LIST = (
227 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000228 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
229 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000230 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000231 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000232 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000233 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000234 )
235
236 # Path regexp that should be excluded from being considered containing source
237 # files. Don't modify this list from a presubmit script!
238 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000239 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000240 r".*\bexperimental[\\\/].*",
241 r".*\bthird_party[\\\/].*",
242 # Output directories (just in case)
243 r".*\bDebug[\\\/].*",
244 r".*\bRelease[\\\/].*",
245 r".*\bxcodebuild[\\\/].*",
246 r".*\bsconsbuild[\\\/].*",
247 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000248 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000249 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000250 r"(|.*[\\\/])\.git[\\\/].*",
251 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000252 # There is no point in processing a patch file.
253 r".+\.diff$",
254 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000255 )
256
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000257 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000258 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000259 """Builds an InputApi object.
260
261 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000262 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000263 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000264 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000265 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000266 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000267 # Version number of the presubmit_support script.
268 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000270 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000271 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000272 # TBD
273 self.host_url = 'http://codereview.chromium.org'
274 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000275 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276
277 # We expose various modules and functions as attributes of the input_api
278 # so that presubmit scripts don't have to import them.
279 self.basename = os.path.basename
280 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000281 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000283 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000284 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000285 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000286 self.os_listdir = os.listdir
287 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000288 self.os_path = os.path
289 self.pickle = pickle
290 self.marshal = marshal
291 self.re = re
292 self.subprocess = subprocess
293 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000294 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000295 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000296 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000297 self.urllib2 = urllib2
298
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000299 # To easily fork python.
300 self.python_executable = sys.executable
301 self.environ = os.environ
302
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000303 # InputApi.platform is the platform you're currently running on.
304 self.platform = sys.platform
305
306 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000307 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000308
309 # We carry the canned checks so presubmit scripts can easily use them.
310 self.canned_checks = presubmit_canned_checks
311
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000312 # TODO(dpranke): figure out a list of all approved owners for a repo
313 # in order to be able to handle wildcard OWNERS files?
314 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000315 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000316 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000317 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000318
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000319 # Replace <hash_map> and <hash_set> as headers that need to be included
320 # with "base/hash_tables.h" instead.
321 # Access to a protected member _XX of a client class
322 # pylint: disable=W0212
323 self.cpplint._re_pattern_templates = [
324 (a, b, 'base/hash_tables.h')
325 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
326 for (a, b, header) in cpplint._re_pattern_templates
327 ]
328
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000329 def PresubmitLocalPath(self):
330 """Returns the local path of the presubmit script currently being run.
331
332 This is useful if you don't want to hard-code absolute paths in the
333 presubmit script. For example, It can be used to find another file
334 relative to the PRESUBMIT.py script, so the whole tree can be branched and
335 the presubmit script still works, without editing its content.
336 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000337 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000338
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000339 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000340 """Translate a depot path to a local path (relative to client root).
341
342 Args:
343 Depot path as a string.
344
345 Returns:
346 The local path of the depot path under the user's current client, or None
347 if the file is not mapped.
348
349 Remember to check for the None case and show an appropriate error!
350 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000351 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
352 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000353
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000354 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000355 """Translate a local path to a depot path.
356
357 Args:
358 Local path (relative to current directory, or absolute) as a string.
359
360 Returns:
361 The depot path (SVN URL) of the file if mapped, otherwise None.
362 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000363 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
364 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000365
sail@chromium.org5538e022011-05-12 17:53:16 +0000366 def AffectedFiles(self, include_dirs=False, include_deletes=True,
367 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000368 """Same as input_api.change.AffectedFiles() except only lists files
369 (and optionally directories) in the same directory as the current presubmit
370 script, or subdirectories thereof.
371 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000372 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000373 if len(dir_with_slash) == 1:
374 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000375
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000376 return filter(
377 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000378 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000379
380 def LocalPaths(self, include_dirs=False):
381 """Returns local paths of input_api.AffectedFiles()."""
382 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
383
384 def AbsoluteLocalPaths(self, include_dirs=False):
385 """Returns absolute local paths of input_api.AffectedFiles()."""
386 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
387
388 def ServerPaths(self, include_dirs=False):
389 """Returns server paths of input_api.AffectedFiles()."""
390 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
391
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000392 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000393 """Same as input_api.change.AffectedTextFiles() except only lists files
394 in the same directory as the current presubmit script, or subdirectories
395 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000396 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000397 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000398 warn("AffectedTextFiles(include_deletes=%s)"
399 " is deprecated and ignored" % str(include_deletes),
400 category=DeprecationWarning,
401 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000402 return filter(lambda x: x.IsTextFile(),
403 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000404
maruel@chromium.org3410d912009-06-09 20:56:16 +0000405 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
406 """Filters out files that aren't considered "source file".
407
408 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
409 and InputApi.DEFAULT_BLACK_LIST is used respectively.
410
411 The lists will be compiled as regular expression and
412 AffectedFile.LocalPath() needs to pass both list.
413
414 Note: Copy-paste this function to suit your needs or use a lambda function.
415 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000416 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000417 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000418 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000419 if self.re.match(item, local_path):
420 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000421 return True
422 return False
423 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
424 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
425
426 def AffectedSourceFiles(self, source_file):
427 """Filter the list of AffectedTextFiles by the function source_file.
428
429 If source_file is None, InputApi.FilterSourceFile() is used.
430 """
431 if not source_file:
432 source_file = self.FilterSourceFile
433 return filter(source_file, self.AffectedTextFiles())
434
435 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000436 """An iterator over all text lines in "new" version of changed files.
437
438 Only lists lines from new or modified text files in the change that are
439 contained by the directory of the currently executing presubmit script.
440
441 This is useful for doing line-by-line regex checks, like checking for
442 trailing whitespace.
443
444 Yields:
445 a 3 tuple:
446 the AffectedFile instance of the current file;
447 integer line number (1-based); and
448 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000449
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000450 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000451 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000452 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000453 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000455 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000456 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000457
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000458 Deny reading anything outside the repository.
459 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000460 if isinstance(file_item, AffectedFile):
461 file_item = file_item.AbsoluteLocalPath()
462 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000463 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000464 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000465
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000466 @property
467 def tbr(self):
468 """Returns if a change is TBR'ed."""
469 return 'TBR' in self.change.tags
470
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000471 @staticmethod
472 def RunTests(tests_mix, parallel=True):
473 tests = []
474 msgs = []
475 for t in tests_mix:
476 if isinstance(t, OutputApi.PresubmitResult):
477 msgs.append(t)
478 else:
479 assert issubclass(t.message, _PresubmitResult)
480 tests.append(t)
ilevy@chromium.org9c60ab22013-05-16 20:04:00 +0000481 if tests and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000482 pool = multiprocessing.Pool()
483 # async recipe works around multiprocessing bug handling Ctrl-C
484 msgs.extend(pool.map_async(CallCommand, tests).get(99999))
485 pool.close()
486 pool.join()
487 else:
488 msgs.extend(map(CallCommand, tests))
489 return [m for m in msgs if m]
490
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000491
492class AffectedFile(object):
493 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000494 # Method could be a function
495 # pylint: disable=R0201
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000496 def __init__(self, path, action, repository_root):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000497 self._path = path
498 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000499 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000500 self._is_directory = None
501 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000502 self._cached_changed_contents = None
503 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000504 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000505
506 def ServerPath(self):
507 """Returns a path string that identifies the file in the SCM system.
508
509 Returns the empty string if the file does not exist in SCM.
510 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000511 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000512
513 def LocalPath(self):
514 """Returns the path of this file on the local disk relative to client root.
515 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000516 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000517
518 def AbsoluteLocalPath(self):
519 """Returns the absolute path of this file on the local disk.
520 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000521 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000522
523 def IsDirectory(self):
524 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000525 if self._is_directory is None:
526 path = self.AbsoluteLocalPath()
527 self._is_directory = (os.path.exists(path) and
528 os.path.isdir(path))
529 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000530
531 def Action(self):
532 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000533 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
534 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000535 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000536
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000537 def Property(self, property_name):
538 """Returns the specified SCM property of this file, or None if no such
539 property.
540 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000541 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000542
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000543 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000544 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000545
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000546 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000547 raise NotImplementedError() # Implement when needed
548
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000549 def NewContents(self):
550 """Returns an iterator over the lines in the new version of file.
551
552 The new version is the file in the user's workspace, i.e. the "right hand
553 side".
554
555 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000556 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000557 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000558 if self._cached_new_contents is None:
559 self._cached_new_contents = []
560 if not self.IsDirectory():
561 try:
562 self._cached_new_contents = gclient_utils.FileRead(
563 self.AbsoluteLocalPath(), 'rU').splitlines()
564 except IOError:
565 pass # File not found? That's fine; maybe it was deleted.
566 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000567
568 def OldContents(self):
569 """Returns an iterator over the lines in the old version of file.
570
571 The old version is the file in depot, i.e. the "left hand side".
572 """
573 raise NotImplementedError() # Implement when needed
574
575 def OldFileTempPath(self):
576 """Returns the path on local disk where the old contents resides.
577
578 The old version is the file in depot, i.e. the "left hand side".
579 This is a read-only cached copy of the old contents. *DO NOT* try to
580 modify this file.
581 """
582 raise NotImplementedError() # Implement if/when needed.
583
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000584 def ChangedContents(self):
585 """Returns a list of tuples (line number, line text) of all new lines.
586
587 This relies on the scm diff output describing each changed code section
588 with a line of the form
589
590 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
591 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000592 if self._cached_changed_contents is not None:
593 return self._cached_changed_contents[:]
594 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000595 line_num = 0
596
597 if self.IsDirectory():
598 return []
599
600 for line in self.GenerateScmDiff().splitlines():
601 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
602 if m:
603 line_num = int(m.groups(1)[0])
604 continue
605 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000606 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000607 if not line.startswith('-'):
608 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000609 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000610
maruel@chromium.org5de13972009-06-10 18:16:06 +0000611 def __str__(self):
612 return self.LocalPath()
613
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000614 def GenerateScmDiff(self):
615 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000616
maruel@chromium.org58407af2011-04-12 23:15:57 +0000617
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000618class SvnAffectedFile(AffectedFile):
619 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000620 # Method 'NNN' is abstract in class 'NNN' but is not overridden
621 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000622
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000623 def __init__(self, *args, **kwargs):
624 AffectedFile.__init__(self, *args, **kwargs)
625 self._server_path = None
626 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000627 self._diff = None
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000628
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000629 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000630 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000631 self._server_path = scm.SVN.CaptureLocalInfo(
632 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000633 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000634
635 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000636 if self._is_directory is None:
637 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000638 if os.path.exists(path):
639 # Retrieve directly from the file system; it is much faster than
640 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000641 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000642 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000643 self._is_directory = scm.SVN.CaptureLocalInfo(
644 [self.LocalPath()], self._local_root
645 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000646 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000647
648 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000649 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000650 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000651 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000652 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000653
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000654 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000655 if self._is_text_file is None:
656 if self.Action() == 'D':
657 # A deleted file is not a text file.
658 self._is_text_file = False
659 elif self.IsDirectory():
660 self._is_text_file = False
661 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000662 mime_type = scm.SVN.GetFileProperty(
663 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000664 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
665 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000666
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000667 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000668 if self._diff is None:
669 self._diff = scm.SVN.GenerateDiff(
670 [self.LocalPath()], self._local_root, False, None)
671 return self._diff
maruel@chromium.org1f312812011-02-10 01:33:57 +0000672
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000673
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000674class GitAffectedFile(AffectedFile):
675 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000676 # Method 'NNN' is abstract in class 'NNN' but is not overridden
677 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000678
679 def __init__(self, *args, **kwargs):
680 AffectedFile.__init__(self, *args, **kwargs)
681 self._server_path = None
682 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000683 self._diff = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000684
685 def ServerPath(self):
686 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000687 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000688 return self._server_path
689
690 def IsDirectory(self):
691 if self._is_directory is None:
692 path = self.AbsoluteLocalPath()
693 if os.path.exists(path):
694 # Retrieve directly from the file system; it is much faster than
695 # querying subversion, especially on Windows.
696 self._is_directory = os.path.isdir(path)
697 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000698 self._is_directory = False
699 return self._is_directory
700
701 def Property(self, property_name):
702 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000703 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000704 return self._properties[property_name]
705
706 def IsTextFile(self):
707 if self._is_text_file is None:
708 if self.Action() == 'D':
709 # A deleted file is not a text file.
710 self._is_text_file = False
711 elif self.IsDirectory():
712 self._is_text_file = False
713 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000714 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
715 return self._is_text_file
716
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000717 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000718 if self._diff is None:
719 self._diff = scm.GIT.GenerateDiff(
720 self._local_root, files=[self.LocalPath(),])
721 return self._diff
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000722
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000723
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000724class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000725 """Describe a change.
726
727 Used directly by the presubmit scripts to query the current change being
728 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000729
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000730 Instance members:
731 tags: Dictionnary of KEY=VALUE pairs found in the change description.
732 self.KEY: equivalent to tags['KEY']
733 """
734
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000735 _AFFECTED_FILES = AffectedFile
736
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000737 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000738 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000739 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000740 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741
maruel@chromium.org58407af2011-04-12 23:15:57 +0000742 def __init__(
743 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000744 if files is None:
745 files = []
746 self._name = name
747 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000748 # Convert root into an absolute path.
749 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000750 self.issue = issue
751 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000752 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000753
754 # From the description text, build up a dictionary of key/value pairs
755 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000756 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000757 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000758 for line in self._full_description.splitlines():
maruel@chromium.org428342a2011-11-10 15:46:33 +0000759 m = self.TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000760 if m:
761 self.tags[m.group('key')] = m.group('value')
762 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000763 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000764
765 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000766 self._description_without_tags = (
767 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000768
maruel@chromium.orge085d812011-10-10 19:49:15 +0000769 assert all(
770 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
771
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000772 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000773 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
774 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000775 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000776
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000777 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000778 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000779 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000780
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000781 def DescriptionText(self):
782 """Returns the user-entered changelist description, minus tags.
783
784 Any line in the user-provided description starting with e.g. "FOO="
785 (whitespace permitted before and around) is considered a tag line. Such
786 lines are stripped out of the description this function returns.
787 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000788 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000789
790 def FullDescriptionText(self):
791 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000792 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000793
794 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000795 """Returns the repository (checkout) root directory for this change,
796 as an absolute path.
797 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000798 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000799
800 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000801 """Return tags directly as attributes on the object."""
802 if not re.match(r"^[A-Z_]*$", attr):
803 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000804 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000805
sail@chromium.org5538e022011-05-12 17:53:16 +0000806 def AffectedFiles(self, include_dirs=False, include_deletes=True,
807 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000808 """Returns a list of AffectedFile instances for all files in the change.
809
810 Args:
811 include_deletes: If false, deleted files will be filtered out.
812 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000813 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000814
815 Returns:
816 [AffectedFile(path, action), AffectedFile(path, action)]
817 """
818 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000819 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000821 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000822
sail@chromium.org5538e022011-05-12 17:53:16 +0000823 affected = filter(file_filter, affected)
824
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825 if include_deletes:
826 return affected
827 else:
828 return filter(lambda x: x.Action() != 'D', affected)
829
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000830 def AffectedTextFiles(self, include_deletes=None):
831 """Return a list of the existing text files in a change."""
832 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000833 warn("AffectedTextFiles(include_deletes=%s)"
834 " is deprecated and ignored" % str(include_deletes),
835 category=DeprecationWarning,
836 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000837 return filter(lambda x: x.IsTextFile(),
838 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839
840 def LocalPaths(self, include_dirs=False):
841 """Convenience function."""
842 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
843
844 def AbsoluteLocalPaths(self, include_dirs=False):
845 """Convenience function."""
846 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
847
848 def ServerPaths(self, include_dirs=False):
849 """Convenience function."""
850 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
851
852 def RightHandSideLines(self):
853 """An iterator over all text lines in "new" version of changed files.
854
855 Lists lines from new or modified text files in the change.
856
857 This is useful for doing line-by-line regex checks, like checking for
858 trailing whitespace.
859
860 Yields:
861 a 3 tuple:
862 the AffectedFile instance of the current file;
863 integer line number (1-based); and
864 the contents of the line as a string.
865 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000866 return _RightHandSideLinesImpl(
867 x for x in self.AffectedFiles(include_deletes=False)
868 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869
870
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000871class SvnChange(Change):
872 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000873 scm = 'svn'
874 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000875
876 def _GetChangeLists(self):
877 """Get all change lists."""
878 if self._changelists == None:
879 previous_cwd = os.getcwd()
880 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000881 # Need to import here to avoid circular dependency.
882 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000883 self._changelists = gcl.GetModifiedFiles()
884 os.chdir(previous_cwd)
885 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000886
887 def GetAllModifiedFiles(self):
888 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000889 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000890 all_modified_files = []
891 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000892 all_modified_files.extend(
893 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000894 return all_modified_files
895
896 def GetModifiedFiles(self):
897 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000898 changelists = self._GetChangeLists()
899 return [os.path.join(self.RepositoryRoot(), f[1])
900 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000901
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000902
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000903class GitChange(Change):
904 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000905 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000906
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000907
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000908def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000909 """Finds all presubmit files that apply to a given set of source files.
910
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000911 If inherit-review-settings-ok is present right under root, looks for
912 PRESUBMIT.py in directories enclosing root.
913
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000914 Args:
915 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000916 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000917
918 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000919 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000920 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000921 files = [normpath(os.path.join(root, f)) for f in files]
922
923 # List all the individual directories containing files.
924 directories = set([os.path.dirname(f) for f in files])
925
926 # Ignore root if inherit-review-settings-ok is present.
927 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
928 root = None
929
930 # Collect all unique directories that may contain PRESUBMIT.py.
931 candidates = set()
932 for directory in directories:
933 while True:
934 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000936 candidates.add(directory)
937 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000938 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000939 parent_dir = os.path.dirname(directory)
940 if parent_dir == directory:
941 # We hit the system root directory.
942 break
943 directory = parent_dir
944
945 # Look for PRESUBMIT.py in all candidate directories.
946 results = []
947 for directory in sorted(list(candidates)):
948 p = os.path.join(directory, 'PRESUBMIT.py')
949 if os.path.isfile(p):
950 results.append(p)
951
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000952 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000953 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000954
955
thestig@chromium.orgde243452009-10-06 21:02:56 +0000956class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000957 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000958 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000959 """Executes GetPreferredTrySlaves() from a single presubmit script.
960
961 Args:
962 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000963 presubmit_path: Project script to run.
964 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000965
966 Return:
967 A list of try slaves.
968 """
969 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000970 try:
971 exec script_text in context
972 except Exception, e:
973 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000974
975 function_name = 'GetPreferredTrySlaves'
976 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000977 get_preferred_try_slaves = context[function_name]
978 function_info = inspect.getargspec(get_preferred_try_slaves)
979 if len(function_info[0]) == 1:
980 result = get_preferred_try_slaves(project)
981 elif len(function_info[0]) == 2:
982 result = get_preferred_try_slaves(project, change)
983 else:
984 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +0000985 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000986 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000987 'Presubmit functions must return a list, got a %s instead: %s' %
988 (type(result), str(result)))
989 for item in result:
990 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000991 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000992 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000993 raise PresubmitFailure(
994 'Try slave names cannot start/end with whitespace')
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +0000995 if ',' in item:
996 raise PresubmitFailure(
997 'Do not use \',\' separated builder or test names: %s' % item)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000998 else:
999 result = []
1000 return result
1001
1002
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001003def DoGetTrySlaves(change,
1004 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001005 repository_root,
1006 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001007 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001008 verbose,
1009 output_stream):
1010 """Get the list of try servers from the presubmit scripts.
1011
1012 Args:
1013 changed_files: List of modified files.
1014 repository_root: The repository root.
1015 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +00001016 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001017 verbose: Prints debug info.
1018 output_stream: A stream to write debug output to.
1019
1020 Return:
1021 List of try slaves
1022 """
1023 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1024 if not presubmit_files and verbose:
1025 output_stream.write("Warning, no presubmit.py found.\n")
1026 results = []
1027 executer = GetTrySlavesExecuter()
1028 if default_presubmit:
1029 if verbose:
1030 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001031 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +00001032 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001033 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001034 for filename in presubmit_files:
1035 filename = os.path.abspath(filename)
1036 if verbose:
1037 output_stream.write("Running %s\n" % filename)
1038 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001039 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +00001040 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001041 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001042
1043 slaves = list(set(results))
1044 if slaves and verbose:
1045 output_stream.write(', '.join(slaves))
1046 output_stream.write('\n')
1047 return slaves
1048
1049
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001050class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001051 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001052 """
1053 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001054 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001055 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001056 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001057 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001058 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001059 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001060 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001061 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001062
1063 def ExecPresubmitScript(self, script_text, presubmit_path):
1064 """Executes a single presubmit script.
1065
1066 Args:
1067 script_text: The text of the presubmit script.
1068 presubmit_path: The path to the presubmit file (this will be reported via
1069 input_api.PresubmitLocalPath()).
1070
1071 Return:
1072 A list of result objects, empty if no problems.
1073 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001074
1075 # Change to the presubmit file's directory to support local imports.
1076 main_path = os.getcwd()
1077 os.chdir(os.path.dirname(presubmit_path))
1078
1079 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001080 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001081 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001082 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001083 try:
1084 exec script_text in context
1085 except Exception, e:
1086 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001087
1088 # These function names must change if we make substantial changes to
1089 # the presubmit API that are not backwards compatible.
1090 if self.committing:
1091 function_name = 'CheckChangeOnCommit'
1092 else:
1093 function_name = 'CheckChangeOnUpload'
1094 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001095 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001096 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001097 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001098 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001099 if not (isinstance(result, types.TupleType) or
1100 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001101 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001102 'Presubmit functions must return a tuple or list')
1103 for item in result:
1104 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001105 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001106 'All presubmit results must be of types derived from '
1107 'output_api.PresubmitResult')
1108 else:
1109 result = () # no error since the script doesn't care about current event.
1110
chase@chromium.org8e416c82009-10-06 04:30:44 +00001111 # Return the process to the original working directory.
1112 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001113 return result
1114
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001115
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001116def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001117 committing,
1118 verbose,
1119 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001120 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001121 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001122 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001123 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001124 """Runs all presubmit checks that apply to the files in the change.
1125
1126 This finds all PRESUBMIT.py files in directories enclosing the files in the
1127 change (up to the repository root) and calls the relevant entrypoint function
1128 depending on whether the change is being committed or uploaded.
1129
1130 Prints errors, warnings and notifications. Prompts the user for warnings
1131 when needed.
1132
1133 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001134 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001135 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1136 verbose: Prints debug info.
1137 output_stream: A stream to write output from presubmit tests to.
1138 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001139 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001140 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001141 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001142
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001143 Warning:
1144 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1145 SHOULD be sys.stdin.
1146
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001147 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001148 A PresubmitOutput object. Use output.should_continue() to figure out
1149 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001150 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001151 old_environ = os.environ
1152 try:
1153 # Make sure python subprocesses won't generate .pyc files.
1154 os.environ = os.environ.copy()
1155 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001156
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001157 output = PresubmitOutput(input_stream, output_stream)
1158 if committing:
1159 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001160 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001161 output.write("Running presubmit upload checks ...\n")
1162 start_time = time.time()
1163 presubmit_files = ListRelevantPresubmitFiles(
1164 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1165 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001166 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001167 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001168 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001169 if default_presubmit:
1170 if verbose:
1171 output.write("Running default presubmit script.\n")
1172 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1173 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1174 for filename in presubmit_files:
1175 filename = os.path.abspath(filename)
1176 if verbose:
1177 output.write("Running %s\n" % filename)
1178 # Accept CRLF presubmit script.
1179 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1180 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001181
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001182 errors = []
1183 notifications = []
1184 warnings = []
1185 for result in results:
1186 if result.fatal:
1187 errors.append(result)
1188 elif result.should_prompt:
1189 warnings.append(result)
1190 else:
1191 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001192
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001193 output.write('\n')
1194 for name, items in (('Messages', notifications),
1195 ('Warnings', warnings),
1196 ('ERRORS', errors)):
1197 if items:
1198 output.write('** Presubmit %s **\n' % name)
1199 for item in items:
1200 item.handle(output)
1201 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001202
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001203 total_time = time.time() - start_time
1204 if total_time > 1.0:
1205 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001206
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001207 if not errors:
1208 if not warnings:
1209 output.write('Presubmit checks passed.\n')
1210 elif may_prompt:
1211 output.prompt_yes_no('There were presubmit warnings. '
1212 'Are you sure you wish to continue? (y/N): ')
1213 else:
1214 output.fail()
1215
1216 global _ASKED_FOR_FEEDBACK
1217 # Ask for feedback one time out of 5.
1218 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1219 output.write("Was the presubmit check useful? Please send feedback "
1220 "& hate mail to maruel@chromium.org!\n")
1221 _ASKED_FOR_FEEDBACK = True
1222 return output
1223 finally:
1224 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001225
1226
1227def ScanSubDirs(mask, recursive):
1228 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001229 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 +00001230 else:
1231 results = []
1232 for root, dirs, files in os.walk('.'):
1233 if '.svn' in dirs:
1234 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001235 if '.git' in dirs:
1236 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001237 for name in files:
1238 if fnmatch.fnmatch(name, mask):
1239 results.append(os.path.join(root, name))
1240 return results
1241
1242
1243def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001244 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001245 files = []
1246 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001247 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001248 return files
1249
1250
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001251def load_files(options, args):
1252 """Tries to determine the SCM."""
1253 change_scm = scm.determine_scm(options.root)
1254 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001255 if args:
1256 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001257 if change_scm == 'svn':
1258 change_class = SvnChange
1259 if not files:
1260 files = scm.SVN.CaptureStatus([], options.root)
1261 elif change_scm == 'git':
1262 change_class = GitChange
1263 # TODO(maruel): Get upstream.
1264 if not files:
1265 files = scm.GIT.CaptureStatus([], options.root, None)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001266 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001267 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1268 if not files:
1269 return None, None
1270 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001271 return change_class, files
1272
1273
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001274class NonexistantCannedCheckFilter(Exception):
1275 pass
1276
1277
1278@contextlib.contextmanager
1279def canned_check_filter(method_names):
1280 filtered = {}
1281 try:
1282 for method_name in method_names:
1283 if not hasattr(presubmit_canned_checks, method_name):
1284 raise NonexistantCannedCheckFilter(method_name)
1285 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1286 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1287 yield
1288 finally:
1289 for name, method in filtered.iteritems():
1290 setattr(presubmit_canned_checks, name, method)
1291
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001292def CallCommand(cmd_data):
1293 # multiprocessing needs a top level function with a single argument.
1294 cmd_data.kwargs['stdout'] = subprocess.PIPE
1295 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1296 try:
1297 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
1298 if code != 0:
1299 return cmd_data.message('%s failed\n%s' % (cmd_data.name, out))
1300 except OSError as e:
1301 return cmd_data.message(
ilevy@chromium.orge7ceaad2013-04-27 00:58:45 +00001302 '%s exec failure\n %s' % (cmd_data.name, e))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001303
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001304
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001305def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001306 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001307 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001308 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001309 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001310 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1311 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001312 parser.add_option("-r", "--recursive", action="store_true",
1313 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001314 parser.add_option("-v", "--verbose", action="count", default=0,
1315 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001316 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001317 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001318 parser.add_option("--description", default='')
1319 parser.add_option("--issue", type='int', default=0)
1320 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001321 parser.add_option("--root", default=os.getcwd(),
1322 help="Search for PRESUBMIT.py up to this directory. "
1323 "If inherit-review-settings-ok is present in this "
1324 "directory, parent directories up to the root file "
1325 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001326 parser.add_option("--default_presubmit")
1327 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001328 parser.add_option("--skip_canned", action='append', default=[],
1329 help="A list of checks to skip which appear in "
1330 "presubmit_canned_checks. Can be provided multiple times "
1331 "to skip multiple canned checks.")
maruel@chromium.org239f4112011-06-03 20:08:23 +00001332 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1333 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1334 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001335 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1336 help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001337 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001338 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001339 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001340 elif options.verbose:
1341 logging.basicConfig(level=logging.INFO)
1342 else:
1343 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001344 change_class, files = load_files(options, args)
1345 if not change_class:
1346 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001347 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001348 rietveld_obj = None
1349 if options.rietveld_url:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001350 rietveld_obj = rietveld.CachingRietveld(
maruel@chromium.org239f4112011-06-03 20:08:23 +00001351 options.rietveld_url,
1352 options.rietveld_email,
1353 options.rietveld_password)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001354 if options.rietveld_fetch:
1355 assert options.issue
1356 props = rietveld_obj.get_issue_properties(options.issue, False)
1357 options.author = props['owner_email']
1358 options.description = props['description']
1359 logging.info('Got author: "%s"', options.author)
1360 logging.info('Got description: """\n%s\n"""', options.description)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001361 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001362 with canned_check_filter(options.skip_canned):
1363 results = DoPresubmitChecks(
1364 change_class(options.name,
1365 options.description,
1366 options.root,
1367 files,
1368 options.issue,
1369 options.patchset,
1370 options.author),
1371 options.commit,
1372 options.verbose,
1373 sys.stdout,
1374 sys.stdin,
1375 options.default_presubmit,
1376 options.may_prompt,
1377 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001378 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001379 except NonexistantCannedCheckFilter, e:
1380 print >> sys.stderr, (
1381 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1382 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001383 except PresubmitFailure, e:
1384 print >> sys.stderr, e
1385 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1386 print >> sys.stderr, 'If all fails, contact maruel@'
1387 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001388
1389
1390if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001391 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001392 sys.exit(Main(None))