blob: 0b72f4656b72480d28e72283faa7997749a090c5 [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
estade@chromium.orgae7af922012-01-27 14:51:13 +0000201 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
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')
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +0000929 if ',' in item:
930 raise PresubmitFailure(
931 'Do not use \',\' separated builder or test names: %s' % item)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000932 else:
933 result = []
934 return result
935
936
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000937def DoGetTrySlaves(change,
938 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000939 repository_root,
940 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000941 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000942 verbose,
943 output_stream):
944 """Get the list of try servers from the presubmit scripts.
945
946 Args:
947 changed_files: List of modified files.
948 repository_root: The repository root.
949 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000950 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000951 verbose: Prints debug info.
952 output_stream: A stream to write debug output to.
953
954 Return:
955 List of try slaves
956 """
957 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
958 if not presubmit_files and verbose:
959 output_stream.write("Warning, no presubmit.py found.\n")
960 results = []
961 executer = GetTrySlavesExecuter()
962 if default_presubmit:
963 if verbose:
964 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000965 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000966 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000967 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000968 for filename in presubmit_files:
969 filename = os.path.abspath(filename)
970 if verbose:
971 output_stream.write("Running %s\n" % filename)
972 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000973 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000974 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000975 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000976
977 slaves = list(set(results))
978 if slaves and verbose:
979 output_stream.write(', '.join(slaves))
980 output_stream.write('\n')
981 return slaves
982
983
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000985 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000986 """
987 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000988 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000989 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000990 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000991 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000992 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000993 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000994 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000995 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996
997 def ExecPresubmitScript(self, script_text, presubmit_path):
998 """Executes a single presubmit script.
999
1000 Args:
1001 script_text: The text of the presubmit script.
1002 presubmit_path: The path to the presubmit file (this will be reported via
1003 input_api.PresubmitLocalPath()).
1004
1005 Return:
1006 A list of result objects, empty if no problems.
1007 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001008
1009 # Change to the presubmit file's directory to support local imports.
1010 main_path = os.getcwd()
1011 os.chdir(os.path.dirname(presubmit_path))
1012
1013 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001014 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001015 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001016 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001017 try:
1018 exec script_text in context
1019 except Exception, e:
1020 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001021
1022 # These function names must change if we make substantial changes to
1023 # the presubmit API that are not backwards compatible.
1024 if self.committing:
1025 function_name = 'CheckChangeOnCommit'
1026 else:
1027 function_name = 'CheckChangeOnUpload'
1028 if function_name in context:
1029 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001030 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001031 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001032 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033 if not (isinstance(result, types.TupleType) or
1034 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001035 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036 'Presubmit functions must return a tuple or list')
1037 for item in result:
1038 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001039 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001040 'All presubmit results must be of types derived from '
1041 'output_api.PresubmitResult')
1042 else:
1043 result = () # no error since the script doesn't care about current event.
1044
chase@chromium.org8e416c82009-10-06 04:30:44 +00001045 # Return the process to the original working directory.
1046 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047 return result
1048
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001049
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001050def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001051 committing,
1052 verbose,
1053 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001054 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001055 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001056 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001057 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001058 """Runs all presubmit checks that apply to the files in the change.
1059
1060 This finds all PRESUBMIT.py files in directories enclosing the files in the
1061 change (up to the repository root) and calls the relevant entrypoint function
1062 depending on whether the change is being committed or uploaded.
1063
1064 Prints errors, warnings and notifications. Prompts the user for warnings
1065 when needed.
1066
1067 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001068 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001069 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1070 verbose: Prints debug info.
1071 output_stream: A stream to write output from presubmit tests to.
1072 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001073 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001074 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001075 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001076
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001077 Warning:
1078 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1079 SHOULD be sys.stdin.
1080
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001081 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001082 A PresubmitOutput object. Use output.should_continue() to figure out
1083 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001084 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001085 old_environ = os.environ
1086 try:
1087 # Make sure python subprocesses won't generate .pyc files.
1088 os.environ = os.environ.copy()
1089 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001090
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001091 output = PresubmitOutput(input_stream, output_stream)
1092 if committing:
1093 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001094 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001095 output.write("Running presubmit upload checks ...\n")
1096 start_time = time.time()
1097 presubmit_files = ListRelevantPresubmitFiles(
1098 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1099 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001100 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001101 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001102 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001103 if default_presubmit:
1104 if verbose:
1105 output.write("Running default presubmit script.\n")
1106 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1107 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1108 for filename in presubmit_files:
1109 filename = os.path.abspath(filename)
1110 if verbose:
1111 output.write("Running %s\n" % filename)
1112 # Accept CRLF presubmit script.
1113 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1114 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001115
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001116 errors = []
1117 notifications = []
1118 warnings = []
1119 for result in results:
1120 if result.fatal:
1121 errors.append(result)
1122 elif result.should_prompt:
1123 warnings.append(result)
1124 else:
1125 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001126
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001127 output.write('\n')
1128 for name, items in (('Messages', notifications),
1129 ('Warnings', warnings),
1130 ('ERRORS', errors)):
1131 if items:
1132 output.write('** Presubmit %s **\n' % name)
1133 for item in items:
1134 item.handle(output)
1135 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001136
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001137 total_time = time.time() - start_time
1138 if total_time > 1.0:
1139 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001140
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001141 if not errors:
1142 if not warnings:
1143 output.write('Presubmit checks passed.\n')
1144 elif may_prompt:
1145 output.prompt_yes_no('There were presubmit warnings. '
1146 'Are you sure you wish to continue? (y/N): ')
1147 else:
1148 output.fail()
1149
1150 global _ASKED_FOR_FEEDBACK
1151 # Ask for feedback one time out of 5.
1152 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1153 output.write("Was the presubmit check useful? Please send feedback "
1154 "& hate mail to maruel@chromium.org!\n")
1155 _ASKED_FOR_FEEDBACK = True
1156 return output
1157 finally:
1158 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001159
1160
1161def ScanSubDirs(mask, recursive):
1162 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001163 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 +00001164 else:
1165 results = []
1166 for root, dirs, files in os.walk('.'):
1167 if '.svn' in dirs:
1168 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001169 if '.git' in dirs:
1170 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001171 for name in files:
1172 if fnmatch.fnmatch(name, mask):
1173 results.append(os.path.join(root, name))
1174 return results
1175
1176
1177def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001178 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001179 files = []
1180 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001181 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001182 return files
1183
1184
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001185def load_files(options, args):
1186 """Tries to determine the SCM."""
1187 change_scm = scm.determine_scm(options.root)
1188 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001189 if args:
1190 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001191 if change_scm == 'svn':
1192 change_class = SvnChange
1193 if not files:
1194 files = scm.SVN.CaptureStatus([], options.root)
1195 elif change_scm == 'git':
1196 change_class = GitChange
1197 # TODO(maruel): Get upstream.
1198 if not files:
1199 files = scm.GIT.CaptureStatus([], options.root, None)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001200 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001201 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1202 if not files:
1203 return None, None
1204 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001205 return change_class, files
1206
1207
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001208def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001209 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001210 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001211 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001212 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001213 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1214 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001215 parser.add_option("-r", "--recursive", action="store_true",
1216 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001217 parser.add_option("-v", "--verbose", action="count", default=0,
1218 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001219 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001220 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001221 parser.add_option("--description", default='')
1222 parser.add_option("--issue", type='int', default=0)
1223 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001224 parser.add_option("--root", default=os.getcwd(),
1225 help="Search for PRESUBMIT.py up to this directory. "
1226 "If inherit-review-settings-ok is present in this "
1227 "directory, parent directories up to the root file "
1228 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001229 parser.add_option("--default_presubmit")
1230 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org239f4112011-06-03 20:08:23 +00001231 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1232 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1233 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001234 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001235 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001236 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001237 elif options.verbose:
1238 logging.basicConfig(level=logging.INFO)
1239 else:
1240 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001241 change_class, files = load_files(options, args)
1242 if not change_class:
1243 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001244 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001245 rietveld_obj = None
1246 if options.rietveld_url:
1247 rietveld_obj = rietveld.Rietveld(
1248 options.rietveld_url,
1249 options.rietveld_email,
1250 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001251 try:
1252 results = DoPresubmitChecks(
1253 change_class(options.name,
1254 options.description,
1255 options.root,
1256 files,
1257 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001258 options.patchset,
1259 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001260 options.commit,
1261 options.verbose,
1262 sys.stdout,
1263 sys.stdin,
1264 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001265 options.may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001266 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001267 return not results.should_continue()
1268 except PresubmitFailure, e:
1269 print >> sys.stderr, e
1270 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1271 print >> sys.stderr, 'If all fails, contact maruel@'
1272 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001273
1274
1275if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001276 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001277 sys.exit(Main(None))