blob: 75977553d22d281e6af777f1d99834f8c9e58c54 [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 = (
198 r".*\bexperimental[\\\/].*",
199 r".*\bthird_party[\\\/].*",
200 # Output directories (just in case)
201 r".*\bDebug[\\\/].*",
202 r".*\bRelease[\\\/].*",
203 r".*\bxcodebuild[\\\/].*",
204 r".*\bsconsbuild[\\\/].*",
205 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000206 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000207 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000208 r"(|.*[\\\/])\.git[\\\/].*",
209 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000210 # There is no point in processing a patch file.
211 r".+\.diff$",
212 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000213 )
214
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000215 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000216 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000217 """Builds an InputApi object.
218
219 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000220 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000221 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000222 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000223 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000224 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000225 # Version number of the presubmit_support script.
226 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000227 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000228 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000229 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000230 # TBD
231 self.host_url = 'http://codereview.chromium.org'
232 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000233 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000234
235 # We expose various modules and functions as attributes of the input_api
236 # so that presubmit scripts don't have to import them.
237 self.basename = os.path.basename
238 self.cPickle = cPickle
239 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000240 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000241 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000242 self.os_listdir = os.listdir
243 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000244 self.os_path = os.path
245 self.pickle = pickle
246 self.marshal = marshal
247 self.re = re
248 self.subprocess = subprocess
249 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000250 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000251 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000252 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000253 self.urllib2 = urllib2
254
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000255 # To easily fork python.
256 self.python_executable = sys.executable
257 self.environ = os.environ
258
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000259 # InputApi.platform is the platform you're currently running on.
260 self.platform = sys.platform
261
262 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000263 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000264
265 # We carry the canned checks so presubmit scripts can easily use them.
266 self.canned_checks = presubmit_canned_checks
267
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000268 # TODO(dpranke): figure out a list of all approved owners for a repo
269 # in order to be able to handle wildcard OWNERS files?
270 self.owners_db = owners.Database(change.RepositoryRoot(),
271 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000272 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000273
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000274 def PresubmitLocalPath(self):
275 """Returns the local path of the presubmit script currently being run.
276
277 This is useful if you don't want to hard-code absolute paths in the
278 presubmit script. For example, It can be used to find another file
279 relative to the PRESUBMIT.py script, so the whole tree can be branched and
280 the presubmit script still works, without editing its content.
281 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000282 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000284 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000285 """Translate a depot path to a local path (relative to client root).
286
287 Args:
288 Depot path as a string.
289
290 Returns:
291 The local path of the depot path under the user's current client, or None
292 if the file is not mapped.
293
294 Remember to check for the None case and show an appropriate error!
295 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000296 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
297 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000298
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000299 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000300 """Translate a local path to a depot path.
301
302 Args:
303 Local path (relative to current directory, or absolute) as a string.
304
305 Returns:
306 The depot path (SVN URL) of the file if mapped, otherwise None.
307 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000308 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
309 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000310
sail@chromium.org5538e022011-05-12 17:53:16 +0000311 def AffectedFiles(self, include_dirs=False, include_deletes=True,
312 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000313 """Same as input_api.change.AffectedFiles() except only lists files
314 (and optionally directories) in the same directory as the current presubmit
315 script, or subdirectories thereof.
316 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000317 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000318 if len(dir_with_slash) == 1:
319 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000320
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000321 return filter(
322 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000323 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000324
325 def LocalPaths(self, include_dirs=False):
326 """Returns local paths of input_api.AffectedFiles()."""
327 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
328
329 def AbsoluteLocalPaths(self, include_dirs=False):
330 """Returns absolute local paths of input_api.AffectedFiles()."""
331 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
332
333 def ServerPaths(self, include_dirs=False):
334 """Returns server paths of input_api.AffectedFiles()."""
335 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
336
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000337 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000338 """Same as input_api.change.AffectedTextFiles() except only lists files
339 in the same directory as the current presubmit script, or subdirectories
340 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000341 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000342 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000343 warn("AffectedTextFiles(include_deletes=%s)"
344 " is deprecated and ignored" % str(include_deletes),
345 category=DeprecationWarning,
346 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000347 return filter(lambda x: x.IsTextFile(),
348 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349
maruel@chromium.org3410d912009-06-09 20:56:16 +0000350 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
351 """Filters out files that aren't considered "source file".
352
353 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
354 and InputApi.DEFAULT_BLACK_LIST is used respectively.
355
356 The lists will be compiled as regular expression and
357 AffectedFile.LocalPath() needs to pass both list.
358
359 Note: Copy-paste this function to suit your needs or use a lambda function.
360 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000361 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000362 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000363 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000364 if self.re.match(item, local_path):
365 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000366 return True
367 return False
368 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
369 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
370
371 def AffectedSourceFiles(self, source_file):
372 """Filter the list of AffectedTextFiles by the function source_file.
373
374 If source_file is None, InputApi.FilterSourceFile() is used.
375 """
376 if not source_file:
377 source_file = self.FilterSourceFile
378 return filter(source_file, self.AffectedTextFiles())
379
380 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000381 """An iterator over all text lines in "new" version of changed files.
382
383 Only lists lines from new or modified text files in the change that are
384 contained by the directory of the currently executing presubmit script.
385
386 This is useful for doing line-by-line regex checks, like checking for
387 trailing whitespace.
388
389 Yields:
390 a 3 tuple:
391 the AffectedFile instance of the current file;
392 integer line number (1-based); and
393 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000394
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000395 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000396 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000397 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000398 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000399
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000400 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000401 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000402
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000403 Deny reading anything outside the repository.
404 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000405 if isinstance(file_item, AffectedFile):
406 file_item = file_item.AbsoluteLocalPath()
407 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000408 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000409 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000410
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000411 @property
412 def tbr(self):
413 """Returns if a change is TBR'ed."""
414 return 'TBR' in self.change.tags
415
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000416
417class AffectedFile(object):
418 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000419 # Method could be a function
420 # pylint: disable=R0201
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000421 def __init__(self, path, action, repository_root):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000422 self._path = path
423 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000424 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000425 self._is_directory = None
426 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000427 self._cached_changed_contents = None
428 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000429 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000430
431 def ServerPath(self):
432 """Returns a path string that identifies the file in the SCM system.
433
434 Returns the empty string if the file does not exist in SCM.
435 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000436 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000437
438 def LocalPath(self):
439 """Returns the path of this file on the local disk relative to client root.
440 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000441 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000442
443 def AbsoluteLocalPath(self):
444 """Returns the absolute path of this file on the local disk.
445 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000446 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000447
448 def IsDirectory(self):
449 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000450 if self._is_directory is None:
451 path = self.AbsoluteLocalPath()
452 self._is_directory = (os.path.exists(path) and
453 os.path.isdir(path))
454 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455
456 def Action(self):
457 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000458 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
459 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000460 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000461
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000462 def Property(self, property_name):
463 """Returns the specified SCM property of this file, or None if no such
464 property.
465 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000466 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000467
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000468 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000469 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000470
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000471 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000472 raise NotImplementedError() # Implement when needed
473
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000474 def NewContents(self):
475 """Returns an iterator over the lines in the new version of file.
476
477 The new version is the file in the user's workspace, i.e. the "right hand
478 side".
479
480 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000481 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000482 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000483 if self._cached_new_contents is None:
484 self._cached_new_contents = []
485 if not self.IsDirectory():
486 try:
487 self._cached_new_contents = gclient_utils.FileRead(
488 self.AbsoluteLocalPath(), 'rU').splitlines()
489 except IOError:
490 pass # File not found? That's fine; maybe it was deleted.
491 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000492
493 def OldContents(self):
494 """Returns an iterator over the lines in the old version of file.
495
496 The old version is the file in depot, i.e. the "left hand side".
497 """
498 raise NotImplementedError() # Implement when needed
499
500 def OldFileTempPath(self):
501 """Returns the path on local disk where the old contents resides.
502
503 The old version is the file in depot, i.e. the "left hand side".
504 This is a read-only cached copy of the old contents. *DO NOT* try to
505 modify this file.
506 """
507 raise NotImplementedError() # Implement if/when needed.
508
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000509 def ChangedContents(self):
510 """Returns a list of tuples (line number, line text) of all new lines.
511
512 This relies on the scm diff output describing each changed code section
513 with a line of the form
514
515 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
516 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000517 if self._cached_changed_contents is not None:
518 return self._cached_changed_contents[:]
519 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000520 line_num = 0
521
522 if self.IsDirectory():
523 return []
524
525 for line in self.GenerateScmDiff().splitlines():
526 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
527 if m:
528 line_num = int(m.groups(1)[0])
529 continue
530 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000531 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000532 if not line.startswith('-'):
533 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000534 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000535
maruel@chromium.org5de13972009-06-10 18:16:06 +0000536 def __str__(self):
537 return self.LocalPath()
538
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000539 def GenerateScmDiff(self):
540 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000541
maruel@chromium.org58407af2011-04-12 23:15:57 +0000542
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000543class SvnAffectedFile(AffectedFile):
544 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000545 # Method 'NNN' is abstract in class 'NNN' but is not overridden
546 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000547
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000548 def __init__(self, *args, **kwargs):
549 AffectedFile.__init__(self, *args, **kwargs)
550 self._server_path = None
551 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000552 self._diff = None
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000553
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000554 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000555 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000556 self._server_path = scm.SVN.CaptureLocalInfo(
557 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000558 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000559
560 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000561 if self._is_directory is None:
562 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000563 if os.path.exists(path):
564 # Retrieve directly from the file system; it is much faster than
565 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000566 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000567 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000568 self._is_directory = scm.SVN.CaptureLocalInfo(
569 [self.LocalPath()], self._local_root
570 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000571 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000572
573 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000574 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000575 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000576 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000577 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000578
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000579 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000580 if self._is_text_file is None:
581 if self.Action() == 'D':
582 # A deleted file is not a text file.
583 self._is_text_file = False
584 elif self.IsDirectory():
585 self._is_text_file = False
586 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000587 mime_type = scm.SVN.GetFileProperty(
588 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000589 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
590 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000591
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000592 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000593 if self._diff is None:
594 self._diff = scm.SVN.GenerateDiff(
595 [self.LocalPath()], self._local_root, False, None)
596 return self._diff
maruel@chromium.org1f312812011-02-10 01:33:57 +0000597
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000598
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000599class GitAffectedFile(AffectedFile):
600 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000601 # Method 'NNN' is abstract in class 'NNN' but is not overridden
602 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000603
604 def __init__(self, *args, **kwargs):
605 AffectedFile.__init__(self, *args, **kwargs)
606 self._server_path = None
607 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000608 self._diff = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000609
610 def ServerPath(self):
611 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000612 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000613 return self._server_path
614
615 def IsDirectory(self):
616 if self._is_directory is None:
617 path = self.AbsoluteLocalPath()
618 if os.path.exists(path):
619 # Retrieve directly from the file system; it is much faster than
620 # querying subversion, especially on Windows.
621 self._is_directory = os.path.isdir(path)
622 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000623 self._is_directory = False
624 return self._is_directory
625
626 def Property(self, property_name):
627 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000628 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000629 return self._properties[property_name]
630
631 def IsTextFile(self):
632 if self._is_text_file is None:
633 if self.Action() == 'D':
634 # A deleted file is not a text file.
635 self._is_text_file = False
636 elif self.IsDirectory():
637 self._is_text_file = False
638 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000639 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
640 return self._is_text_file
641
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000642 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000643 if self._diff is None:
644 self._diff = scm.GIT.GenerateDiff(
645 self._local_root, files=[self.LocalPath(),])
646 return self._diff
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000647
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000648
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000649class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000650 """Describe a change.
651
652 Used directly by the presubmit scripts to query the current change being
653 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000654
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000655 Instance members:
656 tags: Dictionnary of KEY=VALUE pairs found in the change description.
657 self.KEY: equivalent to tags['KEY']
658 """
659
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000660 _AFFECTED_FILES = AffectedFile
661
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000662 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000663 TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000664 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000665 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000666
maruel@chromium.org58407af2011-04-12 23:15:57 +0000667 def __init__(
668 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000669 if files is None:
670 files = []
671 self._name = name
672 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000673 # Convert root into an absolute path.
674 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000675 self.issue = issue
676 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000677 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678
679 # From the description text, build up a dictionary of key/value pairs
680 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000681 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000683 for line in self._full_description.splitlines():
maruel@chromium.org428342a2011-11-10 15:46:33 +0000684 m = self.TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685 if m:
686 self.tags[m.group('key')] = m.group('value')
687 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000688 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000689
690 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000691 self._description_without_tags = (
692 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693
maruel@chromium.orge085d812011-10-10 19:49:15 +0000694 assert all(
695 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
696
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000697 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000698 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
699 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000700 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000701
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000702 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000704 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000705
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706 def DescriptionText(self):
707 """Returns the user-entered changelist description, minus tags.
708
709 Any line in the user-provided description starting with e.g. "FOO="
710 (whitespace permitted before and around) is considered a tag line. Such
711 lines are stripped out of the description this function returns.
712 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000713 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714
715 def FullDescriptionText(self):
716 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000717 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000718
719 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000720 """Returns the repository (checkout) root directory for this change,
721 as an absolute path.
722 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000723 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000724
725 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000726 """Return tags directly as attributes on the object."""
727 if not re.match(r"^[A-Z_]*$", attr):
728 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000729 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000730
sail@chromium.org5538e022011-05-12 17:53:16 +0000731 def AffectedFiles(self, include_dirs=False, include_deletes=True,
732 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000733 """Returns a list of AffectedFile instances for all files in the change.
734
735 Args:
736 include_deletes: If false, deleted files will be filtered out.
737 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000738 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000739
740 Returns:
741 [AffectedFile(path, action), AffectedFile(path, action)]
742 """
743 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000744 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000745 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000746 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747
sail@chromium.org5538e022011-05-12 17:53:16 +0000748 affected = filter(file_filter, affected)
749
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000750 if include_deletes:
751 return affected
752 else:
753 return filter(lambda x: x.Action() != 'D', affected)
754
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000755 def AffectedTextFiles(self, include_deletes=None):
756 """Return a list of the existing text files in a change."""
757 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000758 warn("AffectedTextFiles(include_deletes=%s)"
759 " is deprecated and ignored" % str(include_deletes),
760 category=DeprecationWarning,
761 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000762 return filter(lambda x: x.IsTextFile(),
763 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000764
765 def LocalPaths(self, include_dirs=False):
766 """Convenience function."""
767 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
768
769 def AbsoluteLocalPaths(self, include_dirs=False):
770 """Convenience function."""
771 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
772
773 def ServerPaths(self, include_dirs=False):
774 """Convenience function."""
775 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
776
777 def RightHandSideLines(self):
778 """An iterator over all text lines in "new" version of changed files.
779
780 Lists lines from new or modified text files in the change.
781
782 This is useful for doing line-by-line regex checks, like checking for
783 trailing whitespace.
784
785 Yields:
786 a 3 tuple:
787 the AffectedFile instance of the current file;
788 integer line number (1-based); and
789 the contents of the line as a string.
790 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000791 return _RightHandSideLinesImpl(
792 x for x in self.AffectedFiles(include_deletes=False)
793 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000794
795
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000796class SvnChange(Change):
797 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000798 scm = 'svn'
799 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000800
801 def _GetChangeLists(self):
802 """Get all change lists."""
803 if self._changelists == None:
804 previous_cwd = os.getcwd()
805 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000806 # Need to import here to avoid circular dependency.
807 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000808 self._changelists = gcl.GetModifiedFiles()
809 os.chdir(previous_cwd)
810 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000811
812 def GetAllModifiedFiles(self):
813 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000814 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000815 all_modified_files = []
816 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000817 all_modified_files.extend(
818 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000819 return all_modified_files
820
821 def GetModifiedFiles(self):
822 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000823 changelists = self._GetChangeLists()
824 return [os.path.join(self.RepositoryRoot(), f[1])
825 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000826
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000827
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000828class GitChange(Change):
829 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000830 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000831
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000832
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000833def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000834 """Finds all presubmit files that apply to a given set of source files.
835
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000836 If inherit-review-settings-ok is present right under root, looks for
837 PRESUBMIT.py in directories enclosing root.
838
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839 Args:
840 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000841 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842
843 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000844 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000845 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000846 files = [normpath(os.path.join(root, f)) for f in files]
847
848 # List all the individual directories containing files.
849 directories = set([os.path.dirname(f) for f in files])
850
851 # Ignore root if inherit-review-settings-ok is present.
852 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
853 root = None
854
855 # Collect all unique directories that may contain PRESUBMIT.py.
856 candidates = set()
857 for directory in directories:
858 while True:
859 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000860 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000861 candidates.add(directory)
862 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000863 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000864 parent_dir = os.path.dirname(directory)
865 if parent_dir == directory:
866 # We hit the system root directory.
867 break
868 directory = parent_dir
869
870 # Look for PRESUBMIT.py in all candidate directories.
871 results = []
872 for directory in sorted(list(candidates)):
873 p = os.path.join(directory, 'PRESUBMIT.py')
874 if os.path.isfile(p):
875 results.append(p)
876
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000877 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000878 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000879
880
thestig@chromium.orgde243452009-10-06 21:02:56 +0000881class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000882 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000883 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000884 """Executes GetPreferredTrySlaves() from a single presubmit script.
885
886 Args:
887 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000888 presubmit_path: Project script to run.
889 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000890
891 Return:
892 A list of try slaves.
893 """
894 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000895 try:
896 exec script_text in context
897 except Exception, e:
898 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000899
900 function_name = 'GetPreferredTrySlaves'
901 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000902 get_preferred_try_slaves = context[function_name]
903 function_info = inspect.getargspec(get_preferred_try_slaves)
904 if len(function_info[0]) == 1:
905 result = get_preferred_try_slaves(project)
906 elif len(function_info[0]) == 2:
907 result = get_preferred_try_slaves(project, change)
908 else:
909 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +0000910 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000911 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000912 'Presubmit functions must return a list, got a %s instead: %s' %
913 (type(result), str(result)))
914 for item in result:
915 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000916 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000917 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000918 raise PresubmitFailure(
919 'Try slave names cannot start/end with whitespace')
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +0000920 if ',' in item:
921 raise PresubmitFailure(
922 'Do not use \',\' separated builder or test names: %s' % item)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000923 else:
924 result = []
925 return result
926
927
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000928def DoGetTrySlaves(change,
929 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000930 repository_root,
931 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000932 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000933 verbose,
934 output_stream):
935 """Get the list of try servers from the presubmit scripts.
936
937 Args:
938 changed_files: List of modified files.
939 repository_root: The repository root.
940 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000941 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000942 verbose: Prints debug info.
943 output_stream: A stream to write debug output to.
944
945 Return:
946 List of try slaves
947 """
948 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
949 if not presubmit_files and verbose:
950 output_stream.write("Warning, no presubmit.py found.\n")
951 results = []
952 executer = GetTrySlavesExecuter()
953 if default_presubmit:
954 if verbose:
955 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000956 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000957 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000958 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000959 for filename in presubmit_files:
960 filename = os.path.abspath(filename)
961 if verbose:
962 output_stream.write("Running %s\n" % filename)
963 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000964 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000965 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000966 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000967
968 slaves = list(set(results))
969 if slaves and verbose:
970 output_stream.write(', '.join(slaves))
971 output_stream.write('\n')
972 return slaves
973
974
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000975class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000976 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000977 """
978 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000979 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000981 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000982 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000983 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000985 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000986 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000987
988 def ExecPresubmitScript(self, script_text, presubmit_path):
989 """Executes a single presubmit script.
990
991 Args:
992 script_text: The text of the presubmit script.
993 presubmit_path: The path to the presubmit file (this will be reported via
994 input_api.PresubmitLocalPath()).
995
996 Return:
997 A list of result objects, empty if no problems.
998 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000999
1000 # Change to the presubmit file's directory to support local imports.
1001 main_path = os.getcwd()
1002 os.chdir(os.path.dirname(presubmit_path))
1003
1004 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001005 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001006 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001008 try:
1009 exec script_text in context
1010 except Exception, e:
1011 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001012
1013 # These function names must change if we make substantial changes to
1014 # the presubmit API that are not backwards compatible.
1015 if self.committing:
1016 function_name = 'CheckChangeOnCommit'
1017 else:
1018 function_name = 'CheckChangeOnUpload'
1019 if function_name in context:
1020 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001021 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001022 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001023 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 if not (isinstance(result, types.TupleType) or
1025 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001026 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001027 'Presubmit functions must return a tuple or list')
1028 for item in result:
1029 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001030 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001031 'All presubmit results must be of types derived from '
1032 'output_api.PresubmitResult')
1033 else:
1034 result = () # no error since the script doesn't care about current event.
1035
chase@chromium.org8e416c82009-10-06 04:30:44 +00001036 # Return the process to the original working directory.
1037 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 return result
1039
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001040
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001041def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001042 committing,
1043 verbose,
1044 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001045 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001046 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001047 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001048 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001049 """Runs all presubmit checks that apply to the files in the change.
1050
1051 This finds all PRESUBMIT.py files in directories enclosing the files in the
1052 change (up to the repository root) and calls the relevant entrypoint function
1053 depending on whether the change is being committed or uploaded.
1054
1055 Prints errors, warnings and notifications. Prompts the user for warnings
1056 when needed.
1057
1058 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001059 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001060 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1061 verbose: Prints debug info.
1062 output_stream: A stream to write output from presubmit tests to.
1063 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001064 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001065 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001066 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001067
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001068 Warning:
1069 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1070 SHOULD be sys.stdin.
1071
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001072 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001073 A PresubmitOutput object. Use output.should_continue() to figure out
1074 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001075 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001076 old_environ = os.environ
1077 try:
1078 # Make sure python subprocesses won't generate .pyc files.
1079 os.environ = os.environ.copy()
1080 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001081
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001082 output = PresubmitOutput(input_stream, output_stream)
1083 if committing:
1084 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001085 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001086 output.write("Running presubmit upload checks ...\n")
1087 start_time = time.time()
1088 presubmit_files = ListRelevantPresubmitFiles(
1089 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1090 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001091 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001092 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001093 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001094 if default_presubmit:
1095 if verbose:
1096 output.write("Running default presubmit script.\n")
1097 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1098 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1099 for filename in presubmit_files:
1100 filename = os.path.abspath(filename)
1101 if verbose:
1102 output.write("Running %s\n" % filename)
1103 # Accept CRLF presubmit script.
1104 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1105 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001106
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001107 errors = []
1108 notifications = []
1109 warnings = []
1110 for result in results:
1111 if result.fatal:
1112 errors.append(result)
1113 elif result.should_prompt:
1114 warnings.append(result)
1115 else:
1116 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001117
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001118 output.write('\n')
1119 for name, items in (('Messages', notifications),
1120 ('Warnings', warnings),
1121 ('ERRORS', errors)):
1122 if items:
1123 output.write('** Presubmit %s **\n' % name)
1124 for item in items:
1125 item.handle(output)
1126 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001127
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001128 total_time = time.time() - start_time
1129 if total_time > 1.0:
1130 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001131
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001132 if not errors:
1133 if not warnings:
1134 output.write('Presubmit checks passed.\n')
1135 elif may_prompt:
1136 output.prompt_yes_no('There were presubmit warnings. '
1137 'Are you sure you wish to continue? (y/N): ')
1138 else:
1139 output.fail()
1140
1141 global _ASKED_FOR_FEEDBACK
1142 # Ask for feedback one time out of 5.
1143 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1144 output.write("Was the presubmit check useful? Please send feedback "
1145 "& hate mail to maruel@chromium.org!\n")
1146 _ASKED_FOR_FEEDBACK = True
1147 return output
1148 finally:
1149 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001150
1151
1152def ScanSubDirs(mask, recursive):
1153 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001154 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 +00001155 else:
1156 results = []
1157 for root, dirs, files in os.walk('.'):
1158 if '.svn' in dirs:
1159 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001160 if '.git' in dirs:
1161 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001162 for name in files:
1163 if fnmatch.fnmatch(name, mask):
1164 results.append(os.path.join(root, name))
1165 return results
1166
1167
1168def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001169 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001170 files = []
1171 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001172 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001173 return files
1174
1175
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001176def load_files(options, args):
1177 """Tries to determine the SCM."""
1178 change_scm = scm.determine_scm(options.root)
1179 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001180 if args:
1181 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001182 if change_scm == 'svn':
1183 change_class = SvnChange
1184 if not files:
1185 files = scm.SVN.CaptureStatus([], options.root)
1186 elif change_scm == 'git':
1187 change_class = GitChange
1188 # TODO(maruel): Get upstream.
1189 if not files:
1190 files = scm.GIT.CaptureStatus([], options.root, None)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001191 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001192 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1193 if not files:
1194 return None, None
1195 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001196 return change_class, files
1197
1198
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001199def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001200 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001201 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001202 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001203 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001204 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1205 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001206 parser.add_option("-r", "--recursive", action="store_true",
1207 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001208 parser.add_option("-v", "--verbose", action="count", default=0,
1209 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001210 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001211 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001212 parser.add_option("--description", default='')
1213 parser.add_option("--issue", type='int', default=0)
1214 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001215 parser.add_option("--root", default=os.getcwd(),
1216 help="Search for PRESUBMIT.py up to this directory. "
1217 "If inherit-review-settings-ok is present in this "
1218 "directory, parent directories up to the root file "
1219 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001220 parser.add_option("--default_presubmit")
1221 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001222 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1223 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1224 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001225 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001226 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001227 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001228 elif options.verbose:
1229 logging.basicConfig(level=logging.INFO)
1230 else:
1231 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001232 change_class, files = load_files(options, args)
1233 if not change_class:
1234 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001235 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001236 rietveld_obj = None
1237 if options.rietveld_url:
1238 rietveld_obj = rietveld.Rietveld(
1239 options.rietveld_url,
1240 options.rietveld_email,
1241 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001242 try:
1243 results = DoPresubmitChecks(
1244 change_class(options.name,
1245 options.description,
1246 options.root,
1247 files,
1248 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001249 options.patchset,
1250 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001251 options.commit,
1252 options.verbose,
1253 sys.stdout,
1254 sys.stdin,
1255 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001256 options.may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001257 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001258 return not results.should_continue()
1259 except PresubmitFailure, e:
1260 print >> sys.stderr, e
1261 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1262 print >> sys.stderr, 'If all fails, contact maruel@'
1263 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001264
1265
1266if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001267 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001268 sys.exit(Main(None))