blob: a292bd78d42884a6aad1afb5a0407d8f3471830f [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00009__version__ = '1.6.1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
15import cPickle # Exposed through the API.
16import cStringIO # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000017import fnmatch
18import glob
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000019import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000020import marshal # Exposed through the API.
21import optparse
22import os # Somewhat exposed through the API.
23import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000024import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000025import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import sys # Parts exposed through API.
27import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000028import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000029import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000030import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000031import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000033from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000035try:
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000036 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000037except ImportError:
38 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000039 import json # pylint: disable=F0401
40 except ImportError:
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000041 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000042 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000043 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000044
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000045# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000046import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000047import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000048import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000050import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000051import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000052import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
54
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000055# Ask for feedback only once in program lifetime.
56_ASKED_FOR_FEEDBACK = False
57
58
maruel@chromium.org899e1c12011-04-07 17:03:18 +000059class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060 pass
61
62
63def normpath(path):
64 '''Version of os.path.normpath that also changes backward slashes to
65 forward slashes when not running on Windows.
66 '''
67 # This is safe to always do because the Windows version of os.path.normpath
68 # will replace forward slashes with backward slashes.
69 path = path.replace(os.sep, '/')
70 return os.path.normpath(path)
71
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000072
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000073def _RightHandSideLinesImpl(affected_files):
74 """Implements RightHandSideLines for InputApi and GclChange."""
75 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000076 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000077 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000078 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000079
80
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000081class PresubmitOutput(object):
82 def __init__(self, input_stream=None, output_stream=None):
83 self.input_stream = input_stream
84 self.output_stream = output_stream
85 self.reviewers = []
86 self.written_output = []
87 self.error_count = 0
88
89 def prompt_yes_no(self, prompt_string):
90 self.write(prompt_string)
91 if self.input_stream:
92 response = self.input_stream.readline().strip().lower()
93 if response not in ('y', 'yes'):
94 self.fail()
95 else:
96 self.fail()
97
98 def fail(self):
99 self.error_count += 1
100
101 def should_continue(self):
102 return not self.error_count
103
104 def write(self, s):
105 self.written_output.append(s)
106 if self.output_stream:
107 self.output_stream.write(s)
108
109 def getvalue(self):
110 return ''.join(self.written_output)
111
112
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000113class OutputApi(object):
114 """This class (more like a module) gets passed to presubmit scripts so that
115 they can specify various types of results.
116 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000117 class PresubmitResult(object):
118 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000119 fatal = False
120 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000121
122 def __init__(self, message, items=None, long_text=''):
123 """
124 message: A short one-line message to indicate errors.
125 items: A list of short strings to indicate where errors occurred.
126 long_text: multi-line text output, e.g. from another tool
127 """
128 self._message = message
129 self._items = []
130 if items:
131 self._items = items
132 self._long_text = long_text.rstrip()
133
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000134 def handle(self, output):
135 output.write(self._message)
136 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000137 for index, item in enumerate(self._items):
138 output.write(' ')
139 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000140 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000141 if index < len(self._items) - 1:
142 output.write(' \\')
143 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000144 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000145 output.write('\n***************\n')
146 # Write separately in case it's unicode.
147 output.write(self._long_text)
148 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000149 if self.fatal:
150 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000151
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000152 class PresubmitAddReviewers(PresubmitResult):
153 """Add some suggested reviewers to the change."""
154 def __init__(self, reviewers):
155 super(OutputApi.PresubmitAddReviewers, self).__init__('')
156 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000157
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000158 def handle(self, output):
159 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000160
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000161 class PresubmitError(PresubmitResult):
162 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000163 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000164
165 class PresubmitPromptWarning(PresubmitResult):
166 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000167 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000168
169 class PresubmitNotifyResult(PresubmitResult):
170 """Just print something to the screen -- but it's not even a warning."""
171 pass
172
173 class MailTextResult(PresubmitResult):
174 """A warning that should be included in the review request email."""
175 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000176 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000177 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000178
179
180class InputApi(object):
181 """An instance of this object is passed to presubmit scripts so they can
182 know stuff about the change they're looking at.
183 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000184 # Method could be a function
185 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000186
maruel@chromium.org3410d912009-06-09 20:56:16 +0000187 # File extensions that are considered source files from a style guide
188 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000189 #
190 # Files without an extension aren't included in the list. If you want to
191 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
192 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000193 DEFAULT_WHITE_LIST = (
194 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000195 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
196 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000197 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000198 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000199 # Other
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000200 r".+\.java$", r".+\.mk$", r".+\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000201 )
202
203 # Path regexp that should be excluded from being considered containing source
204 # files. Don't modify this list from a presubmit script!
205 DEFAULT_BLACK_LIST = (
206 r".*\bexperimental[\\\/].*",
207 r".*\bthird_party[\\\/].*",
208 # Output directories (just in case)
209 r".*\bDebug[\\\/].*",
210 r".*\bRelease[\\\/].*",
211 r".*\bxcodebuild[\\\/].*",
212 r".*\bsconsbuild[\\\/].*",
213 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000214 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000215 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000216 r"(|.*[\\\/])\.git[\\\/].*",
217 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000218 )
219
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000220 def __init__(self, change, presubmit_path, is_committing, tbr,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000221 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000222 """Builds an InputApi object.
223
224 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000225 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000227 is_committing: True if the change is about to be committed.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000228 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
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
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000235 self.tbr = tbr
maruel@chromium.org239f4112011-06-03 20:08:23 +0000236 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000237 # TBD
238 self.host_url = 'http://codereview.chromium.org'
239 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000240 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000241
242 # We expose various modules and functions as attributes of the input_api
243 # so that presubmit scripts don't have to import them.
244 self.basename = os.path.basename
245 self.cPickle = cPickle
246 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000247 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000248 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000249 self.os_listdir = os.listdir
250 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000251 self.os_path = os.path
252 self.pickle = pickle
253 self.marshal = marshal
254 self.re = re
255 self.subprocess = subprocess
256 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000257 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000258 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000259 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000260 self.urllib2 = urllib2
261
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000262 # To easily fork python.
263 self.python_executable = sys.executable
264 self.environ = os.environ
265
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000266 # InputApi.platform is the platform you're currently running on.
267 self.platform = sys.platform
268
269 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000270 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271
272 # We carry the canned checks so presubmit scripts can easily use them.
273 self.canned_checks = presubmit_canned_checks
274
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000275 # TODO(dpranke): figure out a list of all approved owners for a repo
276 # in order to be able to handle wildcard OWNERS files?
277 self.owners_db = owners.Database(change.RepositoryRoot(),
278 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000279 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000280
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000281 def PresubmitLocalPath(self):
282 """Returns the local path of the presubmit script currently being run.
283
284 This is useful if you don't want to hard-code absolute paths in the
285 presubmit script. For example, It can be used to find another file
286 relative to the PRESUBMIT.py script, so the whole tree can be branched and
287 the presubmit script still works, without editing its content.
288 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000289 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000290
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000291 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000292 """Translate a depot path to a local path (relative to client root).
293
294 Args:
295 Depot path as a string.
296
297 Returns:
298 The local path of the depot path under the user's current client, or None
299 if the file is not mapped.
300
301 Remember to check for the None case and show an appropriate error!
302 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000303 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000304 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305 return local_path
306
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000307 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000308 """Translate a local path to a depot path.
309
310 Args:
311 Local path (relative to current directory, or absolute) as a string.
312
313 Returns:
314 The depot path (SVN URL) of the file if mapped, otherwise None.
315 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000316 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000317 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000318 return depot_path
319
sail@chromium.org5538e022011-05-12 17:53:16 +0000320 def AffectedFiles(self, include_dirs=False, include_deletes=True,
321 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322 """Same as input_api.change.AffectedFiles() except only lists files
323 (and optionally directories) in the same directory as the current presubmit
324 script, or subdirectories thereof.
325 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000326 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000327 if len(dir_with_slash) == 1:
328 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000329
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000330 return filter(
331 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000332 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000333
334 def LocalPaths(self, include_dirs=False):
335 """Returns local paths of input_api.AffectedFiles()."""
336 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
337
338 def AbsoluteLocalPaths(self, include_dirs=False):
339 """Returns absolute local paths of input_api.AffectedFiles()."""
340 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
341
342 def ServerPaths(self, include_dirs=False):
343 """Returns server paths of input_api.AffectedFiles()."""
344 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
345
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000346 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 """Same as input_api.change.AffectedTextFiles() except only lists files
348 in the same directory as the current presubmit script, or subdirectories
349 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000351 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000352 warn("AffectedTextFiles(include_deletes=%s)"
353 " is deprecated and ignored" % str(include_deletes),
354 category=DeprecationWarning,
355 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000356 return filter(lambda x: x.IsTextFile(),
357 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000358
maruel@chromium.org3410d912009-06-09 20:56:16 +0000359 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
360 """Filters out files that aren't considered "source file".
361
362 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
363 and InputApi.DEFAULT_BLACK_LIST is used respectively.
364
365 The lists will be compiled as regular expression and
366 AffectedFile.LocalPath() needs to pass both list.
367
368 Note: Copy-paste this function to suit your needs or use a lambda function.
369 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000370 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000371 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000372 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000373 if self.re.match(item, local_path):
374 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000375 return True
376 return False
377 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
378 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
379
380 def AffectedSourceFiles(self, source_file):
381 """Filter the list of AffectedTextFiles by the function source_file.
382
383 If source_file is None, InputApi.FilterSourceFile() is used.
384 """
385 if not source_file:
386 source_file = self.FilterSourceFile
387 return filter(source_file, self.AffectedTextFiles())
388
389 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000390 """An iterator over all text lines in "new" version of changed files.
391
392 Only lists lines from new or modified text files in the change that are
393 contained by the directory of the currently executing presubmit script.
394
395 This is useful for doing line-by-line regex checks, like checking for
396 trailing whitespace.
397
398 Yields:
399 a 3 tuple:
400 the AffectedFile instance of the current file;
401 integer line number (1-based); and
402 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000403
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000404 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000406 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000407 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000408
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000409 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000410 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000411
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000412 Deny reading anything outside the repository.
413 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000414 if isinstance(file_item, AffectedFile):
415 file_item = file_item.AbsoluteLocalPath()
416 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000417 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000418 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000419
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000420
421class AffectedFile(object):
422 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000423 # Method could be a function
424 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000425 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000426 self._path = path
427 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000428 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000429 self._is_directory = None
430 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000431 self._cached_changed_contents = None
432 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000433 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000434
435 def ServerPath(self):
436 """Returns a path string that identifies the file in the SCM system.
437
438 Returns the empty string if the file does not exist in SCM.
439 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000440 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000441
442 def LocalPath(self):
443 """Returns the path of this file on the local disk relative to client root.
444 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000445 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000446
447 def AbsoluteLocalPath(self):
448 """Returns the absolute path of this file on the local disk.
449 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000450 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000451
452 def IsDirectory(self):
453 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000454 if self._is_directory is None:
455 path = self.AbsoluteLocalPath()
456 self._is_directory = (os.path.exists(path) and
457 os.path.isdir(path))
458 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000459
460 def Action(self):
461 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000462 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
463 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000464 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000465
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000466 def Property(self, property_name):
467 """Returns the specified SCM property of this file, or None if no such
468 property.
469 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000470 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000471
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000472 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000473 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000474
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000475 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000476 raise NotImplementedError() # Implement when needed
477
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000478 def NewContents(self):
479 """Returns an iterator over the lines in the new version of file.
480
481 The new version is the file in the user's workspace, i.e. the "right hand
482 side".
483
484 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000485 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000486 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000487 if self._cached_new_contents is None:
488 self._cached_new_contents = []
489 if not self.IsDirectory():
490 try:
491 self._cached_new_contents = gclient_utils.FileRead(
492 self.AbsoluteLocalPath(), 'rU').splitlines()
493 except IOError:
494 pass # File not found? That's fine; maybe it was deleted.
495 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000496
497 def OldContents(self):
498 """Returns an iterator over the lines in the old version of file.
499
500 The old version is the file in depot, i.e. the "left hand side".
501 """
502 raise NotImplementedError() # Implement when needed
503
504 def OldFileTempPath(self):
505 """Returns the path on local disk where the old contents resides.
506
507 The old version is the file in depot, i.e. the "left hand side".
508 This is a read-only cached copy of the old contents. *DO NOT* try to
509 modify this file.
510 """
511 raise NotImplementedError() # Implement if/when needed.
512
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000513 def ChangedContents(self):
514 """Returns a list of tuples (line number, line text) of all new lines.
515
516 This relies on the scm diff output describing each changed code section
517 with a line of the form
518
519 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
520 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000521 if self._cached_changed_contents is not None:
522 return self._cached_changed_contents[:]
523 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000524 line_num = 0
525
526 if self.IsDirectory():
527 return []
528
529 for line in self.GenerateScmDiff().splitlines():
530 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
531 if m:
532 line_num = int(m.groups(1)[0])
533 continue
534 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000535 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000536 if not line.startswith('-'):
537 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000538 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000539
maruel@chromium.org5de13972009-06-10 18:16:06 +0000540 def __str__(self):
541 return self.LocalPath()
542
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000543 def GenerateScmDiff(self):
544 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000545
maruel@chromium.org58407af2011-04-12 23:15:57 +0000546
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000547class SvnAffectedFile(AffectedFile):
548 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000549 # Method 'NNN' is abstract in class 'NNN' but is not overridden
550 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000551
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000552 def __init__(self, *args, **kwargs):
553 AffectedFile.__init__(self, *args, **kwargs)
554 self._server_path = None
555 self._is_text_file = None
556
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000557 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000558 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000559 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000560 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000561 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000562
563 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000564 if self._is_directory is None:
565 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000566 if os.path.exists(path):
567 # Retrieve directly from the file system; it is much faster than
568 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000569 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000570 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000571 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000572 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000573 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000574
575 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000576 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000577 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000578 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000579 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000580
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000581 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000582 if self._is_text_file is None:
583 if self.Action() == 'D':
584 # A deleted file is not a text file.
585 self._is_text_file = False
586 elif self.IsDirectory():
587 self._is_text_file = False
588 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000589 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
590 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000591 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
592 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000593
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000594 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000595 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
596
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000597
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000598class GitAffectedFile(AffectedFile):
599 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000600 # Method 'NNN' is abstract in class 'NNN' but is not overridden
601 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000602
603 def __init__(self, *args, **kwargs):
604 AffectedFile.__init__(self, *args, **kwargs)
605 self._server_path = None
606 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000607
608 def ServerPath(self):
609 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000610 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000611 return self._server_path
612
613 def IsDirectory(self):
614 if self._is_directory is None:
615 path = self.AbsoluteLocalPath()
616 if os.path.exists(path):
617 # Retrieve directly from the file system; it is much faster than
618 # querying subversion, especially on Windows.
619 self._is_directory = os.path.isdir(path)
620 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000621 self._is_directory = False
622 return self._is_directory
623
624 def Property(self, property_name):
625 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000626 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000627 return self._properties[property_name]
628
629 def IsTextFile(self):
630 if self._is_text_file is None:
631 if self.Action() == 'D':
632 # A deleted file is not a text file.
633 self._is_text_file = False
634 elif self.IsDirectory():
635 self._is_text_file = False
636 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000637 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
638 return self._is_text_file
639
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000640 def GenerateScmDiff(self):
641 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000642
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000643
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000644class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000645 """Describe a change.
646
647 Used directly by the presubmit scripts to query the current change being
648 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000649
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000650 Instance members:
651 tags: Dictionnary of KEY=VALUE pairs found in the change description.
652 self.KEY: equivalent to tags['KEY']
653 """
654
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000655 _AFFECTED_FILES = AffectedFile
656
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000657 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000658 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000659 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000660 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661
maruel@chromium.org58407af2011-04-12 23:15:57 +0000662 def __init__(
663 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000664 if files is None:
665 files = []
666 self._name = name
667 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000668 # Convert root into an absolute path.
669 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000670 self.issue = issue
671 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000672 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000673
674 # From the description text, build up a dictionary of key/value pairs
675 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000676 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000677 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000678 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000679 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680 if m:
681 self.tags[m.group('key')] = m.group('value')
682 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000683 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684
685 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000686 self._description_without_tags = (
687 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000689 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000690 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
691 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000692 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000694 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000695 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000696 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698 def DescriptionText(self):
699 """Returns the user-entered changelist description, minus tags.
700
701 Any line in the user-provided description starting with e.g. "FOO="
702 (whitespace permitted before and around) is considered a tag line. Such
703 lines are stripped out of the description this function returns.
704 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000705 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706
707 def FullDescriptionText(self):
708 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000709 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000710
711 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000712 """Returns the repository (checkout) root directory for this change,
713 as an absolute path.
714 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000715 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000716
717 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000718 """Return tags directly as attributes on the object."""
719 if not re.match(r"^[A-Z_]*$", attr):
720 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000721 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000722
sail@chromium.org5538e022011-05-12 17:53:16 +0000723 def AffectedFiles(self, include_dirs=False, include_deletes=True,
724 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725 """Returns a list of AffectedFile instances for all files in the change.
726
727 Args:
728 include_deletes: If false, deleted files will be filtered out.
729 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000730 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731
732 Returns:
733 [AffectedFile(path, action), AffectedFile(path, action)]
734 """
735 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000736 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000737 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000738 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000739
sail@chromium.org5538e022011-05-12 17:53:16 +0000740 affected = filter(file_filter, affected)
741
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742 if include_deletes:
743 return affected
744 else:
745 return filter(lambda x: x.Action() != 'D', affected)
746
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000747 def AffectedTextFiles(self, include_deletes=None):
748 """Return a list of the existing text files in a change."""
749 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000750 warn("AffectedTextFiles(include_deletes=%s)"
751 " is deprecated and ignored" % str(include_deletes),
752 category=DeprecationWarning,
753 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000754 return filter(lambda x: x.IsTextFile(),
755 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000756
757 def LocalPaths(self, include_dirs=False):
758 """Convenience function."""
759 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
760
761 def AbsoluteLocalPaths(self, include_dirs=False):
762 """Convenience function."""
763 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
764
765 def ServerPaths(self, include_dirs=False):
766 """Convenience function."""
767 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
768
769 def RightHandSideLines(self):
770 """An iterator over all text lines in "new" version of changed files.
771
772 Lists lines from new or modified text files in the change.
773
774 This is useful for doing line-by-line regex checks, like checking for
775 trailing whitespace.
776
777 Yields:
778 a 3 tuple:
779 the AffectedFile instance of the current file;
780 integer line number (1-based); and
781 the contents of the line as a string.
782 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000783 return _RightHandSideLinesImpl(
784 x for x in self.AffectedFiles(include_deletes=False)
785 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000786
787
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000788class SvnChange(Change):
789 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000790 scm = 'svn'
791 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000792
793 def _GetChangeLists(self):
794 """Get all change lists."""
795 if self._changelists == None:
796 previous_cwd = os.getcwd()
797 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000798 # Need to import here to avoid circular dependency.
799 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000800 self._changelists = gcl.GetModifiedFiles()
801 os.chdir(previous_cwd)
802 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000803
804 def GetAllModifiedFiles(self):
805 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000806 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000807 all_modified_files = []
808 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000809 all_modified_files.extend(
810 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000811 return all_modified_files
812
813 def GetModifiedFiles(self):
814 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000815 changelists = self._GetChangeLists()
816 return [os.path.join(self.RepositoryRoot(), f[1])
817 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000818
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000819
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000820class GitChange(Change):
821 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000822 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000823
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000824
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000825def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000826 """Finds all presubmit files that apply to a given set of source files.
827
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000828 If inherit-review-settings-ok is present right under root, looks for
829 PRESUBMIT.py in directories enclosing root.
830
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000831 Args:
832 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000833 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000834
835 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000836 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000837 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000838 files = [normpath(os.path.join(root, f)) for f in files]
839
840 # List all the individual directories containing files.
841 directories = set([os.path.dirname(f) for f in files])
842
843 # Ignore root if inherit-review-settings-ok is present.
844 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
845 root = None
846
847 # Collect all unique directories that may contain PRESUBMIT.py.
848 candidates = set()
849 for directory in directories:
850 while True:
851 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000852 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000853 candidates.add(directory)
854 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000855 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000856 parent_dir = os.path.dirname(directory)
857 if parent_dir == directory:
858 # We hit the system root directory.
859 break
860 directory = parent_dir
861
862 # Look for PRESUBMIT.py in all candidate directories.
863 results = []
864 for directory in sorted(list(candidates)):
865 p = os.path.join(directory, 'PRESUBMIT.py')
866 if os.path.isfile(p):
867 results.append(p)
868
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000869 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000870 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000871
872
thestig@chromium.orgde243452009-10-06 21:02:56 +0000873class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000874 @staticmethod
bradnelson@google.com78230022011-05-24 18:55:19 +0000875 def ExecPresubmitScript(script_text, presubmit_path, project):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000876 """Executes GetPreferredTrySlaves() from a single presubmit script.
877
878 Args:
879 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000880 presubmit_path: Project script to run.
881 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000882
883 Return:
884 A list of try slaves.
885 """
886 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000887 try:
888 exec script_text in context
889 except Exception, e:
890 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000891
892 function_name = 'GetPreferredTrySlaves'
893 if function_name in context:
bradnelson@google.com78230022011-05-24 18:55:19 +0000894 try:
895 result = eval(function_name + '(' + repr(project) + ')', context)
896 except TypeError:
897 result = eval(function_name + '()', context)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000898 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000899 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000900 'Presubmit functions must return a list, got a %s instead: %s' %
901 (type(result), str(result)))
902 for item in result:
903 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000904 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000905 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000906 raise PresubmitFailure(
907 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000908 else:
909 result = []
910 return result
911
912
913def DoGetTrySlaves(changed_files,
914 repository_root,
915 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000916 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000917 verbose,
918 output_stream):
919 """Get the list of try servers from the presubmit scripts.
920
921 Args:
922 changed_files: List of modified files.
923 repository_root: The repository root.
924 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000925 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000926 verbose: Prints debug info.
927 output_stream: A stream to write debug output to.
928
929 Return:
930 List of try slaves
931 """
932 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
933 if not presubmit_files and verbose:
934 output_stream.write("Warning, no presubmit.py found.\n")
935 results = []
936 executer = GetTrySlavesExecuter()
937 if default_presubmit:
938 if verbose:
939 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000940 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000941 results += executer.ExecPresubmitScript(
942 default_presubmit, fake_path, project)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000943 for filename in presubmit_files:
944 filename = os.path.abspath(filename)
945 if verbose:
946 output_stream.write("Running %s\n" % filename)
947 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000948 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000949 results += executer.ExecPresubmitScript(
950 presubmit_script, filename, project)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000951
952 slaves = list(set(results))
953 if slaves and verbose:
954 output_stream.write(', '.join(slaves))
955 output_stream.write('\n')
956 return slaves
957
958
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000959class PresubmitExecuter(object):
maruel@chromium.org239f4112011-06-03 20:08:23 +0000960 def __init__(self, change, committing, tbr, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000961 """
962 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000963 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000965 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
maruel@chromium.org239f4112011-06-03 20:08:23 +0000966 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000967 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000968 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000969 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000970 self.tbr = tbr
maruel@chromium.org239f4112011-06-03 20:08:23 +0000971 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000972 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973
974 def ExecPresubmitScript(self, script_text, presubmit_path):
975 """Executes a single presubmit script.
976
977 Args:
978 script_text: The text of the presubmit script.
979 presubmit_path: The path to the presubmit file (this will be reported via
980 input_api.PresubmitLocalPath()).
981
982 Return:
983 A list of result objects, empty if no problems.
984 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000985
986 # Change to the presubmit file's directory to support local imports.
987 main_path = os.getcwd()
988 os.chdir(os.path.dirname(presubmit_path))
989
990 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000991 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000992 self.tbr, self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000993 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000994 try:
995 exec script_text in context
996 except Exception, e:
997 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998
999 # These function names must change if we make substantial changes to
1000 # the presubmit API that are not backwards compatible.
1001 if self.committing:
1002 function_name = 'CheckChangeOnCommit'
1003 else:
1004 function_name = 'CheckChangeOnUpload'
1005 if function_name in context:
1006 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001007 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001009 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 if not (isinstance(result, types.TupleType) or
1011 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001012 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013 'Presubmit functions must return a tuple or list')
1014 for item in result:
1015 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001016 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001017 'All presubmit results must be of types derived from '
1018 'output_api.PresubmitResult')
1019 else:
1020 result = () # no error since the script doesn't care about current event.
1021
chase@chromium.org8e416c82009-10-06 04:30:44 +00001022 # Return the process to the original working directory.
1023 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 return result
1025
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001026
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001027def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001028 committing,
1029 verbose,
1030 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001031 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001032 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001033 may_prompt,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001034 tbr,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001035 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036 """Runs all presubmit checks that apply to the files in the change.
1037
1038 This finds all PRESUBMIT.py files in directories enclosing the files in the
1039 change (up to the repository root) and calls the relevant entrypoint function
1040 depending on whether the change is being committed or uploaded.
1041
1042 Prints errors, warnings and notifications. Prompts the user for warnings
1043 when needed.
1044
1045 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001046 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1048 verbose: Prints debug info.
1049 output_stream: A stream to write output from presubmit tests to.
1050 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001051 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001052 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001053 tbr: was --tbr specified to skip any reviewer/owner checks?
maruel@chromium.org239f4112011-06-03 20:08:23 +00001054 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001055
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001056 Warning:
1057 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1058 SHOULD be sys.stdin.
1059
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001060 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001061 A PresubmitOutput object. Use output.should_continue() to figure out
1062 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001063 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001064 old_environ = os.environ
1065 try:
1066 # Make sure python subprocesses won't generate .pyc files.
1067 os.environ = os.environ.copy()
1068 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001069
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001070 output = PresubmitOutput(input_stream, output_stream)
1071 if committing:
1072 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001073 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001074 output.write("Running presubmit upload checks ...\n")
1075 start_time = time.time()
1076 presubmit_files = ListRelevantPresubmitFiles(
1077 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1078 if not presubmit_files and verbose:
1079 output.write("Warning, no presubmit.py found.\n")
1080 results = []
maruel@chromium.org239f4112011-06-03 20:08:23 +00001081 executer = PresubmitExecuter(change, committing, tbr, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001082 if default_presubmit:
1083 if verbose:
1084 output.write("Running default presubmit script.\n")
1085 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1086 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1087 for filename in presubmit_files:
1088 filename = os.path.abspath(filename)
1089 if verbose:
1090 output.write("Running %s\n" % filename)
1091 # Accept CRLF presubmit script.
1092 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1093 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001094
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001095 errors = []
1096 notifications = []
1097 warnings = []
1098 for result in results:
1099 if result.fatal:
1100 errors.append(result)
1101 elif result.should_prompt:
1102 warnings.append(result)
1103 else:
1104 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001105
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001106 output.write('\n')
1107 for name, items in (('Messages', notifications),
1108 ('Warnings', warnings),
1109 ('ERRORS', errors)):
1110 if items:
1111 output.write('** Presubmit %s **\n' % name)
1112 for item in items:
1113 item.handle(output)
1114 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001115
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001116 total_time = time.time() - start_time
1117 if total_time > 1.0:
1118 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001119
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001120 if not errors:
1121 if not warnings:
1122 output.write('Presubmit checks passed.\n')
1123 elif may_prompt:
1124 output.prompt_yes_no('There were presubmit warnings. '
1125 'Are you sure you wish to continue? (y/N): ')
1126 else:
1127 output.fail()
1128
1129 global _ASKED_FOR_FEEDBACK
1130 # Ask for feedback one time out of 5.
1131 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1132 output.write("Was the presubmit check useful? Please send feedback "
1133 "& hate mail to maruel@chromium.org!\n")
1134 _ASKED_FOR_FEEDBACK = True
1135 return output
1136 finally:
1137 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001138
1139
1140def ScanSubDirs(mask, recursive):
1141 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001142 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 +00001143 else:
1144 results = []
1145 for root, dirs, files in os.walk('.'):
1146 if '.svn' in dirs:
1147 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001148 if '.git' in dirs:
1149 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001150 for name in files:
1151 if fnmatch.fnmatch(name, mask):
1152 results.append(os.path.join(root, name))
1153 return results
1154
1155
1156def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001157 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001158 files = []
1159 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001160 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161 return files
1162
1163
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001164def load_files(options, args):
1165 """Tries to determine the SCM."""
1166 change_scm = scm.determine_scm(options.root)
1167 files = []
1168 if change_scm == 'svn':
1169 change_class = SvnChange
1170 status_fn = scm.SVN.CaptureStatus
1171 elif change_scm == 'git':
1172 change_class = GitChange
1173 status_fn = scm.GIT.CaptureStatus
1174 else:
1175 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1176 if not args:
1177 return None, None
1178 change_class = Change
1179 if args:
1180 files = ParseFiles(args, options.recursive)
1181 else:
1182 # Grab modified files.
1183 files = status_fn([options.root])
1184 return change_class, files
1185
1186
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001187def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001188 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001189 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001190 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001191 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001192 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1193 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001194 parser.add_option("-r", "--recursive", action="store_true",
1195 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001196 parser.add_option("-v", "--verbose", action="count", default=0,
1197 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001198 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001199 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001200 parser.add_option("--description", default='')
1201 parser.add_option("--issue", type='int', default=0)
1202 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001203 parser.add_option("--root", default=os.getcwd(),
1204 help="Search for PRESUBMIT.py up to this directory. "
1205 "If inherit-review-settings-ok is present in this "
1206 "directory, parent directories up to the root file "
1207 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001208 parser.add_option("--default_presubmit")
1209 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001210 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1211 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1212 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001213 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001214 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001215 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001216 elif options.verbose:
1217 logging.basicConfig(level=logging.INFO)
1218 else:
1219 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001220 change_class, files = load_files(options, args)
1221 if not change_class:
1222 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001223 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001224 rietveld_obj = None
1225 if options.rietveld_url:
1226 rietveld_obj = rietveld.Rietveld(
1227 options.rietveld_url,
1228 options.rietveld_email,
1229 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001230 try:
1231 results = DoPresubmitChecks(
1232 change_class(options.name,
1233 options.description,
1234 options.root,
1235 files,
1236 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001237 options.patchset,
1238 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001239 options.commit,
1240 options.verbose,
1241 sys.stdout,
1242 sys.stdin,
1243 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001244 options.may_prompt,
1245 False,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001246 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001247 return not results.should_continue()
1248 except PresubmitFailure, e:
1249 print >> sys.stderr, e
1250 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1251 print >> sys.stderr, 'If all fails, contact maruel@'
1252 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001253
1254
1255if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001256 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001257 sys.exit(Main(None))