blob: fe1c2878c5c27b64e75dfd36e2b456ac84aa43af [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.
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.
25import optparse
26import os # Somewhat exposed through the API.
27import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000028import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000029import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000030import sys # Parts exposed through API.
31import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000032import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000033import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000035import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000037from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000038
39# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000040import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000041import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000042import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000043import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000044import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000045import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000046import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000047
48
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000049# Ask for feedback only once in program lifetime.
50_ASKED_FOR_FEEDBACK = False
51
52
maruel@chromium.org899e1c12011-04-07 17:03:18 +000053class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000054 pass
55
56
57def normpath(path):
58 '''Version of os.path.normpath that also changes backward slashes to
59 forward slashes when not running on Windows.
60 '''
61 # This is safe to always do because the Windows version of os.path.normpath
62 # will replace forward slashes with backward slashes.
63 path = path.replace(os.sep, '/')
64 return os.path.normpath(path)
65
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000066
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000067def _RightHandSideLinesImpl(affected_files):
68 """Implements RightHandSideLines for InputApi and GclChange."""
69 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000070 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000071 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000072 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000073
74
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000075class PresubmitOutput(object):
76 def __init__(self, input_stream=None, output_stream=None):
77 self.input_stream = input_stream
78 self.output_stream = output_stream
79 self.reviewers = []
80 self.written_output = []
81 self.error_count = 0
82
83 def prompt_yes_no(self, prompt_string):
84 self.write(prompt_string)
85 if self.input_stream:
86 response = self.input_stream.readline().strip().lower()
87 if response not in ('y', 'yes'):
88 self.fail()
89 else:
90 self.fail()
91
92 def fail(self):
93 self.error_count += 1
94
95 def should_continue(self):
96 return not self.error_count
97
98 def write(self, s):
99 self.written_output.append(s)
100 if self.output_stream:
101 self.output_stream.write(s)
102
103 def getvalue(self):
104 return ''.join(self.written_output)
105
106
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000107class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000108 """An instance of OutputApi gets passed to presubmit scripts so that they
109 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000110 """
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000111 def __init__(self, is_committing):
112 self.is_committing = is_committing
113
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000114 class PresubmitResult(object):
115 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000116 fatal = False
117 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000118
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 = []
127 if items:
128 self._items = items
129 self._long_text = long_text.rstrip()
130
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000131 def handle(self, output):
132 output.write(self._message)
133 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000134 for index, item in enumerate(self._items):
135 output.write(' ')
136 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000137 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000138 if index < len(self._items) - 1:
139 output.write(' \\')
140 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000141 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000142 output.write('\n***************\n')
143 # Write separately in case it's unicode.
144 output.write(self._long_text)
145 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000146 if self.fatal:
147 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000148
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000149 class PresubmitAddReviewers(PresubmitResult):
150 """Add some suggested reviewers to the change."""
151 def __init__(self, reviewers):
152 super(OutputApi.PresubmitAddReviewers, self).__init__('')
153 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000154
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000155 def handle(self, output):
156 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000157
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000158 class PresubmitError(PresubmitResult):
159 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000160 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000161
162 class PresubmitPromptWarning(PresubmitResult):
163 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000164 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000165
166 class PresubmitNotifyResult(PresubmitResult):
167 """Just print something to the screen -- but it's not even a warning."""
168 pass
169
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000170 def PresubmitPromptOrNotify(self, *args, **kwargs):
171 """Warn the user when uploading, but only notify if committing."""
172 if self.is_committing:
173 return self.PresubmitNotifyResult(*args, **kwargs)
174 return self.PresubmitPromptWarning(*args, **kwargs)
175
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000176 class MailTextResult(PresubmitResult):
177 """A warning that should be included in the review request email."""
178 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000179 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000180 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000181
182
183class InputApi(object):
184 """An instance of this object is passed to presubmit scripts so they can
185 know stuff about the change they're looking at.
186 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000187 # Method could be a function
188 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000189
maruel@chromium.org3410d912009-06-09 20:56:16 +0000190 # File extensions that are considered source files from a style guide
191 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000192 #
193 # Files without an extension aren't included in the list. If you want to
194 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
195 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000196 DEFAULT_WHITE_LIST = (
197 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000198 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
199 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000200 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000201 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000202 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000203 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000204 )
205
206 # Path regexp that should be excluded from being considered containing source
207 # files. Don't modify this list from a presubmit script!
208 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000209 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000210 r".*\bexperimental[\\\/].*",
211 r".*\bthird_party[\\\/].*",
212 # Output directories (just in case)
213 r".*\bDebug[\\\/].*",
214 r".*\bRelease[\\\/].*",
215 r".*\bxcodebuild[\\\/].*",
216 r".*\bsconsbuild[\\\/].*",
217 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000218 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000219 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000220 r"(|.*[\\\/])\.git[\\\/].*",
221 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000222 # There is no point in processing a patch file.
223 r".+\.diff$",
224 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000225 )
226
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000227 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000228 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000229 """Builds an InputApi object.
230
231 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000232 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000233 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000234 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000235 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000236 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000237 # Version number of the presubmit_support script.
238 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000239 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000240 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000241 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000242 # TBD
243 self.host_url = 'http://codereview.chromium.org'
244 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000245 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000246
247 # We expose various modules and functions as attributes of the input_api
248 # so that presubmit scripts don't have to import them.
249 self.basename = os.path.basename
250 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000251 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000252 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000253 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000254 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000255 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000256 self.os_listdir = os.listdir
257 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000258 self.os_path = os.path
259 self.pickle = pickle
260 self.marshal = marshal
261 self.re = re
262 self.subprocess = subprocess
263 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000264 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000265 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000266 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267 self.urllib2 = urllib2
268
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000269 # To easily fork python.
270 self.python_executable = sys.executable
271 self.environ = os.environ
272
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273 # InputApi.platform is the platform you're currently running on.
274 self.platform = sys.platform
275
276 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000277 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000278
279 # We carry the canned checks so presubmit scripts can easily use them.
280 self.canned_checks = presubmit_canned_checks
281
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000282 # TODO(dpranke): figure out a list of all approved owners for a repo
283 # in order to be able to handle wildcard OWNERS files?
284 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000285 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000286 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000287
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000288 # Replace <hash_map> and <hash_set> as headers that need to be included
289 # with "base/hash_tables.h" instead.
290 # Access to a protected member _XX of a client class
291 # pylint: disable=W0212
292 self.cpplint._re_pattern_templates = [
293 (a, b, 'base/hash_tables.h')
294 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
295 for (a, b, header) in cpplint._re_pattern_templates
296 ]
297
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000298 def PresubmitLocalPath(self):
299 """Returns the local path of the presubmit script currently being run.
300
301 This is useful if you don't want to hard-code absolute paths in the
302 presubmit script. For example, It can be used to find another file
303 relative to the PRESUBMIT.py script, so the whole tree can be branched and
304 the presubmit script still works, without editing its content.
305 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000306 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000307
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000308 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000309 """Translate a depot path to a local path (relative to client root).
310
311 Args:
312 Depot path as a string.
313
314 Returns:
315 The local path of the depot path under the user's current client, or None
316 if the file is not mapped.
317
318 Remember to check for the None case and show an appropriate error!
319 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000320 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
321 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000323 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000324 """Translate a local path to a depot path.
325
326 Args:
327 Local path (relative to current directory, or absolute) as a string.
328
329 Returns:
330 The depot path (SVN URL) of the file if mapped, otherwise None.
331 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000332 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
333 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000334
sail@chromium.org5538e022011-05-12 17:53:16 +0000335 def AffectedFiles(self, include_dirs=False, include_deletes=True,
336 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000337 """Same as input_api.change.AffectedFiles() except only lists files
338 (and optionally directories) in the same directory as the current presubmit
339 script, or subdirectories thereof.
340 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000341 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000342 if len(dir_with_slash) == 1:
343 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000344
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000345 return filter(
346 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000347 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000348
349 def LocalPaths(self, include_dirs=False):
350 """Returns local paths of input_api.AffectedFiles()."""
351 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
352
353 def AbsoluteLocalPaths(self, include_dirs=False):
354 """Returns absolute local paths of input_api.AffectedFiles()."""
355 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
356
357 def ServerPaths(self, include_dirs=False):
358 """Returns server paths of input_api.AffectedFiles()."""
359 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
360
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000361 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000362 """Same as input_api.change.AffectedTextFiles() except only lists files
363 in the same directory as the current presubmit script, or subdirectories
364 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000365 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000366 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000367 warn("AffectedTextFiles(include_deletes=%s)"
368 " is deprecated and ignored" % str(include_deletes),
369 category=DeprecationWarning,
370 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000371 return filter(lambda x: x.IsTextFile(),
372 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000373
maruel@chromium.org3410d912009-06-09 20:56:16 +0000374 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
375 """Filters out files that aren't considered "source file".
376
377 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
378 and InputApi.DEFAULT_BLACK_LIST is used respectively.
379
380 The lists will be compiled as regular expression and
381 AffectedFile.LocalPath() needs to pass both list.
382
383 Note: Copy-paste this function to suit your needs or use a lambda function.
384 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000385 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000386 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000387 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000388 if self.re.match(item, local_path):
389 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000390 return True
391 return False
392 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
393 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
394
395 def AffectedSourceFiles(self, source_file):
396 """Filter the list of AffectedTextFiles by the function source_file.
397
398 If source_file is None, InputApi.FilterSourceFile() is used.
399 """
400 if not source_file:
401 source_file = self.FilterSourceFile
402 return filter(source_file, self.AffectedTextFiles())
403
404 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405 """An iterator over all text lines in "new" version of changed files.
406
407 Only lists lines from new or modified text files in the change that are
408 contained by the directory of the currently executing presubmit script.
409
410 This is useful for doing line-by-line regex checks, like checking for
411 trailing whitespace.
412
413 Yields:
414 a 3 tuple:
415 the AffectedFile instance of the current file;
416 integer line number (1-based); and
417 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000418
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000419 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000420 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000421 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000422 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000423
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000424 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000425 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000426
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000427 Deny reading anything outside the repository.
428 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000429 if isinstance(file_item, AffectedFile):
430 file_item = file_item.AbsoluteLocalPath()
431 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000432 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000433 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000434
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000435 @property
436 def tbr(self):
437 """Returns if a change is TBR'ed."""
438 return 'TBR' in self.change.tags
439
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000440
441class AffectedFile(object):
442 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000443 # Method could be a function
444 # pylint: disable=R0201
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000445 def __init__(self, path, action, repository_root):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000446 self._path = path
447 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000448 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000449 self._is_directory = None
450 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000451 self._cached_changed_contents = None
452 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000453 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454
455 def ServerPath(self):
456 """Returns a path string that identifies the file in the SCM system.
457
458 Returns the empty string if the file does not exist in SCM.
459 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000460 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000461
462 def LocalPath(self):
463 """Returns the path of this file on the local disk relative to client root.
464 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000465 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000466
467 def AbsoluteLocalPath(self):
468 """Returns the absolute path of this file on the local disk.
469 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000470 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000471
472 def IsDirectory(self):
473 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000474 if self._is_directory is None:
475 path = self.AbsoluteLocalPath()
476 self._is_directory = (os.path.exists(path) and
477 os.path.isdir(path))
478 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000479
480 def Action(self):
481 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000482 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
483 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000484 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000485
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000486 def Property(self, property_name):
487 """Returns the specified SCM property of this file, or None if no such
488 property.
489 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000490 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000491
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000492 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000493 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000494
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000495 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000496 raise NotImplementedError() # Implement when needed
497
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000498 def NewContents(self):
499 """Returns an iterator over the lines in the new version of file.
500
501 The new version is the file in the user's workspace, i.e. the "right hand
502 side".
503
504 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000505 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000506 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000507 if self._cached_new_contents is None:
508 self._cached_new_contents = []
509 if not self.IsDirectory():
510 try:
511 self._cached_new_contents = gclient_utils.FileRead(
512 self.AbsoluteLocalPath(), 'rU').splitlines()
513 except IOError:
514 pass # File not found? That's fine; maybe it was deleted.
515 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000516
517 def OldContents(self):
518 """Returns an iterator over the lines in the old version of file.
519
520 The old version is the file in depot, i.e. the "left hand side".
521 """
522 raise NotImplementedError() # Implement when needed
523
524 def OldFileTempPath(self):
525 """Returns the path on local disk where the old contents resides.
526
527 The old version is the file in depot, i.e. the "left hand side".
528 This is a read-only cached copy of the old contents. *DO NOT* try to
529 modify this file.
530 """
531 raise NotImplementedError() # Implement if/when needed.
532
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000533 def ChangedContents(self):
534 """Returns a list of tuples (line number, line text) of all new lines.
535
536 This relies on the scm diff output describing each changed code section
537 with a line of the form
538
539 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
540 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000541 if self._cached_changed_contents is not None:
542 return self._cached_changed_contents[:]
543 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000544 line_num = 0
545
546 if self.IsDirectory():
547 return []
548
549 for line in self.GenerateScmDiff().splitlines():
550 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
551 if m:
552 line_num = int(m.groups(1)[0])
553 continue
554 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000555 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000556 if not line.startswith('-'):
557 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000558 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000559
maruel@chromium.org5de13972009-06-10 18:16:06 +0000560 def __str__(self):
561 return self.LocalPath()
562
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000563 def GenerateScmDiff(self):
564 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000565
maruel@chromium.org58407af2011-04-12 23:15:57 +0000566
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000567class SvnAffectedFile(AffectedFile):
568 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000569 # Method 'NNN' is abstract in class 'NNN' but is not overridden
570 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000571
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000572 def __init__(self, *args, **kwargs):
573 AffectedFile.__init__(self, *args, **kwargs)
574 self._server_path = None
575 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000576 self._diff = None
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000577
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000578 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000579 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000580 self._server_path = scm.SVN.CaptureLocalInfo(
581 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000582 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000583
584 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000585 if self._is_directory is None:
586 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000587 if os.path.exists(path):
588 # Retrieve directly from the file system; it is much faster than
589 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000590 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000591 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000592 self._is_directory = scm.SVN.CaptureLocalInfo(
593 [self.LocalPath()], self._local_root
594 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000595 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000596
597 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000598 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000599 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000600 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000601 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000602
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000603 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000604 if self._is_text_file is None:
605 if self.Action() == 'D':
606 # A deleted file is not a text file.
607 self._is_text_file = False
608 elif self.IsDirectory():
609 self._is_text_file = False
610 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000611 mime_type = scm.SVN.GetFileProperty(
612 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000613 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
614 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000615
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000616 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000617 if self._diff is None:
618 self._diff = scm.SVN.GenerateDiff(
619 [self.LocalPath()], self._local_root, False, None)
620 return self._diff
maruel@chromium.org1f312812011-02-10 01:33:57 +0000621
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000622
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000623class GitAffectedFile(AffectedFile):
624 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000625 # Method 'NNN' is abstract in class 'NNN' but is not overridden
626 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000627
628 def __init__(self, *args, **kwargs):
629 AffectedFile.__init__(self, *args, **kwargs)
630 self._server_path = None
631 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000632 self._diff = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000633
634 def ServerPath(self):
635 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000636 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000637 return self._server_path
638
639 def IsDirectory(self):
640 if self._is_directory is None:
641 path = self.AbsoluteLocalPath()
642 if os.path.exists(path):
643 # Retrieve directly from the file system; it is much faster than
644 # querying subversion, especially on Windows.
645 self._is_directory = os.path.isdir(path)
646 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000647 self._is_directory = False
648 return self._is_directory
649
650 def Property(self, property_name):
651 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000652 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000653 return self._properties[property_name]
654
655 def IsTextFile(self):
656 if self._is_text_file is None:
657 if self.Action() == 'D':
658 # A deleted file is not a text file.
659 self._is_text_file = False
660 elif self.IsDirectory():
661 self._is_text_file = False
662 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000663 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
664 return self._is_text_file
665
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000666 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000667 if self._diff is None:
668 self._diff = scm.GIT.GenerateDiff(
669 self._local_root, files=[self.LocalPath(),])
670 return self._diff
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000671
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000672
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000673class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000674 """Describe a change.
675
676 Used directly by the presubmit scripts to query the current change being
677 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000678
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000679 Instance members:
680 tags: Dictionnary of KEY=VALUE pairs found in the change description.
681 self.KEY: equivalent to tags['KEY']
682 """
683
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000684 _AFFECTED_FILES = AffectedFile
685
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000686 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000687 TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000688 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000689 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690
maruel@chromium.org58407af2011-04-12 23:15:57 +0000691 def __init__(
692 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000693 if files is None:
694 files = []
695 self._name = name
696 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000697 # Convert root into an absolute path.
698 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000699 self.issue = issue
700 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000701 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702
703 # From the description text, build up a dictionary of key/value pairs
704 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000705 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000707 for line in self._full_description.splitlines():
maruel@chromium.org428342a2011-11-10 15:46:33 +0000708 m = self.TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709 if m:
710 self.tags[m.group('key')] = m.group('value')
711 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000712 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000713
714 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000715 self._description_without_tags = (
716 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717
maruel@chromium.orge085d812011-10-10 19:49:15 +0000718 assert all(
719 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
720
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000721 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000722 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
723 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000724 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000726 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000728 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000730 def DescriptionText(self):
731 """Returns the user-entered changelist description, minus tags.
732
733 Any line in the user-provided description starting with e.g. "FOO="
734 (whitespace permitted before and around) is considered a tag line. Such
735 lines are stripped out of the description this function returns.
736 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000737 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000738
739 def FullDescriptionText(self):
740 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000741 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742
743 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000744 """Returns the repository (checkout) root directory for this change,
745 as an absolute path.
746 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000747 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748
749 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000750 """Return tags directly as attributes on the object."""
751 if not re.match(r"^[A-Z_]*$", attr):
752 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000753 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000754
sail@chromium.org5538e022011-05-12 17:53:16 +0000755 def AffectedFiles(self, include_dirs=False, include_deletes=True,
756 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000757 """Returns a list of AffectedFile instances for all files in the change.
758
759 Args:
760 include_deletes: If false, deleted files will be filtered out.
761 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000762 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000763
764 Returns:
765 [AffectedFile(path, action), AffectedFile(path, action)]
766 """
767 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000768 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000769 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000770 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000771
sail@chromium.org5538e022011-05-12 17:53:16 +0000772 affected = filter(file_filter, affected)
773
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000774 if include_deletes:
775 return affected
776 else:
777 return filter(lambda x: x.Action() != 'D', affected)
778
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000779 def AffectedTextFiles(self, include_deletes=None):
780 """Return a list of the existing text files in a change."""
781 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000782 warn("AffectedTextFiles(include_deletes=%s)"
783 " is deprecated and ignored" % str(include_deletes),
784 category=DeprecationWarning,
785 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000786 return filter(lambda x: x.IsTextFile(),
787 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000788
789 def LocalPaths(self, include_dirs=False):
790 """Convenience function."""
791 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
792
793 def AbsoluteLocalPaths(self, include_dirs=False):
794 """Convenience function."""
795 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
796
797 def ServerPaths(self, include_dirs=False):
798 """Convenience function."""
799 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
800
801 def RightHandSideLines(self):
802 """An iterator over all text lines in "new" version of changed files.
803
804 Lists lines from new or modified text files in the change.
805
806 This is useful for doing line-by-line regex checks, like checking for
807 trailing whitespace.
808
809 Yields:
810 a 3 tuple:
811 the AffectedFile instance of the current file;
812 integer line number (1-based); and
813 the contents of the line as a string.
814 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000815 return _RightHandSideLinesImpl(
816 x for x in self.AffectedFiles(include_deletes=False)
817 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000818
819
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000820class SvnChange(Change):
821 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000822 scm = 'svn'
823 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000824
825 def _GetChangeLists(self):
826 """Get all change lists."""
827 if self._changelists == None:
828 previous_cwd = os.getcwd()
829 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000830 # Need to import here to avoid circular dependency.
831 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000832 self._changelists = gcl.GetModifiedFiles()
833 os.chdir(previous_cwd)
834 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000835
836 def GetAllModifiedFiles(self):
837 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000838 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000839 all_modified_files = []
840 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000841 all_modified_files.extend(
842 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000843 return all_modified_files
844
845 def GetModifiedFiles(self):
846 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000847 changelists = self._GetChangeLists()
848 return [os.path.join(self.RepositoryRoot(), f[1])
849 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000850
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000851
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000852class GitChange(Change):
853 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000854 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000855
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000856
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000857def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000858 """Finds all presubmit files that apply to a given set of source files.
859
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000860 If inherit-review-settings-ok is present right under root, looks for
861 PRESUBMIT.py in directories enclosing root.
862
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000863 Args:
864 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000865 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000866
867 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000868 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000870 files = [normpath(os.path.join(root, f)) for f in files]
871
872 # List all the individual directories containing files.
873 directories = set([os.path.dirname(f) for f in files])
874
875 # Ignore root if inherit-review-settings-ok is present.
876 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
877 root = None
878
879 # Collect all unique directories that may contain PRESUBMIT.py.
880 candidates = set()
881 for directory in directories:
882 while True:
883 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000884 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000885 candidates.add(directory)
886 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000887 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000888 parent_dir = os.path.dirname(directory)
889 if parent_dir == directory:
890 # We hit the system root directory.
891 break
892 directory = parent_dir
893
894 # Look for PRESUBMIT.py in all candidate directories.
895 results = []
896 for directory in sorted(list(candidates)):
897 p = os.path.join(directory, 'PRESUBMIT.py')
898 if os.path.isfile(p):
899 results.append(p)
900
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000901 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000902 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000903
904
thestig@chromium.orgde243452009-10-06 21:02:56 +0000905class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000906 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000907 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000908 """Executes GetPreferredTrySlaves() from a single presubmit script.
909
910 Args:
911 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000912 presubmit_path: Project script to run.
913 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000914
915 Return:
916 A list of try slaves.
917 """
918 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000919 try:
920 exec script_text in context
921 except Exception, e:
922 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000923
924 function_name = 'GetPreferredTrySlaves'
925 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000926 get_preferred_try_slaves = context[function_name]
927 function_info = inspect.getargspec(get_preferred_try_slaves)
928 if len(function_info[0]) == 1:
929 result = get_preferred_try_slaves(project)
930 elif len(function_info[0]) == 2:
931 result = get_preferred_try_slaves(project, change)
932 else:
933 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +0000934 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000935 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000936 'Presubmit functions must return a list, got a %s instead: %s' %
937 (type(result), str(result)))
938 for item in result:
939 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000940 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000941 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000942 raise PresubmitFailure(
943 'Try slave names cannot start/end with whitespace')
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +0000944 if ',' in item:
945 raise PresubmitFailure(
946 'Do not use \',\' separated builder or test names: %s' % item)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000947 else:
948 result = []
949 return result
950
951
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000952def DoGetTrySlaves(change,
953 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000954 repository_root,
955 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000956 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000957 verbose,
958 output_stream):
959 """Get the list of try servers from the presubmit scripts.
960
961 Args:
962 changed_files: List of modified files.
963 repository_root: The repository root.
964 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000965 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000966 verbose: Prints debug info.
967 output_stream: A stream to write debug output to.
968
969 Return:
970 List of try slaves
971 """
972 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
973 if not presubmit_files and verbose:
974 output_stream.write("Warning, no presubmit.py found.\n")
975 results = []
976 executer = GetTrySlavesExecuter()
977 if default_presubmit:
978 if verbose:
979 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000980 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000981 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000982 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000983 for filename in presubmit_files:
984 filename = os.path.abspath(filename)
985 if verbose:
986 output_stream.write("Running %s\n" % filename)
987 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000988 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000989 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000990 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000991
992 slaves = list(set(results))
993 if slaves and verbose:
994 output_stream.write(', '.join(slaves))
995 output_stream.write('\n')
996 return slaves
997
998
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000999class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001000 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001001 """
1002 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001003 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001004 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001005 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001007 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001009 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001010 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001011
1012 def ExecPresubmitScript(self, script_text, presubmit_path):
1013 """Executes a single presubmit script.
1014
1015 Args:
1016 script_text: The text of the presubmit script.
1017 presubmit_path: The path to the presubmit file (this will be reported via
1018 input_api.PresubmitLocalPath()).
1019
1020 Return:
1021 A list of result objects, empty if no problems.
1022 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001023
1024 # Change to the presubmit file's directory to support local imports.
1025 main_path = os.getcwd()
1026 os.chdir(os.path.dirname(presubmit_path))
1027
1028 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001029 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001030 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001031 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001032 try:
1033 exec script_text in context
1034 except Exception, e:
1035 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036
1037 # These function names must change if we make substantial changes to
1038 # the presubmit API that are not backwards compatible.
1039 if self.committing:
1040 function_name = 'CheckChangeOnCommit'
1041 else:
1042 function_name = 'CheckChangeOnUpload'
1043 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001044 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001045 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001046 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001047 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001048 if not (isinstance(result, types.TupleType) or
1049 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001050 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001051 'Presubmit functions must return a tuple or list')
1052 for item in result:
1053 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001054 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001055 'All presubmit results must be of types derived from '
1056 'output_api.PresubmitResult')
1057 else:
1058 result = () # no error since the script doesn't care about current event.
1059
chase@chromium.org8e416c82009-10-06 04:30:44 +00001060 # Return the process to the original working directory.
1061 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001062 return result
1063
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001064
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001065def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001066 committing,
1067 verbose,
1068 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001069 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001070 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001071 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001072 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001073 """Runs all presubmit checks that apply to the files in the change.
1074
1075 This finds all PRESUBMIT.py files in directories enclosing the files in the
1076 change (up to the repository root) and calls the relevant entrypoint function
1077 depending on whether the change is being committed or uploaded.
1078
1079 Prints errors, warnings and notifications. Prompts the user for warnings
1080 when needed.
1081
1082 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001083 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001084 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1085 verbose: Prints debug info.
1086 output_stream: A stream to write output from presubmit tests to.
1087 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001088 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001089 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001090 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001091
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001092 Warning:
1093 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1094 SHOULD be sys.stdin.
1095
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001096 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001097 A PresubmitOutput object. Use output.should_continue() to figure out
1098 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001099 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001100 old_environ = os.environ
1101 try:
1102 # Make sure python subprocesses won't generate .pyc files.
1103 os.environ = os.environ.copy()
1104 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001105
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001106 output = PresubmitOutput(input_stream, output_stream)
1107 if committing:
1108 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001109 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001110 output.write("Running presubmit upload checks ...\n")
1111 start_time = time.time()
1112 presubmit_files = ListRelevantPresubmitFiles(
1113 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1114 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001115 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001116 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001117 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001118 if default_presubmit:
1119 if verbose:
1120 output.write("Running default presubmit script.\n")
1121 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1122 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1123 for filename in presubmit_files:
1124 filename = os.path.abspath(filename)
1125 if verbose:
1126 output.write("Running %s\n" % filename)
1127 # Accept CRLF presubmit script.
1128 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1129 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001130
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001131 errors = []
1132 notifications = []
1133 warnings = []
1134 for result in results:
1135 if result.fatal:
1136 errors.append(result)
1137 elif result.should_prompt:
1138 warnings.append(result)
1139 else:
1140 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001141
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001142 output.write('\n')
1143 for name, items in (('Messages', notifications),
1144 ('Warnings', warnings),
1145 ('ERRORS', errors)):
1146 if items:
1147 output.write('** Presubmit %s **\n' % name)
1148 for item in items:
1149 item.handle(output)
1150 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001151
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001152 total_time = time.time() - start_time
1153 if total_time > 1.0:
1154 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001155
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001156 if not errors:
1157 if not warnings:
1158 output.write('Presubmit checks passed.\n')
1159 elif may_prompt:
1160 output.prompt_yes_no('There were presubmit warnings. '
1161 'Are you sure you wish to continue? (y/N): ')
1162 else:
1163 output.fail()
1164
1165 global _ASKED_FOR_FEEDBACK
1166 # Ask for feedback one time out of 5.
1167 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1168 output.write("Was the presubmit check useful? Please send feedback "
1169 "& hate mail to maruel@chromium.org!\n")
1170 _ASKED_FOR_FEEDBACK = True
1171 return output
1172 finally:
1173 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001174
1175
1176def ScanSubDirs(mask, recursive):
1177 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001178 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 +00001179 else:
1180 results = []
1181 for root, dirs, files in os.walk('.'):
1182 if '.svn' in dirs:
1183 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001184 if '.git' in dirs:
1185 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001186 for name in files:
1187 if fnmatch.fnmatch(name, mask):
1188 results.append(os.path.join(root, name))
1189 return results
1190
1191
1192def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001193 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001194 files = []
1195 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001196 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001197 return files
1198
1199
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001200def load_files(options, args):
1201 """Tries to determine the SCM."""
1202 change_scm = scm.determine_scm(options.root)
1203 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001204 if args:
1205 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001206 if change_scm == 'svn':
1207 change_class = SvnChange
1208 if not files:
1209 files = scm.SVN.CaptureStatus([], options.root)
1210 elif change_scm == 'git':
1211 change_class = GitChange
1212 # TODO(maruel): Get upstream.
1213 if not files:
1214 files = scm.GIT.CaptureStatus([], options.root, None)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001215 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001216 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1217 if not files:
1218 return None, None
1219 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001220 return change_class, files
1221
1222
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001223class NonexistantCannedCheckFilter(Exception):
1224 pass
1225
1226
1227@contextlib.contextmanager
1228def canned_check_filter(method_names):
1229 filtered = {}
1230 try:
1231 for method_name in method_names:
1232 if not hasattr(presubmit_canned_checks, method_name):
1233 raise NonexistantCannedCheckFilter(method_name)
1234 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1235 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1236 yield
1237 finally:
1238 for name, method in filtered.iteritems():
1239 setattr(presubmit_canned_checks, name, method)
1240
1241
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001242def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001243 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001244 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001245 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001246 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001247 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1248 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001249 parser.add_option("-r", "--recursive", action="store_true",
1250 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001251 parser.add_option("-v", "--verbose", action="count", default=0,
1252 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001253 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001254 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001255 parser.add_option("--description", default='')
1256 parser.add_option("--issue", type='int', default=0)
1257 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001258 parser.add_option("--root", default=os.getcwd(),
1259 help="Search for PRESUBMIT.py up to this directory. "
1260 "If inherit-review-settings-ok is present in this "
1261 "directory, parent directories up to the root file "
1262 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001263 parser.add_option("--default_presubmit")
1264 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001265 parser.add_option("--skip_canned", action='append', default=[],
1266 help="A list of checks to skip which appear in "
1267 "presubmit_canned_checks. Can be provided multiple times "
1268 "to skip multiple canned checks.")
maruel@chromium.org239f4112011-06-03 20:08:23 +00001269 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1270 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1271 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001272 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001273 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001274 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001275 elif options.verbose:
1276 logging.basicConfig(level=logging.INFO)
1277 else:
1278 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001279 change_class, files = load_files(options, args)
1280 if not change_class:
1281 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001282 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001283 rietveld_obj = None
1284 if options.rietveld_url:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001285 rietveld_obj = rietveld.CachingRietveld(
maruel@chromium.org239f4112011-06-03 20:08:23 +00001286 options.rietveld_url,
1287 options.rietveld_email,
1288 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001289 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001290 with canned_check_filter(options.skip_canned):
1291 results = DoPresubmitChecks(
1292 change_class(options.name,
1293 options.description,
1294 options.root,
1295 files,
1296 options.issue,
1297 options.patchset,
1298 options.author),
1299 options.commit,
1300 options.verbose,
1301 sys.stdout,
1302 sys.stdin,
1303 options.default_presubmit,
1304 options.may_prompt,
1305 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001306 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001307 except NonexistantCannedCheckFilter, e:
1308 print >> sys.stderr, (
1309 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1310 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001311 except PresubmitFailure, e:
1312 print >> sys.stderr, e
1313 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1314 print >> sys.stderr, 'If all fails, contact maruel@'
1315 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001316
1317
1318if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001319 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001320 sys.exit(Main(None))