blob: 85b3a3f70ccdc096fb807dd5978de18c1e7fb79b [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.org3410d912009-06-09 20:56:16 +0000219 )
220
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000221 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000222 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000223 """Builds an InputApi object.
224
225 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000226 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000227 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000228 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000229 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000231 # Version number of the presubmit_support script.
232 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000233 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000234 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000235 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000236 # TBD
237 self.host_url = 'http://codereview.chromium.org'
238 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000239 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000240
241 # We expose various modules and functions as attributes of the input_api
242 # so that presubmit scripts don't have to import them.
243 self.basename = os.path.basename
244 self.cPickle = cPickle
245 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000246 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000247 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000248 self.os_listdir = os.listdir
249 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000250 self.os_path = os.path
251 self.pickle = pickle
252 self.marshal = marshal
253 self.re = re
254 self.subprocess = subprocess
255 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000256 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000257 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000258 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000259 self.urllib2 = urllib2
260
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000261 # To easily fork python.
262 self.python_executable = sys.executable
263 self.environ = os.environ
264
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000265 # InputApi.platform is the platform you're currently running on.
266 self.platform = sys.platform
267
268 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000269 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000270
271 # We carry the canned checks so presubmit scripts can easily use them.
272 self.canned_checks = presubmit_canned_checks
273
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000274 # TODO(dpranke): figure out a list of all approved owners for a repo
275 # in order to be able to handle wildcard OWNERS files?
276 self.owners_db = owners.Database(change.RepositoryRoot(),
277 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000278 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000279
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000280 def PresubmitLocalPath(self):
281 """Returns the local path of the presubmit script currently being run.
282
283 This is useful if you don't want to hard-code absolute paths in the
284 presubmit script. For example, It can be used to find another file
285 relative to the PRESUBMIT.py script, so the whole tree can be branched and
286 the presubmit script still works, without editing its content.
287 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000288 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000290 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000291 """Translate a depot path to a local path (relative to client root).
292
293 Args:
294 Depot path as a string.
295
296 Returns:
297 The local path of the depot path under the user's current client, or None
298 if the file is not mapped.
299
300 Remember to check for the None case and show an appropriate error!
301 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000302 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000303 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000304 return local_path
305
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000306 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000307 """Translate a local path to a depot path.
308
309 Args:
310 Local path (relative to current directory, or absolute) as a string.
311
312 Returns:
313 The depot path (SVN URL) of the file if mapped, otherwise None.
314 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000315 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000316 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000317 return depot_path
318
sail@chromium.org5538e022011-05-12 17:53:16 +0000319 def AffectedFiles(self, include_dirs=False, include_deletes=True,
320 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000321 """Same as input_api.change.AffectedFiles() except only lists files
322 (and optionally directories) in the same directory as the current presubmit
323 script, or subdirectories thereof.
324 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000325 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000326 if len(dir_with_slash) == 1:
327 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000328
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000329 return filter(
330 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000331 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000332
333 def LocalPaths(self, include_dirs=False):
334 """Returns local paths of input_api.AffectedFiles()."""
335 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
336
337 def AbsoluteLocalPaths(self, include_dirs=False):
338 """Returns absolute local paths of input_api.AffectedFiles()."""
339 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
340
341 def ServerPaths(self, include_dirs=False):
342 """Returns server paths of input_api.AffectedFiles()."""
343 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
344
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000345 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000346 """Same as input_api.change.AffectedTextFiles() except only lists files
347 in the same directory as the current presubmit script, or subdirectories
348 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000350 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000351 warn("AffectedTextFiles(include_deletes=%s)"
352 " is deprecated and ignored" % str(include_deletes),
353 category=DeprecationWarning,
354 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000355 return filter(lambda x: x.IsTextFile(),
356 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357
maruel@chromium.org3410d912009-06-09 20:56:16 +0000358 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
359 """Filters out files that aren't considered "source file".
360
361 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
362 and InputApi.DEFAULT_BLACK_LIST is used respectively.
363
364 The lists will be compiled as regular expression and
365 AffectedFile.LocalPath() needs to pass both list.
366
367 Note: Copy-paste this function to suit your needs or use a lambda function.
368 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000369 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000370 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000371 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000372 if self.re.match(item, local_path):
373 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000374 return True
375 return False
376 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
377 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
378
379 def AffectedSourceFiles(self, source_file):
380 """Filter the list of AffectedTextFiles by the function source_file.
381
382 If source_file is None, InputApi.FilterSourceFile() is used.
383 """
384 if not source_file:
385 source_file = self.FilterSourceFile
386 return filter(source_file, self.AffectedTextFiles())
387
388 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000389 """An iterator over all text lines in "new" version of changed files.
390
391 Only lists lines from new or modified text files in the change that are
392 contained by the directory of the currently executing presubmit script.
393
394 This is useful for doing line-by-line regex checks, like checking for
395 trailing whitespace.
396
397 Yields:
398 a 3 tuple:
399 the AffectedFile instance of the current file;
400 integer line number (1-based); and
401 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000402
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000403 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000404 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000405 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000406 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000408 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000409 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000410
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000411 Deny reading anything outside the repository.
412 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000413 if isinstance(file_item, AffectedFile):
414 file_item = file_item.AbsoluteLocalPath()
415 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000416 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000417 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000418
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000419 @property
420 def tbr(self):
421 """Returns if a change is TBR'ed."""
422 return 'TBR' in self.change.tags
423
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000424
425class AffectedFile(object):
426 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000427 # Method could be a function
428 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000429 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000430 self._path = path
431 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000432 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000433 self._is_directory = None
434 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000435 self._cached_changed_contents = None
436 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000437 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438
439 def ServerPath(self):
440 """Returns a path string that identifies the file in the SCM system.
441
442 Returns the empty string if the file does not exist in SCM.
443 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000444 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000445
446 def LocalPath(self):
447 """Returns the path of this file on the local disk relative to client root.
448 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000449 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000450
451 def AbsoluteLocalPath(self):
452 """Returns the absolute path of this file on the local disk.
453 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000454 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455
456 def IsDirectory(self):
457 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000458 if self._is_directory is None:
459 path = self.AbsoluteLocalPath()
460 self._is_directory = (os.path.exists(path) and
461 os.path.isdir(path))
462 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000463
464 def Action(self):
465 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000466 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
467 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000468 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000469
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000470 def Property(self, property_name):
471 """Returns the specified SCM property of this file, or None if no such
472 property.
473 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000474 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000475
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000476 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000477 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000478
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000479 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000480 raise NotImplementedError() # Implement when needed
481
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000482 def NewContents(self):
483 """Returns an iterator over the lines in the new version of file.
484
485 The new version is the file in the user's workspace, i.e. the "right hand
486 side".
487
488 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000489 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000490 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000491 if self._cached_new_contents is None:
492 self._cached_new_contents = []
493 if not self.IsDirectory():
494 try:
495 self._cached_new_contents = gclient_utils.FileRead(
496 self.AbsoluteLocalPath(), 'rU').splitlines()
497 except IOError:
498 pass # File not found? That's fine; maybe it was deleted.
499 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000500
501 def OldContents(self):
502 """Returns an iterator over the lines in the old version of file.
503
504 The old version is the file in depot, i.e. the "left hand side".
505 """
506 raise NotImplementedError() # Implement when needed
507
508 def OldFileTempPath(self):
509 """Returns the path on local disk where the old contents resides.
510
511 The old version is the file in depot, i.e. the "left hand side".
512 This is a read-only cached copy of the old contents. *DO NOT* try to
513 modify this file.
514 """
515 raise NotImplementedError() # Implement if/when needed.
516
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000517 def ChangedContents(self):
518 """Returns a list of tuples (line number, line text) of all new lines.
519
520 This relies on the scm diff output describing each changed code section
521 with a line of the form
522
523 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
524 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000525 if self._cached_changed_contents is not None:
526 return self._cached_changed_contents[:]
527 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000528 line_num = 0
529
530 if self.IsDirectory():
531 return []
532
533 for line in self.GenerateScmDiff().splitlines():
534 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
535 if m:
536 line_num = int(m.groups(1)[0])
537 continue
538 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000539 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000540 if not line.startswith('-'):
541 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000542 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000543
maruel@chromium.org5de13972009-06-10 18:16:06 +0000544 def __str__(self):
545 return self.LocalPath()
546
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000547 def GenerateScmDiff(self):
548 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000549
maruel@chromium.org58407af2011-04-12 23:15:57 +0000550
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000551class SvnAffectedFile(AffectedFile):
552 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000553 # Method 'NNN' is abstract in class 'NNN' but is not overridden
554 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000555
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000556 def __init__(self, *args, **kwargs):
557 AffectedFile.__init__(self, *args, **kwargs)
558 self._server_path = None
559 self._is_text_file = None
560
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000561 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000562 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000563 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000564 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000565 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000566
567 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000568 if self._is_directory is None:
569 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000570 if os.path.exists(path):
571 # Retrieve directly from the file system; it is much faster than
572 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000573 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000574 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000575 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000576 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000577 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000578
579 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000580 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000581 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000582 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000583 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000584
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000585 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000586 if self._is_text_file is None:
587 if self.Action() == 'D':
588 # A deleted file is not a text file.
589 self._is_text_file = False
590 elif self.IsDirectory():
591 self._is_text_file = False
592 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000593 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
594 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000595 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
596 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000597
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000598 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000599 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
600
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000601
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000602class GitAffectedFile(AffectedFile):
603 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000604 # Method 'NNN' is abstract in class 'NNN' but is not overridden
605 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000606
607 def __init__(self, *args, **kwargs):
608 AffectedFile.__init__(self, *args, **kwargs)
609 self._server_path = None
610 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000611
612 def ServerPath(self):
613 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000614 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000615 return self._server_path
616
617 def IsDirectory(self):
618 if self._is_directory is None:
619 path = self.AbsoluteLocalPath()
620 if os.path.exists(path):
621 # Retrieve directly from the file system; it is much faster than
622 # querying subversion, especially on Windows.
623 self._is_directory = os.path.isdir(path)
624 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000625 self._is_directory = False
626 return self._is_directory
627
628 def Property(self, property_name):
629 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000630 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000631 return self._properties[property_name]
632
633 def IsTextFile(self):
634 if self._is_text_file is None:
635 if self.Action() == 'D':
636 # A deleted file is not a text file.
637 self._is_text_file = False
638 elif self.IsDirectory():
639 self._is_text_file = False
640 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000641 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
642 return self._is_text_file
643
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000644 def GenerateScmDiff(self):
645 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000646
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000647
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000648class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000649 """Describe a change.
650
651 Used directly by the presubmit scripts to query the current change being
652 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000653
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000654 Instance members:
655 tags: Dictionnary of KEY=VALUE pairs found in the change description.
656 self.KEY: equivalent to tags['KEY']
657 """
658
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000659 _AFFECTED_FILES = AffectedFile
660
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000661 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000662 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000663 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000664 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000665
maruel@chromium.org58407af2011-04-12 23:15:57 +0000666 def __init__(
667 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000668 if files is None:
669 files = []
670 self._name = name
671 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000672 # Convert root into an absolute path.
673 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000674 self.issue = issue
675 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000676 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000677
678 # From the description text, build up a dictionary of key/value pairs
679 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000680 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000682 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000683 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684 if m:
685 self.tags[m.group('key')] = m.group('value')
686 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000687 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688
689 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000690 self._description_without_tags = (
691 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000692
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000693 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000694 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
695 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000696 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000698 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000699 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000700 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000701
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702 def DescriptionText(self):
703 """Returns the user-entered changelist description, minus tags.
704
705 Any line in the user-provided description starting with e.g. "FOO="
706 (whitespace permitted before and around) is considered a tag line. Such
707 lines are stripped out of the description this function returns.
708 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000709 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000710
711 def FullDescriptionText(self):
712 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000713 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714
715 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000716 """Returns the repository (checkout) root directory for this change,
717 as an absolute path.
718 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000719 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000720
721 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000722 """Return tags directly as attributes on the object."""
723 if not re.match(r"^[A-Z_]*$", attr):
724 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000725 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000726
sail@chromium.org5538e022011-05-12 17:53:16 +0000727 def AffectedFiles(self, include_dirs=False, include_deletes=True,
728 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729 """Returns a list of AffectedFile instances for all files in the change.
730
731 Args:
732 include_deletes: If false, deleted files will be filtered out.
733 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000734 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735
736 Returns:
737 [AffectedFile(path, action), AffectedFile(path, action)]
738 """
739 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000740 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000742 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000743
sail@chromium.org5538e022011-05-12 17:53:16 +0000744 affected = filter(file_filter, affected)
745
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000746 if include_deletes:
747 return affected
748 else:
749 return filter(lambda x: x.Action() != 'D', affected)
750
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000751 def AffectedTextFiles(self, include_deletes=None):
752 """Return a list of the existing text files in a change."""
753 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000754 warn("AffectedTextFiles(include_deletes=%s)"
755 " is deprecated and ignored" % str(include_deletes),
756 category=DeprecationWarning,
757 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000758 return filter(lambda x: x.IsTextFile(),
759 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000760
761 def LocalPaths(self, include_dirs=False):
762 """Convenience function."""
763 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
764
765 def AbsoluteLocalPaths(self, include_dirs=False):
766 """Convenience function."""
767 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
768
769 def ServerPaths(self, include_dirs=False):
770 """Convenience function."""
771 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
772
773 def RightHandSideLines(self):
774 """An iterator over all text lines in "new" version of changed files.
775
776 Lists lines from new or modified text files in the change.
777
778 This is useful for doing line-by-line regex checks, like checking for
779 trailing whitespace.
780
781 Yields:
782 a 3 tuple:
783 the AffectedFile instance of the current file;
784 integer line number (1-based); and
785 the contents of the line as a string.
786 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000787 return _RightHandSideLinesImpl(
788 x for x in self.AffectedFiles(include_deletes=False)
789 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000790
791
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000792class SvnChange(Change):
793 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000794 scm = 'svn'
795 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000796
797 def _GetChangeLists(self):
798 """Get all change lists."""
799 if self._changelists == None:
800 previous_cwd = os.getcwd()
801 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000802 # Need to import here to avoid circular dependency.
803 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000804 self._changelists = gcl.GetModifiedFiles()
805 os.chdir(previous_cwd)
806 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000807
808 def GetAllModifiedFiles(self):
809 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000810 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000811 all_modified_files = []
812 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000813 all_modified_files.extend(
814 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000815 return all_modified_files
816
817 def GetModifiedFiles(self):
818 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000819 changelists = self._GetChangeLists()
820 return [os.path.join(self.RepositoryRoot(), f[1])
821 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000822
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000823
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000824class GitChange(Change):
825 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000826 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000827
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000828
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000829def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 """Finds all presubmit files that apply to a given set of source files.
831
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000832 If inherit-review-settings-ok is present right under root, looks for
833 PRESUBMIT.py in directories enclosing root.
834
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000835 Args:
836 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000837 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838
839 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000840 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000841 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000842 files = [normpath(os.path.join(root, f)) for f in files]
843
844 # List all the individual directories containing files.
845 directories = set([os.path.dirname(f) for f in files])
846
847 # Ignore root if inherit-review-settings-ok is present.
848 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
849 root = None
850
851 # Collect all unique directories that may contain PRESUBMIT.py.
852 candidates = set()
853 for directory in directories:
854 while True:
855 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000856 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000857 candidates.add(directory)
858 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000859 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000860 parent_dir = os.path.dirname(directory)
861 if parent_dir == directory:
862 # We hit the system root directory.
863 break
864 directory = parent_dir
865
866 # Look for PRESUBMIT.py in all candidate directories.
867 results = []
868 for directory in sorted(list(candidates)):
869 p = os.path.join(directory, 'PRESUBMIT.py')
870 if os.path.isfile(p):
871 results.append(p)
872
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000873 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000874 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000875
876
thestig@chromium.orgde243452009-10-06 21:02:56 +0000877class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000878 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000879 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000880 """Executes GetPreferredTrySlaves() from a single presubmit script.
881
882 Args:
883 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000884 presubmit_path: Project script to run.
885 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000886
887 Return:
888 A list of try slaves.
889 """
890 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000891 try:
892 exec script_text in context
893 except Exception, e:
894 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000895
896 function_name = 'GetPreferredTrySlaves'
897 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000898 get_preferred_try_slaves = context[function_name]
899 function_info = inspect.getargspec(get_preferred_try_slaves)
900 if len(function_info[0]) == 1:
901 result = get_preferred_try_slaves(project)
902 elif len(function_info[0]) == 2:
903 result = get_preferred_try_slaves(project, change)
904 else:
905 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +0000906 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000907 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000908 'Presubmit functions must return a list, got a %s instead: %s' %
909 (type(result), str(result)))
910 for item in result:
911 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000912 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000913 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000914 raise PresubmitFailure(
915 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000916 else:
917 result = []
918 return result
919
920
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000921def DoGetTrySlaves(change,
922 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000923 repository_root,
924 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000925 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000926 verbose,
927 output_stream):
928 """Get the list of try servers from the presubmit scripts.
929
930 Args:
931 changed_files: List of modified files.
932 repository_root: The repository root.
933 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000934 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000935 verbose: Prints debug info.
936 output_stream: A stream to write debug output to.
937
938 Return:
939 List of try slaves
940 """
941 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
942 if not presubmit_files and verbose:
943 output_stream.write("Warning, no presubmit.py found.\n")
944 results = []
945 executer = GetTrySlavesExecuter()
946 if default_presubmit:
947 if verbose:
948 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000949 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000950 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000951 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000952 for filename in presubmit_files:
953 filename = os.path.abspath(filename)
954 if verbose:
955 output_stream.write("Running %s\n" % filename)
956 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000957 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000958 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000959 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000960
961 slaves = list(set(results))
962 if slaves and verbose:
963 output_stream.write(', '.join(slaves))
964 output_stream.write('\n')
965 return slaves
966
967
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000968class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000969 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000970 """
971 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000972 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000974 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000975 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000976 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000977 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000978 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000979 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980
981 def ExecPresubmitScript(self, script_text, presubmit_path):
982 """Executes a single presubmit script.
983
984 Args:
985 script_text: The text of the presubmit script.
986 presubmit_path: The path to the presubmit file (this will be reported via
987 input_api.PresubmitLocalPath()).
988
989 Return:
990 A list of result objects, empty if no problems.
991 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000992
993 # Change to the presubmit file's directory to support local imports.
994 main_path = os.getcwd()
995 os.chdir(os.path.dirname(presubmit_path))
996
997 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000998 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000999 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001001 try:
1002 exec script_text in context
1003 except Exception, e:
1004 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001005
1006 # These function names must change if we make substantial changes to
1007 # the presubmit API that are not backwards compatible.
1008 if self.committing:
1009 function_name = 'CheckChangeOnCommit'
1010 else:
1011 function_name = 'CheckChangeOnUpload'
1012 if function_name in context:
1013 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001014 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001016 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001017 if not (isinstance(result, types.TupleType) or
1018 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001019 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001020 'Presubmit functions must return a tuple or list')
1021 for item in result:
1022 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001023 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 'All presubmit results must be of types derived from '
1025 'output_api.PresubmitResult')
1026 else:
1027 result = () # no error since the script doesn't care about current event.
1028
chase@chromium.org8e416c82009-10-06 04:30:44 +00001029 # Return the process to the original working directory.
1030 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001031 return result
1032
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001033
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001034def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035 committing,
1036 verbose,
1037 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001038 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001039 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001040 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001041 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001042 """Runs all presubmit checks that apply to the files in the change.
1043
1044 This finds all PRESUBMIT.py files in directories enclosing the files in the
1045 change (up to the repository root) and calls the relevant entrypoint function
1046 depending on whether the change is being committed or uploaded.
1047
1048 Prints errors, warnings and notifications. Prompts the user for warnings
1049 when needed.
1050
1051 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001052 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001053 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1054 verbose: Prints debug info.
1055 output_stream: A stream to write output from presubmit tests to.
1056 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001057 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001058 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001059 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001060
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001061 Warning:
1062 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1063 SHOULD be sys.stdin.
1064
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001065 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001066 A PresubmitOutput object. Use output.should_continue() to figure out
1067 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001068 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001069 old_environ = os.environ
1070 try:
1071 # Make sure python subprocesses won't generate .pyc files.
1072 os.environ = os.environ.copy()
1073 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001074
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001075 output = PresubmitOutput(input_stream, output_stream)
1076 if committing:
1077 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001078 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001079 output.write("Running presubmit upload checks ...\n")
1080 start_time = time.time()
1081 presubmit_files = ListRelevantPresubmitFiles(
1082 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1083 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001084 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001085 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001086 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001087 if default_presubmit:
1088 if verbose:
1089 output.write("Running default presubmit script.\n")
1090 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1091 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1092 for filename in presubmit_files:
1093 filename = os.path.abspath(filename)
1094 if verbose:
1095 output.write("Running %s\n" % filename)
1096 # Accept CRLF presubmit script.
1097 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1098 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001099
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001100 errors = []
1101 notifications = []
1102 warnings = []
1103 for result in results:
1104 if result.fatal:
1105 errors.append(result)
1106 elif result.should_prompt:
1107 warnings.append(result)
1108 else:
1109 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001110
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001111 output.write('\n')
1112 for name, items in (('Messages', notifications),
1113 ('Warnings', warnings),
1114 ('ERRORS', errors)):
1115 if items:
1116 output.write('** Presubmit %s **\n' % name)
1117 for item in items:
1118 item.handle(output)
1119 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001120
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001121 total_time = time.time() - start_time
1122 if total_time > 1.0:
1123 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001124
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001125 if not errors:
1126 if not warnings:
1127 output.write('Presubmit checks passed.\n')
1128 elif may_prompt:
1129 output.prompt_yes_no('There were presubmit warnings. '
1130 'Are you sure you wish to continue? (y/N): ')
1131 else:
1132 output.fail()
1133
1134 global _ASKED_FOR_FEEDBACK
1135 # Ask for feedback one time out of 5.
1136 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1137 output.write("Was the presubmit check useful? Please send feedback "
1138 "& hate mail to maruel@chromium.org!\n")
1139 _ASKED_FOR_FEEDBACK = True
1140 return output
1141 finally:
1142 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001143
1144
1145def ScanSubDirs(mask, recursive):
1146 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001147 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 +00001148 else:
1149 results = []
1150 for root, dirs, files in os.walk('.'):
1151 if '.svn' in dirs:
1152 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001153 if '.git' in dirs:
1154 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001155 for name in files:
1156 if fnmatch.fnmatch(name, mask):
1157 results.append(os.path.join(root, name))
1158 return results
1159
1160
1161def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001162 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001163 files = []
1164 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001165 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001166 return files
1167
1168
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001169def load_files(options, args):
1170 """Tries to determine the SCM."""
1171 change_scm = scm.determine_scm(options.root)
1172 files = []
1173 if change_scm == 'svn':
1174 change_class = SvnChange
1175 status_fn = scm.SVN.CaptureStatus
1176 elif change_scm == 'git':
1177 change_class = GitChange
1178 status_fn = scm.GIT.CaptureStatus
1179 else:
1180 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1181 if not args:
1182 return None, None
1183 change_class = Change
1184 if args:
1185 files = ParseFiles(args, options.recursive)
1186 else:
1187 # Grab modified files.
1188 files = status_fn([options.root])
1189 return change_class, files
1190
1191
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001192def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001193 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001194 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001195 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001196 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001197 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1198 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001199 parser.add_option("-r", "--recursive", action="store_true",
1200 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001201 parser.add_option("-v", "--verbose", action="count", default=0,
1202 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001203 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001204 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001205 parser.add_option("--description", default='')
1206 parser.add_option("--issue", type='int', default=0)
1207 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001208 parser.add_option("--root", default=os.getcwd(),
1209 help="Search for PRESUBMIT.py up to this directory. "
1210 "If inherit-review-settings-ok is present in this "
1211 "directory, parent directories up to the root file "
1212 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001213 parser.add_option("--default_presubmit")
1214 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001215 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1216 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1217 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001218 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001219 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001220 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001221 elif options.verbose:
1222 logging.basicConfig(level=logging.INFO)
1223 else:
1224 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001225 change_class, files = load_files(options, args)
1226 if not change_class:
1227 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001228 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001229 rietveld_obj = None
1230 if options.rietveld_url:
1231 rietveld_obj = rietveld.Rietveld(
1232 options.rietveld_url,
1233 options.rietveld_email,
1234 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001235 try:
1236 results = DoPresubmitChecks(
1237 change_class(options.name,
1238 options.description,
1239 options.root,
1240 files,
1241 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001242 options.patchset,
1243 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001244 options.commit,
1245 options.verbose,
1246 sys.stdout,
1247 sys.stdin,
1248 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001249 options.may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001250 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001251 return not results.should_continue()
1252 except PresubmitFailure, e:
1253 print >> sys.stderr, e
1254 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1255 print >> sys.stderr, 'If all fails, contact maruel@'
1256 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001257
1258
1259if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001260 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001261 sys.exit(Main(None))