blob: 79a61f84a7a0f409eeef28aaa1e795591bc0f2db [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.orgdf1595a2009-06-11 02:00:13 +000020import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import marshal # Exposed through the API.
22import optparse
23import os # Somewhat exposed through the API.
24import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000025import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027import sys # Parts exposed through API.
28import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000029import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000030import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000032import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000033import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000034from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000035
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000036try:
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000037 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000038except ImportError:
39 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000040 import json # pylint: disable=F0401
41 except ImportError:
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000042 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000043 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000044 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000045
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000046# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000047import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000048import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000049import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000051import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000052import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000053import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000054
55
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000056# Ask for feedback only once in program lifetime.
57_ASKED_FOR_FEEDBACK = False
58
59
maruel@chromium.org899e1c12011-04-07 17:03:18 +000060class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000061 pass
62
63
64def normpath(path):
65 '''Version of os.path.normpath that also changes backward slashes to
66 forward slashes when not running on Windows.
67 '''
68 # This is safe to always do because the Windows version of os.path.normpath
69 # will replace forward slashes with backward slashes.
70 path = path.replace(os.sep, '/')
71 return os.path.normpath(path)
72
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000073
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000074def _RightHandSideLinesImpl(affected_files):
75 """Implements RightHandSideLines for InputApi and GclChange."""
76 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000077 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000079 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000080
81
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000082class PresubmitOutput(object):
83 def __init__(self, input_stream=None, output_stream=None):
84 self.input_stream = input_stream
85 self.output_stream = output_stream
86 self.reviewers = []
87 self.written_output = []
88 self.error_count = 0
89
90 def prompt_yes_no(self, prompt_string):
91 self.write(prompt_string)
92 if self.input_stream:
93 response = self.input_stream.readline().strip().lower()
94 if response not in ('y', 'yes'):
95 self.fail()
96 else:
97 self.fail()
98
99 def fail(self):
100 self.error_count += 1
101
102 def should_continue(self):
103 return not self.error_count
104
105 def write(self, s):
106 self.written_output.append(s)
107 if self.output_stream:
108 self.output_stream.write(s)
109
110 def getvalue(self):
111 return ''.join(self.written_output)
112
113
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000114class OutputApi(object):
115 """This class (more like a module) gets passed to presubmit scripts so that
116 they can specify various types of results.
117 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000118 class PresubmitResult(object):
119 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000120 fatal = False
121 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000122
123 def __init__(self, message, items=None, long_text=''):
124 """
125 message: A short one-line message to indicate errors.
126 items: A list of short strings to indicate where errors occurred.
127 long_text: multi-line text output, e.g. from another tool
128 """
129 self._message = message
130 self._items = []
131 if items:
132 self._items = items
133 self._long_text = long_text.rstrip()
134
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000135 def handle(self, output):
136 output.write(self._message)
137 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000138 for index, item in enumerate(self._items):
139 output.write(' ')
140 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000141 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000142 if index < len(self._items) - 1:
143 output.write(' \\')
144 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000145 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000146 output.write('\n***************\n')
147 # Write separately in case it's unicode.
148 output.write(self._long_text)
149 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000150 if self.fatal:
151 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000152
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000153 class PresubmitAddReviewers(PresubmitResult):
154 """Add some suggested reviewers to the change."""
155 def __init__(self, reviewers):
156 super(OutputApi.PresubmitAddReviewers, self).__init__('')
157 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000158
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000159 def handle(self, output):
160 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000161
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000162 class PresubmitError(PresubmitResult):
163 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000164 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000165
166 class PresubmitPromptWarning(PresubmitResult):
167 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000168 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000169
170 class PresubmitNotifyResult(PresubmitResult):
171 """Just print something to the screen -- but it's not even a warning."""
172 pass
173
174 class MailTextResult(PresubmitResult):
175 """A warning that should be included in the review request email."""
176 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000177 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000178 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000179
180
181class InputApi(object):
182 """An instance of this object is passed to presubmit scripts so they can
183 know stuff about the change they're looking at.
184 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000185 # Method could be a function
186 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000187
maruel@chromium.org3410d912009-06-09 20:56:16 +0000188 # File extensions that are considered source files from a style guide
189 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000190 #
191 # Files without an extension aren't included in the list. If you want to
192 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
193 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000194 DEFAULT_WHITE_LIST = (
195 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000196 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
197 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000198 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000199 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000200 # Other
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000201 r".+\.java$", r".+\.mk$", r".+\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000202 )
203
204 # Path regexp that should be excluded from being considered containing source
205 # files. Don't modify this list from a presubmit script!
206 DEFAULT_BLACK_LIST = (
207 r".*\bexperimental[\\\/].*",
208 r".*\bthird_party[\\\/].*",
209 # Output directories (just in case)
210 r".*\bDebug[\\\/].*",
211 r".*\bRelease[\\\/].*",
212 r".*\bxcodebuild[\\\/].*",
213 r".*\bsconsbuild[\\\/].*",
214 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000215 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000216 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000217 r"(|.*[\\\/])\.git[\\\/].*",
218 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000219 # There is no point in processing a patch file.
220 r".+\.diff$",
221 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000222 )
223
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000224 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000225 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226 """Builds an InputApi object.
227
228 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000229 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000231 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000232 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000233 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000234 # Version number of the presubmit_support script.
235 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000236 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000237 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000238 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000239 # TBD
240 self.host_url = 'http://codereview.chromium.org'
241 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000242 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000243
244 # We expose various modules and functions as attributes of the input_api
245 # so that presubmit scripts don't have to import them.
246 self.basename = os.path.basename
247 self.cPickle = cPickle
248 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000249 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000250 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000251 self.os_listdir = os.listdir
252 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000253 self.os_path = os.path
254 self.pickle = pickle
255 self.marshal = marshal
256 self.re = re
257 self.subprocess = subprocess
258 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000259 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000260 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000261 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000262 self.urllib2 = urllib2
263
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000264 # To easily fork python.
265 self.python_executable = sys.executable
266 self.environ = os.environ
267
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268 # InputApi.platform is the platform you're currently running on.
269 self.platform = sys.platform
270
271 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000272 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273
274 # We carry the canned checks so presubmit scripts can easily use them.
275 self.canned_checks = presubmit_canned_checks
276
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000277 # TODO(dpranke): figure out a list of all approved owners for a repo
278 # in order to be able to handle wildcard OWNERS files?
279 self.owners_db = owners.Database(change.RepositoryRoot(),
280 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000281 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000282
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283 def PresubmitLocalPath(self):
284 """Returns the local path of the presubmit script currently being run.
285
286 This is useful if you don't want to hard-code absolute paths in the
287 presubmit script. For example, It can be used to find another file
288 relative to the PRESUBMIT.py script, so the whole tree can be branched and
289 the presubmit script still works, without editing its content.
290 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000291 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000292
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000293 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000294 """Translate a depot path to a local path (relative to client root).
295
296 Args:
297 Depot path as a string.
298
299 Returns:
300 The local path of the depot path under the user's current client, or None
301 if the file is not mapped.
302
303 Remember to check for the None case and show an appropriate error!
304 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000305 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
306 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000307
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000308 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000309 """Translate a local path to a depot path.
310
311 Args:
312 Local path (relative to current directory, or absolute) as a string.
313
314 Returns:
315 The depot path (SVN URL) of the file if mapped, otherwise None.
316 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000317 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
318 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000319
sail@chromium.org5538e022011-05-12 17:53:16 +0000320 def AffectedFiles(self, include_dirs=False, include_deletes=True,
321 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322 """Same as input_api.change.AffectedFiles() except only lists files
323 (and optionally directories) in the same directory as the current presubmit
324 script, or subdirectories thereof.
325 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000326 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000327 if len(dir_with_slash) == 1:
328 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000329
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000330 return filter(
331 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000332 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000333
334 def LocalPaths(self, include_dirs=False):
335 """Returns local paths of input_api.AffectedFiles()."""
336 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
337
338 def AbsoluteLocalPaths(self, include_dirs=False):
339 """Returns absolute local paths of input_api.AffectedFiles()."""
340 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
341
342 def ServerPaths(self, include_dirs=False):
343 """Returns server paths of input_api.AffectedFiles()."""
344 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
345
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000346 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 """Same as input_api.change.AffectedTextFiles() except only lists files
348 in the same directory as the current presubmit script, or subdirectories
349 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000351 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000352 warn("AffectedTextFiles(include_deletes=%s)"
353 " is deprecated and ignored" % str(include_deletes),
354 category=DeprecationWarning,
355 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000356 return filter(lambda x: x.IsTextFile(),
357 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000358
maruel@chromium.org3410d912009-06-09 20:56:16 +0000359 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
360 """Filters out files that aren't considered "source file".
361
362 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
363 and InputApi.DEFAULT_BLACK_LIST is used respectively.
364
365 The lists will be compiled as regular expression and
366 AffectedFile.LocalPath() needs to pass both list.
367
368 Note: Copy-paste this function to suit your needs or use a lambda function.
369 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000370 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000371 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000372 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000373 if self.re.match(item, local_path):
374 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000375 return True
376 return False
377 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
378 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
379
380 def AffectedSourceFiles(self, source_file):
381 """Filter the list of AffectedTextFiles by the function source_file.
382
383 If source_file is None, InputApi.FilterSourceFile() is used.
384 """
385 if not source_file:
386 source_file = self.FilterSourceFile
387 return filter(source_file, self.AffectedTextFiles())
388
389 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000390 """An iterator over all text lines in "new" version of changed files.
391
392 Only lists lines from new or modified text files in the change that are
393 contained by the directory of the currently executing presubmit script.
394
395 This is useful for doing line-by-line regex checks, like checking for
396 trailing whitespace.
397
398 Yields:
399 a 3 tuple:
400 the AffectedFile instance of the current file;
401 integer line number (1-based); and
402 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000403
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000404 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000406 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000407 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000408
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000409 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000410 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000411
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000412 Deny reading anything outside the repository.
413 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000414 if isinstance(file_item, AffectedFile):
415 file_item = file_item.AbsoluteLocalPath()
416 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000417 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000418 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000419
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000420 @property
421 def tbr(self):
422 """Returns if a change is TBR'ed."""
423 return 'TBR' in self.change.tags
424
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000425
426class AffectedFile(object):
427 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000428 # Method could be a function
429 # pylint: disable=R0201
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000430 def __init__(self, path, action, repository_root):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000431 self._path = path
432 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000433 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000434 self._is_directory = None
435 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000436 self._cached_changed_contents = None
437 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000438 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000439
440 def ServerPath(self):
441 """Returns a path string that identifies the file in the SCM system.
442
443 Returns the empty string if the file does not exist in SCM.
444 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000445 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000446
447 def LocalPath(self):
448 """Returns the path of this file on the local disk relative to client root.
449 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000450 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000451
452 def AbsoluteLocalPath(self):
453 """Returns the absolute path of this file on the local disk.
454 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000455 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000456
457 def IsDirectory(self):
458 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000459 if self._is_directory is None:
460 path = self.AbsoluteLocalPath()
461 self._is_directory = (os.path.exists(path) and
462 os.path.isdir(path))
463 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000464
465 def Action(self):
466 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000467 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
468 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000469 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000470
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000471 def Property(self, property_name):
472 """Returns the specified SCM property of this file, or None if no such
473 property.
474 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000475 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000476
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000477 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000478 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000479
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000480 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000481 raise NotImplementedError() # Implement when needed
482
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000483 def NewContents(self):
484 """Returns an iterator over the lines in the new version of file.
485
486 The new version is the file in the user's workspace, i.e. the "right hand
487 side".
488
489 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000490 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000491 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000492 if self._cached_new_contents is None:
493 self._cached_new_contents = []
494 if not self.IsDirectory():
495 try:
496 self._cached_new_contents = gclient_utils.FileRead(
497 self.AbsoluteLocalPath(), 'rU').splitlines()
498 except IOError:
499 pass # File not found? That's fine; maybe it was deleted.
500 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000501
502 def OldContents(self):
503 """Returns an iterator over the lines in the old version of file.
504
505 The old version is the file in depot, i.e. the "left hand side".
506 """
507 raise NotImplementedError() # Implement when needed
508
509 def OldFileTempPath(self):
510 """Returns the path on local disk where the old contents resides.
511
512 The old version is the file in depot, i.e. the "left hand side".
513 This is a read-only cached copy of the old contents. *DO NOT* try to
514 modify this file.
515 """
516 raise NotImplementedError() # Implement if/when needed.
517
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000518 def ChangedContents(self):
519 """Returns a list of tuples (line number, line text) of all new lines.
520
521 This relies on the scm diff output describing each changed code section
522 with a line of the form
523
524 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
525 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000526 if self._cached_changed_contents is not None:
527 return self._cached_changed_contents[:]
528 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000529 line_num = 0
530
531 if self.IsDirectory():
532 return []
533
534 for line in self.GenerateScmDiff().splitlines():
535 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
536 if m:
537 line_num = int(m.groups(1)[0])
538 continue
539 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000540 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000541 if not line.startswith('-'):
542 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000543 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000544
maruel@chromium.org5de13972009-06-10 18:16:06 +0000545 def __str__(self):
546 return self.LocalPath()
547
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000548 def GenerateScmDiff(self):
549 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000550
maruel@chromium.org58407af2011-04-12 23:15:57 +0000551
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000552class SvnAffectedFile(AffectedFile):
553 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000554 # Method 'NNN' is abstract in class 'NNN' but is not overridden
555 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000556
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000557 def __init__(self, *args, **kwargs):
558 AffectedFile.__init__(self, *args, **kwargs)
559 self._server_path = None
560 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000561 self._diff = None
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000562
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000563 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000564 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000565 self._server_path = scm.SVN.CaptureLocalInfo(
566 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000567 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000568
569 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000570 if self._is_directory is None:
571 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000572 if os.path.exists(path):
573 # Retrieve directly from the file system; it is much faster than
574 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000575 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000576 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000577 self._is_directory = scm.SVN.CaptureLocalInfo(
578 [self.LocalPath()], self._local_root
579 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000580 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000581
582 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000583 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000584 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000585 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000586 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000587
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000588 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000589 if self._is_text_file is None:
590 if self.Action() == 'D':
591 # A deleted file is not a text file.
592 self._is_text_file = False
593 elif self.IsDirectory():
594 self._is_text_file = False
595 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000596 mime_type = scm.SVN.GetFileProperty(
597 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000598 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
599 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000600
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000601 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000602 if self._diff is None:
603 self._diff = scm.SVN.GenerateDiff(
604 [self.LocalPath()], self._local_root, False, None)
605 return self._diff
maruel@chromium.org1f312812011-02-10 01:33:57 +0000606
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000607
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000608class GitAffectedFile(AffectedFile):
609 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000610 # Method 'NNN' is abstract in class 'NNN' but is not overridden
611 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000612
613 def __init__(self, *args, **kwargs):
614 AffectedFile.__init__(self, *args, **kwargs)
615 self._server_path = None
616 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000617 self._diff = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000618
619 def ServerPath(self):
620 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000621 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000622 return self._server_path
623
624 def IsDirectory(self):
625 if self._is_directory is None:
626 path = self.AbsoluteLocalPath()
627 if os.path.exists(path):
628 # Retrieve directly from the file system; it is much faster than
629 # querying subversion, especially on Windows.
630 self._is_directory = os.path.isdir(path)
631 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000632 self._is_directory = False
633 return self._is_directory
634
635 def Property(self, property_name):
636 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000637 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000638 return self._properties[property_name]
639
640 def IsTextFile(self):
641 if self._is_text_file is None:
642 if self.Action() == 'D':
643 # A deleted file is not a text file.
644 self._is_text_file = False
645 elif self.IsDirectory():
646 self._is_text_file = False
647 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000648 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
649 return self._is_text_file
650
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000651 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000652 if self._diff is None:
653 self._diff = scm.GIT.GenerateDiff(
654 self._local_root, files=[self.LocalPath(),])
655 return self._diff
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000656
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000657
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000658class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000659 """Describe a change.
660
661 Used directly by the presubmit scripts to query the current change being
662 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000663
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000664 Instance members:
665 tags: Dictionnary of KEY=VALUE pairs found in the change description.
666 self.KEY: equivalent to tags['KEY']
667 """
668
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000669 _AFFECTED_FILES = AffectedFile
670
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000671 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000672 TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000673 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000674 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000675
maruel@chromium.org58407af2011-04-12 23:15:57 +0000676 def __init__(
677 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000678 if files is None:
679 files = []
680 self._name = name
681 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000682 # Convert root into an absolute path.
683 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000684 self.issue = issue
685 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000686 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
688 # From the description text, build up a dictionary of key/value pairs
689 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000690 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000691 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000692 for line in self._full_description.splitlines():
maruel@chromium.org428342a2011-11-10 15:46:33 +0000693 m = self.TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694 if m:
695 self.tags[m.group('key')] = m.group('value')
696 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000697 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698
699 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000700 self._description_without_tags = (
701 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702
maruel@chromium.orge085d812011-10-10 19:49:15 +0000703 assert all(
704 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
705
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000706 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000707 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
708 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000709 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000710
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000711 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000712 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000713 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000715 def DescriptionText(self):
716 """Returns the user-entered changelist description, minus tags.
717
718 Any line in the user-provided description starting with e.g. "FOO="
719 (whitespace permitted before and around) is considered a tag line. Such
720 lines are stripped out of the description this function returns.
721 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000722 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000723
724 def FullDescriptionText(self):
725 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000726 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727
728 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000729 """Returns the repository (checkout) root directory for this change,
730 as an absolute path.
731 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000732 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000733
734 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000735 """Return tags directly as attributes on the object."""
736 if not re.match(r"^[A-Z_]*$", attr):
737 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000738 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000739
sail@chromium.org5538e022011-05-12 17:53:16 +0000740 def AffectedFiles(self, include_dirs=False, include_deletes=True,
741 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742 """Returns a list of AffectedFile instances for all files in the change.
743
744 Args:
745 include_deletes: If false, deleted files will be filtered out.
746 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000747 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748
749 Returns:
750 [AffectedFile(path, action), AffectedFile(path, action)]
751 """
752 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000753 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000754 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000755 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000756
sail@chromium.org5538e022011-05-12 17:53:16 +0000757 affected = filter(file_filter, affected)
758
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000759 if include_deletes:
760 return affected
761 else:
762 return filter(lambda x: x.Action() != 'D', affected)
763
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000764 def AffectedTextFiles(self, include_deletes=None):
765 """Return a list of the existing text files in a change."""
766 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000767 warn("AffectedTextFiles(include_deletes=%s)"
768 " is deprecated and ignored" % str(include_deletes),
769 category=DeprecationWarning,
770 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000771 return filter(lambda x: x.IsTextFile(),
772 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000773
774 def LocalPaths(self, include_dirs=False):
775 """Convenience function."""
776 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
777
778 def AbsoluteLocalPaths(self, include_dirs=False):
779 """Convenience function."""
780 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
781
782 def ServerPaths(self, include_dirs=False):
783 """Convenience function."""
784 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
785
786 def RightHandSideLines(self):
787 """An iterator over all text lines in "new" version of changed files.
788
789 Lists lines from new or modified text files in the change.
790
791 This is useful for doing line-by-line regex checks, like checking for
792 trailing whitespace.
793
794 Yields:
795 a 3 tuple:
796 the AffectedFile instance of the current file;
797 integer line number (1-based); and
798 the contents of the line as a string.
799 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000800 return _RightHandSideLinesImpl(
801 x for x in self.AffectedFiles(include_deletes=False)
802 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000803
804
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000805class SvnChange(Change):
806 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000807 scm = 'svn'
808 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000809
810 def _GetChangeLists(self):
811 """Get all change lists."""
812 if self._changelists == None:
813 previous_cwd = os.getcwd()
814 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000815 # Need to import here to avoid circular dependency.
816 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000817 self._changelists = gcl.GetModifiedFiles()
818 os.chdir(previous_cwd)
819 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000820
821 def GetAllModifiedFiles(self):
822 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000823 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000824 all_modified_files = []
825 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000826 all_modified_files.extend(
827 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000828 return all_modified_files
829
830 def GetModifiedFiles(self):
831 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000832 changelists = self._GetChangeLists()
833 return [os.path.join(self.RepositoryRoot(), f[1])
834 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000835
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000836
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000837class GitChange(Change):
838 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000839 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000840
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000841
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000842def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843 """Finds all presubmit files that apply to a given set of source files.
844
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000845 If inherit-review-settings-ok is present right under root, looks for
846 PRESUBMIT.py in directories enclosing root.
847
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000848 Args:
849 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000850 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000851
852 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000853 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000854 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000855 files = [normpath(os.path.join(root, f)) for f in files]
856
857 # List all the individual directories containing files.
858 directories = set([os.path.dirname(f) for f in files])
859
860 # Ignore root if inherit-review-settings-ok is present.
861 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
862 root = None
863
864 # Collect all unique directories that may contain PRESUBMIT.py.
865 candidates = set()
866 for directory in directories:
867 while True:
868 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000870 candidates.add(directory)
871 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000872 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000873 parent_dir = os.path.dirname(directory)
874 if parent_dir == directory:
875 # We hit the system root directory.
876 break
877 directory = parent_dir
878
879 # Look for PRESUBMIT.py in all candidate directories.
880 results = []
881 for directory in sorted(list(candidates)):
882 p = os.path.join(directory, 'PRESUBMIT.py')
883 if os.path.isfile(p):
884 results.append(p)
885
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000886 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000887 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000888
889
thestig@chromium.orgde243452009-10-06 21:02:56 +0000890class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000891 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000892 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000893 """Executes GetPreferredTrySlaves() from a single presubmit script.
894
895 Args:
896 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000897 presubmit_path: Project script to run.
898 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000899
900 Return:
901 A list of try slaves.
902 """
903 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000904 try:
905 exec script_text in context
906 except Exception, e:
907 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000908
909 function_name = 'GetPreferredTrySlaves'
910 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000911 get_preferred_try_slaves = context[function_name]
912 function_info = inspect.getargspec(get_preferred_try_slaves)
913 if len(function_info[0]) == 1:
914 result = get_preferred_try_slaves(project)
915 elif len(function_info[0]) == 2:
916 result = get_preferred_try_slaves(project, change)
917 else:
918 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +0000919 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000920 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000921 'Presubmit functions must return a list, got a %s instead: %s' %
922 (type(result), str(result)))
923 for item in result:
924 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000925 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000926 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000927 raise PresubmitFailure(
928 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000929 else:
930 result = []
931 return result
932
933
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000934def DoGetTrySlaves(change,
935 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000936 repository_root,
937 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000938 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000939 verbose,
940 output_stream):
941 """Get the list of try servers from the presubmit scripts.
942
943 Args:
944 changed_files: List of modified files.
945 repository_root: The repository root.
946 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000947 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000948 verbose: Prints debug info.
949 output_stream: A stream to write debug output to.
950
951 Return:
952 List of try slaves
953 """
954 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
955 if not presubmit_files and verbose:
956 output_stream.write("Warning, no presubmit.py found.\n")
957 results = []
958 executer = GetTrySlavesExecuter()
959 if default_presubmit:
960 if verbose:
961 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000962 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000963 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000964 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000965 for filename in presubmit_files:
966 filename = os.path.abspath(filename)
967 if verbose:
968 output_stream.write("Running %s\n" % filename)
969 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000970 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000971 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000972 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000973
974 slaves = list(set(results))
975 if slaves and verbose:
976 output_stream.write(', '.join(slaves))
977 output_stream.write('\n')
978 return slaves
979
980
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000982 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983 """
984 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000985 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000986 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000987 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000988 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000989 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000990 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000991 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000992 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000993
994 def ExecPresubmitScript(self, script_text, presubmit_path):
995 """Executes a single presubmit script.
996
997 Args:
998 script_text: The text of the presubmit script.
999 presubmit_path: The path to the presubmit file (this will be reported via
1000 input_api.PresubmitLocalPath()).
1001
1002 Return:
1003 A list of result objects, empty if no problems.
1004 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001005
1006 # Change to the presubmit file's directory to support local imports.
1007 main_path = os.getcwd()
1008 os.chdir(os.path.dirname(presubmit_path))
1009
1010 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001011 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001012 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001014 try:
1015 exec script_text in context
1016 except Exception, e:
1017 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018
1019 # These function names must change if we make substantial changes to
1020 # the presubmit API that are not backwards compatible.
1021 if self.committing:
1022 function_name = 'CheckChangeOnCommit'
1023 else:
1024 function_name = 'CheckChangeOnUpload'
1025 if function_name in context:
1026 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001027 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001028 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001029 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001030 if not (isinstance(result, types.TupleType) or
1031 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001032 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033 'Presubmit functions must return a tuple or list')
1034 for item in result:
1035 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001036 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037 'All presubmit results must be of types derived from '
1038 'output_api.PresubmitResult')
1039 else:
1040 result = () # no error since the script doesn't care about current event.
1041
chase@chromium.org8e416c82009-10-06 04:30:44 +00001042 # Return the process to the original working directory.
1043 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001044 return result
1045
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001046
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001047def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001048 committing,
1049 verbose,
1050 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001051 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001052 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001053 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001054 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001055 """Runs all presubmit checks that apply to the files in the change.
1056
1057 This finds all PRESUBMIT.py files in directories enclosing the files in the
1058 change (up to the repository root) and calls the relevant entrypoint function
1059 depending on whether the change is being committed or uploaded.
1060
1061 Prints errors, warnings and notifications. Prompts the user for warnings
1062 when needed.
1063
1064 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001065 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001066 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1067 verbose: Prints debug info.
1068 output_stream: A stream to write output from presubmit tests to.
1069 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001070 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001071 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001072 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001073
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001074 Warning:
1075 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1076 SHOULD be sys.stdin.
1077
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001078 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001079 A PresubmitOutput object. Use output.should_continue() to figure out
1080 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001081 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001082 old_environ = os.environ
1083 try:
1084 # Make sure python subprocesses won't generate .pyc files.
1085 os.environ = os.environ.copy()
1086 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001087
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001088 output = PresubmitOutput(input_stream, output_stream)
1089 if committing:
1090 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001091 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001092 output.write("Running presubmit upload checks ...\n")
1093 start_time = time.time()
1094 presubmit_files = ListRelevantPresubmitFiles(
1095 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1096 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001097 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001098 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001099 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001100 if default_presubmit:
1101 if verbose:
1102 output.write("Running default presubmit script.\n")
1103 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1104 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1105 for filename in presubmit_files:
1106 filename = os.path.abspath(filename)
1107 if verbose:
1108 output.write("Running %s\n" % filename)
1109 # Accept CRLF presubmit script.
1110 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1111 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001112
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001113 errors = []
1114 notifications = []
1115 warnings = []
1116 for result in results:
1117 if result.fatal:
1118 errors.append(result)
1119 elif result.should_prompt:
1120 warnings.append(result)
1121 else:
1122 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001123
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001124 output.write('\n')
1125 for name, items in (('Messages', notifications),
1126 ('Warnings', warnings),
1127 ('ERRORS', errors)):
1128 if items:
1129 output.write('** Presubmit %s **\n' % name)
1130 for item in items:
1131 item.handle(output)
1132 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001133
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001134 total_time = time.time() - start_time
1135 if total_time > 1.0:
1136 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001137
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001138 if not errors:
1139 if not warnings:
1140 output.write('Presubmit checks passed.\n')
1141 elif may_prompt:
1142 output.prompt_yes_no('There were presubmit warnings. '
1143 'Are you sure you wish to continue? (y/N): ')
1144 else:
1145 output.fail()
1146
1147 global _ASKED_FOR_FEEDBACK
1148 # Ask for feedback one time out of 5.
1149 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1150 output.write("Was the presubmit check useful? Please send feedback "
1151 "& hate mail to maruel@chromium.org!\n")
1152 _ASKED_FOR_FEEDBACK = True
1153 return output
1154 finally:
1155 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001156
1157
1158def ScanSubDirs(mask, recursive):
1159 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001160 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 +00001161 else:
1162 results = []
1163 for root, dirs, files in os.walk('.'):
1164 if '.svn' in dirs:
1165 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001166 if '.git' in dirs:
1167 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001168 for name in files:
1169 if fnmatch.fnmatch(name, mask):
1170 results.append(os.path.join(root, name))
1171 return results
1172
1173
1174def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001175 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001176 files = []
1177 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001178 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001179 return files
1180
1181
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001182def load_files(options, args):
1183 """Tries to determine the SCM."""
1184 change_scm = scm.determine_scm(options.root)
1185 files = []
1186 if change_scm == 'svn':
1187 change_class = SvnChange
1188 status_fn = scm.SVN.CaptureStatus
1189 elif change_scm == 'git':
1190 change_class = GitChange
1191 status_fn = scm.GIT.CaptureStatus
1192 else:
1193 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1194 if not args:
1195 return None, None
1196 change_class = Change
1197 if args:
1198 files = ParseFiles(args, options.recursive)
1199 else:
1200 # Grab modified files.
1201 files = status_fn([options.root])
1202 return change_class, files
1203
1204
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001205def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001206 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001207 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001208 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001209 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001210 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1211 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001212 parser.add_option("-r", "--recursive", action="store_true",
1213 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001214 parser.add_option("-v", "--verbose", action="count", default=0,
1215 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001216 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001217 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001218 parser.add_option("--description", default='')
1219 parser.add_option("--issue", type='int', default=0)
1220 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001221 parser.add_option("--root", default=os.getcwd(),
1222 help="Search for PRESUBMIT.py up to this directory. "
1223 "If inherit-review-settings-ok is present in this "
1224 "directory, parent directories up to the root file "
1225 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001226 parser.add_option("--default_presubmit")
1227 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001228 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1229 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1230 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001231 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001232 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001233 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001234 elif options.verbose:
1235 logging.basicConfig(level=logging.INFO)
1236 else:
1237 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001238 change_class, files = load_files(options, args)
1239 if not change_class:
1240 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001241 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001242 rietveld_obj = None
1243 if options.rietveld_url:
1244 rietveld_obj = rietveld.Rietveld(
1245 options.rietveld_url,
1246 options.rietveld_email,
1247 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001248 try:
1249 results = DoPresubmitChecks(
1250 change_class(options.name,
1251 options.description,
1252 options.root,
1253 files,
1254 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001255 options.patchset,
1256 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001257 options.commit,
1258 options.verbose,
1259 sys.stdout,
1260 sys.stdin,
1261 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001262 options.may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001263 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001264 return not results.should_continue()
1265 except PresubmitFailure, e:
1266 print >> sys.stderr, e
1267 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1268 print >> sys.stderr, 'If all fails, contact maruel@'
1269 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001270
1271
1272if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001273 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001274 sys.exit(Main(None))