blob: 513e22ab821ba342a0a4c2015e7f1a37e762c2fd [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
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00009__version__ = '1.6.1'
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
15import cPickle # Exposed through the API.
16import cStringIO # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000017import fnmatch
18import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000019import inspect
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000020import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000021import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000022import marshal # Exposed through the API.
23import optparse
24import os # Somewhat exposed through the API.
25import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000026import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000028import sys # Parts exposed through API.
29import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000030import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000031import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000033import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000035from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036
37# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000038import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000039import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000040import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000041import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000042import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000043import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000044import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000045
46
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000047# Ask for feedback only once in program lifetime.
48_ASKED_FOR_FEEDBACK = False
49
50
maruel@chromium.org899e1c12011-04-07 17:03:18 +000051class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052 pass
53
54
55def normpath(path):
56 '''Version of os.path.normpath that also changes backward slashes to
57 forward slashes when not running on Windows.
58 '''
59 # This is safe to always do because the Windows version of os.path.normpath
60 # will replace forward slashes with backward slashes.
61 path = path.replace(os.sep, '/')
62 return os.path.normpath(path)
63
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000064
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000065def _RightHandSideLinesImpl(affected_files):
66 """Implements RightHandSideLines for InputApi and GclChange."""
67 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000068 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000069 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000070 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000071
72
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000073class PresubmitOutput(object):
74 def __init__(self, input_stream=None, output_stream=None):
75 self.input_stream = input_stream
76 self.output_stream = output_stream
77 self.reviewers = []
78 self.written_output = []
79 self.error_count = 0
80
81 def prompt_yes_no(self, prompt_string):
82 self.write(prompt_string)
83 if self.input_stream:
84 response = self.input_stream.readline().strip().lower()
85 if response not in ('y', 'yes'):
86 self.fail()
87 else:
88 self.fail()
89
90 def fail(self):
91 self.error_count += 1
92
93 def should_continue(self):
94 return not self.error_count
95
96 def write(self, s):
97 self.written_output.append(s)
98 if self.output_stream:
99 self.output_stream.write(s)
100
101 def getvalue(self):
102 return ''.join(self.written_output)
103
104
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000105class OutputApi(object):
106 """This class (more like a module) gets passed to presubmit scripts so that
107 they can specify various types of results.
108 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000109 class PresubmitResult(object):
110 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000111 fatal = False
112 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000113
114 def __init__(self, message, items=None, long_text=''):
115 """
116 message: A short one-line message to indicate errors.
117 items: A list of short strings to indicate where errors occurred.
118 long_text: multi-line text output, e.g. from another tool
119 """
120 self._message = message
121 self._items = []
122 if items:
123 self._items = items
124 self._long_text = long_text.rstrip()
125
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000126 def handle(self, output):
127 output.write(self._message)
128 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000129 for index, item in enumerate(self._items):
130 output.write(' ')
131 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000132 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000133 if index < len(self._items) - 1:
134 output.write(' \\')
135 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000136 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000137 output.write('\n***************\n')
138 # Write separately in case it's unicode.
139 output.write(self._long_text)
140 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000141 if self.fatal:
142 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000143
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000144 class PresubmitAddReviewers(PresubmitResult):
145 """Add some suggested reviewers to the change."""
146 def __init__(self, reviewers):
147 super(OutputApi.PresubmitAddReviewers, self).__init__('')
148 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000149
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000150 def handle(self, output):
151 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000152
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000153 class PresubmitError(PresubmitResult):
154 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000155 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000156
157 class PresubmitPromptWarning(PresubmitResult):
158 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000159 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000160
161 class PresubmitNotifyResult(PresubmitResult):
162 """Just print something to the screen -- but it's not even a warning."""
163 pass
164
165 class MailTextResult(PresubmitResult):
166 """A warning that should be included in the review request email."""
167 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000168 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000169 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000170
171
172class InputApi(object):
173 """An instance of this object is passed to presubmit scripts so they can
174 know stuff about the change they're looking at.
175 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000176 # Method could be a function
177 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000178
maruel@chromium.org3410d912009-06-09 20:56:16 +0000179 # File extensions that are considered source files from a style guide
180 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000181 #
182 # Files without an extension aren't included in the list. If you want to
183 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
184 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000185 DEFAULT_WHITE_LIST = (
186 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000187 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
188 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000189 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000190 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000191 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000192 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000193 )
194
195 # Path regexp that should be excluded from being considered containing source
196 # files. Don't modify this list from a presubmit script!
197 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000198 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000199 r".*\bexperimental[\\\/].*",
200 r".*\bthird_party[\\\/].*",
201 # Output directories (just in case)
202 r".*\bDebug[\\\/].*",
203 r".*\bRelease[\\\/].*",
204 r".*\bxcodebuild[\\\/].*",
205 r".*\bsconsbuild[\\\/].*",
206 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000207 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000208 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000209 r"(|.*[\\\/])\.git[\\\/].*",
210 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000211 # There is no point in processing a patch file.
212 r".+\.diff$",
213 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000214 )
215
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000216 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000217 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000218 """Builds an InputApi object.
219
220 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000221 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000222 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000223 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000224 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000225 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000226 # Version number of the presubmit_support script.
227 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000229 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000230 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000231 # TBD
232 self.host_url = 'http://codereview.chromium.org'
233 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000234 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000235
236 # We expose various modules and functions as attributes of the input_api
237 # so that presubmit scripts don't have to import them.
238 self.basename = os.path.basename
239 self.cPickle = cPickle
240 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000241 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000242 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000243 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000244 self.os_listdir = os.listdir
245 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000246 self.os_path = os.path
247 self.pickle = pickle
248 self.marshal = marshal
249 self.re = re
250 self.subprocess = subprocess
251 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000252 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000253 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000254 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000255 self.urllib2 = urllib2
256
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000257 # To easily fork python.
258 self.python_executable = sys.executable
259 self.environ = os.environ
260
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000261 # InputApi.platform is the platform you're currently running on.
262 self.platform = sys.platform
263
264 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000265 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000266
267 # We carry the canned checks so presubmit scripts can easily use them.
268 self.canned_checks = presubmit_canned_checks
269
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000270 # TODO(dpranke): figure out a list of all approved owners for a repo
271 # in order to be able to handle wildcard OWNERS files?
272 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000273 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000274 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000275
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276 def PresubmitLocalPath(self):
277 """Returns the local path of the presubmit script currently being run.
278
279 This is useful if you don't want to hard-code absolute paths in the
280 presubmit script. For example, It can be used to find another file
281 relative to the PRESUBMIT.py script, so the whole tree can be branched and
282 the presubmit script still works, without editing its content.
283 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000284 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000285
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000286 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000287 """Translate a depot path to a local path (relative to client root).
288
289 Args:
290 Depot path as a string.
291
292 Returns:
293 The local path of the depot path under the user's current client, or None
294 if the file is not mapped.
295
296 Remember to check for the None case and show an appropriate error!
297 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000298 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
299 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000300
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000301 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000302 """Translate a local path to a depot path.
303
304 Args:
305 Local path (relative to current directory, or absolute) as a string.
306
307 Returns:
308 The depot path (SVN URL) of the file if mapped, otherwise None.
309 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000310 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
311 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000312
sail@chromium.org5538e022011-05-12 17:53:16 +0000313 def AffectedFiles(self, include_dirs=False, include_deletes=True,
314 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000315 """Same as input_api.change.AffectedFiles() except only lists files
316 (and optionally directories) in the same directory as the current presubmit
317 script, or subdirectories thereof.
318 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000319 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000320 if len(dir_with_slash) == 1:
321 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000322
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000323 return filter(
324 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000325 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000326
327 def LocalPaths(self, include_dirs=False):
328 """Returns local paths of input_api.AffectedFiles()."""
329 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
330
331 def AbsoluteLocalPaths(self, include_dirs=False):
332 """Returns absolute local paths of input_api.AffectedFiles()."""
333 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
334
335 def ServerPaths(self, include_dirs=False):
336 """Returns server paths of input_api.AffectedFiles()."""
337 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
338
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000339 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000340 """Same as input_api.change.AffectedTextFiles() except only lists files
341 in the same directory as the current presubmit script, or subdirectories
342 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000343 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000344 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000345 warn("AffectedTextFiles(include_deletes=%s)"
346 " is deprecated and ignored" % str(include_deletes),
347 category=DeprecationWarning,
348 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000349 return filter(lambda x: x.IsTextFile(),
350 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000351
maruel@chromium.org3410d912009-06-09 20:56:16 +0000352 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
353 """Filters out files that aren't considered "source file".
354
355 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
356 and InputApi.DEFAULT_BLACK_LIST is used respectively.
357
358 The lists will be compiled as regular expression and
359 AffectedFile.LocalPath() needs to pass both list.
360
361 Note: Copy-paste this function to suit your needs or use a lambda function.
362 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000363 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000364 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000365 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000366 if self.re.match(item, local_path):
367 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000368 return True
369 return False
370 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
371 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
372
373 def AffectedSourceFiles(self, source_file):
374 """Filter the list of AffectedTextFiles by the function source_file.
375
376 If source_file is None, InputApi.FilterSourceFile() is used.
377 """
378 if not source_file:
379 source_file = self.FilterSourceFile
380 return filter(source_file, self.AffectedTextFiles())
381
382 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000383 """An iterator over all text lines in "new" version of changed files.
384
385 Only lists lines from new or modified text files in the change that are
386 contained by the directory of the currently executing presubmit script.
387
388 This is useful for doing line-by-line regex checks, like checking for
389 trailing whitespace.
390
391 Yields:
392 a 3 tuple:
393 the AffectedFile instance of the current file;
394 integer line number (1-based); and
395 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000396
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000397 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000398 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000399 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000400 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000401
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000402 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000403 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000404
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000405 Deny reading anything outside the repository.
406 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000407 if isinstance(file_item, AffectedFile):
408 file_item = file_item.AbsoluteLocalPath()
409 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000410 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000411 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000412
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000413 @property
414 def tbr(self):
415 """Returns if a change is TBR'ed."""
416 return 'TBR' in self.change.tags
417
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000418
419class AffectedFile(object):
420 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000421 # Method could be a function
422 # pylint: disable=R0201
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000423 def __init__(self, path, action, repository_root):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000424 self._path = path
425 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000426 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000427 self._is_directory = None
428 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000429 self._cached_changed_contents = None
430 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000431 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000432
433 def ServerPath(self):
434 """Returns a path string that identifies the file in the SCM system.
435
436 Returns the empty string if the file does not exist in SCM.
437 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000438 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000439
440 def LocalPath(self):
441 """Returns the path of this file on the local disk relative to client root.
442 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000443 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000444
445 def AbsoluteLocalPath(self):
446 """Returns the absolute path of this file on the local disk.
447 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000448 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000449
450 def IsDirectory(self):
451 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000452 if self._is_directory is None:
453 path = self.AbsoluteLocalPath()
454 self._is_directory = (os.path.exists(path) and
455 os.path.isdir(path))
456 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000457
458 def Action(self):
459 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000460 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
461 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000462 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000463
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000464 def Property(self, property_name):
465 """Returns the specified SCM property of this file, or None if no such
466 property.
467 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000468 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000469
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000470 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000471 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000472
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000473 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000474 raise NotImplementedError() # Implement when needed
475
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000476 def NewContents(self):
477 """Returns an iterator over the lines in the new version of file.
478
479 The new version is the file in the user's workspace, i.e. the "right hand
480 side".
481
482 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000483 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000485 if self._cached_new_contents is None:
486 self._cached_new_contents = []
487 if not self.IsDirectory():
488 try:
489 self._cached_new_contents = gclient_utils.FileRead(
490 self.AbsoluteLocalPath(), 'rU').splitlines()
491 except IOError:
492 pass # File not found? That's fine; maybe it was deleted.
493 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000494
495 def OldContents(self):
496 """Returns an iterator over the lines in the old version of file.
497
498 The old version is the file in depot, i.e. the "left hand side".
499 """
500 raise NotImplementedError() # Implement when needed
501
502 def OldFileTempPath(self):
503 """Returns the path on local disk where the old contents resides.
504
505 The old version is the file in depot, i.e. the "left hand side".
506 This is a read-only cached copy of the old contents. *DO NOT* try to
507 modify this file.
508 """
509 raise NotImplementedError() # Implement if/when needed.
510
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000511 def ChangedContents(self):
512 """Returns a list of tuples (line number, line text) of all new lines.
513
514 This relies on the scm diff output describing each changed code section
515 with a line of the form
516
517 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
518 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000519 if self._cached_changed_contents is not None:
520 return self._cached_changed_contents[:]
521 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000522 line_num = 0
523
524 if self.IsDirectory():
525 return []
526
527 for line in self.GenerateScmDiff().splitlines():
528 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
529 if m:
530 line_num = int(m.groups(1)[0])
531 continue
532 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000533 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000534 if not line.startswith('-'):
535 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000536 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000537
maruel@chromium.org5de13972009-06-10 18:16:06 +0000538 def __str__(self):
539 return self.LocalPath()
540
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000541 def GenerateScmDiff(self):
542 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000543
maruel@chromium.org58407af2011-04-12 23:15:57 +0000544
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000545class SvnAffectedFile(AffectedFile):
546 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000547 # Method 'NNN' is abstract in class 'NNN' but is not overridden
548 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000549
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000550 def __init__(self, *args, **kwargs):
551 AffectedFile.__init__(self, *args, **kwargs)
552 self._server_path = None
553 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000554 self._diff = None
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000555
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000556 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000557 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000558 self._server_path = scm.SVN.CaptureLocalInfo(
559 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000560 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000561
562 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000563 if self._is_directory is None:
564 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000565 if os.path.exists(path):
566 # Retrieve directly from the file system; it is much faster than
567 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000568 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000569 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000570 self._is_directory = scm.SVN.CaptureLocalInfo(
571 [self.LocalPath()], self._local_root
572 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000573 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000574
575 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000576 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000577 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000578 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000579 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000580
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000581 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000582 if self._is_text_file is None:
583 if self.Action() == 'D':
584 # A deleted file is not a text file.
585 self._is_text_file = False
586 elif self.IsDirectory():
587 self._is_text_file = False
588 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000589 mime_type = scm.SVN.GetFileProperty(
590 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000591 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
592 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000593
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000594 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000595 if self._diff is None:
596 self._diff = scm.SVN.GenerateDiff(
597 [self.LocalPath()], self._local_root, False, None)
598 return self._diff
maruel@chromium.org1f312812011-02-10 01:33:57 +0000599
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000600
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000601class GitAffectedFile(AffectedFile):
602 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000603 # Method 'NNN' is abstract in class 'NNN' but is not overridden
604 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000605
606 def __init__(self, *args, **kwargs):
607 AffectedFile.__init__(self, *args, **kwargs)
608 self._server_path = None
609 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000610 self._diff = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000611
612 def ServerPath(self):
613 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000614 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000615 return self._server_path
616
617 def IsDirectory(self):
618 if self._is_directory is None:
619 path = self.AbsoluteLocalPath()
620 if os.path.exists(path):
621 # Retrieve directly from the file system; it is much faster than
622 # querying subversion, especially on Windows.
623 self._is_directory = os.path.isdir(path)
624 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000625 self._is_directory = False
626 return self._is_directory
627
628 def Property(self, property_name):
629 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000630 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000631 return self._properties[property_name]
632
633 def IsTextFile(self):
634 if self._is_text_file is None:
635 if self.Action() == 'D':
636 # A deleted file is not a text file.
637 self._is_text_file = False
638 elif self.IsDirectory():
639 self._is_text_file = False
640 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000641 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
642 return self._is_text_file
643
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000644 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000645 if self._diff is None:
646 self._diff = scm.GIT.GenerateDiff(
647 self._local_root, files=[self.LocalPath(),])
648 return self._diff
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000649
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000650
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000651class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000652 """Describe a change.
653
654 Used directly by the presubmit scripts to query the current change being
655 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000656
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000657 Instance members:
658 tags: Dictionnary of KEY=VALUE pairs found in the change description.
659 self.KEY: equivalent to tags['KEY']
660 """
661
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000662 _AFFECTED_FILES = AffectedFile
663
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000664 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000665 TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000666 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000667 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668
maruel@chromium.org58407af2011-04-12 23:15:57 +0000669 def __init__(
670 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000671 if files is None:
672 files = []
673 self._name = name
674 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000675 # Convert root into an absolute path.
676 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000677 self.issue = issue
678 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000679 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680
681 # From the description text, build up a dictionary of key/value pairs
682 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000683 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000685 for line in self._full_description.splitlines():
maruel@chromium.org428342a2011-11-10 15:46:33 +0000686 m = self.TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687 if m:
688 self.tags[m.group('key')] = m.group('value')
689 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000690 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000691
692 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000693 self._description_without_tags = (
694 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000695
maruel@chromium.orge085d812011-10-10 19:49:15 +0000696 assert all(
697 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
698
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000699 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000700 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
701 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000702 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000704 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000705 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000706 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000707
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000708 def DescriptionText(self):
709 """Returns the user-entered changelist description, minus tags.
710
711 Any line in the user-provided description starting with e.g. "FOO="
712 (whitespace permitted before and around) is considered a tag line. Such
713 lines are stripped out of the description this function returns.
714 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000715 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000716
717 def FullDescriptionText(self):
718 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000719 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000720
721 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000722 """Returns the repository (checkout) root directory for this change,
723 as an absolute path.
724 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000725 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000726
727 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000728 """Return tags directly as attributes on the object."""
729 if not re.match(r"^[A-Z_]*$", attr):
730 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000731 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000732
sail@chromium.org5538e022011-05-12 17:53:16 +0000733 def AffectedFiles(self, include_dirs=False, include_deletes=True,
734 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735 """Returns a list of AffectedFile instances for all files in the change.
736
737 Args:
738 include_deletes: If false, deleted files will be filtered out.
739 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000740 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741
742 Returns:
743 [AffectedFile(path, action), AffectedFile(path, action)]
744 """
745 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000746 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000748 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749
sail@chromium.org5538e022011-05-12 17:53:16 +0000750 affected = filter(file_filter, affected)
751
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000752 if include_deletes:
753 return affected
754 else:
755 return filter(lambda x: x.Action() != 'D', affected)
756
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000757 def AffectedTextFiles(self, include_deletes=None):
758 """Return a list of the existing text files in a change."""
759 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000760 warn("AffectedTextFiles(include_deletes=%s)"
761 " is deprecated and ignored" % str(include_deletes),
762 category=DeprecationWarning,
763 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000764 return filter(lambda x: x.IsTextFile(),
765 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000766
767 def LocalPaths(self, include_dirs=False):
768 """Convenience function."""
769 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
770
771 def AbsoluteLocalPaths(self, include_dirs=False):
772 """Convenience function."""
773 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
774
775 def ServerPaths(self, include_dirs=False):
776 """Convenience function."""
777 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
778
779 def RightHandSideLines(self):
780 """An iterator over all text lines in "new" version of changed files.
781
782 Lists lines from new or modified text files in the change.
783
784 This is useful for doing line-by-line regex checks, like checking for
785 trailing whitespace.
786
787 Yields:
788 a 3 tuple:
789 the AffectedFile instance of the current file;
790 integer line number (1-based); and
791 the contents of the line as a string.
792 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000793 return _RightHandSideLinesImpl(
794 x for x in self.AffectedFiles(include_deletes=False)
795 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000796
797
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000798class SvnChange(Change):
799 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000800 scm = 'svn'
801 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000802
803 def _GetChangeLists(self):
804 """Get all change lists."""
805 if self._changelists == None:
806 previous_cwd = os.getcwd()
807 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000808 # Need to import here to avoid circular dependency.
809 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000810 self._changelists = gcl.GetModifiedFiles()
811 os.chdir(previous_cwd)
812 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000813
814 def GetAllModifiedFiles(self):
815 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000816 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000817 all_modified_files = []
818 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000819 all_modified_files.extend(
820 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000821 return all_modified_files
822
823 def GetModifiedFiles(self):
824 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000825 changelists = self._GetChangeLists()
826 return [os.path.join(self.RepositoryRoot(), f[1])
827 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000828
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000829
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000830class GitChange(Change):
831 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000832 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000833
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000834
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000835def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836 """Finds all presubmit files that apply to a given set of source files.
837
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000838 If inherit-review-settings-ok is present right under root, looks for
839 PRESUBMIT.py in directories enclosing root.
840
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000841 Args:
842 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000843 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000844
845 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000846 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000848 files = [normpath(os.path.join(root, f)) for f in files]
849
850 # List all the individual directories containing files.
851 directories = set([os.path.dirname(f) for f in files])
852
853 # Ignore root if inherit-review-settings-ok is present.
854 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
855 root = None
856
857 # Collect all unique directories that may contain PRESUBMIT.py.
858 candidates = set()
859 for directory in directories:
860 while True:
861 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000862 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000863 candidates.add(directory)
864 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000865 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000866 parent_dir = os.path.dirname(directory)
867 if parent_dir == directory:
868 # We hit the system root directory.
869 break
870 directory = parent_dir
871
872 # Look for PRESUBMIT.py in all candidate directories.
873 results = []
874 for directory in sorted(list(candidates)):
875 p = os.path.join(directory, 'PRESUBMIT.py')
876 if os.path.isfile(p):
877 results.append(p)
878
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000879 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000880 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881
882
thestig@chromium.orgde243452009-10-06 21:02:56 +0000883class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000884 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000885 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000886 """Executes GetPreferredTrySlaves() from a single presubmit script.
887
888 Args:
889 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000890 presubmit_path: Project script to run.
891 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000892
893 Return:
894 A list of try slaves.
895 """
896 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000897 try:
898 exec script_text in context
899 except Exception, e:
900 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000901
902 function_name = 'GetPreferredTrySlaves'
903 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000904 get_preferred_try_slaves = context[function_name]
905 function_info = inspect.getargspec(get_preferred_try_slaves)
906 if len(function_info[0]) == 1:
907 result = get_preferred_try_slaves(project)
908 elif len(function_info[0]) == 2:
909 result = get_preferred_try_slaves(project, change)
910 else:
911 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +0000912 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000913 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000914 'Presubmit functions must return a list, got a %s instead: %s' %
915 (type(result), str(result)))
916 for item in result:
917 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000918 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000919 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000920 raise PresubmitFailure(
921 'Try slave names cannot start/end with whitespace')
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +0000922 if ',' in item:
923 raise PresubmitFailure(
924 'Do not use \',\' separated builder or test names: %s' % item)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000925 else:
926 result = []
927 return result
928
929
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000930def DoGetTrySlaves(change,
931 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000932 repository_root,
933 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000934 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000935 verbose,
936 output_stream):
937 """Get the list of try servers from the presubmit scripts.
938
939 Args:
940 changed_files: List of modified files.
941 repository_root: The repository root.
942 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000943 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000944 verbose: Prints debug info.
945 output_stream: A stream to write debug output to.
946
947 Return:
948 List of try slaves
949 """
950 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
951 if not presubmit_files and verbose:
952 output_stream.write("Warning, no presubmit.py found.\n")
953 results = []
954 executer = GetTrySlavesExecuter()
955 if default_presubmit:
956 if verbose:
957 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000958 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000959 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000960 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000961 for filename in presubmit_files:
962 filename = os.path.abspath(filename)
963 if verbose:
964 output_stream.write("Running %s\n" % filename)
965 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000966 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000967 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000968 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000969
970 slaves = list(set(results))
971 if slaves and verbose:
972 output_stream.write(', '.join(slaves))
973 output_stream.write('\n')
974 return slaves
975
976
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000977class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000978 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979 """
980 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000981 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000982 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000983 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000985 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000986 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000987 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000988 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000989
990 def ExecPresubmitScript(self, script_text, presubmit_path):
991 """Executes a single presubmit script.
992
993 Args:
994 script_text: The text of the presubmit script.
995 presubmit_path: The path to the presubmit file (this will be reported via
996 input_api.PresubmitLocalPath()).
997
998 Return:
999 A list of result objects, empty if no problems.
1000 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001001
1002 # Change to the presubmit file's directory to support local imports.
1003 main_path = os.getcwd()
1004 os.chdir(os.path.dirname(presubmit_path))
1005
1006 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001007 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001008 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001009 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001010 try:
1011 exec script_text in context
1012 except Exception, e:
1013 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001014
1015 # These function names must change if we make substantial changes to
1016 # the presubmit API that are not backwards compatible.
1017 if self.committing:
1018 function_name = 'CheckChangeOnCommit'
1019 else:
1020 function_name = 'CheckChangeOnUpload'
1021 if function_name in context:
1022 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001023 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001025 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001026 if not (isinstance(result, types.TupleType) or
1027 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001028 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001029 'Presubmit functions must return a tuple or list')
1030 for item in result:
1031 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001032 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033 'All presubmit results must be of types derived from '
1034 'output_api.PresubmitResult')
1035 else:
1036 result = () # no error since the script doesn't care about current event.
1037
chase@chromium.org8e416c82009-10-06 04:30:44 +00001038 # Return the process to the original working directory.
1039 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001040 return result
1041
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001042
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001043def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001044 committing,
1045 verbose,
1046 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001047 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001048 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001049 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001050 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001051 """Runs all presubmit checks that apply to the files in the change.
1052
1053 This finds all PRESUBMIT.py files in directories enclosing the files in the
1054 change (up to the repository root) and calls the relevant entrypoint function
1055 depending on whether the change is being committed or uploaded.
1056
1057 Prints errors, warnings and notifications. Prompts the user for warnings
1058 when needed.
1059
1060 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001061 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001062 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1063 verbose: Prints debug info.
1064 output_stream: A stream to write output from presubmit tests to.
1065 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001066 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001067 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001068 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001069
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001070 Warning:
1071 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1072 SHOULD be sys.stdin.
1073
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001074 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001075 A PresubmitOutput object. Use output.should_continue() to figure out
1076 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001077 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001078 old_environ = os.environ
1079 try:
1080 # Make sure python subprocesses won't generate .pyc files.
1081 os.environ = os.environ.copy()
1082 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001083
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001084 output = PresubmitOutput(input_stream, output_stream)
1085 if committing:
1086 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001087 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001088 output.write("Running presubmit upload checks ...\n")
1089 start_time = time.time()
1090 presubmit_files = ListRelevantPresubmitFiles(
1091 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1092 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001093 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001094 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001095 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001096 if default_presubmit:
1097 if verbose:
1098 output.write("Running default presubmit script.\n")
1099 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1100 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1101 for filename in presubmit_files:
1102 filename = os.path.abspath(filename)
1103 if verbose:
1104 output.write("Running %s\n" % filename)
1105 # Accept CRLF presubmit script.
1106 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1107 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001108
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001109 errors = []
1110 notifications = []
1111 warnings = []
1112 for result in results:
1113 if result.fatal:
1114 errors.append(result)
1115 elif result.should_prompt:
1116 warnings.append(result)
1117 else:
1118 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001119
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001120 output.write('\n')
1121 for name, items in (('Messages', notifications),
1122 ('Warnings', warnings),
1123 ('ERRORS', errors)):
1124 if items:
1125 output.write('** Presubmit %s **\n' % name)
1126 for item in items:
1127 item.handle(output)
1128 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001129
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001130 total_time = time.time() - start_time
1131 if total_time > 1.0:
1132 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001133
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001134 if not errors:
1135 if not warnings:
1136 output.write('Presubmit checks passed.\n')
1137 elif may_prompt:
1138 output.prompt_yes_no('There were presubmit warnings. '
1139 'Are you sure you wish to continue? (y/N): ')
1140 else:
1141 output.fail()
1142
1143 global _ASKED_FOR_FEEDBACK
1144 # Ask for feedback one time out of 5.
1145 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1146 output.write("Was the presubmit check useful? Please send feedback "
1147 "& hate mail to maruel@chromium.org!\n")
1148 _ASKED_FOR_FEEDBACK = True
1149 return output
1150 finally:
1151 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001152
1153
1154def ScanSubDirs(mask, recursive):
1155 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001156 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 +00001157 else:
1158 results = []
1159 for root, dirs, files in os.walk('.'):
1160 if '.svn' in dirs:
1161 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001162 if '.git' in dirs:
1163 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001164 for name in files:
1165 if fnmatch.fnmatch(name, mask):
1166 results.append(os.path.join(root, name))
1167 return results
1168
1169
1170def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001171 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001172 files = []
1173 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001174 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001175 return files
1176
1177
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001178def load_files(options, args):
1179 """Tries to determine the SCM."""
1180 change_scm = scm.determine_scm(options.root)
1181 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001182 if args:
1183 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001184 if change_scm == 'svn':
1185 change_class = SvnChange
1186 if not files:
1187 files = scm.SVN.CaptureStatus([], options.root)
1188 elif change_scm == 'git':
1189 change_class = GitChange
1190 # TODO(maruel): Get upstream.
1191 if not files:
1192 files = scm.GIT.CaptureStatus([], options.root, None)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001193 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001194 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1195 if not files:
1196 return None, None
1197 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001198 return change_class, files
1199
1200
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001201def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001202 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001203 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001204 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001205 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001206 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1207 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001208 parser.add_option("-r", "--recursive", action="store_true",
1209 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001210 parser.add_option("-v", "--verbose", action="count", default=0,
1211 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001212 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001213 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001214 parser.add_option("--description", default='')
1215 parser.add_option("--issue", type='int', default=0)
1216 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001217 parser.add_option("--root", default=os.getcwd(),
1218 help="Search for PRESUBMIT.py up to this directory. "
1219 "If inherit-review-settings-ok is present in this "
1220 "directory, parent directories up to the root file "
1221 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001222 parser.add_option("--default_presubmit")
1223 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001224 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1225 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1226 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001227 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001228 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001229 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001230 elif options.verbose:
1231 logging.basicConfig(level=logging.INFO)
1232 else:
1233 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001234 change_class, files = load_files(options, args)
1235 if not change_class:
1236 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001237 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001238 rietveld_obj = None
1239 if options.rietveld_url:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001240 rietveld_obj = rietveld.CachingRietveld(
maruel@chromium.org239f4112011-06-03 20:08:23 +00001241 options.rietveld_url,
1242 options.rietveld_email,
1243 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001244 try:
1245 results = DoPresubmitChecks(
1246 change_class(options.name,
1247 options.description,
1248 options.root,
1249 files,
1250 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001251 options.patchset,
1252 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001253 options.commit,
1254 options.verbose,
1255 sys.stdout,
1256 sys.stdin,
1257 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001258 options.may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001259 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001260 return not results.should_continue()
1261 except PresubmitFailure, e:
1262 print >> sys.stderr, e
1263 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1264 print >> sys.stderr, 'If all fails, contact maruel@'
1265 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001266
1267
1268if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001269 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001270 sys.exit(Main(None))