blob: b1fdcb2fb7f1b527674650983b52a77a79154e56 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
maruel@chromium.org3bbf2942012-01-10 16:52:06 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00009__version__ = '1.6.1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
15import cPickle # Exposed through the API.
16import cStringIO # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000017import fnmatch
18import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000019import inspect
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000020import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000021import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000022import marshal # Exposed through the API.
23import optparse
24import os # Somewhat exposed through the API.
25import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000026import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000028import sys # Parts exposed through API.
29import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000030import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000031import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000033import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000035from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036
37# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000038import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000039import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000040import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000041import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000042import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000043import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000044import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000045
46
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000047# Ask for feedback only once in program lifetime.
48_ASKED_FOR_FEEDBACK = False
49
50
maruel@chromium.org899e1c12011-04-07 17:03:18 +000051class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052 pass
53
54
55def normpath(path):
56 '''Version of os.path.normpath that also changes backward slashes to
57 forward slashes when not running on Windows.
58 '''
59 # This is safe to always do because the Windows version of os.path.normpath
60 # will replace forward slashes with backward slashes.
61 path = path.replace(os.sep, '/')
62 return os.path.normpath(path)
63
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000064
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000065def _RightHandSideLinesImpl(affected_files):
66 """Implements RightHandSideLines for InputApi and GclChange."""
67 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000068 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000069 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000070 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000071
72
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000073class PresubmitOutput(object):
74 def __init__(self, input_stream=None, output_stream=None):
75 self.input_stream = input_stream
76 self.output_stream = output_stream
77 self.reviewers = []
78 self.written_output = []
79 self.error_count = 0
80
81 def prompt_yes_no(self, prompt_string):
82 self.write(prompt_string)
83 if self.input_stream:
84 response = self.input_stream.readline().strip().lower()
85 if response not in ('y', 'yes'):
86 self.fail()
87 else:
88 self.fail()
89
90 def fail(self):
91 self.error_count += 1
92
93 def should_continue(self):
94 return not self.error_count
95
96 def write(self, s):
97 self.written_output.append(s)
98 if self.output_stream:
99 self.output_stream.write(s)
100
101 def getvalue(self):
102 return ''.join(self.written_output)
103
104
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000105class OutputApi(object):
106 """This class (more like a module) gets passed to presubmit scripts so that
107 they can specify various types of results.
108 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000109 class PresubmitResult(object):
110 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000111 fatal = False
112 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000113
114 def __init__(self, message, items=None, long_text=''):
115 """
116 message: A short one-line message to indicate errors.
117 items: A list of short strings to indicate where errors occurred.
118 long_text: multi-line text output, e.g. from another tool
119 """
120 self._message = message
121 self._items = []
122 if items:
123 self._items = items
124 self._long_text = long_text.rstrip()
125
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000126 def handle(self, output):
127 output.write(self._message)
128 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000129 for index, item in enumerate(self._items):
130 output.write(' ')
131 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000132 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000133 if index < len(self._items) - 1:
134 output.write(' \\')
135 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000136 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000137 output.write('\n***************\n')
138 # Write separately in case it's unicode.
139 output.write(self._long_text)
140 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000141 if self.fatal:
142 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000143
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000144 class PresubmitAddReviewers(PresubmitResult):
145 """Add some suggested reviewers to the change."""
146 def __init__(self, reviewers):
147 super(OutputApi.PresubmitAddReviewers, self).__init__('')
148 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000149
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000150 def handle(self, output):
151 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000152
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000153 class PresubmitError(PresubmitResult):
154 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000155 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000156
157 class PresubmitPromptWarning(PresubmitResult):
158 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000159 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000160
161 class PresubmitNotifyResult(PresubmitResult):
162 """Just print something to the screen -- but it's not even a warning."""
163 pass
164
165 class MailTextResult(PresubmitResult):
166 """A warning that should be included in the review request email."""
167 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000168 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000169 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000170
171
172class InputApi(object):
173 """An instance of this object is passed to presubmit scripts so they can
174 know stuff about the change they're looking at.
175 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000176 # Method could be a function
177 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000178
maruel@chromium.org3410d912009-06-09 20:56:16 +0000179 # File extensions that are considered source files from a style guide
180 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000181 #
182 # Files without an extension aren't included in the list. If you want to
183 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
184 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000185 DEFAULT_WHITE_LIST = (
186 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000187 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
188 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000189 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000190 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000191 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000192 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000193 )
194
195 # Path regexp that should be excluded from being considered containing source
196 # files. Don't modify this list from a presubmit script!
197 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000198 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000199 r".*\bexperimental[\\\/].*",
200 r".*\bthird_party[\\\/].*",
201 # Output directories (just in case)
202 r".*\bDebug[\\\/].*",
203 r".*\bRelease[\\\/].*",
204 r".*\bxcodebuild[\\\/].*",
205 r".*\bsconsbuild[\\\/].*",
206 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000207 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000208 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000209 r"(|.*[\\\/])\.git[\\\/].*",
210 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000211 # There is no point in processing a patch file.
212 r".+\.diff$",
213 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000214 )
215
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000216 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000217 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000218 """Builds an InputApi object.
219
220 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000221 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000222 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000223 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000224 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000225 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000226 # Version number of the presubmit_support script.
227 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000229 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000230 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000231 # TBD
232 self.host_url = 'http://codereview.chromium.org'
233 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000234 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000235
236 # We expose various modules and functions as attributes of the input_api
237 # so that presubmit scripts don't have to import them.
238 self.basename = os.path.basename
239 self.cPickle = cPickle
240 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000241 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000242 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000243 self.os_listdir = os.listdir
244 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000245 self.os_path = os.path
246 self.pickle = pickle
247 self.marshal = marshal
248 self.re = re
249 self.subprocess = subprocess
250 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000251 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000252 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000253 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000254 self.urllib2 = urllib2
255
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000256 # To easily fork python.
257 self.python_executable = sys.executable
258 self.environ = os.environ
259
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000260 # InputApi.platform is the platform you're currently running on.
261 self.platform = sys.platform
262
263 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000264 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000265
266 # We carry the canned checks so presubmit scripts can easily use them.
267 self.canned_checks = presubmit_canned_checks
268
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000269 # TODO(dpranke): figure out a list of all approved owners for a repo
270 # in order to be able to handle wildcard OWNERS files?
271 self.owners_db = owners.Database(change.RepositoryRoot(),
272 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000273 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000274
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000275 def PresubmitLocalPath(self):
276 """Returns the local path of the presubmit script currently being run.
277
278 This is useful if you don't want to hard-code absolute paths in the
279 presubmit script. For example, It can be used to find another file
280 relative to the PRESUBMIT.py script, so the whole tree can be branched and
281 the presubmit script still works, without editing its content.
282 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000283 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000284
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000285 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286 """Translate a depot path to a local path (relative to client root).
287
288 Args:
289 Depot path as a string.
290
291 Returns:
292 The local path of the depot path under the user's current client, or None
293 if the file is not mapped.
294
295 Remember to check for the None case and show an appropriate error!
296 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000297 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
298 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000299
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000300 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000301 """Translate a local path to a depot path.
302
303 Args:
304 Local path (relative to current directory, or absolute) as a string.
305
306 Returns:
307 The depot path (SVN URL) of the file if mapped, otherwise None.
308 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000309 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
310 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000311
sail@chromium.org5538e022011-05-12 17:53:16 +0000312 def AffectedFiles(self, include_dirs=False, include_deletes=True,
313 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000314 """Same as input_api.change.AffectedFiles() except only lists files
315 (and optionally directories) in the same directory as the current presubmit
316 script, or subdirectories thereof.
317 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000318 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000319 if len(dir_with_slash) == 1:
320 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000321
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000322 return filter(
323 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000324 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000325
326 def LocalPaths(self, include_dirs=False):
327 """Returns local paths of input_api.AffectedFiles()."""
328 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
329
330 def AbsoluteLocalPaths(self, include_dirs=False):
331 """Returns absolute local paths of input_api.AffectedFiles()."""
332 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
333
334 def ServerPaths(self, include_dirs=False):
335 """Returns server paths of input_api.AffectedFiles()."""
336 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
337
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000338 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000339 """Same as input_api.change.AffectedTextFiles() except only lists files
340 in the same directory as the current presubmit script, or subdirectories
341 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000342 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000343 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000344 warn("AffectedTextFiles(include_deletes=%s)"
345 " is deprecated and ignored" % str(include_deletes),
346 category=DeprecationWarning,
347 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000348 return filter(lambda x: x.IsTextFile(),
349 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350
maruel@chromium.org3410d912009-06-09 20:56:16 +0000351 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
352 """Filters out files that aren't considered "source file".
353
354 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
355 and InputApi.DEFAULT_BLACK_LIST is used respectively.
356
357 The lists will be compiled as regular expression and
358 AffectedFile.LocalPath() needs to pass both list.
359
360 Note: Copy-paste this function to suit your needs or use a lambda function.
361 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000362 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000363 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000364 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000365 if self.re.match(item, local_path):
366 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000367 return True
368 return False
369 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
370 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
371
372 def AffectedSourceFiles(self, source_file):
373 """Filter the list of AffectedTextFiles by the function source_file.
374
375 If source_file is None, InputApi.FilterSourceFile() is used.
376 """
377 if not source_file:
378 source_file = self.FilterSourceFile
379 return filter(source_file, self.AffectedTextFiles())
380
381 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000382 """An iterator over all text lines in "new" version of changed files.
383
384 Only lists lines from new or modified text files in the change that are
385 contained by the directory of the currently executing presubmit script.
386
387 This is useful for doing line-by-line regex checks, like checking for
388 trailing whitespace.
389
390 Yields:
391 a 3 tuple:
392 the AffectedFile instance of the current file;
393 integer line number (1-based); and
394 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000395
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000396 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000397 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000398 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000399 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000400
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000401 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000402 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000403
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000404 Deny reading anything outside the repository.
405 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000406 if isinstance(file_item, AffectedFile):
407 file_item = file_item.AbsoluteLocalPath()
408 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000409 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000410 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000411
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000412 @property
413 def tbr(self):
414 """Returns if a change is TBR'ed."""
415 return 'TBR' in self.change.tags
416
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417
418class AffectedFile(object):
419 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000420 # Method could be a function
421 # pylint: disable=R0201
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000422 def __init__(self, path, action, repository_root):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000423 self._path = path
424 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000425 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000426 self._is_directory = None
427 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000428 self._cached_changed_contents = None
429 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000430 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431
432 def ServerPath(self):
433 """Returns a path string that identifies the file in the SCM system.
434
435 Returns the empty string if the file does not exist in SCM.
436 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000437 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438
439 def LocalPath(self):
440 """Returns the path of this file on the local disk relative to client root.
441 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000442 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000443
444 def AbsoluteLocalPath(self):
445 """Returns the absolute path of this file on the local disk.
446 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000447 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448
449 def IsDirectory(self):
450 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000451 if self._is_directory is None:
452 path = self.AbsoluteLocalPath()
453 self._is_directory = (os.path.exists(path) and
454 os.path.isdir(path))
455 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000456
457 def Action(self):
458 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000459 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
460 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000461 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000462
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000463 def Property(self, property_name):
464 """Returns the specified SCM property of this file, or None if no such
465 property.
466 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000467 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000468
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000469 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000470 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000471
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000472 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000473 raise NotImplementedError() # Implement when needed
474
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000475 def NewContents(self):
476 """Returns an iterator over the lines in the new version of file.
477
478 The new version is the file in the user's workspace, i.e. the "right hand
479 side".
480
481 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000482 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000483 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000484 if self._cached_new_contents is None:
485 self._cached_new_contents = []
486 if not self.IsDirectory():
487 try:
488 self._cached_new_contents = gclient_utils.FileRead(
489 self.AbsoluteLocalPath(), 'rU').splitlines()
490 except IOError:
491 pass # File not found? That's fine; maybe it was deleted.
492 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000493
494 def OldContents(self):
495 """Returns an iterator over the lines in the old version of file.
496
497 The old version is the file in depot, i.e. the "left hand side".
498 """
499 raise NotImplementedError() # Implement when needed
500
501 def OldFileTempPath(self):
502 """Returns the path on local disk where the old contents resides.
503
504 The old version is the file in depot, i.e. the "left hand side".
505 This is a read-only cached copy of the old contents. *DO NOT* try to
506 modify this file.
507 """
508 raise NotImplementedError() # Implement if/when needed.
509
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000510 def ChangedContents(self):
511 """Returns a list of tuples (line number, line text) of all new lines.
512
513 This relies on the scm diff output describing each changed code section
514 with a line of the form
515
516 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
517 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000518 if self._cached_changed_contents is not None:
519 return self._cached_changed_contents[:]
520 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000521 line_num = 0
522
523 if self.IsDirectory():
524 return []
525
526 for line in self.GenerateScmDiff().splitlines():
527 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
528 if m:
529 line_num = int(m.groups(1)[0])
530 continue
531 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000532 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000533 if not line.startswith('-'):
534 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000535 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000536
maruel@chromium.org5de13972009-06-10 18:16:06 +0000537 def __str__(self):
538 return self.LocalPath()
539
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000540 def GenerateScmDiff(self):
541 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000542
maruel@chromium.org58407af2011-04-12 23:15:57 +0000543
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000544class SvnAffectedFile(AffectedFile):
545 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000546 # Method 'NNN' is abstract in class 'NNN' but is not overridden
547 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000548
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000549 def __init__(self, *args, **kwargs):
550 AffectedFile.__init__(self, *args, **kwargs)
551 self._server_path = None
552 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000553 self._diff = None
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000554
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.orgd579fcf2011-12-13 20:36:03 +0000557 self._server_path = scm.SVN.CaptureLocalInfo(
558 [self.LocalPath()], self._local_root).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.orgd579fcf2011-12-13 20:36:03 +0000569 self._is_directory = scm.SVN.CaptureLocalInfo(
570 [self.LocalPath()], self._local_root
571 ).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.orgd579fcf2011-12-13 20:36:03 +0000577 self.LocalPath(), property_name, self._local_root).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.orgd579fcf2011-12-13 20:36:03 +0000588 mime_type = scm.SVN.GetFileProperty(
589 self.LocalPath(), 'svn:mime-type', self._local_root)
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.org3bbf2942012-01-10 16:52:06 +0000594 if self._diff is None:
595 self._diff = scm.SVN.GenerateDiff(
596 [self.LocalPath()], self._local_root, False, None)
597 return self._diff
maruel@chromium.org1f312812011-02-10 01:33:57 +0000598
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000599
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000600class GitAffectedFile(AffectedFile):
601 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000602 # Method 'NNN' is abstract in class 'NNN' but is not overridden
603 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000604
605 def __init__(self, *args, **kwargs):
606 AffectedFile.__init__(self, *args, **kwargs)
607 self._server_path = None
608 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000609 self._diff = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000610
611 def ServerPath(self):
612 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000613 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000614 return self._server_path
615
616 def IsDirectory(self):
617 if self._is_directory is None:
618 path = self.AbsoluteLocalPath()
619 if os.path.exists(path):
620 # Retrieve directly from the file system; it is much faster than
621 # querying subversion, especially on Windows.
622 self._is_directory = os.path.isdir(path)
623 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000624 self._is_directory = False
625 return self._is_directory
626
627 def Property(self, property_name):
628 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000629 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000630 return self._properties[property_name]
631
632 def IsTextFile(self):
633 if self._is_text_file is None:
634 if self.Action() == 'D':
635 # A deleted file is not a text file.
636 self._is_text_file = False
637 elif self.IsDirectory():
638 self._is_text_file = False
639 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000640 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
641 return self._is_text_file
642
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000643 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000644 if self._diff is None:
645 self._diff = scm.GIT.GenerateDiff(
646 self._local_root, files=[self.LocalPath(),])
647 return self._diff
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000648
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000649
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000650class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000651 """Describe a change.
652
653 Used directly by the presubmit scripts to query the current change being
654 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000655
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000656 Instance members:
657 tags: Dictionnary of KEY=VALUE pairs found in the change description.
658 self.KEY: equivalent to tags['KEY']
659 """
660
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000661 _AFFECTED_FILES = AffectedFile
662
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000663 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000664 TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000665 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000666 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000667
maruel@chromium.org58407af2011-04-12 23:15:57 +0000668 def __init__(
669 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000670 if files is None:
671 files = []
672 self._name = name
673 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000674 # Convert root into an absolute path.
675 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000676 self.issue = issue
677 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000678 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679
680 # From the description text, build up a dictionary of key/value pairs
681 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000682 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000684 for line in self._full_description.splitlines():
maruel@chromium.org428342a2011-11-10 15:46:33 +0000685 m = self.TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000686 if m:
687 self.tags[m.group('key')] = m.group('value')
688 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000689 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690
691 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000692 self._description_without_tags = (
693 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694
maruel@chromium.orge085d812011-10-10 19:49:15 +0000695 assert all(
696 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
697
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000698 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000699 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
700 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000701 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000703 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000705 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000707 def DescriptionText(self):
708 """Returns the user-entered changelist description, minus tags.
709
710 Any line in the user-provided description starting with e.g. "FOO="
711 (whitespace permitted before and around) is considered a tag line. Such
712 lines are stripped out of the description this function returns.
713 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000714 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000715
716 def FullDescriptionText(self):
717 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000718 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719
720 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000721 """Returns the repository (checkout) root directory for this change,
722 as an absolute path.
723 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000724 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725
726 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000727 """Return tags directly as attributes on the object."""
728 if not re.match(r"^[A-Z_]*$", attr):
729 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000730 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731
sail@chromium.org5538e022011-05-12 17:53:16 +0000732 def AffectedFiles(self, include_dirs=False, include_deletes=True,
733 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000734 """Returns a list of AffectedFile instances for all files in the change.
735
736 Args:
737 include_deletes: If false, deleted files will be filtered out.
738 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000739 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000740
741 Returns:
742 [AffectedFile(path, action), AffectedFile(path, action)]
743 """
744 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000745 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000746 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000747 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748
sail@chromium.org5538e022011-05-12 17:53:16 +0000749 affected = filter(file_filter, affected)
750
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000751 if include_deletes:
752 return affected
753 else:
754 return filter(lambda x: x.Action() != 'D', affected)
755
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000756 def AffectedTextFiles(self, include_deletes=None):
757 """Return a list of the existing text files in a change."""
758 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000759 warn("AffectedTextFiles(include_deletes=%s)"
760 " is deprecated and ignored" % str(include_deletes),
761 category=DeprecationWarning,
762 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000763 return filter(lambda x: x.IsTextFile(),
764 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000765
766 def LocalPaths(self, include_dirs=False):
767 """Convenience function."""
768 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
769
770 def AbsoluteLocalPaths(self, include_dirs=False):
771 """Convenience function."""
772 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
773
774 def ServerPaths(self, include_dirs=False):
775 """Convenience function."""
776 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
777
778 def RightHandSideLines(self):
779 """An iterator over all text lines in "new" version of changed files.
780
781 Lists lines from new or modified text files in the change.
782
783 This is useful for doing line-by-line regex checks, like checking for
784 trailing whitespace.
785
786 Yields:
787 a 3 tuple:
788 the AffectedFile instance of the current file;
789 integer line number (1-based); and
790 the contents of the line as a string.
791 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000792 return _RightHandSideLinesImpl(
793 x for x in self.AffectedFiles(include_deletes=False)
794 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000795
796
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000797class SvnChange(Change):
798 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000799 scm = 'svn'
800 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000801
802 def _GetChangeLists(self):
803 """Get all change lists."""
804 if self._changelists == None:
805 previous_cwd = os.getcwd()
806 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000807 # Need to import here to avoid circular dependency.
808 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000809 self._changelists = gcl.GetModifiedFiles()
810 os.chdir(previous_cwd)
811 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000812
813 def GetAllModifiedFiles(self):
814 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000815 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000816 all_modified_files = []
817 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000818 all_modified_files.extend(
819 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000820 return all_modified_files
821
822 def GetModifiedFiles(self):
823 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000824 changelists = self._GetChangeLists()
825 return [os.path.join(self.RepositoryRoot(), f[1])
826 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000827
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000828
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000829class GitChange(Change):
830 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000831 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000832
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000833
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000834def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000835 """Finds all presubmit files that apply to a given set of source files.
836
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000837 If inherit-review-settings-ok is present right under root, looks for
838 PRESUBMIT.py in directories enclosing root.
839
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000840 Args:
841 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000842 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843
844 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000845 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000846 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000847 files = [normpath(os.path.join(root, f)) for f in files]
848
849 # List all the individual directories containing files.
850 directories = set([os.path.dirname(f) for f in files])
851
852 # Ignore root if inherit-review-settings-ok is present.
853 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
854 root = None
855
856 # Collect all unique directories that may contain PRESUBMIT.py.
857 candidates = set()
858 for directory in directories:
859 while True:
860 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000861 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000862 candidates.add(directory)
863 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000864 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000865 parent_dir = os.path.dirname(directory)
866 if parent_dir == directory:
867 # We hit the system root directory.
868 break
869 directory = parent_dir
870
871 # Look for PRESUBMIT.py in all candidate directories.
872 results = []
873 for directory in sorted(list(candidates)):
874 p = os.path.join(directory, 'PRESUBMIT.py')
875 if os.path.isfile(p):
876 results.append(p)
877
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000878 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000879 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000880
881
thestig@chromium.orgde243452009-10-06 21:02:56 +0000882class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000883 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000884 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000885 """Executes GetPreferredTrySlaves() from a single presubmit script.
886
887 Args:
888 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000889 presubmit_path: Project script to run.
890 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000891
892 Return:
893 A list of try slaves.
894 """
895 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000896 try:
897 exec script_text in context
898 except Exception, e:
899 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000900
901 function_name = 'GetPreferredTrySlaves'
902 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000903 get_preferred_try_slaves = context[function_name]
904 function_info = inspect.getargspec(get_preferred_try_slaves)
905 if len(function_info[0]) == 1:
906 result = get_preferred_try_slaves(project)
907 elif len(function_info[0]) == 2:
908 result = get_preferred_try_slaves(project, change)
909 else:
910 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +0000911 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000912 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000913 'Presubmit functions must return a list, got a %s instead: %s' %
914 (type(result), str(result)))
915 for item in result:
916 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000917 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000918 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000919 raise PresubmitFailure(
920 'Try slave names cannot start/end with whitespace')
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +0000921 if ',' in item:
922 raise PresubmitFailure(
923 'Do not use \',\' separated builder or test names: %s' % item)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000924 else:
925 result = []
926 return result
927
928
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000929def DoGetTrySlaves(change,
930 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000931 repository_root,
932 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000933 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000934 verbose,
935 output_stream):
936 """Get the list of try servers from the presubmit scripts.
937
938 Args:
939 changed_files: List of modified files.
940 repository_root: The repository root.
941 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000942 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000943 verbose: Prints debug info.
944 output_stream: A stream to write debug output to.
945
946 Return:
947 List of try slaves
948 """
949 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
950 if not presubmit_files and verbose:
951 output_stream.write("Warning, no presubmit.py found.\n")
952 results = []
953 executer = GetTrySlavesExecuter()
954 if default_presubmit:
955 if verbose:
956 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000957 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000958 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000959 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000960 for filename in presubmit_files:
961 filename = os.path.abspath(filename)
962 if verbose:
963 output_stream.write("Running %s\n" % filename)
964 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000965 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000966 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000967 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000968
969 slaves = list(set(results))
970 if slaves and verbose:
971 output_stream.write(', '.join(slaves))
972 output_stream.write('\n')
973 return slaves
974
975
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000976class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000977 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978 """
979 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000980 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000982 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000984 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000985 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000986 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000987 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000988
989 def ExecPresubmitScript(self, script_text, presubmit_path):
990 """Executes a single presubmit script.
991
992 Args:
993 script_text: The text of the presubmit script.
994 presubmit_path: The path to the presubmit file (this will be reported via
995 input_api.PresubmitLocalPath()).
996
997 Return:
998 A list of result objects, empty if no problems.
999 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001000
1001 # Change to the presubmit file's directory to support local imports.
1002 main_path = os.getcwd()
1003 os.chdir(os.path.dirname(presubmit_path))
1004
1005 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001006 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001007 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001009 try:
1010 exec script_text in context
1011 except Exception, e:
1012 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013
1014 # These function names must change if we make substantial changes to
1015 # the presubmit API that are not backwards compatible.
1016 if self.committing:
1017 function_name = 'CheckChangeOnCommit'
1018 else:
1019 function_name = 'CheckChangeOnUpload'
1020 if function_name in context:
1021 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001022 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001023 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001024 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001025 if not (isinstance(result, types.TupleType) or
1026 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001027 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001028 'Presubmit functions must return a tuple or list')
1029 for item in result:
1030 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001031 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001032 'All presubmit results must be of types derived from '
1033 'output_api.PresubmitResult')
1034 else:
1035 result = () # no error since the script doesn't care about current event.
1036
chase@chromium.org8e416c82009-10-06 04:30:44 +00001037 # Return the process to the original working directory.
1038 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001039 return result
1040
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001041
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001042def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001043 committing,
1044 verbose,
1045 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001046 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001047 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001048 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001049 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001050 """Runs all presubmit checks that apply to the files in the change.
1051
1052 This finds all PRESUBMIT.py files in directories enclosing the files in the
1053 change (up to the repository root) and calls the relevant entrypoint function
1054 depending on whether the change is being committed or uploaded.
1055
1056 Prints errors, warnings and notifications. Prompts the user for warnings
1057 when needed.
1058
1059 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001060 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001061 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1062 verbose: Prints debug info.
1063 output_stream: A stream to write output from presubmit tests to.
1064 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001065 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001066 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001067 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001068
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001069 Warning:
1070 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1071 SHOULD be sys.stdin.
1072
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001073 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001074 A PresubmitOutput object. Use output.should_continue() to figure out
1075 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001076 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001077 old_environ = os.environ
1078 try:
1079 # Make sure python subprocesses won't generate .pyc files.
1080 os.environ = os.environ.copy()
1081 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001082
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001083 output = PresubmitOutput(input_stream, output_stream)
1084 if committing:
1085 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001086 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001087 output.write("Running presubmit upload checks ...\n")
1088 start_time = time.time()
1089 presubmit_files = ListRelevantPresubmitFiles(
1090 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1091 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001092 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001093 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001094 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001095 if default_presubmit:
1096 if verbose:
1097 output.write("Running default presubmit script.\n")
1098 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1099 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1100 for filename in presubmit_files:
1101 filename = os.path.abspath(filename)
1102 if verbose:
1103 output.write("Running %s\n" % filename)
1104 # Accept CRLF presubmit script.
1105 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1106 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001107
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001108 errors = []
1109 notifications = []
1110 warnings = []
1111 for result in results:
1112 if result.fatal:
1113 errors.append(result)
1114 elif result.should_prompt:
1115 warnings.append(result)
1116 else:
1117 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001118
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001119 output.write('\n')
1120 for name, items in (('Messages', notifications),
1121 ('Warnings', warnings),
1122 ('ERRORS', errors)):
1123 if items:
1124 output.write('** Presubmit %s **\n' % name)
1125 for item in items:
1126 item.handle(output)
1127 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001128
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001129 total_time = time.time() - start_time
1130 if total_time > 1.0:
1131 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001132
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001133 if not errors:
1134 if not warnings:
1135 output.write('Presubmit checks passed.\n')
1136 elif may_prompt:
1137 output.prompt_yes_no('There were presubmit warnings. '
1138 'Are you sure you wish to continue? (y/N): ')
1139 else:
1140 output.fail()
1141
1142 global _ASKED_FOR_FEEDBACK
1143 # Ask for feedback one time out of 5.
1144 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1145 output.write("Was the presubmit check useful? Please send feedback "
1146 "& hate mail to maruel@chromium.org!\n")
1147 _ASKED_FOR_FEEDBACK = True
1148 return output
1149 finally:
1150 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001151
1152
1153def ScanSubDirs(mask, recursive):
1154 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001155 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 +00001156 else:
1157 results = []
1158 for root, dirs, files in os.walk('.'):
1159 if '.svn' in dirs:
1160 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001161 if '.git' in dirs:
1162 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001163 for name in files:
1164 if fnmatch.fnmatch(name, mask):
1165 results.append(os.path.join(root, name))
1166 return results
1167
1168
1169def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001170 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001171 files = []
1172 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001173 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001174 return files
1175
1176
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001177def load_files(options, args):
1178 """Tries to determine the SCM."""
1179 change_scm = scm.determine_scm(options.root)
1180 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001181 if args:
1182 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001183 if change_scm == 'svn':
1184 change_class = SvnChange
1185 if not files:
1186 files = scm.SVN.CaptureStatus([], options.root)
1187 elif change_scm == 'git':
1188 change_class = GitChange
1189 # TODO(maruel): Get upstream.
1190 if not files:
1191 files = scm.GIT.CaptureStatus([], options.root, None)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001192 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001193 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1194 if not files:
1195 return None, None
1196 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001197 return change_class, files
1198
1199
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001200def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001201 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001202 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001203 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001204 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001205 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1206 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001207 parser.add_option("-r", "--recursive", action="store_true",
1208 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001209 parser.add_option("-v", "--verbose", action="count", default=0,
1210 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001211 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001212 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001213 parser.add_option("--description", default='')
1214 parser.add_option("--issue", type='int', default=0)
1215 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001216 parser.add_option("--root", default=os.getcwd(),
1217 help="Search for PRESUBMIT.py up to this directory. "
1218 "If inherit-review-settings-ok is present in this "
1219 "directory, parent directories up to the root file "
1220 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001221 parser.add_option("--default_presubmit")
1222 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001223 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1224 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1225 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001226 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001227 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001228 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001229 elif options.verbose:
1230 logging.basicConfig(level=logging.INFO)
1231 else:
1232 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001233 change_class, files = load_files(options, args)
1234 if not change_class:
1235 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001236 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001237 rietveld_obj = None
1238 if options.rietveld_url:
1239 rietveld_obj = rietveld.Rietveld(
1240 options.rietveld_url,
1241 options.rietveld_email,
1242 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001243 try:
1244 results = DoPresubmitChecks(
1245 change_class(options.name,
1246 options.description,
1247 options.root,
1248 files,
1249 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001250 options.patchset,
1251 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001252 options.commit,
1253 options.verbose,
1254 sys.stdout,
1255 sys.stdin,
1256 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001257 options.may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001258 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001259 return not results.should_continue()
1260 except PresubmitFailure, e:
1261 print >> sys.stderr, e
1262 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1263 print >> sys.stderr, 'If all fails, contact maruel@'
1264 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001265
1266
1267if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001268 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001269 sys.exit(Main(None))