blob: 074403398114a5165e14df76d20844702b280164 [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.org5aeb7dd2009-11-17 18:09:01 +000050import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000051import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052
53
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000054# Ask for feedback only once in program lifetime.
55_ASKED_FOR_FEEDBACK = False
56
57
maruel@chromium.org899e1c12011-04-07 17:03:18 +000058class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059 pass
60
61
62def normpath(path):
63 '''Version of os.path.normpath that also changes backward slashes to
64 forward slashes when not running on Windows.
65 '''
66 # This is safe to always do because the Windows version of os.path.normpath
67 # will replace forward slashes with backward slashes.
68 path = path.replace(os.sep, '/')
69 return os.path.normpath(path)
70
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000071
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000072def _RightHandSideLinesImpl(affected_files):
73 """Implements RightHandSideLines for InputApi and GclChange."""
74 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000075 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000076 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000077 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078
79
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000080class PresubmitOutput(object):
81 def __init__(self, input_stream=None, output_stream=None):
82 self.input_stream = input_stream
83 self.output_stream = output_stream
84 self.reviewers = []
85 self.written_output = []
86 self.error_count = 0
87
88 def prompt_yes_no(self, prompt_string):
89 self.write(prompt_string)
90 if self.input_stream:
91 response = self.input_stream.readline().strip().lower()
92 if response not in ('y', 'yes'):
93 self.fail()
94 else:
95 self.fail()
96
97 def fail(self):
98 self.error_count += 1
99
100 def should_continue(self):
101 return not self.error_count
102
103 def write(self, s):
104 self.written_output.append(s)
105 if self.output_stream:
106 self.output_stream.write(s)
107
108 def getvalue(self):
109 return ''.join(self.written_output)
110
111
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000112class OutputApi(object):
113 """This class (more like a module) gets passed to presubmit scripts so that
114 they can specify various types of results.
115 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000116 class PresubmitResult(object):
117 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000118 fatal = False
119 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000120
121 def __init__(self, message, items=None, long_text=''):
122 """
123 message: A short one-line message to indicate errors.
124 items: A list of short strings to indicate where errors occurred.
125 long_text: multi-line text output, e.g. from another tool
126 """
127 self._message = message
128 self._items = []
129 if items:
130 self._items = items
131 self._long_text = long_text.rstrip()
132
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000133 def handle(self, output):
134 output.write(self._message)
135 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000136 for index, item in enumerate(self._items):
137 output.write(' ')
138 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000139 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000140 if index < len(self._items) - 1:
141 output.write(' \\')
142 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000143 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000144 output.write('\n***************\n')
145 # Write separately in case it's unicode.
146 output.write(self._long_text)
147 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000148 if self.fatal:
149 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000150
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000151 class PresubmitAddReviewers(PresubmitResult):
152 """Add some suggested reviewers to the change."""
153 def __init__(self, reviewers):
154 super(OutputApi.PresubmitAddReviewers, self).__init__('')
155 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000156
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000157 def handle(self, output):
158 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000159
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000160 class PresubmitError(PresubmitResult):
161 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000162 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000163
164 class PresubmitPromptWarning(PresubmitResult):
165 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000166 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000167
168 class PresubmitNotifyResult(PresubmitResult):
169 """Just print something to the screen -- but it's not even a warning."""
170 pass
171
172 class MailTextResult(PresubmitResult):
173 """A warning that should be included in the review request email."""
174 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000175 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000176 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000177
178
179class InputApi(object):
180 """An instance of this object is passed to presubmit scripts so they can
181 know stuff about the change they're looking at.
182 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000183 # Method could be a function
184 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000185
maruel@chromium.org3410d912009-06-09 20:56:16 +0000186 # File extensions that are considered source files from a style guide
187 # perspective. Don't modify this list from a presubmit script!
188 DEFAULT_WHITE_LIST = (
189 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000190 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
191 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000192 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000193 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000194 # No extension at all, note that ALL CAPS files are black listed in
195 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000196 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000197 # Other
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000198 r".+\.java$", r".+\.mk$", r".+\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000199 )
200
201 # Path regexp that should be excluded from being considered containing source
202 # files. Don't modify this list from a presubmit script!
203 DEFAULT_BLACK_LIST = (
204 r".*\bexperimental[\\\/].*",
205 r".*\bthird_party[\\\/].*",
206 # Output directories (just in case)
207 r".*\bDebug[\\\/].*",
208 r".*\bRelease[\\\/].*",
209 r".*\bxcodebuild[\\\/].*",
210 r".*\bsconsbuild[\\\/].*",
211 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000212 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000213 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000214 r"(|.*[\\\/])\.git[\\\/].*",
215 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000216 )
217
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000218 def __init__(self, change, presubmit_path, is_committing, tbr,
219 rietveld, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000220 """Builds an InputApi object.
221
222 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000223 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000224 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000225 is_committing: True if the change is about to be committed.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000226 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000227 rietveld: rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000229 # Version number of the presubmit_support script.
230 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000231 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000232 self.is_committing = is_committing
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000233 self.tbr = tbr
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000234 self.rietveld = rietveld
235 # TBD
236 self.host_url = 'http://codereview.chromium.org'
237 if self.rietveld:
238 self.host_url = rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000239
240 # We expose various modules and functions as attributes of the input_api
241 # so that presubmit scripts don't have to import them.
242 self.basename = os.path.basename
243 self.cPickle = cPickle
244 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000245 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000246 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000247 self.os_listdir = os.listdir
248 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000249 self.os_path = os.path
250 self.pickle = pickle
251 self.marshal = marshal
252 self.re = re
253 self.subprocess = subprocess
254 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000255 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000256 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000257 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000258 self.urllib2 = urllib2
259
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000260 # To easily fork python.
261 self.python_executable = sys.executable
262 self.environ = os.environ
263
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000264 # InputApi.platform is the platform you're currently running on.
265 self.platform = sys.platform
266
267 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000268 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269
270 # We carry the canned checks so presubmit scripts can easily use them.
271 self.canned_checks = presubmit_canned_checks
272
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000273 # TODO(dpranke): figure out a list of all approved owners for a repo
274 # in order to be able to handle wildcard OWNERS files?
275 self.owners_db = owners.Database(change.RepositoryRoot(),
276 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000277 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000278
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000279 def PresubmitLocalPath(self):
280 """Returns the local path of the presubmit script currently being run.
281
282 This is useful if you don't want to hard-code absolute paths in the
283 presubmit script. For example, It can be used to find another file
284 relative to the PRESUBMIT.py script, so the whole tree can be branched and
285 the presubmit script still works, without editing its content.
286 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000287 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000288
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000289 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000290 """Translate a depot path to a local path (relative to client root).
291
292 Args:
293 Depot path as a string.
294
295 Returns:
296 The local path of the depot path under the user's current client, or None
297 if the file is not mapped.
298
299 Remember to check for the None case and show an appropriate error!
300 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000301 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000302 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000303 return local_path
304
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000305 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000306 """Translate a local path to a depot path.
307
308 Args:
309 Local path (relative to current directory, or absolute) as a string.
310
311 Returns:
312 The depot path (SVN URL) of the file if mapped, otherwise None.
313 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000314 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000315 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000316 return depot_path
317
sail@chromium.org5538e022011-05-12 17:53:16 +0000318 def AffectedFiles(self, include_dirs=False, include_deletes=True,
319 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000320 """Same as input_api.change.AffectedFiles() except only lists files
321 (and optionally directories) in the same directory as the current presubmit
322 script, or subdirectories thereof.
323 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000324 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000325 if len(dir_with_slash) == 1:
326 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000327
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000328 return filter(
329 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000330 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000331
332 def LocalPaths(self, include_dirs=False):
333 """Returns local paths of input_api.AffectedFiles()."""
334 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
335
336 def AbsoluteLocalPaths(self, include_dirs=False):
337 """Returns absolute local paths of input_api.AffectedFiles()."""
338 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
339
340 def ServerPaths(self, include_dirs=False):
341 """Returns server paths of input_api.AffectedFiles()."""
342 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
343
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000344 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345 """Same as input_api.change.AffectedTextFiles() except only lists files
346 in the same directory as the current presubmit script, or subdirectories
347 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000348 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000349 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000350 warn("AffectedTextFiles(include_deletes=%s)"
351 " is deprecated and ignored" % str(include_deletes),
352 category=DeprecationWarning,
353 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000354 return filter(lambda x: x.IsTextFile(),
355 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000356
maruel@chromium.org3410d912009-06-09 20:56:16 +0000357 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
358 """Filters out files that aren't considered "source file".
359
360 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
361 and InputApi.DEFAULT_BLACK_LIST is used respectively.
362
363 The lists will be compiled as regular expression and
364 AffectedFile.LocalPath() needs to pass both list.
365
366 Note: Copy-paste this function to suit your needs or use a lambda function.
367 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000368 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000369 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000370 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000371 if self.re.match(item, local_path):
372 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000373 return True
374 return False
375 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
376 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
377
378 def AffectedSourceFiles(self, source_file):
379 """Filter the list of AffectedTextFiles by the function source_file.
380
381 If source_file is None, InputApi.FilterSourceFile() is used.
382 """
383 if not source_file:
384 source_file = self.FilterSourceFile
385 return filter(source_file, self.AffectedTextFiles())
386
387 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000388 """An iterator over all text lines in "new" version of changed files.
389
390 Only lists lines from new or modified text files in the change that are
391 contained by the directory of the currently executing presubmit script.
392
393 This is useful for doing line-by-line regex checks, like checking for
394 trailing whitespace.
395
396 Yields:
397 a 3 tuple:
398 the AffectedFile instance of the current file;
399 integer line number (1-based); and
400 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000401
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000402 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000403 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000404 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000405 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000406
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000407 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000408 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000409
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000410 Deny reading anything outside the repository.
411 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000412 if isinstance(file_item, AffectedFile):
413 file_item = file_item.AbsoluteLocalPath()
414 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000415 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000416 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000417
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000418
419class AffectedFile(object):
420 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000421 # Method could be a function
422 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000423 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000424 self._path = path
425 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000426 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000427 self._is_directory = None
428 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000429 self._cached_changed_contents = None
430 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000431 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000432
433 def ServerPath(self):
434 """Returns a path string that identifies the file in the SCM system.
435
436 Returns the empty string if the file does not exist in SCM.
437 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000438 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000439
440 def LocalPath(self):
441 """Returns the path of this file on the local disk relative to client root.
442 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000443 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000444
445 def AbsoluteLocalPath(self):
446 """Returns the absolute path of this file on the local disk.
447 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000448 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000449
450 def IsDirectory(self):
451 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000452 if self._is_directory is None:
453 path = self.AbsoluteLocalPath()
454 self._is_directory = (os.path.exists(path) and
455 os.path.isdir(path))
456 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000457
458 def Action(self):
459 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000460 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
461 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000462 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000463
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000464 def Property(self, property_name):
465 """Returns the specified SCM property of this file, or None if no such
466 property.
467 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000468 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000469
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000470 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000471 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000472
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000473 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000474 raise NotImplementedError() # Implement when needed
475
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000476 def NewContents(self):
477 """Returns an iterator over the lines in the new version of file.
478
479 The new version is the file in the user's workspace, i.e. the "right hand
480 side".
481
482 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000483 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000485 if self._cached_new_contents is None:
486 self._cached_new_contents = []
487 if not self.IsDirectory():
488 try:
489 self._cached_new_contents = gclient_utils.FileRead(
490 self.AbsoluteLocalPath(), 'rU').splitlines()
491 except IOError:
492 pass # File not found? That's fine; maybe it was deleted.
493 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000494
495 def OldContents(self):
496 """Returns an iterator over the lines in the old version of file.
497
498 The old version is the file in depot, i.e. the "left hand side".
499 """
500 raise NotImplementedError() # Implement when needed
501
502 def OldFileTempPath(self):
503 """Returns the path on local disk where the old contents resides.
504
505 The old version is the file in depot, i.e. the "left hand side".
506 This is a read-only cached copy of the old contents. *DO NOT* try to
507 modify this file.
508 """
509 raise NotImplementedError() # Implement if/when needed.
510
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000511 def ChangedContents(self):
512 """Returns a list of tuples (line number, line text) of all new lines.
513
514 This relies on the scm diff output describing each changed code section
515 with a line of the form
516
517 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
518 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000519 if self._cached_changed_contents is not None:
520 return self._cached_changed_contents[:]
521 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000522 line_num = 0
523
524 if self.IsDirectory():
525 return []
526
527 for line in self.GenerateScmDiff().splitlines():
528 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
529 if m:
530 line_num = int(m.groups(1)[0])
531 continue
532 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000533 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000534 if not line.startswith('-'):
535 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000536 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000537
maruel@chromium.org5de13972009-06-10 18:16:06 +0000538 def __str__(self):
539 return self.LocalPath()
540
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000541 def GenerateScmDiff(self):
542 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000543
maruel@chromium.org58407af2011-04-12 23:15:57 +0000544
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000545class SvnAffectedFile(AffectedFile):
546 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000547 # Method 'NNN' is abstract in class 'NNN' but is not overridden
548 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000549
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000550 def __init__(self, *args, **kwargs):
551 AffectedFile.__init__(self, *args, **kwargs)
552 self._server_path = None
553 self._is_text_file = None
554
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000555 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000556 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000557 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000558 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000559 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000560
561 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000562 if self._is_directory is None:
563 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000564 if os.path.exists(path):
565 # Retrieve directly from the file system; it is much faster than
566 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000567 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000568 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000569 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000570 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000571 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000572
573 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000574 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000575 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000576 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000577 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000578
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000579 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000580 if self._is_text_file is None:
581 if self.Action() == 'D':
582 # A deleted file is not a text file.
583 self._is_text_file = False
584 elif self.IsDirectory():
585 self._is_text_file = False
586 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000587 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
588 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000589 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
590 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000591
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000592 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000593 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
594
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000595
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000596class GitAffectedFile(AffectedFile):
597 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000598 # Method 'NNN' is abstract in class 'NNN' but is not overridden
599 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000600
601 def __init__(self, *args, **kwargs):
602 AffectedFile.__init__(self, *args, **kwargs)
603 self._server_path = None
604 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000605
606 def ServerPath(self):
607 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000608 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000609 return self._server_path
610
611 def IsDirectory(self):
612 if self._is_directory is None:
613 path = self.AbsoluteLocalPath()
614 if os.path.exists(path):
615 # Retrieve directly from the file system; it is much faster than
616 # querying subversion, especially on Windows.
617 self._is_directory = os.path.isdir(path)
618 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000619 self._is_directory = False
620 return self._is_directory
621
622 def Property(self, property_name):
623 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000624 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000625 return self._properties[property_name]
626
627 def IsTextFile(self):
628 if self._is_text_file is None:
629 if self.Action() == 'D':
630 # A deleted file is not a text file.
631 self._is_text_file = False
632 elif self.IsDirectory():
633 self._is_text_file = False
634 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000635 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
636 return self._is_text_file
637
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000638 def GenerateScmDiff(self):
639 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000640
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000641
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000642class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000643 """Describe a change.
644
645 Used directly by the presubmit scripts to query the current change being
646 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000647
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000648 Instance members:
649 tags: Dictionnary of KEY=VALUE pairs found in the change description.
650 self.KEY: equivalent to tags['KEY']
651 """
652
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000653 _AFFECTED_FILES = AffectedFile
654
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000655 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000656 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000657 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000658 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000659
maruel@chromium.org58407af2011-04-12 23:15:57 +0000660 def __init__(
661 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000662 if files is None:
663 files = []
664 self._name = name
665 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000666 # Convert root into an absolute path.
667 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000668 self.issue = issue
669 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000670 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000671
672 # From the description text, build up a dictionary of key/value pairs
673 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000674 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000675 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000676 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000677 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678 if m:
679 self.tags[m.group('key')] = m.group('value')
680 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000681 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682
683 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000684 self._description_without_tags = (
685 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000686
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000687 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000688 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
689 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000690 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000691
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000692 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000694 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000695
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000696 def DescriptionText(self):
697 """Returns the user-entered changelist description, minus tags.
698
699 Any line in the user-provided description starting with e.g. "FOO="
700 (whitespace permitted before and around) is considered a tag line. Such
701 lines are stripped out of the description this function returns.
702 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000703 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704
705 def FullDescriptionText(self):
706 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000707 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000708
709 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000710 """Returns the repository (checkout) root directory for this change,
711 as an absolute path.
712 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000713 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714
715 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000716 """Return tags directly as attributes on the object."""
717 if not re.match(r"^[A-Z_]*$", attr):
718 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000719 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000720
sail@chromium.org5538e022011-05-12 17:53:16 +0000721 def AffectedFiles(self, include_dirs=False, include_deletes=True,
722 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000723 """Returns a list of AffectedFile instances for all files in the change.
724
725 Args:
726 include_deletes: If false, deleted files will be filtered out.
727 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000728 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729
730 Returns:
731 [AffectedFile(path, action), AffectedFile(path, action)]
732 """
733 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000734 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000736 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000737
sail@chromium.org5538e022011-05-12 17:53:16 +0000738 affected = filter(file_filter, affected)
739
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000740 if include_deletes:
741 return affected
742 else:
743 return filter(lambda x: x.Action() != 'D', affected)
744
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000745 def AffectedTextFiles(self, include_deletes=None):
746 """Return a list of the existing text files in a change."""
747 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000748 warn("AffectedTextFiles(include_deletes=%s)"
749 " is deprecated and ignored" % str(include_deletes),
750 category=DeprecationWarning,
751 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000752 return filter(lambda x: x.IsTextFile(),
753 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000754
755 def LocalPaths(self, include_dirs=False):
756 """Convenience function."""
757 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
758
759 def AbsoluteLocalPaths(self, include_dirs=False):
760 """Convenience function."""
761 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
762
763 def ServerPaths(self, include_dirs=False):
764 """Convenience function."""
765 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
766
767 def RightHandSideLines(self):
768 """An iterator over all text lines in "new" version of changed files.
769
770 Lists lines from new or modified text files in the change.
771
772 This is useful for doing line-by-line regex checks, like checking for
773 trailing whitespace.
774
775 Yields:
776 a 3 tuple:
777 the AffectedFile instance of the current file;
778 integer line number (1-based); and
779 the contents of the line as a string.
780 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000781 return _RightHandSideLinesImpl(
782 x for x in self.AffectedFiles(include_deletes=False)
783 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000784
785
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000786class SvnChange(Change):
787 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000788 scm = 'svn'
789 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000790
791 def _GetChangeLists(self):
792 """Get all change lists."""
793 if self._changelists == None:
794 previous_cwd = os.getcwd()
795 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000796 # Need to import here to avoid circular dependency.
797 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000798 self._changelists = gcl.GetModifiedFiles()
799 os.chdir(previous_cwd)
800 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000801
802 def GetAllModifiedFiles(self):
803 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000804 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000805 all_modified_files = []
806 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000807 all_modified_files.extend(
808 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000809 return all_modified_files
810
811 def GetModifiedFiles(self):
812 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000813 changelists = self._GetChangeLists()
814 return [os.path.join(self.RepositoryRoot(), f[1])
815 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000816
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000817
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000818class GitChange(Change):
819 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000820 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000821
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000822
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000823def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000824 """Finds all presubmit files that apply to a given set of source files.
825
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000826 If inherit-review-settings-ok is present right under root, looks for
827 PRESUBMIT.py in directories enclosing root.
828
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000829 Args:
830 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000831 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000832
833 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000834 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000835 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000836 files = [normpath(os.path.join(root, f)) for f in files]
837
838 # List all the individual directories containing files.
839 directories = set([os.path.dirname(f) for f in files])
840
841 # Ignore root if inherit-review-settings-ok is present.
842 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
843 root = None
844
845 # Collect all unique directories that may contain PRESUBMIT.py.
846 candidates = set()
847 for directory in directories:
848 while True:
849 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000850 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000851 candidates.add(directory)
852 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000853 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000854 parent_dir = os.path.dirname(directory)
855 if parent_dir == directory:
856 # We hit the system root directory.
857 break
858 directory = parent_dir
859
860 # Look for PRESUBMIT.py in all candidate directories.
861 results = []
862 for directory in sorted(list(candidates)):
863 p = os.path.join(directory, 'PRESUBMIT.py')
864 if os.path.isfile(p):
865 results.append(p)
866
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000867 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000868 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869
870
thestig@chromium.orgde243452009-10-06 21:02:56 +0000871class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000872 @staticmethod
bradnelson@google.com78230022011-05-24 18:55:19 +0000873 def ExecPresubmitScript(script_text, presubmit_path, project):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000874 """Executes GetPreferredTrySlaves() from a single presubmit script.
875
876 Args:
877 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000878 presubmit_path: Project script to run.
879 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000880
881 Return:
882 A list of try slaves.
883 """
884 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000885 try:
886 exec script_text in context
887 except Exception, e:
888 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000889
890 function_name = 'GetPreferredTrySlaves'
891 if function_name in context:
bradnelson@google.com78230022011-05-24 18:55:19 +0000892 try:
893 result = eval(function_name + '(' + repr(project) + ')', context)
894 except TypeError:
895 result = eval(function_name + '()', context)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000896 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000897 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000898 'Presubmit functions must return a list, got a %s instead: %s' %
899 (type(result), str(result)))
900 for item in result:
901 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000902 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000903 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000904 raise PresubmitFailure(
905 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000906 else:
907 result = []
908 return result
909
910
911def DoGetTrySlaves(changed_files,
912 repository_root,
913 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000914 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000915 verbose,
916 output_stream):
917 """Get the list of try servers from the presubmit scripts.
918
919 Args:
920 changed_files: List of modified files.
921 repository_root: The repository root.
922 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000923 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000924 verbose: Prints debug info.
925 output_stream: A stream to write debug output to.
926
927 Return:
928 List of try slaves
929 """
930 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
931 if not presubmit_files and verbose:
932 output_stream.write("Warning, no presubmit.py found.\n")
933 results = []
934 executer = GetTrySlavesExecuter()
935 if default_presubmit:
936 if verbose:
937 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000938 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000939 results += executer.ExecPresubmitScript(
940 default_presubmit, fake_path, project)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000941 for filename in presubmit_files:
942 filename = os.path.abspath(filename)
943 if verbose:
944 output_stream.write("Running %s\n" % filename)
945 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000946 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000947 results += executer.ExecPresubmitScript(
948 presubmit_script, filename, project)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000949
950 slaves = list(set(results))
951 if slaves and verbose:
952 output_stream.write(', '.join(slaves))
953 output_stream.write('\n')
954 return slaves
955
956
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000957class PresubmitExecuter(object):
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000958 def __init__(self, change, committing, tbr, rietveld, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000959 """
960 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000961 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000962 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000963 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000964 rietveld: rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000965 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000966 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000967 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000968 self.tbr = tbr
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000969 self.rietveld = rietveld
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000970 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000971
972 def ExecPresubmitScript(self, script_text, presubmit_path):
973 """Executes a single presubmit script.
974
975 Args:
976 script_text: The text of the presubmit script.
977 presubmit_path: The path to the presubmit file (this will be reported via
978 input_api.PresubmitLocalPath()).
979
980 Return:
981 A list of result objects, empty if no problems.
982 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000983
984 # Change to the presubmit file's directory to support local imports.
985 main_path = os.getcwd()
986 os.chdir(os.path.dirname(presubmit_path))
987
988 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000989 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000990 self.tbr, self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000991 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000992 try:
993 exec script_text in context
994 except Exception, e:
995 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996
997 # These function names must change if we make substantial changes to
998 # the presubmit API that are not backwards compatible.
999 if self.committing:
1000 function_name = 'CheckChangeOnCommit'
1001 else:
1002 function_name = 'CheckChangeOnUpload'
1003 if function_name in context:
1004 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001005 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001007 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008 if not (isinstance(result, types.TupleType) or
1009 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001010 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001011 'Presubmit functions must return a tuple or list')
1012 for item in result:
1013 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001014 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015 'All presubmit results must be of types derived from '
1016 'output_api.PresubmitResult')
1017 else:
1018 result = () # no error since the script doesn't care about current event.
1019
chase@chromium.org8e416c82009-10-06 04:30:44 +00001020 # Return the process to the original working directory.
1021 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001022 return result
1023
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001024
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001025def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001026 committing,
1027 verbose,
1028 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001029 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001030 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001031 may_prompt,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001032 tbr,
1033 rietveld):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001034 """Runs all presubmit checks that apply to the files in the change.
1035
1036 This finds all PRESUBMIT.py files in directories enclosing the files in the
1037 change (up to the repository root) and calls the relevant entrypoint function
1038 depending on whether the change is being committed or uploaded.
1039
1040 Prints errors, warnings and notifications. Prompts the user for warnings
1041 when needed.
1042
1043 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001044 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001045 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1046 verbose: Prints debug info.
1047 output_stream: A stream to write output from presubmit tests to.
1048 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001049 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001050 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001051 tbr: was --tbr specified to skip any reviewer/owner checks?
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001052 rietveld: rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001053
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001054 Warning:
1055 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1056 SHOULD be sys.stdin.
1057
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001058 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001059 A PresubmitOutput object. Use output.should_continue() to figure out
1060 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001061 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001062 old_environ = os.environ
1063 try:
1064 # Make sure python subprocesses won't generate .pyc files.
1065 os.environ = os.environ.copy()
1066 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001067
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001068 output = PresubmitOutput(input_stream, output_stream)
1069 if committing:
1070 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001071 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001072 output.write("Running presubmit upload checks ...\n")
1073 start_time = time.time()
1074 presubmit_files = ListRelevantPresubmitFiles(
1075 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1076 if not presubmit_files and verbose:
1077 output.write("Warning, no presubmit.py found.\n")
1078 results = []
1079 executer = PresubmitExecuter(change, committing, tbr, rietveld, verbose)
1080 if default_presubmit:
1081 if verbose:
1082 output.write("Running default presubmit script.\n")
1083 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1084 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1085 for filename in presubmit_files:
1086 filename = os.path.abspath(filename)
1087 if verbose:
1088 output.write("Running %s\n" % filename)
1089 # Accept CRLF presubmit script.
1090 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1091 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001092
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001093 errors = []
1094 notifications = []
1095 warnings = []
1096 for result in results:
1097 if result.fatal:
1098 errors.append(result)
1099 elif result.should_prompt:
1100 warnings.append(result)
1101 else:
1102 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001103
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001104 output.write('\n')
1105 for name, items in (('Messages', notifications),
1106 ('Warnings', warnings),
1107 ('ERRORS', errors)):
1108 if items:
1109 output.write('** Presubmit %s **\n' % name)
1110 for item in items:
1111 item.handle(output)
1112 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001113
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001114 total_time = time.time() - start_time
1115 if total_time > 1.0:
1116 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001117
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001118 if not errors:
1119 if not warnings:
1120 output.write('Presubmit checks passed.\n')
1121 elif may_prompt:
1122 output.prompt_yes_no('There were presubmit warnings. '
1123 'Are you sure you wish to continue? (y/N): ')
1124 else:
1125 output.fail()
1126
1127 global _ASKED_FOR_FEEDBACK
1128 # Ask for feedback one time out of 5.
1129 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1130 output.write("Was the presubmit check useful? Please send feedback "
1131 "& hate mail to maruel@chromium.org!\n")
1132 _ASKED_FOR_FEEDBACK = True
1133 return output
1134 finally:
1135 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001136
1137
1138def ScanSubDirs(mask, recursive):
1139 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001140 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 +00001141 else:
1142 results = []
1143 for root, dirs, files in os.walk('.'):
1144 if '.svn' in dirs:
1145 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001146 if '.git' in dirs:
1147 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001148 for name in files:
1149 if fnmatch.fnmatch(name, mask):
1150 results.append(os.path.join(root, name))
1151 return results
1152
1153
1154def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001155 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001156 files = []
1157 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001158 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001159 return files
1160
1161
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001162def load_files(options, args):
1163 """Tries to determine the SCM."""
1164 change_scm = scm.determine_scm(options.root)
1165 files = []
1166 if change_scm == 'svn':
1167 change_class = SvnChange
1168 status_fn = scm.SVN.CaptureStatus
1169 elif change_scm == 'git':
1170 change_class = GitChange
1171 status_fn = scm.GIT.CaptureStatus
1172 else:
1173 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1174 if not args:
1175 return None, None
1176 change_class = Change
1177 if args:
1178 files = ParseFiles(args, options.recursive)
1179 else:
1180 # Grab modified files.
1181 files = status_fn([options.root])
1182 return change_class, files
1183
1184
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001185def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001186 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001187 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001188 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001189 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001190 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1191 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001192 parser.add_option("-r", "--recursive", action="store_true",
1193 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001194 parser.add_option("-v", "--verbose", action="count", default=0,
1195 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001196 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001197 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001198 parser.add_option("--description", default='')
1199 parser.add_option("--issue", type='int', default=0)
1200 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001201 parser.add_option("--root", default=os.getcwd(),
1202 help="Search for PRESUBMIT.py up to this directory. "
1203 "If inherit-review-settings-ok is present in this "
1204 "directory, parent directories up to the root file "
1205 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001206 parser.add_option("--default_presubmit")
1207 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001208 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001209 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001210 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001211 elif options.verbose:
1212 logging.basicConfig(level=logging.INFO)
1213 else:
1214 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001215 change_class, files = load_files(options, args)
1216 if not change_class:
1217 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001218 logging.info('Found %d file(s).' % len(files))
1219 try:
1220 results = DoPresubmitChecks(
1221 change_class(options.name,
1222 options.description,
1223 options.root,
1224 files,
1225 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001226 options.patchset,
1227 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001228 options.commit,
1229 options.verbose,
1230 sys.stdout,
1231 sys.stdin,
1232 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001233 options.may_prompt,
1234 False,
1235 None)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001236 return not results.should_continue()
1237 except PresubmitFailure, e:
1238 print >> sys.stderr, e
1239 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1240 print >> sys.stderr, 'If all fails, contact maruel@'
1241 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001242
1243
1244if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001245 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001246 sys.exit(Main(None))