blob: b2a94bc24b9b6f53a453cc98c4ede8295104a198 [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
asvitkine@chromium.org15169952011-09-27 14:30:53 +000019import inspect
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000020import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import marshal # Exposed through the API.
22import optparse
23import os # Somewhat exposed through the API.
24import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000025import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027import sys # Parts exposed through API.
28import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000029import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000030import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000032import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000033import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000034from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000035
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000036try:
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000037 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000038except ImportError:
39 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000040 import json # pylint: disable=F0401
41 except ImportError:
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000042 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000043 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000044 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000045
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000046# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000047import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000048import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000049import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000051import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000052import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000053import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000054
55
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000056# Ask for feedback only once in program lifetime.
57_ASKED_FOR_FEEDBACK = False
58
59
maruel@chromium.org899e1c12011-04-07 17:03:18 +000060class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000061 pass
62
63
64def normpath(path):
65 '''Version of os.path.normpath that also changes backward slashes to
66 forward slashes when not running on Windows.
67 '''
68 # This is safe to always do because the Windows version of os.path.normpath
69 # will replace forward slashes with backward slashes.
70 path = path.replace(os.sep, '/')
71 return os.path.normpath(path)
72
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000073
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000074def _RightHandSideLinesImpl(affected_files):
75 """Implements RightHandSideLines for InputApi and GclChange."""
76 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000077 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000079 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000080
81
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000082class PresubmitOutput(object):
83 def __init__(self, input_stream=None, output_stream=None):
84 self.input_stream = input_stream
85 self.output_stream = output_stream
86 self.reviewers = []
87 self.written_output = []
88 self.error_count = 0
89
90 def prompt_yes_no(self, prompt_string):
91 self.write(prompt_string)
92 if self.input_stream:
93 response = self.input_stream.readline().strip().lower()
94 if response not in ('y', 'yes'):
95 self.fail()
96 else:
97 self.fail()
98
99 def fail(self):
100 self.error_count += 1
101
102 def should_continue(self):
103 return not self.error_count
104
105 def write(self, s):
106 self.written_output.append(s)
107 if self.output_stream:
108 self.output_stream.write(s)
109
110 def getvalue(self):
111 return ''.join(self.written_output)
112
113
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000114class OutputApi(object):
115 """This class (more like a module) gets passed to presubmit scripts so that
116 they can specify various types of results.
117 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000118 class PresubmitResult(object):
119 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000120 fatal = False
121 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000122
123 def __init__(self, message, items=None, long_text=''):
124 """
125 message: A short one-line message to indicate errors.
126 items: A list of short strings to indicate where errors occurred.
127 long_text: multi-line text output, e.g. from another tool
128 """
129 self._message = message
130 self._items = []
131 if items:
132 self._items = items
133 self._long_text = long_text.rstrip()
134
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000135 def handle(self, output):
136 output.write(self._message)
137 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000138 for index, item in enumerate(self._items):
139 output.write(' ')
140 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000141 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000142 if index < len(self._items) - 1:
143 output.write(' \\')
144 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000145 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000146 output.write('\n***************\n')
147 # Write separately in case it's unicode.
148 output.write(self._long_text)
149 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000150 if self.fatal:
151 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000152
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000153 class PresubmitAddReviewers(PresubmitResult):
154 """Add some suggested reviewers to the change."""
155 def __init__(self, reviewers):
156 super(OutputApi.PresubmitAddReviewers, self).__init__('')
157 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000158
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000159 def handle(self, output):
160 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000161
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000162 class PresubmitError(PresubmitResult):
163 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000164 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000165
166 class PresubmitPromptWarning(PresubmitResult):
167 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000168 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000169
170 class PresubmitNotifyResult(PresubmitResult):
171 """Just print something to the screen -- but it's not even a warning."""
172 pass
173
174 class MailTextResult(PresubmitResult):
175 """A warning that should be included in the review request email."""
176 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000177 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000178 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000179
180
181class InputApi(object):
182 """An instance of this object is passed to presubmit scripts so they can
183 know stuff about the change they're looking at.
184 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000185 # Method could be a function
186 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000187
maruel@chromium.org3410d912009-06-09 20:56:16 +0000188 # File extensions that are considered source files from a style guide
189 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000190 #
191 # Files without an extension aren't included in the list. If you want to
192 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
193 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000194 DEFAULT_WHITE_LIST = (
195 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000196 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
197 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000198 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000199 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000200 # Other
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000201 r".+\.java$", r".+\.mk$", r".+\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000202 )
203
204 # Path regexp that should be excluded from being considered containing source
205 # files. Don't modify this list from a presubmit script!
206 DEFAULT_BLACK_LIST = (
207 r".*\bexperimental[\\\/].*",
208 r".*\bthird_party[\\\/].*",
209 # Output directories (just in case)
210 r".*\bDebug[\\\/].*",
211 r".*\bRelease[\\\/].*",
212 r".*\bxcodebuild[\\\/].*",
213 r".*\bsconsbuild[\\\/].*",
214 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000215 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000216 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000217 r"(|.*[\\\/])\.git[\\\/].*",
218 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000219 # There is no point in processing a patch file.
220 r".+\.diff$",
221 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000222 )
223
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000224 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000225 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226 """Builds an InputApi object.
227
228 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000229 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000231 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000232 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000233 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000234 # Version number of the presubmit_support script.
235 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000236 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000237 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000238 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000239 # TBD
240 self.host_url = 'http://codereview.chromium.org'
241 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000242 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000243
244 # We expose various modules and functions as attributes of the input_api
245 # so that presubmit scripts don't have to import them.
246 self.basename = os.path.basename
247 self.cPickle = cPickle
248 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000249 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000250 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000251 self.os_listdir = os.listdir
252 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000253 self.os_path = os.path
254 self.pickle = pickle
255 self.marshal = marshal
256 self.re = re
257 self.subprocess = subprocess
258 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000259 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000260 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000261 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000262 self.urllib2 = urllib2
263
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000264 # To easily fork python.
265 self.python_executable = sys.executable
266 self.environ = os.environ
267
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268 # InputApi.platform is the platform you're currently running on.
269 self.platform = sys.platform
270
271 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000272 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273
274 # We carry the canned checks so presubmit scripts can easily use them.
275 self.canned_checks = presubmit_canned_checks
276
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000277 # TODO(dpranke): figure out a list of all approved owners for a repo
278 # in order to be able to handle wildcard OWNERS files?
279 self.owners_db = owners.Database(change.RepositoryRoot(),
280 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000281 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000282
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283 def PresubmitLocalPath(self):
284 """Returns the local path of the presubmit script currently being run.
285
286 This is useful if you don't want to hard-code absolute paths in the
287 presubmit script. For example, It can be used to find another file
288 relative to the PRESUBMIT.py script, so the whole tree can be branched and
289 the presubmit script still works, without editing its content.
290 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000291 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000292
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000293 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000294 """Translate a depot path to a local path (relative to client root).
295
296 Args:
297 Depot path as a string.
298
299 Returns:
300 The local path of the depot path under the user's current client, or None
301 if the file is not mapped.
302
303 Remember to check for the None case and show an appropriate error!
304 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000305 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000306 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000307 return local_path
308
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000309 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000310 """Translate a local path to a depot path.
311
312 Args:
313 Local path (relative to current directory, or absolute) as a string.
314
315 Returns:
316 The depot path (SVN URL) of the file if mapped, otherwise None.
317 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000318 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000319 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000320 return depot_path
321
sail@chromium.org5538e022011-05-12 17:53:16 +0000322 def AffectedFiles(self, include_dirs=False, include_deletes=True,
323 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000324 """Same as input_api.change.AffectedFiles() except only lists files
325 (and optionally directories) in the same directory as the current presubmit
326 script, or subdirectories thereof.
327 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000328 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000329 if len(dir_with_slash) == 1:
330 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000331
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000332 return filter(
333 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000334 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000335
336 def LocalPaths(self, include_dirs=False):
337 """Returns local paths of input_api.AffectedFiles()."""
338 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
339
340 def AbsoluteLocalPaths(self, include_dirs=False):
341 """Returns absolute local paths of input_api.AffectedFiles()."""
342 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
343
344 def ServerPaths(self, include_dirs=False):
345 """Returns server paths of input_api.AffectedFiles()."""
346 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
347
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000348 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349 """Same as input_api.change.AffectedTextFiles() except only lists files
350 in the same directory as the current presubmit script, or subdirectories
351 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000352 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000353 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000354 warn("AffectedTextFiles(include_deletes=%s)"
355 " is deprecated and ignored" % str(include_deletes),
356 category=DeprecationWarning,
357 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000358 return filter(lambda x: x.IsTextFile(),
359 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000360
maruel@chromium.org3410d912009-06-09 20:56:16 +0000361 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
362 """Filters out files that aren't considered "source file".
363
364 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
365 and InputApi.DEFAULT_BLACK_LIST is used respectively.
366
367 The lists will be compiled as regular expression and
368 AffectedFile.LocalPath() needs to pass both list.
369
370 Note: Copy-paste this function to suit your needs or use a lambda function.
371 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000372 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000373 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000374 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000375 if self.re.match(item, local_path):
376 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000377 return True
378 return False
379 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
380 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
381
382 def AffectedSourceFiles(self, source_file):
383 """Filter the list of AffectedTextFiles by the function source_file.
384
385 If source_file is None, InputApi.FilterSourceFile() is used.
386 """
387 if not source_file:
388 source_file = self.FilterSourceFile
389 return filter(source_file, self.AffectedTextFiles())
390
391 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000392 """An iterator over all text lines in "new" version of changed files.
393
394 Only lists lines from new or modified text files in the change that are
395 contained by the directory of the currently executing presubmit script.
396
397 This is useful for doing line-by-line regex checks, like checking for
398 trailing whitespace.
399
400 Yields:
401 a 3 tuple:
402 the AffectedFile instance of the current file;
403 integer line number (1-based); and
404 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000405
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000406 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000408 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000409 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000410
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000411 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000412 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000413
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000414 Deny reading anything outside the repository.
415 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000416 if isinstance(file_item, AffectedFile):
417 file_item = file_item.AbsoluteLocalPath()
418 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000419 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000420 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000421
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000422 @property
423 def tbr(self):
424 """Returns if a change is TBR'ed."""
425 return 'TBR' in self.change.tags
426
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000427
428class AffectedFile(object):
429 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000430 # Method could be a function
431 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000432 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000433 self._path = path
434 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000435 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000436 self._is_directory = None
437 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000438 self._cached_changed_contents = None
439 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000440 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000441
442 def ServerPath(self):
443 """Returns a path string that identifies the file in the SCM system.
444
445 Returns the empty string if the file does not exist in SCM.
446 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000447 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448
449 def LocalPath(self):
450 """Returns the path of this file on the local disk relative to client root.
451 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000452 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000453
454 def AbsoluteLocalPath(self):
455 """Returns the absolute path of this file on the local disk.
456 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000457 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458
459 def IsDirectory(self):
460 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000461 if self._is_directory is None:
462 path = self.AbsoluteLocalPath()
463 self._is_directory = (os.path.exists(path) and
464 os.path.isdir(path))
465 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000466
467 def Action(self):
468 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000469 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
470 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000471 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000472
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000473 def Property(self, property_name):
474 """Returns the specified SCM property of this file, or None if no such
475 property.
476 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000477 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000478
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000479 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000480 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000481
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000482 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000483 raise NotImplementedError() # Implement when needed
484
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000485 def NewContents(self):
486 """Returns an iterator over the lines in the new version of file.
487
488 The new version is the file in the user's workspace, i.e. the "right hand
489 side".
490
491 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000492 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000493 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000494 if self._cached_new_contents is None:
495 self._cached_new_contents = []
496 if not self.IsDirectory():
497 try:
498 self._cached_new_contents = gclient_utils.FileRead(
499 self.AbsoluteLocalPath(), 'rU').splitlines()
500 except IOError:
501 pass # File not found? That's fine; maybe it was deleted.
502 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000503
504 def OldContents(self):
505 """Returns an iterator over the lines in the old version of file.
506
507 The old version is the file in depot, i.e. the "left hand side".
508 """
509 raise NotImplementedError() # Implement when needed
510
511 def OldFileTempPath(self):
512 """Returns the path on local disk where the old contents resides.
513
514 The old version is the file in depot, i.e. the "left hand side".
515 This is a read-only cached copy of the old contents. *DO NOT* try to
516 modify this file.
517 """
518 raise NotImplementedError() # Implement if/when needed.
519
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000520 def ChangedContents(self):
521 """Returns a list of tuples (line number, line text) of all new lines.
522
523 This relies on the scm diff output describing each changed code section
524 with a line of the form
525
526 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
527 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000528 if self._cached_changed_contents is not None:
529 return self._cached_changed_contents[:]
530 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000531 line_num = 0
532
533 if self.IsDirectory():
534 return []
535
536 for line in self.GenerateScmDiff().splitlines():
537 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
538 if m:
539 line_num = int(m.groups(1)[0])
540 continue
541 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000542 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000543 if not line.startswith('-'):
544 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000545 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000546
maruel@chromium.org5de13972009-06-10 18:16:06 +0000547 def __str__(self):
548 return self.LocalPath()
549
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000550 def GenerateScmDiff(self):
551 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000552
maruel@chromium.org58407af2011-04-12 23:15:57 +0000553
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000554class SvnAffectedFile(AffectedFile):
555 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000556 # Method 'NNN' is abstract in class 'NNN' but is not overridden
557 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000558
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000559 def __init__(self, *args, **kwargs):
560 AffectedFile.__init__(self, *args, **kwargs)
561 self._server_path = None
562 self._is_text_file = None
563
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000564 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000565 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000566 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000567 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000568 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000569
570 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000571 if self._is_directory is None:
572 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000573 if os.path.exists(path):
574 # Retrieve directly from the file system; it is much faster than
575 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000576 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000577 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000578 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000579 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000580 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000581
582 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000583 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000584 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000585 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000586 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000587
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000588 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000589 if self._is_text_file is None:
590 if self.Action() == 'D':
591 # A deleted file is not a text file.
592 self._is_text_file = False
593 elif self.IsDirectory():
594 self._is_text_file = False
595 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000596 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
597 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000598 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
599 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000600
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000601 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000602 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
603
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000604
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000605class GitAffectedFile(AffectedFile):
606 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000607 # Method 'NNN' is abstract in class 'NNN' but is not overridden
608 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000609
610 def __init__(self, *args, **kwargs):
611 AffectedFile.__init__(self, *args, **kwargs)
612 self._server_path = None
613 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000614
615 def ServerPath(self):
616 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000617 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000618 return self._server_path
619
620 def IsDirectory(self):
621 if self._is_directory is None:
622 path = self.AbsoluteLocalPath()
623 if os.path.exists(path):
624 # Retrieve directly from the file system; it is much faster than
625 # querying subversion, especially on Windows.
626 self._is_directory = os.path.isdir(path)
627 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000628 self._is_directory = False
629 return self._is_directory
630
631 def Property(self, property_name):
632 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000633 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000634 return self._properties[property_name]
635
636 def IsTextFile(self):
637 if self._is_text_file is None:
638 if self.Action() == 'D':
639 # A deleted file is not a text file.
640 self._is_text_file = False
641 elif self.IsDirectory():
642 self._is_text_file = False
643 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000644 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
645 return self._is_text_file
646
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000647 def GenerateScmDiff(self):
648 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000649
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000650
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000651class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000652 """Describe a change.
653
654 Used directly by the presubmit scripts to query the current change being
655 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000656
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000657 Instance members:
658 tags: Dictionnary of KEY=VALUE pairs found in the change description.
659 self.KEY: equivalent to tags['KEY']
660 """
661
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000662 _AFFECTED_FILES = AffectedFile
663
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000664 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000665 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000666 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000667 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668
maruel@chromium.org58407af2011-04-12 23:15:57 +0000669 def __init__(
670 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000671 if files is None:
672 files = []
673 self._name = name
674 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000675 # Convert root into an absolute path.
676 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000677 self.issue = issue
678 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000679 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680
681 # From the description text, build up a dictionary of key/value pairs
682 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000683 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000685 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000686 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687 if m:
688 self.tags[m.group('key')] = m.group('value')
689 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000690 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000691
692 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000693 self._description_without_tags = (
694 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000695
maruel@chromium.orge085d812011-10-10 19:49:15 +0000696 assert all(
697 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
698
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000699 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000700 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
701 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000702 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000704 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000705 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000706 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000707
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000708 def DescriptionText(self):
709 """Returns the user-entered changelist description, minus tags.
710
711 Any line in the user-provided description starting with e.g. "FOO="
712 (whitespace permitted before and around) is considered a tag line. Such
713 lines are stripped out of the description this function returns.
714 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000715 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000716
717 def FullDescriptionText(self):
718 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000719 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000720
721 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000722 """Returns the repository (checkout) root directory for this change,
723 as an absolute path.
724 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000725 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000726
727 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000728 """Return tags directly as attributes on the object."""
729 if not re.match(r"^[A-Z_]*$", attr):
730 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000731 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000732
sail@chromium.org5538e022011-05-12 17:53:16 +0000733 def AffectedFiles(self, include_dirs=False, include_deletes=True,
734 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735 """Returns a list of AffectedFile instances for all files in the change.
736
737 Args:
738 include_deletes: If false, deleted files will be filtered out.
739 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000740 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741
742 Returns:
743 [AffectedFile(path, action), AffectedFile(path, action)]
744 """
745 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000746 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000748 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749
sail@chromium.org5538e022011-05-12 17:53:16 +0000750 affected = filter(file_filter, affected)
751
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000752 if include_deletes:
753 return affected
754 else:
755 return filter(lambda x: x.Action() != 'D', affected)
756
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000757 def AffectedTextFiles(self, include_deletes=None):
758 """Return a list of the existing text files in a change."""
759 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000760 warn("AffectedTextFiles(include_deletes=%s)"
761 " is deprecated and ignored" % str(include_deletes),
762 category=DeprecationWarning,
763 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000764 return filter(lambda x: x.IsTextFile(),
765 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000766
767 def LocalPaths(self, include_dirs=False):
768 """Convenience function."""
769 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
770
771 def AbsoluteLocalPaths(self, include_dirs=False):
772 """Convenience function."""
773 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
774
775 def ServerPaths(self, include_dirs=False):
776 """Convenience function."""
777 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
778
779 def RightHandSideLines(self):
780 """An iterator over all text lines in "new" version of changed files.
781
782 Lists lines from new or modified text files in the change.
783
784 This is useful for doing line-by-line regex checks, like checking for
785 trailing whitespace.
786
787 Yields:
788 a 3 tuple:
789 the AffectedFile instance of the current file;
790 integer line number (1-based); and
791 the contents of the line as a string.
792 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000793 return _RightHandSideLinesImpl(
794 x for x in self.AffectedFiles(include_deletes=False)
795 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000796
797
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000798class SvnChange(Change):
799 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000800 scm = 'svn'
801 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000802
803 def _GetChangeLists(self):
804 """Get all change lists."""
805 if self._changelists == None:
806 previous_cwd = os.getcwd()
807 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000808 # Need to import here to avoid circular dependency.
809 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000810 self._changelists = gcl.GetModifiedFiles()
811 os.chdir(previous_cwd)
812 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000813
814 def GetAllModifiedFiles(self):
815 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000816 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000817 all_modified_files = []
818 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000819 all_modified_files.extend(
820 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000821 return all_modified_files
822
823 def GetModifiedFiles(self):
824 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000825 changelists = self._GetChangeLists()
826 return [os.path.join(self.RepositoryRoot(), f[1])
827 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000828
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000829
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000830class GitChange(Change):
831 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000832 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000833
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000834
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000835def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836 """Finds all presubmit files that apply to a given set of source files.
837
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000838 If inherit-review-settings-ok is present right under root, looks for
839 PRESUBMIT.py in directories enclosing root.
840
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000841 Args:
842 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000843 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000844
845 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000846 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000848 files = [normpath(os.path.join(root, f)) for f in files]
849
850 # List all the individual directories containing files.
851 directories = set([os.path.dirname(f) for f in files])
852
853 # Ignore root if inherit-review-settings-ok is present.
854 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
855 root = None
856
857 # Collect all unique directories that may contain PRESUBMIT.py.
858 candidates = set()
859 for directory in directories:
860 while True:
861 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000862 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000863 candidates.add(directory)
864 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000865 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000866 parent_dir = os.path.dirname(directory)
867 if parent_dir == directory:
868 # We hit the system root directory.
869 break
870 directory = parent_dir
871
872 # Look for PRESUBMIT.py in all candidate directories.
873 results = []
874 for directory in sorted(list(candidates)):
875 p = os.path.join(directory, 'PRESUBMIT.py')
876 if os.path.isfile(p):
877 results.append(p)
878
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000879 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000880 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881
882
thestig@chromium.orgde243452009-10-06 21:02:56 +0000883class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000884 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000885 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000886 """Executes GetPreferredTrySlaves() from a single presubmit script.
887
888 Args:
889 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000890 presubmit_path: Project script to run.
891 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000892
893 Return:
894 A list of try slaves.
895 """
896 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000897 try:
898 exec script_text in context
899 except Exception, e:
900 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000901
902 function_name = 'GetPreferredTrySlaves'
903 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000904 get_preferred_try_slaves = context[function_name]
905 function_info = inspect.getargspec(get_preferred_try_slaves)
906 if len(function_info[0]) == 1:
907 result = get_preferred_try_slaves(project)
908 elif len(function_info[0]) == 2:
909 result = get_preferred_try_slaves(project, change)
910 else:
911 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +0000912 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000913 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000914 'Presubmit functions must return a list, got a %s instead: %s' %
915 (type(result), str(result)))
916 for item in result:
917 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000918 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000919 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000920 raise PresubmitFailure(
921 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000922 else:
923 result = []
924 return result
925
926
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000927def DoGetTrySlaves(change,
928 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000929 repository_root,
930 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000931 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000932 verbose,
933 output_stream):
934 """Get the list of try servers from the presubmit scripts.
935
936 Args:
937 changed_files: List of modified files.
938 repository_root: The repository root.
939 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000940 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000941 verbose: Prints debug info.
942 output_stream: A stream to write debug output to.
943
944 Return:
945 List of try slaves
946 """
947 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
948 if not presubmit_files and verbose:
949 output_stream.write("Warning, no presubmit.py found.\n")
950 results = []
951 executer = GetTrySlavesExecuter()
952 if default_presubmit:
953 if verbose:
954 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000955 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000956 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000957 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000958 for filename in presubmit_files:
959 filename = os.path.abspath(filename)
960 if verbose:
961 output_stream.write("Running %s\n" % filename)
962 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000963 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000964 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000965 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000966
967 slaves = list(set(results))
968 if slaves and verbose:
969 output_stream.write(', '.join(slaves))
970 output_stream.write('\n')
971 return slaves
972
973
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000974class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000975 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000976 """
977 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000978 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000980 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000982 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000984 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000985 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000986
987 def ExecPresubmitScript(self, script_text, presubmit_path):
988 """Executes a single presubmit script.
989
990 Args:
991 script_text: The text of the presubmit script.
992 presubmit_path: The path to the presubmit file (this will be reported via
993 input_api.PresubmitLocalPath()).
994
995 Return:
996 A list of result objects, empty if no problems.
997 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000998
999 # Change to the presubmit file's directory to support local imports.
1000 main_path = os.getcwd()
1001 os.chdir(os.path.dirname(presubmit_path))
1002
1003 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001004 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001005 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001007 try:
1008 exec script_text in context
1009 except Exception, e:
1010 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001011
1012 # These function names must change if we make substantial changes to
1013 # the presubmit API that are not backwards compatible.
1014 if self.committing:
1015 function_name = 'CheckChangeOnCommit'
1016 else:
1017 function_name = 'CheckChangeOnUpload'
1018 if function_name in context:
1019 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001020 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001021 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001022 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001023 if not (isinstance(result, types.TupleType) or
1024 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001025 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001026 'Presubmit functions must return a tuple or list')
1027 for item in result:
1028 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001029 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001030 'All presubmit results must be of types derived from '
1031 'output_api.PresubmitResult')
1032 else:
1033 result = () # no error since the script doesn't care about current event.
1034
chase@chromium.org8e416c82009-10-06 04:30:44 +00001035 # Return the process to the original working directory.
1036 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037 return result
1038
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001039
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001040def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001041 committing,
1042 verbose,
1043 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001044 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001045 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001046 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001047 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001048 """Runs all presubmit checks that apply to the files in the change.
1049
1050 This finds all PRESUBMIT.py files in directories enclosing the files in the
1051 change (up to the repository root) and calls the relevant entrypoint function
1052 depending on whether the change is being committed or uploaded.
1053
1054 Prints errors, warnings and notifications. Prompts the user for warnings
1055 when needed.
1056
1057 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001058 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001059 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1060 verbose: Prints debug info.
1061 output_stream: A stream to write output from presubmit tests to.
1062 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001063 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001064 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001065 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001066
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001067 Warning:
1068 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1069 SHOULD be sys.stdin.
1070
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001071 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001072 A PresubmitOutput object. Use output.should_continue() to figure out
1073 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001074 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001075 old_environ = os.environ
1076 try:
1077 # Make sure python subprocesses won't generate .pyc files.
1078 os.environ = os.environ.copy()
1079 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001080
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001081 output = PresubmitOutput(input_stream, output_stream)
1082 if committing:
1083 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001084 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001085 output.write("Running presubmit upload checks ...\n")
1086 start_time = time.time()
1087 presubmit_files = ListRelevantPresubmitFiles(
1088 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1089 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001090 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001091 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001092 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001093 if default_presubmit:
1094 if verbose:
1095 output.write("Running default presubmit script.\n")
1096 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1097 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1098 for filename in presubmit_files:
1099 filename = os.path.abspath(filename)
1100 if verbose:
1101 output.write("Running %s\n" % filename)
1102 # Accept CRLF presubmit script.
1103 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1104 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001105
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001106 errors = []
1107 notifications = []
1108 warnings = []
1109 for result in results:
1110 if result.fatal:
1111 errors.append(result)
1112 elif result.should_prompt:
1113 warnings.append(result)
1114 else:
1115 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001116
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001117 output.write('\n')
1118 for name, items in (('Messages', notifications),
1119 ('Warnings', warnings),
1120 ('ERRORS', errors)):
1121 if items:
1122 output.write('** Presubmit %s **\n' % name)
1123 for item in items:
1124 item.handle(output)
1125 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001126
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001127 total_time = time.time() - start_time
1128 if total_time > 1.0:
1129 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001130
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001131 if not errors:
1132 if not warnings:
1133 output.write('Presubmit checks passed.\n')
1134 elif may_prompt:
1135 output.prompt_yes_no('There were presubmit warnings. '
1136 'Are you sure you wish to continue? (y/N): ')
1137 else:
1138 output.fail()
1139
1140 global _ASKED_FOR_FEEDBACK
1141 # Ask for feedback one time out of 5.
1142 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1143 output.write("Was the presubmit check useful? Please send feedback "
1144 "& hate mail to maruel@chromium.org!\n")
1145 _ASKED_FOR_FEEDBACK = True
1146 return output
1147 finally:
1148 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001149
1150
1151def ScanSubDirs(mask, recursive):
1152 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001153 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 +00001154 else:
1155 results = []
1156 for root, dirs, files in os.walk('.'):
1157 if '.svn' in dirs:
1158 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001159 if '.git' in dirs:
1160 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161 for name in files:
1162 if fnmatch.fnmatch(name, mask):
1163 results.append(os.path.join(root, name))
1164 return results
1165
1166
1167def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001168 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001169 files = []
1170 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001171 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001172 return files
1173
1174
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001175def load_files(options, args):
1176 """Tries to determine the SCM."""
1177 change_scm = scm.determine_scm(options.root)
1178 files = []
1179 if change_scm == 'svn':
1180 change_class = SvnChange
1181 status_fn = scm.SVN.CaptureStatus
1182 elif change_scm == 'git':
1183 change_class = GitChange
1184 status_fn = scm.GIT.CaptureStatus
1185 else:
1186 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1187 if not args:
1188 return None, None
1189 change_class = Change
1190 if args:
1191 files = ParseFiles(args, options.recursive)
1192 else:
1193 # Grab modified files.
1194 files = status_fn([options.root])
1195 return change_class, files
1196
1197
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001198def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001199 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001200 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001201 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001202 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001203 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1204 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001205 parser.add_option("-r", "--recursive", action="store_true",
1206 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001207 parser.add_option("-v", "--verbose", action="count", default=0,
1208 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001209 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001210 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001211 parser.add_option("--description", default='')
1212 parser.add_option("--issue", type='int', default=0)
1213 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001214 parser.add_option("--root", default=os.getcwd(),
1215 help="Search for PRESUBMIT.py up to this directory. "
1216 "If inherit-review-settings-ok is present in this "
1217 "directory, parent directories up to the root file "
1218 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001219 parser.add_option("--default_presubmit")
1220 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001221 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1222 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1223 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001224 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001225 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001226 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001227 elif options.verbose:
1228 logging.basicConfig(level=logging.INFO)
1229 else:
1230 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001231 change_class, files = load_files(options, args)
1232 if not change_class:
1233 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001234 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001235 rietveld_obj = None
1236 if options.rietveld_url:
1237 rietveld_obj = rietveld.Rietveld(
1238 options.rietveld_url,
1239 options.rietveld_email,
1240 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001241 try:
1242 results = DoPresubmitChecks(
1243 change_class(options.name,
1244 options.description,
1245 options.root,
1246 files,
1247 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001248 options.patchset,
1249 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001250 options.commit,
1251 options.verbose,
1252 sys.stdout,
1253 sys.stdin,
1254 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001255 options.may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001256 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001257 return not results.should_continue()
1258 except PresubmitFailure, e:
1259 print >> sys.stderr, e
1260 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1261 print >> sys.stderr, 'If all fails, contact maruel@'
1262 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001263
1264
1265if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001266 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001267 sys.exit(Main(None))