blob: e1fcd3d086c209c1172f207f2ed3dc484b2f4c92 [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!
189 DEFAULT_WHITE_LIST = (
190 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000191 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
192 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000193 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000194 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000195 # No extension at all, note that ALL CAPS files are black listed in
196 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000197 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000198 # Other
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000199 r".+\.java$", r".+\.mk$", r".+\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000200 )
201
202 # Path regexp that should be excluded from being considered containing source
203 # files. Don't modify this list from a presubmit script!
204 DEFAULT_BLACK_LIST = (
205 r".*\bexperimental[\\\/].*",
206 r".*\bthird_party[\\\/].*",
207 # Output directories (just in case)
208 r".*\bDebug[\\\/].*",
209 r".*\bRelease[\\\/].*",
210 r".*\bxcodebuild[\\\/].*",
211 r".*\bsconsbuild[\\\/].*",
212 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000213 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000214 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000215 r"(|.*[\\\/])\.git[\\\/].*",
216 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000217 )
218
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000219 def __init__(self, change, presubmit_path, is_committing, tbr,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000220 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000221 """Builds an InputApi object.
222
223 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000224 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000225 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000226 is_committing: True if the change is about to be committed.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000227 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
maruel@chromium.org239f4112011-06-03 20:08:23 +0000228 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000229 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000230 # Version number of the presubmit_support script.
231 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000232 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000233 self.is_committing = is_committing
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000234 self.tbr = tbr
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@google.comfb2b8eb2009-04-23 21:03:42 +0000419
420class AffectedFile(object):
421 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000422 # Method could be a function
423 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000424 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000425 self._path = path
426 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000427 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000428 self._is_directory = None
429 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000430 self._cached_changed_contents = None
431 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000432 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433
434 def ServerPath(self):
435 """Returns a path string that identifies the file in the SCM system.
436
437 Returns the empty string if the file does not exist in SCM.
438 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000439 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000440
441 def LocalPath(self):
442 """Returns the path of this file on the local disk relative to client root.
443 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000444 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000445
446 def AbsoluteLocalPath(self):
447 """Returns the absolute path of this file on the local disk.
448 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000449 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000450
451 def IsDirectory(self):
452 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000453 if self._is_directory is None:
454 path = self.AbsoluteLocalPath()
455 self._is_directory = (os.path.exists(path) and
456 os.path.isdir(path))
457 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458
459 def Action(self):
460 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000461 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
462 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000463 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000464
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000465 def Property(self, property_name):
466 """Returns the specified SCM property of this file, or None if no such
467 property.
468 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000469 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000470
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000471 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000472 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000473
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000474 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000475 raise NotImplementedError() # Implement when needed
476
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000477 def NewContents(self):
478 """Returns an iterator over the lines in the new version of file.
479
480 The new version is the file in the user's workspace, i.e. the "right hand
481 side".
482
483 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000484 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000485 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000486 if self._cached_new_contents is None:
487 self._cached_new_contents = []
488 if not self.IsDirectory():
489 try:
490 self._cached_new_contents = gclient_utils.FileRead(
491 self.AbsoluteLocalPath(), 'rU').splitlines()
492 except IOError:
493 pass # File not found? That's fine; maybe it was deleted.
494 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000495
496 def OldContents(self):
497 """Returns an iterator over the lines in the old version of file.
498
499 The old version is the file in depot, i.e. the "left hand side".
500 """
501 raise NotImplementedError() # Implement when needed
502
503 def OldFileTempPath(self):
504 """Returns the path on local disk where the old contents resides.
505
506 The old version is the file in depot, i.e. the "left hand side".
507 This is a read-only cached copy of the old contents. *DO NOT* try to
508 modify this file.
509 """
510 raise NotImplementedError() # Implement if/when needed.
511
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000512 def ChangedContents(self):
513 """Returns a list of tuples (line number, line text) of all new lines.
514
515 This relies on the scm diff output describing each changed code section
516 with a line of the form
517
518 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
519 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000520 if self._cached_changed_contents is not None:
521 return self._cached_changed_contents[:]
522 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000523 line_num = 0
524
525 if self.IsDirectory():
526 return []
527
528 for line in self.GenerateScmDiff().splitlines():
529 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
530 if m:
531 line_num = int(m.groups(1)[0])
532 continue
533 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000534 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000535 if not line.startswith('-'):
536 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000537 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000538
maruel@chromium.org5de13972009-06-10 18:16:06 +0000539 def __str__(self):
540 return self.LocalPath()
541
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000542 def GenerateScmDiff(self):
543 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000544
maruel@chromium.org58407af2011-04-12 23:15:57 +0000545
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000546class SvnAffectedFile(AffectedFile):
547 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000548 # Method 'NNN' is abstract in class 'NNN' but is not overridden
549 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000550
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000551 def __init__(self, *args, **kwargs):
552 AffectedFile.__init__(self, *args, **kwargs)
553 self._server_path = None
554 self._is_text_file = None
555
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000556 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000557 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000558 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000559 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000560 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000561
562 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000563 if self._is_directory is None:
564 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000565 if os.path.exists(path):
566 # Retrieve directly from the file system; it is much faster than
567 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000568 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000569 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000570 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000571 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000572 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000573
574 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000575 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000576 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000577 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000578 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000579
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000580 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000581 if self._is_text_file is None:
582 if self.Action() == 'D':
583 # A deleted file is not a text file.
584 self._is_text_file = False
585 elif self.IsDirectory():
586 self._is_text_file = False
587 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000588 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
589 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000590 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
591 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000592
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000593 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000594 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
595
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000596
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000597class GitAffectedFile(AffectedFile):
598 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000599 # Method 'NNN' is abstract in class 'NNN' but is not overridden
600 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000601
602 def __init__(self, *args, **kwargs):
603 AffectedFile.__init__(self, *args, **kwargs)
604 self._server_path = None
605 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000606
607 def ServerPath(self):
608 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000609 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000610 return self._server_path
611
612 def IsDirectory(self):
613 if self._is_directory is None:
614 path = self.AbsoluteLocalPath()
615 if os.path.exists(path):
616 # Retrieve directly from the file system; it is much faster than
617 # querying subversion, especially on Windows.
618 self._is_directory = os.path.isdir(path)
619 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000620 self._is_directory = False
621 return self._is_directory
622
623 def Property(self, property_name):
624 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000625 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000626 return self._properties[property_name]
627
628 def IsTextFile(self):
629 if self._is_text_file is None:
630 if self.Action() == 'D':
631 # A deleted file is not a text file.
632 self._is_text_file = False
633 elif self.IsDirectory():
634 self._is_text_file = False
635 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000636 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
637 return self._is_text_file
638
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000639 def GenerateScmDiff(self):
640 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000641
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000642
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000643class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000644 """Describe a change.
645
646 Used directly by the presubmit scripts to query the current change being
647 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000648
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000649 Instance members:
650 tags: Dictionnary of KEY=VALUE pairs found in the change description.
651 self.KEY: equivalent to tags['KEY']
652 """
653
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000654 _AFFECTED_FILES = AffectedFile
655
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000656 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000657 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000658 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000659 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000660
maruel@chromium.org58407af2011-04-12 23:15:57 +0000661 def __init__(
662 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000663 if files is None:
664 files = []
665 self._name = name
666 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000667 # Convert root into an absolute path.
668 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000669 self.issue = issue
670 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000671 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672
673 # From the description text, build up a dictionary of key/value pairs
674 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000675 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000677 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000678 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679 if m:
680 self.tags[m.group('key')] = m.group('value')
681 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000682 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683
684 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000685 self._description_without_tags = (
686 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000688 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000689 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
690 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000691 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000692
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000693 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000695 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000696
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697 def DescriptionText(self):
698 """Returns the user-entered changelist description, minus tags.
699
700 Any line in the user-provided description starting with e.g. "FOO="
701 (whitespace permitted before and around) is considered a tag line. Such
702 lines are stripped out of the description this function returns.
703 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000704 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000705
706 def FullDescriptionText(self):
707 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000708 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709
710 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000711 """Returns the repository (checkout) root directory for this change,
712 as an absolute path.
713 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000714 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000715
716 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000717 """Return tags directly as attributes on the object."""
718 if not re.match(r"^[A-Z_]*$", attr):
719 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000720 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000721
sail@chromium.org5538e022011-05-12 17:53:16 +0000722 def AffectedFiles(self, include_dirs=False, include_deletes=True,
723 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000724 """Returns a list of AffectedFile instances for all files in the change.
725
726 Args:
727 include_deletes: If false, deleted files will be filtered out.
728 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000729 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000730
731 Returns:
732 [AffectedFile(path, action), AffectedFile(path, action)]
733 """
734 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000735 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000737 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000738
sail@chromium.org5538e022011-05-12 17:53:16 +0000739 affected = filter(file_filter, affected)
740
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741 if include_deletes:
742 return affected
743 else:
744 return filter(lambda x: x.Action() != 'D', affected)
745
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000746 def AffectedTextFiles(self, include_deletes=None):
747 """Return a list of the existing text files in a change."""
748 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000749 warn("AffectedTextFiles(include_deletes=%s)"
750 " is deprecated and ignored" % str(include_deletes),
751 category=DeprecationWarning,
752 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000753 return filter(lambda x: x.IsTextFile(),
754 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000755
756 def LocalPaths(self, include_dirs=False):
757 """Convenience function."""
758 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
759
760 def AbsoluteLocalPaths(self, include_dirs=False):
761 """Convenience function."""
762 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
763
764 def ServerPaths(self, include_dirs=False):
765 """Convenience function."""
766 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
767
768 def RightHandSideLines(self):
769 """An iterator over all text lines in "new" version of changed files.
770
771 Lists lines from new or modified text files in the change.
772
773 This is useful for doing line-by-line regex checks, like checking for
774 trailing whitespace.
775
776 Yields:
777 a 3 tuple:
778 the AffectedFile instance of the current file;
779 integer line number (1-based); and
780 the contents of the line as a string.
781 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000782 return _RightHandSideLinesImpl(
783 x for x in self.AffectedFiles(include_deletes=False)
784 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000785
786
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000787class SvnChange(Change):
788 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000789 scm = 'svn'
790 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000791
792 def _GetChangeLists(self):
793 """Get all change lists."""
794 if self._changelists == None:
795 previous_cwd = os.getcwd()
796 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000797 # Need to import here to avoid circular dependency.
798 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000799 self._changelists = gcl.GetModifiedFiles()
800 os.chdir(previous_cwd)
801 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000802
803 def GetAllModifiedFiles(self):
804 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000805 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000806 all_modified_files = []
807 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000808 all_modified_files.extend(
809 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000810 return all_modified_files
811
812 def GetModifiedFiles(self):
813 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000814 changelists = self._GetChangeLists()
815 return [os.path.join(self.RepositoryRoot(), f[1])
816 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000817
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000818
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000819class GitChange(Change):
820 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000821 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000822
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000823
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000824def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825 """Finds all presubmit files that apply to a given set of source files.
826
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000827 If inherit-review-settings-ok is present right under root, looks for
828 PRESUBMIT.py in directories enclosing root.
829
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 Args:
831 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000832 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000833
834 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000835 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000837 files = [normpath(os.path.join(root, f)) for f in files]
838
839 # List all the individual directories containing files.
840 directories = set([os.path.dirname(f) for f in files])
841
842 # Ignore root if inherit-review-settings-ok is present.
843 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
844 root = None
845
846 # Collect all unique directories that may contain PRESUBMIT.py.
847 candidates = set()
848 for directory in directories:
849 while True:
850 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000851 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000852 candidates.add(directory)
853 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000854 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000855 parent_dir = os.path.dirname(directory)
856 if parent_dir == directory:
857 # We hit the system root directory.
858 break
859 directory = parent_dir
860
861 # Look for PRESUBMIT.py in all candidate directories.
862 results = []
863 for directory in sorted(list(candidates)):
864 p = os.path.join(directory, 'PRESUBMIT.py')
865 if os.path.isfile(p):
866 results.append(p)
867
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000868 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000869 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000870
871
thestig@chromium.orgde243452009-10-06 21:02:56 +0000872class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000873 @staticmethod
bradnelson@google.com78230022011-05-24 18:55:19 +0000874 def ExecPresubmitScript(script_text, presubmit_path, project):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000875 """Executes GetPreferredTrySlaves() from a single presubmit script.
876
877 Args:
878 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000879 presubmit_path: Project script to run.
880 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000881
882 Return:
883 A list of try slaves.
884 """
885 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000886 try:
887 exec script_text in context
888 except Exception, e:
889 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000890
891 function_name = 'GetPreferredTrySlaves'
892 if function_name in context:
bradnelson@google.com78230022011-05-24 18:55:19 +0000893 try:
894 result = eval(function_name + '(' + repr(project) + ')', context)
895 except TypeError:
896 result = eval(function_name + '()', context)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000897 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000898 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000899 'Presubmit functions must return a list, got a %s instead: %s' %
900 (type(result), str(result)))
901 for item in result:
902 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000903 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000904 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000905 raise PresubmitFailure(
906 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000907 else:
908 result = []
909 return result
910
911
912def DoGetTrySlaves(changed_files,
913 repository_root,
914 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000915 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000916 verbose,
917 output_stream):
918 """Get the list of try servers from the presubmit scripts.
919
920 Args:
921 changed_files: List of modified files.
922 repository_root: The repository root.
923 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000924 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000925 verbose: Prints debug info.
926 output_stream: A stream to write debug output to.
927
928 Return:
929 List of try slaves
930 """
931 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
932 if not presubmit_files and verbose:
933 output_stream.write("Warning, no presubmit.py found.\n")
934 results = []
935 executer = GetTrySlavesExecuter()
936 if default_presubmit:
937 if verbose:
938 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000939 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000940 results += executer.ExecPresubmitScript(
941 default_presubmit, fake_path, project)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000942 for filename in presubmit_files:
943 filename = os.path.abspath(filename)
944 if verbose:
945 output_stream.write("Running %s\n" % filename)
946 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000947 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000948 results += executer.ExecPresubmitScript(
949 presubmit_script, filename, project)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000950
951 slaves = list(set(results))
952 if slaves and verbose:
953 output_stream.write(', '.join(slaves))
954 output_stream.write('\n')
955 return slaves
956
957
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000958class PresubmitExecuter(object):
maruel@chromium.org239f4112011-06-03 20:08:23 +0000959 def __init__(self, change, committing, tbr, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000960 """
961 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000962 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000963 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000964 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
maruel@chromium.org239f4112011-06-03 20:08:23 +0000965 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000966 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000967 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000968 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000969 self.tbr = tbr
maruel@chromium.org239f4112011-06-03 20:08:23 +0000970 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000971 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000972
973 def ExecPresubmitScript(self, script_text, presubmit_path):
974 """Executes a single presubmit script.
975
976 Args:
977 script_text: The text of the presubmit script.
978 presubmit_path: The path to the presubmit file (this will be reported via
979 input_api.PresubmitLocalPath()).
980
981 Return:
982 A list of result objects, empty if no problems.
983 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000984
985 # Change to the presubmit file's directory to support local imports.
986 main_path = os.getcwd()
987 os.chdir(os.path.dirname(presubmit_path))
988
989 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000990 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000991 self.tbr, self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000993 try:
994 exec script_text in context
995 except Exception, e:
996 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000997
998 # These function names must change if we make substantial changes to
999 # the presubmit API that are not backwards compatible.
1000 if self.committing:
1001 function_name = 'CheckChangeOnCommit'
1002 else:
1003 function_name = 'CheckChangeOnUpload'
1004 if function_name in context:
1005 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001006 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001008 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001009 if not (isinstance(result, types.TupleType) or
1010 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001011 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001012 'Presubmit functions must return a tuple or list')
1013 for item in result:
1014 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001015 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001016 'All presubmit results must be of types derived from '
1017 'output_api.PresubmitResult')
1018 else:
1019 result = () # no error since the script doesn't care about current event.
1020
chase@chromium.org8e416c82009-10-06 04:30:44 +00001021 # Return the process to the original working directory.
1022 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001023 return result
1024
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001025
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001026def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001027 committing,
1028 verbose,
1029 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001030 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001031 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001032 may_prompt,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001033 tbr,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001034 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035 """Runs all presubmit checks that apply to the files in the change.
1036
1037 This finds all PRESUBMIT.py files in directories enclosing the files in the
1038 change (up to the repository root) and calls the relevant entrypoint function
1039 depending on whether the change is being committed or uploaded.
1040
1041 Prints errors, warnings and notifications. Prompts the user for warnings
1042 when needed.
1043
1044 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001045 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001046 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1047 verbose: Prints debug info.
1048 output_stream: A stream to write output from presubmit tests to.
1049 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001050 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001051 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001052 tbr: was --tbr specified to skip any reviewer/owner checks?
maruel@chromium.org239f4112011-06-03 20:08:23 +00001053 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001054
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001055 Warning:
1056 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1057 SHOULD be sys.stdin.
1058
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001059 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001060 A PresubmitOutput object. Use output.should_continue() to figure out
1061 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001062 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001063 old_environ = os.environ
1064 try:
1065 # Make sure python subprocesses won't generate .pyc files.
1066 os.environ = os.environ.copy()
1067 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001068
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001069 output = PresubmitOutput(input_stream, output_stream)
1070 if committing:
1071 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001072 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001073 output.write("Running presubmit upload checks ...\n")
1074 start_time = time.time()
1075 presubmit_files = ListRelevantPresubmitFiles(
1076 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1077 if not presubmit_files and verbose:
1078 output.write("Warning, no presubmit.py found.\n")
1079 results = []
maruel@chromium.org239f4112011-06-03 20:08:23 +00001080 executer = PresubmitExecuter(change, committing, tbr, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001081 if default_presubmit:
1082 if verbose:
1083 output.write("Running default presubmit script.\n")
1084 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1085 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1086 for filename in presubmit_files:
1087 filename = os.path.abspath(filename)
1088 if verbose:
1089 output.write("Running %s\n" % filename)
1090 # Accept CRLF presubmit script.
1091 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1092 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001093
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001094 errors = []
1095 notifications = []
1096 warnings = []
1097 for result in results:
1098 if result.fatal:
1099 errors.append(result)
1100 elif result.should_prompt:
1101 warnings.append(result)
1102 else:
1103 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001104
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001105 output.write('\n')
1106 for name, items in (('Messages', notifications),
1107 ('Warnings', warnings),
1108 ('ERRORS', errors)):
1109 if items:
1110 output.write('** Presubmit %s **\n' % name)
1111 for item in items:
1112 item.handle(output)
1113 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001114
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001115 total_time = time.time() - start_time
1116 if total_time > 1.0:
1117 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001118
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001119 if not errors:
1120 if not warnings:
1121 output.write('Presubmit checks passed.\n')
1122 elif may_prompt:
1123 output.prompt_yes_no('There were presubmit warnings. '
1124 'Are you sure you wish to continue? (y/N): ')
1125 else:
1126 output.fail()
1127
1128 global _ASKED_FOR_FEEDBACK
1129 # Ask for feedback one time out of 5.
1130 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1131 output.write("Was the presubmit check useful? Please send feedback "
1132 "& hate mail to maruel@chromium.org!\n")
1133 _ASKED_FOR_FEEDBACK = True
1134 return output
1135 finally:
1136 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001137
1138
1139def ScanSubDirs(mask, recursive):
1140 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001141 return [x for x in glob.glob(mask) if '.svn' not in x and '.git' not in x]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001142 else:
1143 results = []
1144 for root, dirs, files in os.walk('.'):
1145 if '.svn' in dirs:
1146 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001147 if '.git' in dirs:
1148 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001149 for name in files:
1150 if fnmatch.fnmatch(name, mask):
1151 results.append(os.path.join(root, name))
1152 return results
1153
1154
1155def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001156 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001157 files = []
1158 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001159 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001160 return files
1161
1162
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001163def load_files(options, args):
1164 """Tries to determine the SCM."""
1165 change_scm = scm.determine_scm(options.root)
1166 files = []
1167 if change_scm == 'svn':
1168 change_class = SvnChange
1169 status_fn = scm.SVN.CaptureStatus
1170 elif change_scm == 'git':
1171 change_class = GitChange
1172 status_fn = scm.GIT.CaptureStatus
1173 else:
1174 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1175 if not args:
1176 return None, None
1177 change_class = Change
1178 if args:
1179 files = ParseFiles(args, options.recursive)
1180 else:
1181 # Grab modified files.
1182 files = status_fn([options.root])
1183 return change_class, files
1184
1185
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001186def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001187 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001188 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001189 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001190 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001191 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1192 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001193 parser.add_option("-r", "--recursive", action="store_true",
1194 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001195 parser.add_option("-v", "--verbose", action="count", default=0,
1196 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001197 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001198 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001199 parser.add_option("--description", default='')
1200 parser.add_option("--issue", type='int', default=0)
1201 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001202 parser.add_option("--root", default=os.getcwd(),
1203 help="Search for PRESUBMIT.py up to this directory. "
1204 "If inherit-review-settings-ok is present in this "
1205 "directory, parent directories up to the root file "
1206 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001207 parser.add_option("--default_presubmit")
1208 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001209 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1210 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1211 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001212 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001213 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001214 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001215 elif options.verbose:
1216 logging.basicConfig(level=logging.INFO)
1217 else:
1218 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001219 change_class, files = load_files(options, args)
1220 if not change_class:
1221 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001222 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001223 rietveld_obj = None
1224 if options.rietveld_url:
1225 rietveld_obj = rietveld.Rietveld(
1226 options.rietveld_url,
1227 options.rietveld_email,
1228 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001229 try:
1230 results = DoPresubmitChecks(
1231 change_class(options.name,
1232 options.description,
1233 options.root,
1234 files,
1235 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001236 options.patchset,
1237 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001238 options.commit,
1239 options.verbose,
1240 sys.stdout,
1241 sys.stdin,
1242 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001243 options.may_prompt,
1244 False,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001245 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001246 return not results.should_continue()
1247 except PresubmitFailure, e:
1248 print >> sys.stderr, e
1249 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1250 print >> sys.stderr, 'If all fails, contact maruel@'
1251 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001252
1253
1254if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001255 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001256 sys.exit(Main(None))