blob: 0efaca1c8dea2dff36bdea85e50857992f783b33 [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.orge085d812011-10-10 19:49:15 +0000693 assert all(
694 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
695
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000696 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000697 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
698 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000699 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000701 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000703 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000705 def DescriptionText(self):
706 """Returns the user-entered changelist description, minus tags.
707
708 Any line in the user-provided description starting with e.g. "FOO="
709 (whitespace permitted before and around) is considered a tag line. Such
710 lines are stripped out of the description this function returns.
711 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000712 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000713
714 def FullDescriptionText(self):
715 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000716 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717
718 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000719 """Returns the repository (checkout) root directory for this change,
720 as an absolute path.
721 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000722 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000723
724 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000725 """Return tags directly as attributes on the object."""
726 if not re.match(r"^[A-Z_]*$", attr):
727 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000728 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729
sail@chromium.org5538e022011-05-12 17:53:16 +0000730 def AffectedFiles(self, include_dirs=False, include_deletes=True,
731 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000732 """Returns a list of AffectedFile instances for all files in the change.
733
734 Args:
735 include_deletes: If false, deleted files will be filtered out.
736 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000737 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000738
739 Returns:
740 [AffectedFile(path, action), AffectedFile(path, action)]
741 """
742 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000743 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000744 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000745 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000746
sail@chromium.org5538e022011-05-12 17:53:16 +0000747 affected = filter(file_filter, affected)
748
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749 if include_deletes:
750 return affected
751 else:
752 return filter(lambda x: x.Action() != 'D', affected)
753
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000754 def AffectedTextFiles(self, include_deletes=None):
755 """Return a list of the existing text files in a change."""
756 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000757 warn("AffectedTextFiles(include_deletes=%s)"
758 " is deprecated and ignored" % str(include_deletes),
759 category=DeprecationWarning,
760 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000761 return filter(lambda x: x.IsTextFile(),
762 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000763
764 def LocalPaths(self, include_dirs=False):
765 """Convenience function."""
766 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
767
768 def AbsoluteLocalPaths(self, include_dirs=False):
769 """Convenience function."""
770 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
771
772 def ServerPaths(self, include_dirs=False):
773 """Convenience function."""
774 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
775
776 def RightHandSideLines(self):
777 """An iterator over all text lines in "new" version of changed files.
778
779 Lists lines from new or modified text files in the change.
780
781 This is useful for doing line-by-line regex checks, like checking for
782 trailing whitespace.
783
784 Yields:
785 a 3 tuple:
786 the AffectedFile instance of the current file;
787 integer line number (1-based); and
788 the contents of the line as a string.
789 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000790 return _RightHandSideLinesImpl(
791 x for x in self.AffectedFiles(include_deletes=False)
792 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000793
794
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000795class SvnChange(Change):
796 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000797 scm = 'svn'
798 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000799
800 def _GetChangeLists(self):
801 """Get all change lists."""
802 if self._changelists == None:
803 previous_cwd = os.getcwd()
804 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000805 # Need to import here to avoid circular dependency.
806 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000807 self._changelists = gcl.GetModifiedFiles()
808 os.chdir(previous_cwd)
809 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000810
811 def GetAllModifiedFiles(self):
812 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000813 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000814 all_modified_files = []
815 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000816 all_modified_files.extend(
817 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000818 return all_modified_files
819
820 def GetModifiedFiles(self):
821 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000822 changelists = self._GetChangeLists()
823 return [os.path.join(self.RepositoryRoot(), f[1])
824 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000825
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000826
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000827class GitChange(Change):
828 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000829 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000830
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000831
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000832def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000833 """Finds all presubmit files that apply to a given set of source files.
834
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000835 If inherit-review-settings-ok is present right under root, looks for
836 PRESUBMIT.py in directories enclosing root.
837
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838 Args:
839 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000840 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000841
842 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000843 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000844 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000845 files = [normpath(os.path.join(root, f)) for f in files]
846
847 # List all the individual directories containing files.
848 directories = set([os.path.dirname(f) for f in files])
849
850 # Ignore root if inherit-review-settings-ok is present.
851 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
852 root = None
853
854 # Collect all unique directories that may contain PRESUBMIT.py.
855 candidates = set()
856 for directory in directories:
857 while True:
858 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000859 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000860 candidates.add(directory)
861 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000862 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000863 parent_dir = os.path.dirname(directory)
864 if parent_dir == directory:
865 # We hit the system root directory.
866 break
867 directory = parent_dir
868
869 # Look for PRESUBMIT.py in all candidate directories.
870 results = []
871 for directory in sorted(list(candidates)):
872 p = os.path.join(directory, 'PRESUBMIT.py')
873 if os.path.isfile(p):
874 results.append(p)
875
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000876 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000877 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000878
879
thestig@chromium.orgde243452009-10-06 21:02:56 +0000880class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000881 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000882 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000883 """Executes GetPreferredTrySlaves() from a single presubmit script.
884
885 Args:
886 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000887 presubmit_path: Project script to run.
888 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000889
890 Return:
891 A list of try slaves.
892 """
893 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000894 try:
895 exec script_text in context
896 except Exception, e:
897 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000898
899 function_name = 'GetPreferredTrySlaves'
900 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000901 get_preferred_try_slaves = context[function_name]
902 function_info = inspect.getargspec(get_preferred_try_slaves)
903 if len(function_info[0]) == 1:
904 result = get_preferred_try_slaves(project)
905 elif len(function_info[0]) == 2:
906 result = get_preferred_try_slaves(project, change)
907 else:
908 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +0000909 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000910 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000911 'Presubmit functions must return a list, got a %s instead: %s' %
912 (type(result), str(result)))
913 for item in result:
914 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000915 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000916 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000917 raise PresubmitFailure(
918 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000919 else:
920 result = []
921 return result
922
923
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000924def DoGetTrySlaves(change,
925 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000926 repository_root,
927 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000928 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000929 verbose,
930 output_stream):
931 """Get the list of try servers from the presubmit scripts.
932
933 Args:
934 changed_files: List of modified files.
935 repository_root: The repository root.
936 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000937 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000938 verbose: Prints debug info.
939 output_stream: A stream to write debug output to.
940
941 Return:
942 List of try slaves
943 """
944 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
945 if not presubmit_files and verbose:
946 output_stream.write("Warning, no presubmit.py found.\n")
947 results = []
948 executer = GetTrySlavesExecuter()
949 if default_presubmit:
950 if verbose:
951 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000952 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000953 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000954 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000955 for filename in presubmit_files:
956 filename = os.path.abspath(filename)
957 if verbose:
958 output_stream.write("Running %s\n" % filename)
959 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000960 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000961 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000962 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000963
964 slaves = list(set(results))
965 if slaves and verbose:
966 output_stream.write(', '.join(slaves))
967 output_stream.write('\n')
968 return slaves
969
970
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000971class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000972 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973 """
974 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000975 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000976 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000977 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000979 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000981 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000982 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983
984 def ExecPresubmitScript(self, script_text, presubmit_path):
985 """Executes a single presubmit script.
986
987 Args:
988 script_text: The text of the presubmit script.
989 presubmit_path: The path to the presubmit file (this will be reported via
990 input_api.PresubmitLocalPath()).
991
992 Return:
993 A list of result objects, empty if no problems.
994 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000995
996 # Change to the presubmit file's directory to support local imports.
997 main_path = os.getcwd()
998 os.chdir(os.path.dirname(presubmit_path))
999
1000 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001001 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001002 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001003 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001004 try:
1005 exec script_text in context
1006 except Exception, e:
1007 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008
1009 # These function names must change if we make substantial changes to
1010 # the presubmit API that are not backwards compatible.
1011 if self.committing:
1012 function_name = 'CheckChangeOnCommit'
1013 else:
1014 function_name = 'CheckChangeOnUpload'
1015 if function_name in context:
1016 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001017 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001019 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001020 if not (isinstance(result, types.TupleType) or
1021 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001022 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001023 'Presubmit functions must return a tuple or list')
1024 for item in result:
1025 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001026 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001027 'All presubmit results must be of types derived from '
1028 'output_api.PresubmitResult')
1029 else:
1030 result = () # no error since the script doesn't care about current event.
1031
chase@chromium.org8e416c82009-10-06 04:30:44 +00001032 # Return the process to the original working directory.
1033 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001034 return result
1035
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001036
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001037def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 committing,
1039 verbose,
1040 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001041 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001042 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001043 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001044 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001045 """Runs all presubmit checks that apply to the files in the change.
1046
1047 This finds all PRESUBMIT.py files in directories enclosing the files in the
1048 change (up to the repository root) and calls the relevant entrypoint function
1049 depending on whether the change is being committed or uploaded.
1050
1051 Prints errors, warnings and notifications. Prompts the user for warnings
1052 when needed.
1053
1054 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001055 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001056 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1057 verbose: Prints debug info.
1058 output_stream: A stream to write output from presubmit tests to.
1059 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001060 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001061 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001062 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001063
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001064 Warning:
1065 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1066 SHOULD be sys.stdin.
1067
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001068 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001069 A PresubmitOutput object. Use output.should_continue() to figure out
1070 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001071 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001072 old_environ = os.environ
1073 try:
1074 # Make sure python subprocesses won't generate .pyc files.
1075 os.environ = os.environ.copy()
1076 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001077
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001078 output = PresubmitOutput(input_stream, output_stream)
1079 if committing:
1080 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001081 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001082 output.write("Running presubmit upload checks ...\n")
1083 start_time = time.time()
1084 presubmit_files = ListRelevantPresubmitFiles(
1085 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1086 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001087 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001088 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001089 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001090 if default_presubmit:
1091 if verbose:
1092 output.write("Running default presubmit script.\n")
1093 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1094 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1095 for filename in presubmit_files:
1096 filename = os.path.abspath(filename)
1097 if verbose:
1098 output.write("Running %s\n" % filename)
1099 # Accept CRLF presubmit script.
1100 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1101 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001102
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001103 errors = []
1104 notifications = []
1105 warnings = []
1106 for result in results:
1107 if result.fatal:
1108 errors.append(result)
1109 elif result.should_prompt:
1110 warnings.append(result)
1111 else:
1112 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001113
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001114 output.write('\n')
1115 for name, items in (('Messages', notifications),
1116 ('Warnings', warnings),
1117 ('ERRORS', errors)):
1118 if items:
1119 output.write('** Presubmit %s **\n' % name)
1120 for item in items:
1121 item.handle(output)
1122 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001123
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001124 total_time = time.time() - start_time
1125 if total_time > 1.0:
1126 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001127
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001128 if not errors:
1129 if not warnings:
1130 output.write('Presubmit checks passed.\n')
1131 elif may_prompt:
1132 output.prompt_yes_no('There were presubmit warnings. '
1133 'Are you sure you wish to continue? (y/N): ')
1134 else:
1135 output.fail()
1136
1137 global _ASKED_FOR_FEEDBACK
1138 # Ask for feedback one time out of 5.
1139 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1140 output.write("Was the presubmit check useful? Please send feedback "
1141 "& hate mail to maruel@chromium.org!\n")
1142 _ASKED_FOR_FEEDBACK = True
1143 return output
1144 finally:
1145 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001146
1147
1148def ScanSubDirs(mask, recursive):
1149 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001150 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 +00001151 else:
1152 results = []
1153 for root, dirs, files in os.walk('.'):
1154 if '.svn' in dirs:
1155 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001156 if '.git' in dirs:
1157 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001158 for name in files:
1159 if fnmatch.fnmatch(name, mask):
1160 results.append(os.path.join(root, name))
1161 return results
1162
1163
1164def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001165 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001166 files = []
1167 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001168 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001169 return files
1170
1171
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001172def load_files(options, args):
1173 """Tries to determine the SCM."""
1174 change_scm = scm.determine_scm(options.root)
1175 files = []
1176 if change_scm == 'svn':
1177 change_class = SvnChange
1178 status_fn = scm.SVN.CaptureStatus
1179 elif change_scm == 'git':
1180 change_class = GitChange
1181 status_fn = scm.GIT.CaptureStatus
1182 else:
1183 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1184 if not args:
1185 return None, None
1186 change_class = Change
1187 if args:
1188 files = ParseFiles(args, options.recursive)
1189 else:
1190 # Grab modified files.
1191 files = status_fn([options.root])
1192 return change_class, files
1193
1194
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001195def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001196 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001197 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001198 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001199 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001200 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1201 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001202 parser.add_option("-r", "--recursive", action="store_true",
1203 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001204 parser.add_option("-v", "--verbose", action="count", default=0,
1205 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001206 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001207 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001208 parser.add_option("--description", default='')
1209 parser.add_option("--issue", type='int', default=0)
1210 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001211 parser.add_option("--root", default=os.getcwd(),
1212 help="Search for PRESUBMIT.py up to this directory. "
1213 "If inherit-review-settings-ok is present in this "
1214 "directory, parent directories up to the root file "
1215 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001216 parser.add_option("--default_presubmit")
1217 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001218 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1219 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1220 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001221 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001222 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001223 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001224 elif options.verbose:
1225 logging.basicConfig(level=logging.INFO)
1226 else:
1227 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001228 change_class, files = load_files(options, args)
1229 if not change_class:
1230 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001231 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001232 rietveld_obj = None
1233 if options.rietveld_url:
1234 rietveld_obj = rietveld.Rietveld(
1235 options.rietveld_url,
1236 options.rietveld_email,
1237 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001238 try:
1239 results = DoPresubmitChecks(
1240 change_class(options.name,
1241 options.description,
1242 options.root,
1243 files,
1244 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001245 options.patchset,
1246 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001247 options.commit,
1248 options.verbose,
1249 sys.stdout,
1250 sys.stdin,
1251 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001252 options.may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001253 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001254 return not results.should_continue()
1255 except PresubmitFailure, e:
1256 print >> sys.stderr, e
1257 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1258 print >> sys.stderr, 'If all fails, contact maruel@'
1259 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001260
1261
1262if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001263 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001264 sys.exit(Main(None))