blob: 814d3fc37a0d812bbb2bc977293ffe61113eb6d8 [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.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000017import contextlib
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000018import fnmatch
19import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000020import inspect
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000022import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000023import marshal # Exposed through the API.
24import optparse
25import os # Somewhat exposed through the API.
26import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000027import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000028import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000029import sys # Parts exposed through API.
30import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000031import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000032import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000033import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000034import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000035import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000036from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000037
38# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000039import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000040import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000041import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000042import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000043import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000044import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000045import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000046
47
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000048# Ask for feedback only once in program lifetime.
49_ASKED_FOR_FEEDBACK = False
50
51
maruel@chromium.org899e1c12011-04-07 17:03:18 +000052class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053 pass
54
55
56def normpath(path):
57 '''Version of os.path.normpath that also changes backward slashes to
58 forward slashes when not running on Windows.
59 '''
60 # This is safe to always do because the Windows version of os.path.normpath
61 # will replace forward slashes with backward slashes.
62 path = path.replace(os.sep, '/')
63 return os.path.normpath(path)
64
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000065
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000066def _RightHandSideLinesImpl(affected_files):
67 """Implements RightHandSideLines for InputApi and GclChange."""
68 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000069 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000070 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000071 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000072
73
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000074class PresubmitOutput(object):
75 def __init__(self, input_stream=None, output_stream=None):
76 self.input_stream = input_stream
77 self.output_stream = output_stream
78 self.reviewers = []
79 self.written_output = []
80 self.error_count = 0
81
82 def prompt_yes_no(self, prompt_string):
83 self.write(prompt_string)
84 if self.input_stream:
85 response = self.input_stream.readline().strip().lower()
86 if response not in ('y', 'yes'):
87 self.fail()
88 else:
89 self.fail()
90
91 def fail(self):
92 self.error_count += 1
93
94 def should_continue(self):
95 return not self.error_count
96
97 def write(self, s):
98 self.written_output.append(s)
99 if self.output_stream:
100 self.output_stream.write(s)
101
102 def getvalue(self):
103 return ''.join(self.written_output)
104
105
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000106class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000107 """An instance of OutputApi gets passed to presubmit scripts so that they
108 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000109 """
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000110 def __init__(self, is_committing):
111 self.is_committing = is_committing
112
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000113 class PresubmitResult(object):
114 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000115 fatal = False
116 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000117
118 def __init__(self, message, items=None, long_text=''):
119 """
120 message: A short one-line message to indicate errors.
121 items: A list of short strings to indicate where errors occurred.
122 long_text: multi-line text output, e.g. from another tool
123 """
124 self._message = message
125 self._items = []
126 if items:
127 self._items = items
128 self._long_text = long_text.rstrip()
129
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000130 def handle(self, output):
131 output.write(self._message)
132 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000133 for index, item in enumerate(self._items):
134 output.write(' ')
135 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000136 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000137 if index < len(self._items) - 1:
138 output.write(' \\')
139 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000140 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000141 output.write('\n***************\n')
142 # Write separately in case it's unicode.
143 output.write(self._long_text)
144 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000145 if self.fatal:
146 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000147
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000148 class PresubmitAddReviewers(PresubmitResult):
149 """Add some suggested reviewers to the change."""
150 def __init__(self, reviewers):
151 super(OutputApi.PresubmitAddReviewers, self).__init__('')
152 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000153
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000154 def handle(self, output):
155 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000156
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000157 class PresubmitError(PresubmitResult):
158 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000159 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000160
161 class PresubmitPromptWarning(PresubmitResult):
162 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000163 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000164
165 class PresubmitNotifyResult(PresubmitResult):
166 """Just print something to the screen -- but it's not even a warning."""
167 pass
168
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000169 def PresubmitPromptOrNotify(self, *args, **kwargs):
170 """Warn the user when uploading, but only notify if committing."""
171 if self.is_committing:
172 return self.PresubmitNotifyResult(*args, **kwargs)
173 return self.PresubmitPromptWarning(*args, **kwargs)
174
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000175 class MailTextResult(PresubmitResult):
176 """A warning that should be included in the review request email."""
177 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000178 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000179 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000180
181
182class InputApi(object):
183 """An instance of this object is passed to presubmit scripts so they can
184 know stuff about the change they're looking at.
185 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000186 # Method could be a function
187 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000188
maruel@chromium.org3410d912009-06-09 20:56:16 +0000189 # File extensions that are considered source files from a style guide
190 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000191 #
192 # Files without an extension aren't included in the list. If you want to
193 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
194 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000195 DEFAULT_WHITE_LIST = (
196 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000197 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
198 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000199 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000200 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000201 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000202 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000203 )
204
205 # Path regexp that should be excluded from being considered containing source
206 # files. Don't modify this list from a presubmit script!
207 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000208 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000209 r".*\bexperimental[\\\/].*",
210 r".*\bthird_party[\\\/].*",
211 # Output directories (just in case)
212 r".*\bDebug[\\\/].*",
213 r".*\bRelease[\\\/].*",
214 r".*\bxcodebuild[\\\/].*",
215 r".*\bsconsbuild[\\\/].*",
216 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000217 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000218 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000219 r"(|.*[\\\/])\.git[\\\/].*",
220 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000221 # There is no point in processing a patch file.
222 r".+\.diff$",
223 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000224 )
225
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000226 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000227 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228 """Builds an InputApi object.
229
230 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000231 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000232 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000233 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000234 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000235 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000236 # Version number of the presubmit_support script.
237 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000238 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000239 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000240 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000241 # TBD
242 self.host_url = 'http://codereview.chromium.org'
243 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000244 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000245
246 # We expose various modules and functions as attributes of the input_api
247 # so that presubmit scripts don't have to import them.
248 self.basename = os.path.basename
249 self.cPickle = cPickle
250 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000251 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000252 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000253 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000254 self.os_listdir = os.listdir
255 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256 self.os_path = os.path
257 self.pickle = pickle
258 self.marshal = marshal
259 self.re = re
260 self.subprocess = subprocess
261 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000262 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000263 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000264 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000265 self.urllib2 = urllib2
266
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000267 # To easily fork python.
268 self.python_executable = sys.executable
269 self.environ = os.environ
270
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271 # InputApi.platform is the platform you're currently running on.
272 self.platform = sys.platform
273
274 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000275 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276
277 # We carry the canned checks so presubmit scripts can easily use them.
278 self.canned_checks = presubmit_canned_checks
279
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000280 # TODO(dpranke): figure out a list of all approved owners for a repo
281 # in order to be able to handle wildcard OWNERS files?
282 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000283 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000284 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000285
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286 def PresubmitLocalPath(self):
287 """Returns the local path of the presubmit script currently being run.
288
289 This is useful if you don't want to hard-code absolute paths in the
290 presubmit script. For example, It can be used to find another file
291 relative to the PRESUBMIT.py script, so the whole tree can be branched and
292 the presubmit script still works, without editing its content.
293 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000294 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000295
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000296 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000297 """Translate a depot path to a local path (relative to client root).
298
299 Args:
300 Depot path as a string.
301
302 Returns:
303 The local path of the depot path under the user's current client, or None
304 if the file is not mapped.
305
306 Remember to check for the None case and show an appropriate error!
307 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000308 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
309 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000310
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000311 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000312 """Translate a local path to a depot path.
313
314 Args:
315 Local path (relative to current directory, or absolute) as a string.
316
317 Returns:
318 The depot path (SVN URL) of the file if mapped, otherwise None.
319 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000320 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
321 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322
sail@chromium.org5538e022011-05-12 17:53:16 +0000323 def AffectedFiles(self, include_dirs=False, include_deletes=True,
324 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000325 """Same as input_api.change.AffectedFiles() except only lists files
326 (and optionally directories) in the same directory as the current presubmit
327 script, or subdirectories thereof.
328 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000329 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000330 if len(dir_with_slash) == 1:
331 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000332
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000333 return filter(
334 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000335 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000336
337 def LocalPaths(self, include_dirs=False):
338 """Returns local paths of input_api.AffectedFiles()."""
339 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
340
341 def AbsoluteLocalPaths(self, include_dirs=False):
342 """Returns absolute local paths of input_api.AffectedFiles()."""
343 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
344
345 def ServerPaths(self, include_dirs=False):
346 """Returns server paths of input_api.AffectedFiles()."""
347 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
348
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000349 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350 """Same as input_api.change.AffectedTextFiles() except only lists files
351 in the same directory as the current presubmit script, or subdirectories
352 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000353 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000354 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000355 warn("AffectedTextFiles(include_deletes=%s)"
356 " is deprecated and ignored" % str(include_deletes),
357 category=DeprecationWarning,
358 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000359 return filter(lambda x: x.IsTextFile(),
360 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000361
maruel@chromium.org3410d912009-06-09 20:56:16 +0000362 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
363 """Filters out files that aren't considered "source file".
364
365 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
366 and InputApi.DEFAULT_BLACK_LIST is used respectively.
367
368 The lists will be compiled as regular expression and
369 AffectedFile.LocalPath() needs to pass both list.
370
371 Note: Copy-paste this function to suit your needs or use a lambda function.
372 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000373 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000374 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000375 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000376 if self.re.match(item, local_path):
377 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000378 return True
379 return False
380 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
381 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
382
383 def AffectedSourceFiles(self, source_file):
384 """Filter the list of AffectedTextFiles by the function source_file.
385
386 If source_file is None, InputApi.FilterSourceFile() is used.
387 """
388 if not source_file:
389 source_file = self.FilterSourceFile
390 return filter(source_file, self.AffectedTextFiles())
391
392 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000393 """An iterator over all text lines in "new" version of changed files.
394
395 Only lists lines from new or modified text files in the change that are
396 contained by the directory of the currently executing presubmit script.
397
398 This is useful for doing line-by-line regex checks, like checking for
399 trailing whitespace.
400
401 Yields:
402 a 3 tuple:
403 the AffectedFile instance of the current file;
404 integer line number (1-based); and
405 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000406
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000407 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000408 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000409 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000410 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000411
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000412 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000413 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000414
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000415 Deny reading anything outside the repository.
416 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000417 if isinstance(file_item, AffectedFile):
418 file_item = file_item.AbsoluteLocalPath()
419 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000420 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000421 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000422
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000423 @property
424 def tbr(self):
425 """Returns if a change is TBR'ed."""
426 return 'TBR' in self.change.tags
427
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000428
429class AffectedFile(object):
430 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000431 # Method could be a function
432 # pylint: disable=R0201
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000433 def __init__(self, path, action, repository_root):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000434 self._path = path
435 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000436 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000437 self._is_directory = None
438 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000439 self._cached_changed_contents = None
440 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000441 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000442
443 def ServerPath(self):
444 """Returns a path string that identifies the file in the SCM system.
445
446 Returns the empty string if the file does not exist in SCM.
447 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000448 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000449
450 def LocalPath(self):
451 """Returns the path of this file on the local disk relative to client root.
452 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000453 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454
455 def AbsoluteLocalPath(self):
456 """Returns the absolute path of this file on the local disk.
457 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000458 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000459
460 def IsDirectory(self):
461 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000462 if self._is_directory is None:
463 path = self.AbsoluteLocalPath()
464 self._is_directory = (os.path.exists(path) and
465 os.path.isdir(path))
466 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000467
468 def Action(self):
469 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000470 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
471 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000472 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000473
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000474 def Property(self, property_name):
475 """Returns the specified SCM property of this file, or None if no such
476 property.
477 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000478 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000479
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000480 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000481 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000482
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000483 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000484 raise NotImplementedError() # Implement when needed
485
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000486 def NewContents(self):
487 """Returns an iterator over the lines in the new version of file.
488
489 The new version is the file in the user's workspace, i.e. the "right hand
490 side".
491
492 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000493 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000494 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000495 if self._cached_new_contents is None:
496 self._cached_new_contents = []
497 if not self.IsDirectory():
498 try:
499 self._cached_new_contents = gclient_utils.FileRead(
500 self.AbsoluteLocalPath(), 'rU').splitlines()
501 except IOError:
502 pass # File not found? That's fine; maybe it was deleted.
503 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000504
505 def OldContents(self):
506 """Returns an iterator over the lines in the old version of file.
507
508 The old version is the file in depot, i.e. the "left hand side".
509 """
510 raise NotImplementedError() # Implement when needed
511
512 def OldFileTempPath(self):
513 """Returns the path on local disk where the old contents resides.
514
515 The old version is the file in depot, i.e. the "left hand side".
516 This is a read-only cached copy of the old contents. *DO NOT* try to
517 modify this file.
518 """
519 raise NotImplementedError() # Implement if/when needed.
520
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000521 def ChangedContents(self):
522 """Returns a list of tuples (line number, line text) of all new lines.
523
524 This relies on the scm diff output describing each changed code section
525 with a line of the form
526
527 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
528 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000529 if self._cached_changed_contents is not None:
530 return self._cached_changed_contents[:]
531 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000532 line_num = 0
533
534 if self.IsDirectory():
535 return []
536
537 for line in self.GenerateScmDiff().splitlines():
538 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
539 if m:
540 line_num = int(m.groups(1)[0])
541 continue
542 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000543 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000544 if not line.startswith('-'):
545 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000546 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000547
maruel@chromium.org5de13972009-06-10 18:16:06 +0000548 def __str__(self):
549 return self.LocalPath()
550
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000551 def GenerateScmDiff(self):
552 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000553
maruel@chromium.org58407af2011-04-12 23:15:57 +0000554
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000555class SvnAffectedFile(AffectedFile):
556 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000557 # Method 'NNN' is abstract in class 'NNN' but is not overridden
558 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000559
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000560 def __init__(self, *args, **kwargs):
561 AffectedFile.__init__(self, *args, **kwargs)
562 self._server_path = None
563 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000564 self._diff = None
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000565
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000566 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000567 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000568 self._server_path = scm.SVN.CaptureLocalInfo(
569 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000570 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000571
572 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000573 if self._is_directory is None:
574 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000575 if os.path.exists(path):
576 # Retrieve directly from the file system; it is much faster than
577 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000578 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000579 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000580 self._is_directory = scm.SVN.CaptureLocalInfo(
581 [self.LocalPath()], self._local_root
582 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000583 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000584
585 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000586 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000587 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000588 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000589 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000590
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000591 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000592 if self._is_text_file is None:
593 if self.Action() == 'D':
594 # A deleted file is not a text file.
595 self._is_text_file = False
596 elif self.IsDirectory():
597 self._is_text_file = False
598 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000599 mime_type = scm.SVN.GetFileProperty(
600 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000601 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
602 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000603
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000604 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000605 if self._diff is None:
606 self._diff = scm.SVN.GenerateDiff(
607 [self.LocalPath()], self._local_root, False, None)
608 return self._diff
maruel@chromium.org1f312812011-02-10 01:33:57 +0000609
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000610
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000611class GitAffectedFile(AffectedFile):
612 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000613 # Method 'NNN' is abstract in class 'NNN' but is not overridden
614 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000615
616 def __init__(self, *args, **kwargs):
617 AffectedFile.__init__(self, *args, **kwargs)
618 self._server_path = None
619 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000620 self._diff = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000621
622 def ServerPath(self):
623 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000624 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000625 return self._server_path
626
627 def IsDirectory(self):
628 if self._is_directory is None:
629 path = self.AbsoluteLocalPath()
630 if os.path.exists(path):
631 # Retrieve directly from the file system; it is much faster than
632 # querying subversion, especially on Windows.
633 self._is_directory = os.path.isdir(path)
634 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000635 self._is_directory = False
636 return self._is_directory
637
638 def Property(self, property_name):
639 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000640 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000641 return self._properties[property_name]
642
643 def IsTextFile(self):
644 if self._is_text_file is None:
645 if self.Action() == 'D':
646 # A deleted file is not a text file.
647 self._is_text_file = False
648 elif self.IsDirectory():
649 self._is_text_file = False
650 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000651 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
652 return self._is_text_file
653
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000654 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000655 if self._diff is None:
656 self._diff = scm.GIT.GenerateDiff(
657 self._local_root, files=[self.LocalPath(),])
658 return self._diff
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000659
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000660
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000661class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000662 """Describe a change.
663
664 Used directly by the presubmit scripts to query the current change being
665 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000666
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000667 Instance members:
668 tags: Dictionnary of KEY=VALUE pairs found in the change description.
669 self.KEY: equivalent to tags['KEY']
670 """
671
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000672 _AFFECTED_FILES = AffectedFile
673
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000674 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000675 TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000676 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000677 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678
maruel@chromium.org58407af2011-04-12 23:15:57 +0000679 def __init__(
680 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000681 if files is None:
682 files = []
683 self._name = name
684 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000685 # Convert root into an absolute path.
686 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000687 self.issue = issue
688 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000689 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690
691 # From the description text, build up a dictionary of key/value pairs
692 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000693 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000695 for line in self._full_description.splitlines():
maruel@chromium.org428342a2011-11-10 15:46:33 +0000696 m = self.TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697 if m:
698 self.tags[m.group('key')] = m.group('value')
699 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000700 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000701
702 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000703 self._description_without_tags = (
704 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000705
maruel@chromium.orge085d812011-10-10 19:49:15 +0000706 assert all(
707 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
708
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000709 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000710 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
711 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000712 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000713
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000714 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000715 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000716 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000718 def DescriptionText(self):
719 """Returns the user-entered changelist description, minus tags.
720
721 Any line in the user-provided description starting with e.g. "FOO="
722 (whitespace permitted before and around) is considered a tag line. Such
723 lines are stripped out of the description this function returns.
724 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000725 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000726
727 def FullDescriptionText(self):
728 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000729 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000730
731 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000732 """Returns the repository (checkout) root directory for this change,
733 as an absolute path.
734 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000735 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736
737 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000738 """Return tags directly as attributes on the object."""
739 if not re.match(r"^[A-Z_]*$", attr):
740 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000741 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742
sail@chromium.org5538e022011-05-12 17:53:16 +0000743 def AffectedFiles(self, include_dirs=False, include_deletes=True,
744 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000745 """Returns a list of AffectedFile instances for all files in the change.
746
747 Args:
748 include_deletes: If false, deleted files will be filtered out.
749 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000750 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000751
752 Returns:
753 [AffectedFile(path, action), AffectedFile(path, action)]
754 """
755 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000756 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000757 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000758 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000759
sail@chromium.org5538e022011-05-12 17:53:16 +0000760 affected = filter(file_filter, affected)
761
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000762 if include_deletes:
763 return affected
764 else:
765 return filter(lambda x: x.Action() != 'D', affected)
766
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000767 def AffectedTextFiles(self, include_deletes=None):
768 """Return a list of the existing text files in a change."""
769 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000770 warn("AffectedTextFiles(include_deletes=%s)"
771 " is deprecated and ignored" % str(include_deletes),
772 category=DeprecationWarning,
773 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000774 return filter(lambda x: x.IsTextFile(),
775 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000776
777 def LocalPaths(self, include_dirs=False):
778 """Convenience function."""
779 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
780
781 def AbsoluteLocalPaths(self, include_dirs=False):
782 """Convenience function."""
783 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
784
785 def ServerPaths(self, include_dirs=False):
786 """Convenience function."""
787 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
788
789 def RightHandSideLines(self):
790 """An iterator over all text lines in "new" version of changed files.
791
792 Lists lines from new or modified text files in the change.
793
794 This is useful for doing line-by-line regex checks, like checking for
795 trailing whitespace.
796
797 Yields:
798 a 3 tuple:
799 the AffectedFile instance of the current file;
800 integer line number (1-based); and
801 the contents of the line as a string.
802 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000803 return _RightHandSideLinesImpl(
804 x for x in self.AffectedFiles(include_deletes=False)
805 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000806
807
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000808class SvnChange(Change):
809 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000810 scm = 'svn'
811 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000812
813 def _GetChangeLists(self):
814 """Get all change lists."""
815 if self._changelists == None:
816 previous_cwd = os.getcwd()
817 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000818 # Need to import here to avoid circular dependency.
819 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000820 self._changelists = gcl.GetModifiedFiles()
821 os.chdir(previous_cwd)
822 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000823
824 def GetAllModifiedFiles(self):
825 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000826 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000827 all_modified_files = []
828 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000829 all_modified_files.extend(
830 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000831 return all_modified_files
832
833 def GetModifiedFiles(self):
834 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000835 changelists = self._GetChangeLists()
836 return [os.path.join(self.RepositoryRoot(), f[1])
837 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000838
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000839
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000840class GitChange(Change):
841 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000842 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000843
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000844
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000845def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000846 """Finds all presubmit files that apply to a given set of source files.
847
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000848 If inherit-review-settings-ok is present right under root, looks for
849 PRESUBMIT.py in directories enclosing root.
850
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000851 Args:
852 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000853 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000854
855 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000856 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000857 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000858 files = [normpath(os.path.join(root, f)) for f in files]
859
860 # List all the individual directories containing files.
861 directories = set([os.path.dirname(f) for f in files])
862
863 # Ignore root if inherit-review-settings-ok is present.
864 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
865 root = None
866
867 # Collect all unique directories that may contain PRESUBMIT.py.
868 candidates = set()
869 for directory in directories:
870 while True:
871 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000872 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000873 candidates.add(directory)
874 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000875 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000876 parent_dir = os.path.dirname(directory)
877 if parent_dir == directory:
878 # We hit the system root directory.
879 break
880 directory = parent_dir
881
882 # Look for PRESUBMIT.py in all candidate directories.
883 results = []
884 for directory in sorted(list(candidates)):
885 p = os.path.join(directory, 'PRESUBMIT.py')
886 if os.path.isfile(p):
887 results.append(p)
888
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000889 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000890 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000891
892
thestig@chromium.orgde243452009-10-06 21:02:56 +0000893class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000894 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000895 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000896 """Executes GetPreferredTrySlaves() from a single presubmit script.
897
898 Args:
899 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000900 presubmit_path: Project script to run.
901 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000902
903 Return:
904 A list of try slaves.
905 """
906 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000907 try:
908 exec script_text in context
909 except Exception, e:
910 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000911
912 function_name = 'GetPreferredTrySlaves'
913 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000914 get_preferred_try_slaves = context[function_name]
915 function_info = inspect.getargspec(get_preferred_try_slaves)
916 if len(function_info[0]) == 1:
917 result = get_preferred_try_slaves(project)
918 elif len(function_info[0]) == 2:
919 result = get_preferred_try_slaves(project, change)
920 else:
921 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +0000922 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000923 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000924 'Presubmit functions must return a list, got a %s instead: %s' %
925 (type(result), str(result)))
926 for item in result:
927 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000928 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000929 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000930 raise PresubmitFailure(
931 'Try slave names cannot start/end with whitespace')
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +0000932 if ',' in item:
933 raise PresubmitFailure(
934 'Do not use \',\' separated builder or test names: %s' % item)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000935 else:
936 result = []
937 return result
938
939
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000940def DoGetTrySlaves(change,
941 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000942 repository_root,
943 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000944 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000945 verbose,
946 output_stream):
947 """Get the list of try servers from the presubmit scripts.
948
949 Args:
950 changed_files: List of modified files.
951 repository_root: The repository root.
952 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000953 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000954 verbose: Prints debug info.
955 output_stream: A stream to write debug output to.
956
957 Return:
958 List of try slaves
959 """
960 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
961 if not presubmit_files and verbose:
962 output_stream.write("Warning, no presubmit.py found.\n")
963 results = []
964 executer = GetTrySlavesExecuter()
965 if default_presubmit:
966 if verbose:
967 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000968 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000969 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000970 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000971 for filename in presubmit_files:
972 filename = os.path.abspath(filename)
973 if verbose:
974 output_stream.write("Running %s\n" % filename)
975 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000976 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000977 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000978 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000979
980 slaves = list(set(results))
981 if slaves and verbose:
982 output_stream.write(', '.join(slaves))
983 output_stream.write('\n')
984 return slaves
985
986
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000987class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000988 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000989 """
990 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000991 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000993 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000994 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000995 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000997 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000998 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000999
1000 def ExecPresubmitScript(self, script_text, presubmit_path):
1001 """Executes a single presubmit script.
1002
1003 Args:
1004 script_text: The text of the presubmit script.
1005 presubmit_path: The path to the presubmit file (this will be reported via
1006 input_api.PresubmitLocalPath()).
1007
1008 Return:
1009 A list of result objects, empty if no problems.
1010 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001011
1012 # Change to the presubmit file's directory to support local imports.
1013 main_path = os.getcwd()
1014 os.chdir(os.path.dirname(presubmit_path))
1015
1016 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001017 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001018 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001019 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001020 try:
1021 exec script_text in context
1022 except Exception, e:
1023 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024
1025 # These function names must change if we make substantial changes to
1026 # the presubmit API that are not backwards compatible.
1027 if self.committing:
1028 function_name = 'CheckChangeOnCommit'
1029 else:
1030 function_name = 'CheckChangeOnUpload'
1031 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001032 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001033 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001034 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001035 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036 if not (isinstance(result, types.TupleType) or
1037 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001038 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001039 'Presubmit functions must return a tuple or list')
1040 for item in result:
1041 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001042 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001043 'All presubmit results must be of types derived from '
1044 'output_api.PresubmitResult')
1045 else:
1046 result = () # no error since the script doesn't care about current event.
1047
chase@chromium.org8e416c82009-10-06 04:30:44 +00001048 # Return the process to the original working directory.
1049 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001050 return result
1051
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001052
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001053def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001054 committing,
1055 verbose,
1056 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001057 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001058 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001059 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001060 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001061 """Runs all presubmit checks that apply to the files in the change.
1062
1063 This finds all PRESUBMIT.py files in directories enclosing the files in the
1064 change (up to the repository root) and calls the relevant entrypoint function
1065 depending on whether the change is being committed or uploaded.
1066
1067 Prints errors, warnings and notifications. Prompts the user for warnings
1068 when needed.
1069
1070 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001071 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001072 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1073 verbose: Prints debug info.
1074 output_stream: A stream to write output from presubmit tests to.
1075 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001076 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001077 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001078 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001079
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001080 Warning:
1081 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1082 SHOULD be sys.stdin.
1083
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001084 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001085 A PresubmitOutput object. Use output.should_continue() to figure out
1086 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001087 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001088 old_environ = os.environ
1089 try:
1090 # Make sure python subprocesses won't generate .pyc files.
1091 os.environ = os.environ.copy()
1092 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001093
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001094 output = PresubmitOutput(input_stream, output_stream)
1095 if committing:
1096 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001097 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001098 output.write("Running presubmit upload checks ...\n")
1099 start_time = time.time()
1100 presubmit_files = ListRelevantPresubmitFiles(
1101 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1102 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001103 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001104 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001105 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001106 if default_presubmit:
1107 if verbose:
1108 output.write("Running default presubmit script.\n")
1109 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1110 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1111 for filename in presubmit_files:
1112 filename = os.path.abspath(filename)
1113 if verbose:
1114 output.write("Running %s\n" % filename)
1115 # Accept CRLF presubmit script.
1116 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1117 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001118
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001119 errors = []
1120 notifications = []
1121 warnings = []
1122 for result in results:
1123 if result.fatal:
1124 errors.append(result)
1125 elif result.should_prompt:
1126 warnings.append(result)
1127 else:
1128 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001129
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001130 output.write('\n')
1131 for name, items in (('Messages', notifications),
1132 ('Warnings', warnings),
1133 ('ERRORS', errors)):
1134 if items:
1135 output.write('** Presubmit %s **\n' % name)
1136 for item in items:
1137 item.handle(output)
1138 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001139
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001140 total_time = time.time() - start_time
1141 if total_time > 1.0:
1142 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001143
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001144 if not errors:
1145 if not warnings:
1146 output.write('Presubmit checks passed.\n')
1147 elif may_prompt:
1148 output.prompt_yes_no('There were presubmit warnings. '
1149 'Are you sure you wish to continue? (y/N): ')
1150 else:
1151 output.fail()
1152
1153 global _ASKED_FOR_FEEDBACK
1154 # Ask for feedback one time out of 5.
1155 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1156 output.write("Was the presubmit check useful? Please send feedback "
1157 "& hate mail to maruel@chromium.org!\n")
1158 _ASKED_FOR_FEEDBACK = True
1159 return output
1160 finally:
1161 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001162
1163
1164def ScanSubDirs(mask, recursive):
1165 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001166 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 +00001167 else:
1168 results = []
1169 for root, dirs, files in os.walk('.'):
1170 if '.svn' in dirs:
1171 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001172 if '.git' in dirs:
1173 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001174 for name in files:
1175 if fnmatch.fnmatch(name, mask):
1176 results.append(os.path.join(root, name))
1177 return results
1178
1179
1180def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001181 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001182 files = []
1183 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001184 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001185 return files
1186
1187
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001188def load_files(options, args):
1189 """Tries to determine the SCM."""
1190 change_scm = scm.determine_scm(options.root)
1191 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001192 if args:
1193 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001194 if change_scm == 'svn':
1195 change_class = SvnChange
1196 if not files:
1197 files = scm.SVN.CaptureStatus([], options.root)
1198 elif change_scm == 'git':
1199 change_class = GitChange
1200 # TODO(maruel): Get upstream.
1201 if not files:
1202 files = scm.GIT.CaptureStatus([], options.root, None)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001203 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001204 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1205 if not files:
1206 return None, None
1207 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001208 return change_class, files
1209
1210
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001211class NonexistantCannedCheckFilter(Exception):
1212 pass
1213
1214
1215@contextlib.contextmanager
1216def canned_check_filter(method_names):
1217 filtered = {}
1218 try:
1219 for method_name in method_names:
1220 if not hasattr(presubmit_canned_checks, method_name):
1221 raise NonexistantCannedCheckFilter(method_name)
1222 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1223 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1224 yield
1225 finally:
1226 for name, method in filtered.iteritems():
1227 setattr(presubmit_canned_checks, name, method)
1228
1229
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001230def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001231 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001232 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001233 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001234 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001235 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1236 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001237 parser.add_option("-r", "--recursive", action="store_true",
1238 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001239 parser.add_option("-v", "--verbose", action="count", default=0,
1240 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001241 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001242 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001243 parser.add_option("--description", default='')
1244 parser.add_option("--issue", type='int', default=0)
1245 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001246 parser.add_option("--root", default=os.getcwd(),
1247 help="Search for PRESUBMIT.py up to this directory. "
1248 "If inherit-review-settings-ok is present in this "
1249 "directory, parent directories up to the root file "
1250 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001251 parser.add_option("--default_presubmit")
1252 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001253 parser.add_option("--skip_canned", action='append', default=[],
1254 help="A list of checks to skip which appear in "
1255 "presubmit_canned_checks. Can be provided multiple times "
1256 "to skip multiple canned checks.")
maruel@chromium.org239f4112011-06-03 20:08:23 +00001257 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1258 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1259 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001260 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001261 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001262 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001263 elif options.verbose:
1264 logging.basicConfig(level=logging.INFO)
1265 else:
1266 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001267 change_class, files = load_files(options, args)
1268 if not change_class:
1269 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001270 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001271 rietveld_obj = None
1272 if options.rietveld_url:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001273 rietveld_obj = rietveld.CachingRietveld(
maruel@chromium.org239f4112011-06-03 20:08:23 +00001274 options.rietveld_url,
1275 options.rietveld_email,
1276 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001277 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001278 with canned_check_filter(options.skip_canned):
1279 results = DoPresubmitChecks(
1280 change_class(options.name,
1281 options.description,
1282 options.root,
1283 files,
1284 options.issue,
1285 options.patchset,
1286 options.author),
1287 options.commit,
1288 options.verbose,
1289 sys.stdout,
1290 sys.stdin,
1291 options.default_presubmit,
1292 options.may_prompt,
1293 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001294 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001295 except NonexistantCannedCheckFilter, e:
1296 print >> sys.stderr, (
1297 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1298 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001299 except PresubmitFailure, e:
1300 print >> sys.stderr, e
1301 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1302 print >> sys.stderr, 'If all fails, contact maruel@'
1303 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001304
1305
1306if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001307 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001308 sys.exit(Main(None))