blob: 1a84b59ca1de1f2879fac35bc5c1ab74d3a85aa3 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
2# Copyright (c) 2011 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
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000019import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000020import marshal # Exposed through the API.
21import optparse
22import os # Somewhat exposed through the API.
23import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000024import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000025import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import sys # Parts exposed through API.
27import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000028import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000029import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000030import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000031import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000033from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000035try:
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000036 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000037except ImportError:
38 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000039 import json # pylint: disable=F0401
40 except ImportError:
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000041 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000042 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000043 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000044
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000045# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000046import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000047import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000048import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000050import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000051import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000052import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
54
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000055# Ask for feedback only once in program lifetime.
56_ASKED_FOR_FEEDBACK = False
57
58
maruel@chromium.org899e1c12011-04-07 17:03:18 +000059class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060 pass
61
62
63def normpath(path):
64 '''Version of os.path.normpath that also changes backward slashes to
65 forward slashes when not running on Windows.
66 '''
67 # This is safe to always do because the Windows version of os.path.normpath
68 # will replace forward slashes with backward slashes.
69 path = path.replace(os.sep, '/')
70 return os.path.normpath(path)
71
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000072
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000073def _RightHandSideLinesImpl(affected_files):
74 """Implements RightHandSideLines for InputApi and GclChange."""
75 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000076 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000077 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000078 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000079
80
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000081class PresubmitOutput(object):
82 def __init__(self, input_stream=None, output_stream=None):
83 self.input_stream = input_stream
84 self.output_stream = output_stream
85 self.reviewers = []
86 self.written_output = []
87 self.error_count = 0
88
89 def prompt_yes_no(self, prompt_string):
90 self.write(prompt_string)
91 if self.input_stream:
92 response = self.input_stream.readline().strip().lower()
93 if response not in ('y', 'yes'):
94 self.fail()
95 else:
96 self.fail()
97
98 def fail(self):
99 self.error_count += 1
100
101 def should_continue(self):
102 return not self.error_count
103
104 def write(self, s):
105 self.written_output.append(s)
106 if self.output_stream:
107 self.output_stream.write(s)
108
109 def getvalue(self):
110 return ''.join(self.written_output)
111
112
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000113class OutputApi(object):
114 """This class (more like a module) gets passed to presubmit scripts so that
115 they can specify various types of results.
116 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000117 class PresubmitResult(object):
118 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000119 fatal = False
120 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000121
122 def __init__(self, message, items=None, long_text=''):
123 """
124 message: A short one-line message to indicate errors.
125 items: A list of short strings to indicate where errors occurred.
126 long_text: multi-line text output, e.g. from another tool
127 """
128 self._message = message
129 self._items = []
130 if items:
131 self._items = items
132 self._long_text = long_text.rstrip()
133
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000134 def handle(self, output):
135 output.write(self._message)
136 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000137 for index, item in enumerate(self._items):
138 output.write(' ')
139 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000140 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000141 if index < len(self._items) - 1:
142 output.write(' \\')
143 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000144 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000145 output.write('\n***************\n')
146 # Write separately in case it's unicode.
147 output.write(self._long_text)
148 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000149 if self.fatal:
150 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000151
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000152 class PresubmitAddReviewers(PresubmitResult):
153 """Add some suggested reviewers to the change."""
154 def __init__(self, reviewers):
155 super(OutputApi.PresubmitAddReviewers, self).__init__('')
156 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000157
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000158 def handle(self, output):
159 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000160
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000161 class PresubmitError(PresubmitResult):
162 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000163 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000164
165 class PresubmitPromptWarning(PresubmitResult):
166 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000167 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000168
169 class PresubmitNotifyResult(PresubmitResult):
170 """Just print something to the screen -- but it's not even a warning."""
171 pass
172
173 class MailTextResult(PresubmitResult):
174 """A warning that should be included in the review request email."""
175 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000176 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000177 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000178
179
180class InputApi(object):
181 """An instance of this object is passed to presubmit scripts so they can
182 know stuff about the change they're looking at.
183 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000184 # Method could be a function
185 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000186
maruel@chromium.org3410d912009-06-09 20:56:16 +0000187 # File extensions that are considered source files from a style guide
188 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000189 #
190 # Files without an extension aren't included in the list. If you want to
191 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
192 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000193 DEFAULT_WHITE_LIST = (
194 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000195 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
196 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000197 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000198 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000199 # Other
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000200 r".+\.java$", r".+\.mk$", r".+\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000201 )
202
203 # Path regexp that should be excluded from being considered containing source
204 # files. Don't modify this list from a presubmit script!
205 DEFAULT_BLACK_LIST = (
206 r".*\bexperimental[\\\/].*",
207 r".*\bthird_party[\\\/].*",
208 # Output directories (just in case)
209 r".*\bDebug[\\\/].*",
210 r".*\bRelease[\\\/].*",
211 r".*\bxcodebuild[\\\/].*",
212 r".*\bsconsbuild[\\\/].*",
213 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000214 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000215 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000216 r"(|.*[\\\/])\.git[\\\/].*",
217 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000218 )
219
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000220 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000221 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000222 """Builds an InputApi object.
223
224 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000225 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000227 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000228 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000229 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000230 # Version number of the presubmit_support script.
231 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000232 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000233 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000234 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000235 # TBD
236 self.host_url = 'http://codereview.chromium.org'
237 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000238 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000239
240 # We expose various modules and functions as attributes of the input_api
241 # so that presubmit scripts don't have to import them.
242 self.basename = os.path.basename
243 self.cPickle = cPickle
244 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000245 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000246 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000247 self.os_listdir = os.listdir
248 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000249 self.os_path = os.path
250 self.pickle = pickle
251 self.marshal = marshal
252 self.re = re
253 self.subprocess = subprocess
254 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000255 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000256 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000257 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000258 self.urllib2 = urllib2
259
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000260 # To easily fork python.
261 self.python_executable = sys.executable
262 self.environ = os.environ
263
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000264 # InputApi.platform is the platform you're currently running on.
265 self.platform = sys.platform
266
267 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000268 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269
270 # We carry the canned checks so presubmit scripts can easily use them.
271 self.canned_checks = presubmit_canned_checks
272
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000273 # TODO(dpranke): figure out a list of all approved owners for a repo
274 # in order to be able to handle wildcard OWNERS files?
275 self.owners_db = owners.Database(change.RepositoryRoot(),
276 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000277 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000278
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000279 def PresubmitLocalPath(self):
280 """Returns the local path of the presubmit script currently being run.
281
282 This is useful if you don't want to hard-code absolute paths in the
283 presubmit script. For example, It can be used to find another file
284 relative to the PRESUBMIT.py script, so the whole tree can be branched and
285 the presubmit script still works, without editing its content.
286 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000287 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000288
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000289 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000290 """Translate a depot path to a local path (relative to client root).
291
292 Args:
293 Depot path as a string.
294
295 Returns:
296 The local path of the depot path under the user's current client, or None
297 if the file is not mapped.
298
299 Remember to check for the None case and show an appropriate error!
300 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000301 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000302 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000303 return local_path
304
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000305 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000306 """Translate a local path to a depot path.
307
308 Args:
309 Local path (relative to current directory, or absolute) as a string.
310
311 Returns:
312 The depot path (SVN URL) of the file if mapped, otherwise None.
313 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000314 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000315 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000316 return depot_path
317
sail@chromium.org5538e022011-05-12 17:53:16 +0000318 def AffectedFiles(self, include_dirs=False, include_deletes=True,
319 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000320 """Same as input_api.change.AffectedFiles() except only lists files
321 (and optionally directories) in the same directory as the current presubmit
322 script, or subdirectories thereof.
323 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000324 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000325 if len(dir_with_slash) == 1:
326 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000327
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000328 return filter(
329 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000330 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000331
332 def LocalPaths(self, include_dirs=False):
333 """Returns local paths of input_api.AffectedFiles()."""
334 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
335
336 def AbsoluteLocalPaths(self, include_dirs=False):
337 """Returns absolute local paths of input_api.AffectedFiles()."""
338 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
339
340 def ServerPaths(self, include_dirs=False):
341 """Returns server paths of input_api.AffectedFiles()."""
342 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
343
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000344 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345 """Same as input_api.change.AffectedTextFiles() except only lists files
346 in the same directory as the current presubmit script, or subdirectories
347 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000348 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000349 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000350 warn("AffectedTextFiles(include_deletes=%s)"
351 " is deprecated and ignored" % str(include_deletes),
352 category=DeprecationWarning,
353 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000354 return filter(lambda x: x.IsTextFile(),
355 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000356
maruel@chromium.org3410d912009-06-09 20:56:16 +0000357 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
358 """Filters out files that aren't considered "source file".
359
360 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
361 and InputApi.DEFAULT_BLACK_LIST is used respectively.
362
363 The lists will be compiled as regular expression and
364 AffectedFile.LocalPath() needs to pass both list.
365
366 Note: Copy-paste this function to suit your needs or use a lambda function.
367 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000368 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000369 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000370 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000371 if self.re.match(item, local_path):
372 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000373 return True
374 return False
375 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
376 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
377
378 def AffectedSourceFiles(self, source_file):
379 """Filter the list of AffectedTextFiles by the function source_file.
380
381 If source_file is None, InputApi.FilterSourceFile() is used.
382 """
383 if not source_file:
384 source_file = self.FilterSourceFile
385 return filter(source_file, self.AffectedTextFiles())
386
387 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000388 """An iterator over all text lines in "new" version of changed files.
389
390 Only lists lines from new or modified text files in the change that are
391 contained by the directory of the currently executing presubmit script.
392
393 This is useful for doing line-by-line regex checks, like checking for
394 trailing whitespace.
395
396 Yields:
397 a 3 tuple:
398 the AffectedFile instance of the current file;
399 integer line number (1-based); and
400 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000401
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000402 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000403 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000404 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000405 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000406
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000407 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000408 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000409
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000410 Deny reading anything outside the repository.
411 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000412 if isinstance(file_item, AffectedFile):
413 file_item = file_item.AbsoluteLocalPath()
414 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000415 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000416 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000417
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000418 @property
419 def tbr(self):
420 """Returns if a change is TBR'ed."""
421 return 'TBR' in self.change.tags
422
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000423
424class AffectedFile(object):
425 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000426 # Method could be a function
427 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000428 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000429 self._path = path
430 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000431 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000432 self._is_directory = None
433 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000434 self._cached_changed_contents = None
435 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000436 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000437
438 def ServerPath(self):
439 """Returns a path string that identifies the file in the SCM system.
440
441 Returns the empty string if the file does not exist in SCM.
442 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000443 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000444
445 def LocalPath(self):
446 """Returns the path of this file on the local disk relative to client root.
447 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000448 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000449
450 def AbsoluteLocalPath(self):
451 """Returns the absolute path of this file on the local disk.
452 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000453 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454
455 def IsDirectory(self):
456 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000457 if self._is_directory is None:
458 path = self.AbsoluteLocalPath()
459 self._is_directory = (os.path.exists(path) and
460 os.path.isdir(path))
461 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000462
463 def Action(self):
464 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000465 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
466 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000467 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000468
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000469 def Property(self, property_name):
470 """Returns the specified SCM property of this file, or None if no such
471 property.
472 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000473 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000474
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000475 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000476 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000477
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000478 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000479 raise NotImplementedError() # Implement when needed
480
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000481 def NewContents(self):
482 """Returns an iterator over the lines in the new version of file.
483
484 The new version is the file in the user's workspace, i.e. the "right hand
485 side".
486
487 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000488 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000489 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000490 if self._cached_new_contents is None:
491 self._cached_new_contents = []
492 if not self.IsDirectory():
493 try:
494 self._cached_new_contents = gclient_utils.FileRead(
495 self.AbsoluteLocalPath(), 'rU').splitlines()
496 except IOError:
497 pass # File not found? That's fine; maybe it was deleted.
498 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000499
500 def OldContents(self):
501 """Returns an iterator over the lines in the old version of file.
502
503 The old version is the file in depot, i.e. the "left hand side".
504 """
505 raise NotImplementedError() # Implement when needed
506
507 def OldFileTempPath(self):
508 """Returns the path on local disk where the old contents resides.
509
510 The old version is the file in depot, i.e. the "left hand side".
511 This is a read-only cached copy of the old contents. *DO NOT* try to
512 modify this file.
513 """
514 raise NotImplementedError() # Implement if/when needed.
515
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000516 def ChangedContents(self):
517 """Returns a list of tuples (line number, line text) of all new lines.
518
519 This relies on the scm diff output describing each changed code section
520 with a line of the form
521
522 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
523 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000524 if self._cached_changed_contents is not None:
525 return self._cached_changed_contents[:]
526 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000527 line_num = 0
528
529 if self.IsDirectory():
530 return []
531
532 for line in self.GenerateScmDiff().splitlines():
533 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
534 if m:
535 line_num = int(m.groups(1)[0])
536 continue
537 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000538 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000539 if not line.startswith('-'):
540 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000541 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000542
maruel@chromium.org5de13972009-06-10 18:16:06 +0000543 def __str__(self):
544 return self.LocalPath()
545
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000546 def GenerateScmDiff(self):
547 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000548
maruel@chromium.org58407af2011-04-12 23:15:57 +0000549
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000550class SvnAffectedFile(AffectedFile):
551 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000552 # Method 'NNN' is abstract in class 'NNN' but is not overridden
553 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000554
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000555 def __init__(self, *args, **kwargs):
556 AffectedFile.__init__(self, *args, **kwargs)
557 self._server_path = None
558 self._is_text_file = None
559
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000560 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000561 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000562 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000563 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000564 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000565
566 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000567 if self._is_directory is None:
568 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000569 if os.path.exists(path):
570 # Retrieve directly from the file system; it is much faster than
571 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000572 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000573 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000574 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000575 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000576 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000577
578 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000579 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000580 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000581 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000582 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000583
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000584 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000585 if self._is_text_file is None:
586 if self.Action() == 'D':
587 # A deleted file is not a text file.
588 self._is_text_file = False
589 elif self.IsDirectory():
590 self._is_text_file = False
591 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000592 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
593 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000594 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
595 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000596
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000597 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000598 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
599
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.orgc70a2202009-06-17 12:55:10 +0000610
611 def ServerPath(self):
612 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000613 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000614 return self._server_path
615
616 def IsDirectory(self):
617 if self._is_directory is None:
618 path = self.AbsoluteLocalPath()
619 if os.path.exists(path):
620 # Retrieve directly from the file system; it is much faster than
621 # querying subversion, especially on Windows.
622 self._is_directory = os.path.isdir(path)
623 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000624 self._is_directory = False
625 return self._is_directory
626
627 def Property(self, property_name):
628 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000629 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000630 return self._properties[property_name]
631
632 def IsTextFile(self):
633 if self._is_text_file is None:
634 if self.Action() == 'D':
635 # A deleted file is not a text file.
636 self._is_text_file = False
637 elif self.IsDirectory():
638 self._is_text_file = False
639 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000640 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
641 return self._is_text_file
642
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000643 def GenerateScmDiff(self):
644 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000645
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000646
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000647class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000648 """Describe a change.
649
650 Used directly by the presubmit scripts to query the current change being
651 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000652
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000653 Instance members:
654 tags: Dictionnary of KEY=VALUE pairs found in the change description.
655 self.KEY: equivalent to tags['KEY']
656 """
657
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000658 _AFFECTED_FILES = AffectedFile
659
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000660 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000661 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000662 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000663 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000664
maruel@chromium.org58407af2011-04-12 23:15:57 +0000665 def __init__(
666 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000667 if files is None:
668 files = []
669 self._name = name
670 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000671 # Convert root into an absolute path.
672 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000673 self.issue = issue
674 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000675 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676
677 # From the description text, build up a dictionary of key/value pairs
678 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000679 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000681 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000682 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683 if m:
684 self.tags[m.group('key')] = m.group('value')
685 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000686 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
688 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000689 self._description_without_tags = (
690 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000691
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000692 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000693 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
694 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000695 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000696
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000697 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000699 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000701 def DescriptionText(self):
702 """Returns the user-entered changelist description, minus tags.
703
704 Any line in the user-provided description starting with e.g. "FOO="
705 (whitespace permitted before and around) is considered a tag line. Such
706 lines are stripped out of the description this function returns.
707 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000708 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709
710 def FullDescriptionText(self):
711 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000712 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000713
714 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000715 """Returns the repository (checkout) root directory for this change,
716 as an absolute path.
717 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000718 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719
720 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000721 """Return tags directly as attributes on the object."""
722 if not re.match(r"^[A-Z_]*$", attr):
723 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000724 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725
sail@chromium.org5538e022011-05-12 17:53:16 +0000726 def AffectedFiles(self, include_dirs=False, include_deletes=True,
727 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000728 """Returns a list of AffectedFile instances for all files in the change.
729
730 Args:
731 include_deletes: If false, deleted files will be filtered out.
732 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000733 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000734
735 Returns:
736 [AffectedFile(path, action), AffectedFile(path, action)]
737 """
738 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000739 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000740 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000741 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742
sail@chromium.org5538e022011-05-12 17:53:16 +0000743 affected = filter(file_filter, affected)
744
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000745 if include_deletes:
746 return affected
747 else:
748 return filter(lambda x: x.Action() != 'D', affected)
749
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000750 def AffectedTextFiles(self, include_deletes=None):
751 """Return a list of the existing text files in a change."""
752 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000753 warn("AffectedTextFiles(include_deletes=%s)"
754 " is deprecated and ignored" % str(include_deletes),
755 category=DeprecationWarning,
756 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000757 return filter(lambda x: x.IsTextFile(),
758 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000759
760 def LocalPaths(self, include_dirs=False):
761 """Convenience function."""
762 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
763
764 def AbsoluteLocalPaths(self, include_dirs=False):
765 """Convenience function."""
766 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
767
768 def ServerPaths(self, include_dirs=False):
769 """Convenience function."""
770 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
771
772 def RightHandSideLines(self):
773 """An iterator over all text lines in "new" version of changed files.
774
775 Lists lines from new or modified text files in the change.
776
777 This is useful for doing line-by-line regex checks, like checking for
778 trailing whitespace.
779
780 Yields:
781 a 3 tuple:
782 the AffectedFile instance of the current file;
783 integer line number (1-based); and
784 the contents of the line as a string.
785 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000786 return _RightHandSideLinesImpl(
787 x for x in self.AffectedFiles(include_deletes=False)
788 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000789
790
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000791class SvnChange(Change):
792 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000793 scm = 'svn'
794 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000795
796 def _GetChangeLists(self):
797 """Get all change lists."""
798 if self._changelists == None:
799 previous_cwd = os.getcwd()
800 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000801 # Need to import here to avoid circular dependency.
802 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000803 self._changelists = gcl.GetModifiedFiles()
804 os.chdir(previous_cwd)
805 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000806
807 def GetAllModifiedFiles(self):
808 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000809 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000810 all_modified_files = []
811 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000812 all_modified_files.extend(
813 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000814 return all_modified_files
815
816 def GetModifiedFiles(self):
817 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000818 changelists = self._GetChangeLists()
819 return [os.path.join(self.RepositoryRoot(), f[1])
820 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000821
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000822
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000823class GitChange(Change):
824 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000825 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000826
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000827
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000828def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000829 """Finds all presubmit files that apply to a given set of source files.
830
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000831 If inherit-review-settings-ok is present right under root, looks for
832 PRESUBMIT.py in directories enclosing root.
833
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000834 Args:
835 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000836 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000837
838 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000839 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000840 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000841 files = [normpath(os.path.join(root, f)) for f in files]
842
843 # List all the individual directories containing files.
844 directories = set([os.path.dirname(f) for f in files])
845
846 # Ignore root if inherit-review-settings-ok is present.
847 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
848 root = None
849
850 # Collect all unique directories that may contain PRESUBMIT.py.
851 candidates = set()
852 for directory in directories:
853 while True:
854 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000855 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000856 candidates.add(directory)
857 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000858 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000859 parent_dir = os.path.dirname(directory)
860 if parent_dir == directory:
861 # We hit the system root directory.
862 break
863 directory = parent_dir
864
865 # Look for PRESUBMIT.py in all candidate directories.
866 results = []
867 for directory in sorted(list(candidates)):
868 p = os.path.join(directory, 'PRESUBMIT.py')
869 if os.path.isfile(p):
870 results.append(p)
871
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000872 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000873 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000874
875
thestig@chromium.orgde243452009-10-06 21:02:56 +0000876class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000877 @staticmethod
bradnelson@google.com78230022011-05-24 18:55:19 +0000878 def ExecPresubmitScript(script_text, presubmit_path, project):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000879 """Executes GetPreferredTrySlaves() from a single presubmit script.
880
881 Args:
882 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000883 presubmit_path: Project script to run.
884 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000885
886 Return:
887 A list of try slaves.
888 """
889 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000890 try:
891 exec script_text in context
892 except Exception, e:
893 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000894
895 function_name = 'GetPreferredTrySlaves'
896 if function_name in context:
bradnelson@google.com78230022011-05-24 18:55:19 +0000897 try:
898 result = eval(function_name + '(' + repr(project) + ')', context)
899 except TypeError:
900 result = eval(function_name + '()', context)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000901 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000902 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000903 'Presubmit functions must return a list, got a %s instead: %s' %
904 (type(result), str(result)))
905 for item in result:
906 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000907 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000908 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000909 raise PresubmitFailure(
910 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000911 else:
912 result = []
913 return result
914
915
916def DoGetTrySlaves(changed_files,
917 repository_root,
918 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000919 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000920 verbose,
921 output_stream):
922 """Get the list of try servers from the presubmit scripts.
923
924 Args:
925 changed_files: List of modified files.
926 repository_root: The repository root.
927 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000928 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000929 verbose: Prints debug info.
930 output_stream: A stream to write debug output to.
931
932 Return:
933 List of try slaves
934 """
935 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
936 if not presubmit_files and verbose:
937 output_stream.write("Warning, no presubmit.py found.\n")
938 results = []
939 executer = GetTrySlavesExecuter()
940 if default_presubmit:
941 if verbose:
942 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000943 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000944 results += executer.ExecPresubmitScript(
945 default_presubmit, fake_path, project)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000946 for filename in presubmit_files:
947 filename = os.path.abspath(filename)
948 if verbose:
949 output_stream.write("Running %s\n" % filename)
950 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000951 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000952 results += executer.ExecPresubmitScript(
953 presubmit_script, filename, project)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000954
955 slaves = list(set(results))
956 if slaves and verbose:
957 output_stream.write(', '.join(slaves))
958 output_stream.write('\n')
959 return slaves
960
961
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000962class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000963 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964 """
965 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000966 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000967 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000968 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000969 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000970 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000971 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000972 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000973 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000974
975 def ExecPresubmitScript(self, script_text, presubmit_path):
976 """Executes a single presubmit script.
977
978 Args:
979 script_text: The text of the presubmit script.
980 presubmit_path: The path to the presubmit file (this will be reported via
981 input_api.PresubmitLocalPath()).
982
983 Return:
984 A list of result objects, empty if no problems.
985 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000986
987 # Change to the presubmit file's directory to support local imports.
988 main_path = os.getcwd()
989 os.chdir(os.path.dirname(presubmit_path))
990
991 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000992 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000993 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000994 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000995 try:
996 exec script_text in context
997 except Exception, e:
998 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000999
1000 # These function names must change if we make substantial changes to
1001 # the presubmit API that are not backwards compatible.
1002 if self.committing:
1003 function_name = 'CheckChangeOnCommit'
1004 else:
1005 function_name = 'CheckChangeOnUpload'
1006 if function_name in context:
1007 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001008 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001009 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001010 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001011 if not (isinstance(result, types.TupleType) or
1012 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001013 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001014 'Presubmit functions must return a tuple or list')
1015 for item in result:
1016 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001017 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018 'All presubmit results must be of types derived from '
1019 'output_api.PresubmitResult')
1020 else:
1021 result = () # no error since the script doesn't care about current event.
1022
chase@chromium.org8e416c82009-10-06 04:30:44 +00001023 # Return the process to the original working directory.
1024 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001025 return result
1026
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001027
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001028def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001029 committing,
1030 verbose,
1031 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001032 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001033 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001034 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001035 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036 """Runs all presubmit checks that apply to the files in the change.
1037
1038 This finds all PRESUBMIT.py files in directories enclosing the files in the
1039 change (up to the repository root) and calls the relevant entrypoint function
1040 depending on whether the change is being committed or uploaded.
1041
1042 Prints errors, warnings and notifications. Prompts the user for warnings
1043 when needed.
1044
1045 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001046 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1048 verbose: Prints debug info.
1049 output_stream: A stream to write output from presubmit tests to.
1050 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001051 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001052 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001053 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001054
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001055 Warning:
1056 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1057 SHOULD be sys.stdin.
1058
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001059 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001060 A PresubmitOutput object. Use output.should_continue() to figure out
1061 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001062 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001063 old_environ = os.environ
1064 try:
1065 # Make sure python subprocesses won't generate .pyc files.
1066 os.environ = os.environ.copy()
1067 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001068
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001069 output = PresubmitOutput(input_stream, output_stream)
1070 if committing:
1071 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001072 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001073 output.write("Running presubmit upload checks ...\n")
1074 start_time = time.time()
1075 presubmit_files = ListRelevantPresubmitFiles(
1076 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1077 if not presubmit_files and verbose:
1078 output.write("Warning, no presubmit.py found.\n")
1079 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001080 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001081 if default_presubmit:
1082 if verbose:
1083 output.write("Running default presubmit script.\n")
1084 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1085 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1086 for filename in presubmit_files:
1087 filename = os.path.abspath(filename)
1088 if verbose:
1089 output.write("Running %s\n" % filename)
1090 # Accept CRLF presubmit script.
1091 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1092 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001093
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001094 errors = []
1095 notifications = []
1096 warnings = []
1097 for result in results:
1098 if result.fatal:
1099 errors.append(result)
1100 elif result.should_prompt:
1101 warnings.append(result)
1102 else:
1103 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001104
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001105 output.write('\n')
1106 for name, items in (('Messages', notifications),
1107 ('Warnings', warnings),
1108 ('ERRORS', errors)):
1109 if items:
1110 output.write('** Presubmit %s **\n' % name)
1111 for item in items:
1112 item.handle(output)
1113 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001114
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001115 total_time = time.time() - start_time
1116 if total_time > 1.0:
1117 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001118
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001119 if not errors:
1120 if not warnings:
1121 output.write('Presubmit checks passed.\n')
1122 elif may_prompt:
1123 output.prompt_yes_no('There were presubmit warnings. '
1124 'Are you sure you wish to continue? (y/N): ')
1125 else:
1126 output.fail()
1127
1128 global _ASKED_FOR_FEEDBACK
1129 # Ask for feedback one time out of 5.
1130 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1131 output.write("Was the presubmit check useful? Please send feedback "
1132 "& hate mail to maruel@chromium.org!\n")
1133 _ASKED_FOR_FEEDBACK = True
1134 return output
1135 finally:
1136 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001137
1138
1139def ScanSubDirs(mask, recursive):
1140 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001141 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 +00001142 else:
1143 results = []
1144 for root, dirs, files in os.walk('.'):
1145 if '.svn' in dirs:
1146 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001147 if '.git' in dirs:
1148 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001149 for name in files:
1150 if fnmatch.fnmatch(name, mask):
1151 results.append(os.path.join(root, name))
1152 return results
1153
1154
1155def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001156 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001157 files = []
1158 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001159 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001160 return files
1161
1162
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001163def load_files(options, args):
1164 """Tries to determine the SCM."""
1165 change_scm = scm.determine_scm(options.root)
1166 files = []
1167 if change_scm == 'svn':
1168 change_class = SvnChange
1169 status_fn = scm.SVN.CaptureStatus
1170 elif change_scm == 'git':
1171 change_class = GitChange
1172 status_fn = scm.GIT.CaptureStatus
1173 else:
1174 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1175 if not args:
1176 return None, None
1177 change_class = Change
1178 if args:
1179 files = ParseFiles(args, options.recursive)
1180 else:
1181 # Grab modified files.
1182 files = status_fn([options.root])
1183 return change_class, files
1184
1185
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001186def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001187 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001188 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001189 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001190 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001191 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1192 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001193 parser.add_option("-r", "--recursive", action="store_true",
1194 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001195 parser.add_option("-v", "--verbose", action="count", default=0,
1196 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001197 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001198 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001199 parser.add_option("--description", default='')
1200 parser.add_option("--issue", type='int', default=0)
1201 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001202 parser.add_option("--root", default=os.getcwd(),
1203 help="Search for PRESUBMIT.py up to this directory. "
1204 "If inherit-review-settings-ok is present in this "
1205 "directory, parent directories up to the root file "
1206 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001207 parser.add_option("--default_presubmit")
1208 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001209 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1210 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1211 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001212 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001213 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001214 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001215 elif options.verbose:
1216 logging.basicConfig(level=logging.INFO)
1217 else:
1218 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001219 change_class, files = load_files(options, args)
1220 if not change_class:
1221 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001222 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001223 rietveld_obj = None
1224 if options.rietveld_url:
1225 rietveld_obj = rietveld.Rietveld(
1226 options.rietveld_url,
1227 options.rietveld_email,
1228 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001229 try:
1230 results = DoPresubmitChecks(
1231 change_class(options.name,
1232 options.description,
1233 options.root,
1234 files,
1235 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001236 options.patchset,
1237 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001238 options.commit,
1239 options.verbose,
1240 sys.stdout,
1241 sys.stdin,
1242 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001243 options.may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001244 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001245 return not results.should_continue()
1246 except PresubmitFailure, e:
1247 print >> sys.stderr, e
1248 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1249 print >> sys.stderr, 'If all fails, contact maruel@'
1250 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001251
1252
1253if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001254 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001255 sys.exit(Main(None))