blob: 3239d7b2eb8a2d206e3050449777ca1ed6847340 [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):
107 """This class (more like a module) gets passed to presubmit scripts so that
108 they can specify various types of results.
109 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000110 class PresubmitResult(object):
111 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000112 fatal = False
113 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000114
115 def __init__(self, message, items=None, long_text=''):
116 """
117 message: A short one-line message to indicate errors.
118 items: A list of short strings to indicate where errors occurred.
119 long_text: multi-line text output, e.g. from another tool
120 """
121 self._message = message
122 self._items = []
123 if items:
124 self._items = items
125 self._long_text = long_text.rstrip()
126
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000127 def handle(self, output):
128 output.write(self._message)
129 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000130 for index, item in enumerate(self._items):
131 output.write(' ')
132 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000133 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000134 if index < len(self._items) - 1:
135 output.write(' \\')
136 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000137 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000138 output.write('\n***************\n')
139 # Write separately in case it's unicode.
140 output.write(self._long_text)
141 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000142 if self.fatal:
143 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000144
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000145 class PresubmitAddReviewers(PresubmitResult):
146 """Add some suggested reviewers to the change."""
147 def __init__(self, reviewers):
148 super(OutputApi.PresubmitAddReviewers, self).__init__('')
149 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000150
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000151 def handle(self, output):
152 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000153
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000154 class PresubmitError(PresubmitResult):
155 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000156 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000157
158 class PresubmitPromptWarning(PresubmitResult):
159 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000160 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000161
162 class PresubmitNotifyResult(PresubmitResult):
163 """Just print something to the screen -- but it's not even a warning."""
164 pass
165
166 class MailTextResult(PresubmitResult):
167 """A warning that should be included in the review request email."""
168 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000169 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000170 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000171
172
173class InputApi(object):
174 """An instance of this object is passed to presubmit scripts so they can
175 know stuff about the change they're looking at.
176 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000177 # Method could be a function
178 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000179
maruel@chromium.org3410d912009-06-09 20:56:16 +0000180 # File extensions that are considered source files from a style guide
181 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000182 #
183 # Files without an extension aren't included in the list. If you want to
184 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
185 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000186 DEFAULT_WHITE_LIST = (
187 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000188 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
189 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000190 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000191 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000192 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000193 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000194 )
195
196 # Path regexp that should be excluded from being considered containing source
197 # files. Don't modify this list from a presubmit script!
198 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000199 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000200 r".*\bexperimental[\\\/].*",
201 r".*\bthird_party[\\\/].*",
202 # Output directories (just in case)
203 r".*\bDebug[\\\/].*",
204 r".*\bRelease[\\\/].*",
205 r".*\bxcodebuild[\\\/].*",
206 r".*\bsconsbuild[\\\/].*",
207 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000208 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000209 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000210 r"(|.*[\\\/])\.git[\\\/].*",
211 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000212 # There is no point in processing a patch file.
213 r".+\.diff$",
214 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000215 )
216
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000217 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000218 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000219 """Builds an InputApi object.
220
221 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000222 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000223 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000224 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000225 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000227 # Version number of the presubmit_support script.
228 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000229 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000230 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000231 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000232 # TBD
233 self.host_url = 'http://codereview.chromium.org'
234 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000235 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000236
237 # We expose various modules and functions as attributes of the input_api
238 # so that presubmit scripts don't have to import them.
239 self.basename = os.path.basename
240 self.cPickle = cPickle
241 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000242 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000243 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000244 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000245 self.os_listdir = os.listdir
246 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000247 self.os_path = os.path
248 self.pickle = pickle
249 self.marshal = marshal
250 self.re = re
251 self.subprocess = subprocess
252 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000253 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000254 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000255 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256 self.urllib2 = urllib2
257
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000258 # To easily fork python.
259 self.python_executable = sys.executable
260 self.environ = os.environ
261
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000262 # InputApi.platform is the platform you're currently running on.
263 self.platform = sys.platform
264
265 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000266 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267
268 # We carry the canned checks so presubmit scripts can easily use them.
269 self.canned_checks = presubmit_canned_checks
270
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000271 # TODO(dpranke): figure out a list of all approved owners for a repo
272 # in order to be able to handle wildcard OWNERS files?
273 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000274 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000275 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000276
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000277 def PresubmitLocalPath(self):
278 """Returns the local path of the presubmit script currently being run.
279
280 This is useful if you don't want to hard-code absolute paths in the
281 presubmit script. For example, It can be used to find another file
282 relative to the PRESUBMIT.py script, so the whole tree can be branched and
283 the presubmit script still works, without editing its content.
284 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000285 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000287 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000288 """Translate a depot path to a local path (relative to client root).
289
290 Args:
291 Depot path as a string.
292
293 Returns:
294 The local path of the depot path under the user's current client, or None
295 if the file is not mapped.
296
297 Remember to check for the None case and show an appropriate error!
298 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000299 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
300 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000301
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000302 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000303 """Translate a local path to a depot path.
304
305 Args:
306 Local path (relative to current directory, or absolute) as a string.
307
308 Returns:
309 The depot path (SVN URL) of the file if mapped, otherwise None.
310 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000311 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
312 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000313
sail@chromium.org5538e022011-05-12 17:53:16 +0000314 def AffectedFiles(self, include_dirs=False, include_deletes=True,
315 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000316 """Same as input_api.change.AffectedFiles() except only lists files
317 (and optionally directories) in the same directory as the current presubmit
318 script, or subdirectories thereof.
319 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000320 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000321 if len(dir_with_slash) == 1:
322 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000323
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000324 return filter(
325 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000326 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000327
328 def LocalPaths(self, include_dirs=False):
329 """Returns local paths of input_api.AffectedFiles()."""
330 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
331
332 def AbsoluteLocalPaths(self, include_dirs=False):
333 """Returns absolute local paths of input_api.AffectedFiles()."""
334 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
335
336 def ServerPaths(self, include_dirs=False):
337 """Returns server paths of input_api.AffectedFiles()."""
338 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
339
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000340 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000341 """Same as input_api.change.AffectedTextFiles() except only lists files
342 in the same directory as the current presubmit script, or subdirectories
343 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000345 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000346 warn("AffectedTextFiles(include_deletes=%s)"
347 " is deprecated and ignored" % str(include_deletes),
348 category=DeprecationWarning,
349 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000350 return filter(lambda x: x.IsTextFile(),
351 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000352
maruel@chromium.org3410d912009-06-09 20:56:16 +0000353 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
354 """Filters out files that aren't considered "source file".
355
356 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
357 and InputApi.DEFAULT_BLACK_LIST is used respectively.
358
359 The lists will be compiled as regular expression and
360 AffectedFile.LocalPath() needs to pass both list.
361
362 Note: Copy-paste this function to suit your needs or use a lambda function.
363 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000364 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000365 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000366 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000367 if self.re.match(item, local_path):
368 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000369 return True
370 return False
371 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
372 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
373
374 def AffectedSourceFiles(self, source_file):
375 """Filter the list of AffectedTextFiles by the function source_file.
376
377 If source_file is None, InputApi.FilterSourceFile() is used.
378 """
379 if not source_file:
380 source_file = self.FilterSourceFile
381 return filter(source_file, self.AffectedTextFiles())
382
383 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000384 """An iterator over all text lines in "new" version of changed files.
385
386 Only lists lines from new or modified text files in the change that are
387 contained by the directory of the currently executing presubmit script.
388
389 This is useful for doing line-by-line regex checks, like checking for
390 trailing whitespace.
391
392 Yields:
393 a 3 tuple:
394 the AffectedFile instance of the current file;
395 integer line number (1-based); and
396 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000397
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000398 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000399 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000400 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000401 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000403 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000404 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000405
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000406 Deny reading anything outside the repository.
407 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000408 if isinstance(file_item, AffectedFile):
409 file_item = file_item.AbsoluteLocalPath()
410 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000411 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000412 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000413
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000414 @property
415 def tbr(self):
416 """Returns if a change is TBR'ed."""
417 return 'TBR' in self.change.tags
418
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000419
420class AffectedFile(object):
421 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000422 # Method could be a function
423 # pylint: disable=R0201
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000424 def __init__(self, path, action, repository_root):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000425 self._path = path
426 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000427 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000428 self._is_directory = None
429 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000430 self._cached_changed_contents = None
431 self._cached_new_contents = None
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000432 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433
434 def ServerPath(self):
435 """Returns a path string that identifies the file in the SCM system.
436
437 Returns the empty string if the file does not exist in SCM.
438 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000439 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000440
441 def LocalPath(self):
442 """Returns the path of this file on the local disk relative to client root.
443 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000444 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000445
446 def AbsoluteLocalPath(self):
447 """Returns the absolute path of this file on the local disk.
448 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000449 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000450
451 def IsDirectory(self):
452 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000453 if self._is_directory is None:
454 path = self.AbsoluteLocalPath()
455 self._is_directory = (os.path.exists(path) and
456 os.path.isdir(path))
457 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458
459 def Action(self):
460 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000461 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
462 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000463 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000464
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000465 def Property(self, property_name):
466 """Returns the specified SCM property of this file, or None if no such
467 property.
468 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000469 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000470
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000471 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000472 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000473
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000474 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000475 raise NotImplementedError() # Implement when needed
476
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000477 def NewContents(self):
478 """Returns an iterator over the lines in the new version of file.
479
480 The new version is the file in the user's workspace, i.e. the "right hand
481 side".
482
483 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000484 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000485 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000486 if self._cached_new_contents is None:
487 self._cached_new_contents = []
488 if not self.IsDirectory():
489 try:
490 self._cached_new_contents = gclient_utils.FileRead(
491 self.AbsoluteLocalPath(), 'rU').splitlines()
492 except IOError:
493 pass # File not found? That's fine; maybe it was deleted.
494 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000495
496 def OldContents(self):
497 """Returns an iterator over the lines in the old version of file.
498
499 The old version is the file in depot, i.e. the "left hand side".
500 """
501 raise NotImplementedError() # Implement when needed
502
503 def OldFileTempPath(self):
504 """Returns the path on local disk where the old contents resides.
505
506 The old version is the file in depot, i.e. the "left hand side".
507 This is a read-only cached copy of the old contents. *DO NOT* try to
508 modify this file.
509 """
510 raise NotImplementedError() # Implement if/when needed.
511
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000512 def ChangedContents(self):
513 """Returns a list of tuples (line number, line text) of all new lines.
514
515 This relies on the scm diff output describing each changed code section
516 with a line of the form
517
518 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
519 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000520 if self._cached_changed_contents is not None:
521 return self._cached_changed_contents[:]
522 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000523 line_num = 0
524
525 if self.IsDirectory():
526 return []
527
528 for line in self.GenerateScmDiff().splitlines():
529 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
530 if m:
531 line_num = int(m.groups(1)[0])
532 continue
533 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000534 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000535 if not line.startswith('-'):
536 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000537 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000538
maruel@chromium.org5de13972009-06-10 18:16:06 +0000539 def __str__(self):
540 return self.LocalPath()
541
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000542 def GenerateScmDiff(self):
543 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000544
maruel@chromium.org58407af2011-04-12 23:15:57 +0000545
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000546class SvnAffectedFile(AffectedFile):
547 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000548 # Method 'NNN' is abstract in class 'NNN' but is not overridden
549 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000550
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000551 def __init__(self, *args, **kwargs):
552 AffectedFile.__init__(self, *args, **kwargs)
553 self._server_path = None
554 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000555 self._diff = None
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000556
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000557 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000558 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000559 self._server_path = scm.SVN.CaptureLocalInfo(
560 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000561 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000562
563 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000564 if self._is_directory is None:
565 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000566 if os.path.exists(path):
567 # Retrieve directly from the file system; it is much faster than
568 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000569 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000570 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000571 self._is_directory = scm.SVN.CaptureLocalInfo(
572 [self.LocalPath()], self._local_root
573 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000574 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000575
576 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000577 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000578 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000579 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000580 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000581
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000582 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000583 if self._is_text_file is None:
584 if self.Action() == 'D':
585 # A deleted file is not a text file.
586 self._is_text_file = False
587 elif self.IsDirectory():
588 self._is_text_file = False
589 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000590 mime_type = scm.SVN.GetFileProperty(
591 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000592 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
593 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000594
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000595 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000596 if self._diff is None:
597 self._diff = scm.SVN.GenerateDiff(
598 [self.LocalPath()], self._local_root, False, None)
599 return self._diff
maruel@chromium.org1f312812011-02-10 01:33:57 +0000600
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000601
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000602class GitAffectedFile(AffectedFile):
603 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000604 # Method 'NNN' is abstract in class 'NNN' but is not overridden
605 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000606
607 def __init__(self, *args, **kwargs):
608 AffectedFile.__init__(self, *args, **kwargs)
609 self._server_path = None
610 self._is_text_file = None
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000611 self._diff = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000612
613 def ServerPath(self):
614 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000615 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000616 return self._server_path
617
618 def IsDirectory(self):
619 if self._is_directory is None:
620 path = self.AbsoluteLocalPath()
621 if os.path.exists(path):
622 # Retrieve directly from the file system; it is much faster than
623 # querying subversion, especially on Windows.
624 self._is_directory = os.path.isdir(path)
625 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000626 self._is_directory = False
627 return self._is_directory
628
629 def Property(self, property_name):
630 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000631 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000632 return self._properties[property_name]
633
634 def IsTextFile(self):
635 if self._is_text_file is None:
636 if self.Action() == 'D':
637 # A deleted file is not a text file.
638 self._is_text_file = False
639 elif self.IsDirectory():
640 self._is_text_file = False
641 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000642 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
643 return self._is_text_file
644
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000645 def GenerateScmDiff(self):
maruel@chromium.org3bbf2942012-01-10 16:52:06 +0000646 if self._diff is None:
647 self._diff = scm.GIT.GenerateDiff(
648 self._local_root, files=[self.LocalPath(),])
649 return self._diff
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000650
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000651
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000652class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000653 """Describe a change.
654
655 Used directly by the presubmit scripts to query the current change being
656 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000657
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000658 Instance members:
659 tags: Dictionnary of KEY=VALUE pairs found in the change description.
660 self.KEY: equivalent to tags['KEY']
661 """
662
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000663 _AFFECTED_FILES = AffectedFile
664
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000665 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000666 TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000667 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000668 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000669
maruel@chromium.org58407af2011-04-12 23:15:57 +0000670 def __init__(
671 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000672 if files is None:
673 files = []
674 self._name = name
675 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000676 # Convert root into an absolute path.
677 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000678 self.issue = issue
679 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000680 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681
682 # From the description text, build up a dictionary of key/value pairs
683 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000684 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000686 for line in self._full_description.splitlines():
maruel@chromium.org428342a2011-11-10 15:46:33 +0000687 m = self.TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688 if m:
689 self.tags[m.group('key')] = m.group('value')
690 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000691 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000692
693 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000694 self._description_without_tags = (
695 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000696
maruel@chromium.orge085d812011-10-10 19:49:15 +0000697 assert all(
698 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
699
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000700 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000701 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
702 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000703 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000705 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000707 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000708
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709 def DescriptionText(self):
710 """Returns the user-entered changelist description, minus tags.
711
712 Any line in the user-provided description starting with e.g. "FOO="
713 (whitespace permitted before and around) is considered a tag line. Such
714 lines are stripped out of the description this function returns.
715 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000716 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717
718 def FullDescriptionText(self):
719 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000720 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000721
722 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000723 """Returns the repository (checkout) root directory for this change,
724 as an absolute path.
725 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000726 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727
728 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000729 """Return tags directly as attributes on the object."""
730 if not re.match(r"^[A-Z_]*$", attr):
731 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000732 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000733
sail@chromium.org5538e022011-05-12 17:53:16 +0000734 def AffectedFiles(self, include_dirs=False, include_deletes=True,
735 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736 """Returns a list of AffectedFile instances for all files in the change.
737
738 Args:
739 include_deletes: If false, deleted files will be filtered out.
740 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000741 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742
743 Returns:
744 [AffectedFile(path, action), AffectedFile(path, action)]
745 """
746 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000747 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000749 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000750
sail@chromium.org5538e022011-05-12 17:53:16 +0000751 affected = filter(file_filter, affected)
752
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000753 if include_deletes:
754 return affected
755 else:
756 return filter(lambda x: x.Action() != 'D', affected)
757
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000758 def AffectedTextFiles(self, include_deletes=None):
759 """Return a list of the existing text files in a change."""
760 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000761 warn("AffectedTextFiles(include_deletes=%s)"
762 " is deprecated and ignored" % str(include_deletes),
763 category=DeprecationWarning,
764 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000765 return filter(lambda x: x.IsTextFile(),
766 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000767
768 def LocalPaths(self, include_dirs=False):
769 """Convenience function."""
770 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
771
772 def AbsoluteLocalPaths(self, include_dirs=False):
773 """Convenience function."""
774 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
775
776 def ServerPaths(self, include_dirs=False):
777 """Convenience function."""
778 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
779
780 def RightHandSideLines(self):
781 """An iterator over all text lines in "new" version of changed files.
782
783 Lists lines from new or modified text files in the change.
784
785 This is useful for doing line-by-line regex checks, like checking for
786 trailing whitespace.
787
788 Yields:
789 a 3 tuple:
790 the AffectedFile instance of the current file;
791 integer line number (1-based); and
792 the contents of the line as a string.
793 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000794 return _RightHandSideLinesImpl(
795 x for x in self.AffectedFiles(include_deletes=False)
796 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000797
798
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000799class SvnChange(Change):
800 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000801 scm = 'svn'
802 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000803
804 def _GetChangeLists(self):
805 """Get all change lists."""
806 if self._changelists == None:
807 previous_cwd = os.getcwd()
808 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000809 # Need to import here to avoid circular dependency.
810 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000811 self._changelists = gcl.GetModifiedFiles()
812 os.chdir(previous_cwd)
813 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000814
815 def GetAllModifiedFiles(self):
816 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000817 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000818 all_modified_files = []
819 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000820 all_modified_files.extend(
821 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000822 return all_modified_files
823
824 def GetModifiedFiles(self):
825 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000826 changelists = self._GetChangeLists()
827 return [os.path.join(self.RepositoryRoot(), f[1])
828 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000829
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000830
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000831class GitChange(Change):
832 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000833 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000834
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000835
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000836def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000837 """Finds all presubmit files that apply to a given set of source files.
838
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000839 If inherit-review-settings-ok is present right under root, looks for
840 PRESUBMIT.py in directories enclosing root.
841
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842 Args:
843 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000844 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000845
846 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000847 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000848 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000849 files = [normpath(os.path.join(root, f)) for f in files]
850
851 # List all the individual directories containing files.
852 directories = set([os.path.dirname(f) for f in files])
853
854 # Ignore root if inherit-review-settings-ok is present.
855 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
856 root = None
857
858 # Collect all unique directories that may contain PRESUBMIT.py.
859 candidates = set()
860 for directory in directories:
861 while True:
862 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000863 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000864 candidates.add(directory)
865 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000866 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000867 parent_dir = os.path.dirname(directory)
868 if parent_dir == directory:
869 # We hit the system root directory.
870 break
871 directory = parent_dir
872
873 # Look for PRESUBMIT.py in all candidate directories.
874 results = []
875 for directory in sorted(list(candidates)):
876 p = os.path.join(directory, 'PRESUBMIT.py')
877 if os.path.isfile(p):
878 results.append(p)
879
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000880 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000881 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000882
883
thestig@chromium.orgde243452009-10-06 21:02:56 +0000884class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000885 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000886 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000887 """Executes GetPreferredTrySlaves() from a single presubmit script.
888
889 Args:
890 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +0000891 presubmit_path: Project script to run.
892 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000893
894 Return:
895 A list of try slaves.
896 """
897 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000898 try:
899 exec script_text in context
900 except Exception, e:
901 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000902
903 function_name = 'GetPreferredTrySlaves'
904 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000905 get_preferred_try_slaves = context[function_name]
906 function_info = inspect.getargspec(get_preferred_try_slaves)
907 if len(function_info[0]) == 1:
908 result = get_preferred_try_slaves(project)
909 elif len(function_info[0]) == 2:
910 result = get_preferred_try_slaves(project, change)
911 else:
912 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +0000913 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000914 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000915 'Presubmit functions must return a list, got a %s instead: %s' %
916 (type(result), str(result)))
917 for item in result:
918 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000919 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000920 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000921 raise PresubmitFailure(
922 'Try slave names cannot start/end with whitespace')
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +0000923 if ',' in item:
924 raise PresubmitFailure(
925 'Do not use \',\' separated builder or test names: %s' % item)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000926 else:
927 result = []
928 return result
929
930
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000931def DoGetTrySlaves(change,
932 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000933 repository_root,
934 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +0000935 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +0000936 verbose,
937 output_stream):
938 """Get the list of try servers from the presubmit scripts.
939
940 Args:
941 changed_files: List of modified files.
942 repository_root: The repository root.
943 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +0000944 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +0000945 verbose: Prints debug info.
946 output_stream: A stream to write debug output to.
947
948 Return:
949 List of try slaves
950 """
951 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
952 if not presubmit_files and verbose:
953 output_stream.write("Warning, no presubmit.py found.\n")
954 results = []
955 executer = GetTrySlavesExecuter()
956 if default_presubmit:
957 if verbose:
958 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000959 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
bradnelson@google.com78230022011-05-24 18:55:19 +0000960 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000961 default_presubmit, fake_path, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000962 for filename in presubmit_files:
963 filename = os.path.abspath(filename)
964 if verbose:
965 output_stream.write("Running %s\n" % filename)
966 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000967 presubmit_script = gclient_utils.FileRead(filename, 'rU')
bradnelson@google.com78230022011-05-24 18:55:19 +0000968 results += executer.ExecPresubmitScript(
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000969 presubmit_script, filename, project, change)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000970
971 slaves = list(set(results))
972 if slaves and verbose:
973 output_stream.write(', '.join(slaves))
974 output_stream.write('\n')
975 return slaves
976
977
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000979 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980 """
981 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000982 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000984 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000985 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000986 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000987 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000988 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000989 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000990
991 def ExecPresubmitScript(self, script_text, presubmit_path):
992 """Executes a single presubmit script.
993
994 Args:
995 script_text: The text of the presubmit script.
996 presubmit_path: The path to the presubmit file (this will be reported via
997 input_api.PresubmitLocalPath()).
998
999 Return:
1000 A list of result objects, empty if no problems.
1001 """
chase@chromium.org8e416c82009-10-06 04:30:44 +00001002
1003 # Change to the presubmit file's directory to support local imports.
1004 main_path = os.getcwd()
1005 os.chdir(os.path.dirname(presubmit_path))
1006
1007 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001008 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001009 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001011 try:
1012 exec script_text in context
1013 except Exception, e:
1014 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015
1016 # These function names must change if we make substantial changes to
1017 # the presubmit API that are not backwards compatible.
1018 if self.committing:
1019 function_name = 'CheckChangeOnCommit'
1020 else:
1021 function_name = 'CheckChangeOnUpload'
1022 if function_name in context:
1023 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001024 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001025 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001026 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001027 if not (isinstance(result, types.TupleType) or
1028 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001029 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001030 'Presubmit functions must return a tuple or list')
1031 for item in result:
1032 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001033 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001034 'All presubmit results must be of types derived from '
1035 'output_api.PresubmitResult')
1036 else:
1037 result = () # no error since the script doesn't care about current event.
1038
chase@chromium.org8e416c82009-10-06 04:30:44 +00001039 # Return the process to the original working directory.
1040 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001041 return result
1042
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001043
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001044def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001045 committing,
1046 verbose,
1047 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001048 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001049 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001050 may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001051 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001052 """Runs all presubmit checks that apply to the files in the change.
1053
1054 This finds all PRESUBMIT.py files in directories enclosing the files in the
1055 change (up to the repository root) and calls the relevant entrypoint function
1056 depending on whether the change is being committed or uploaded.
1057
1058 Prints errors, warnings and notifications. Prompts the user for warnings
1059 when needed.
1060
1061 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001062 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001063 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1064 verbose: Prints debug info.
1065 output_stream: A stream to write output from presubmit tests to.
1066 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001067 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001068 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001069 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001070
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001071 Warning:
1072 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1073 SHOULD be sys.stdin.
1074
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001075 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001076 A PresubmitOutput object. Use output.should_continue() to figure out
1077 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001078 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001079 old_environ = os.environ
1080 try:
1081 # Make sure python subprocesses won't generate .pyc files.
1082 os.environ = os.environ.copy()
1083 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001084
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001085 output = PresubmitOutput(input_stream, output_stream)
1086 if committing:
1087 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001088 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001089 output.write("Running presubmit upload checks ...\n")
1090 start_time = time.time()
1091 presubmit_files = ListRelevantPresubmitFiles(
1092 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1093 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001094 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001095 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001096 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001097 if default_presubmit:
1098 if verbose:
1099 output.write("Running default presubmit script.\n")
1100 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1101 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1102 for filename in presubmit_files:
1103 filename = os.path.abspath(filename)
1104 if verbose:
1105 output.write("Running %s\n" % filename)
1106 # Accept CRLF presubmit script.
1107 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1108 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001109
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001110 errors = []
1111 notifications = []
1112 warnings = []
1113 for result in results:
1114 if result.fatal:
1115 errors.append(result)
1116 elif result.should_prompt:
1117 warnings.append(result)
1118 else:
1119 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001120
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001121 output.write('\n')
1122 for name, items in (('Messages', notifications),
1123 ('Warnings', warnings),
1124 ('ERRORS', errors)):
1125 if items:
1126 output.write('** Presubmit %s **\n' % name)
1127 for item in items:
1128 item.handle(output)
1129 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001130
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001131 total_time = time.time() - start_time
1132 if total_time > 1.0:
1133 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001134
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001135 if not errors:
1136 if not warnings:
1137 output.write('Presubmit checks passed.\n')
1138 elif may_prompt:
1139 output.prompt_yes_no('There were presubmit warnings. '
1140 'Are you sure you wish to continue? (y/N): ')
1141 else:
1142 output.fail()
1143
1144 global _ASKED_FOR_FEEDBACK
1145 # Ask for feedback one time out of 5.
1146 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1147 output.write("Was the presubmit check useful? Please send feedback "
1148 "& hate mail to maruel@chromium.org!\n")
1149 _ASKED_FOR_FEEDBACK = True
1150 return output
1151 finally:
1152 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001153
1154
1155def ScanSubDirs(mask, recursive):
1156 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001157 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 +00001158 else:
1159 results = []
1160 for root, dirs, files in os.walk('.'):
1161 if '.svn' in dirs:
1162 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001163 if '.git' in dirs:
1164 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001165 for name in files:
1166 if fnmatch.fnmatch(name, mask):
1167 results.append(os.path.join(root, name))
1168 return results
1169
1170
1171def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001172 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001173 files = []
1174 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001175 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001176 return files
1177
1178
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001179def load_files(options, args):
1180 """Tries to determine the SCM."""
1181 change_scm = scm.determine_scm(options.root)
1182 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001183 if args:
1184 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001185 if change_scm == 'svn':
1186 change_class = SvnChange
1187 if not files:
1188 files = scm.SVN.CaptureStatus([], options.root)
1189 elif change_scm == 'git':
1190 change_class = GitChange
1191 # TODO(maruel): Get upstream.
1192 if not files:
1193 files = scm.GIT.CaptureStatus([], options.root, None)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001194 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001195 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1196 if not files:
1197 return None, None
1198 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001199 return change_class, files
1200
1201
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001202class NonexistantCannedCheckFilter(Exception):
1203 pass
1204
1205
1206@contextlib.contextmanager
1207def canned_check_filter(method_names):
1208 filtered = {}
1209 try:
1210 for method_name in method_names:
1211 if not hasattr(presubmit_canned_checks, method_name):
1212 raise NonexistantCannedCheckFilter(method_name)
1213 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1214 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1215 yield
1216 finally:
1217 for name, method in filtered.iteritems():
1218 setattr(presubmit_canned_checks, name, method)
1219
1220
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001221def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001222 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001223 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001224 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001225 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001226 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1227 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001228 parser.add_option("-r", "--recursive", action="store_true",
1229 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001230 parser.add_option("-v", "--verbose", action="count", default=0,
1231 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001232 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001233 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001234 parser.add_option("--description", default='')
1235 parser.add_option("--issue", type='int', default=0)
1236 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001237 parser.add_option("--root", default=os.getcwd(),
1238 help="Search for PRESUBMIT.py up to this directory. "
1239 "If inherit-review-settings-ok is present in this "
1240 "directory, parent directories up to the root file "
1241 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001242 parser.add_option("--default_presubmit")
1243 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001244 parser.add_option("--skip_canned", action='append', default=[],
1245 help="A list of checks to skip which appear in "
1246 "presubmit_canned_checks. Can be provided multiple times "
1247 "to skip multiple canned checks.")
maruel@chromium.org239f4112011-06-03 20:08:23 +00001248 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1249 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1250 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001251 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001252 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001253 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001254 elif options.verbose:
1255 logging.basicConfig(level=logging.INFO)
1256 else:
1257 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001258 change_class, files = load_files(options, args)
1259 if not change_class:
1260 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001261 logging.info('Found %d file(s).' % len(files))
maruel@chromium.org239f4112011-06-03 20:08:23 +00001262 rietveld_obj = None
1263 if options.rietveld_url:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001264 rietveld_obj = rietveld.CachingRietveld(
maruel@chromium.org239f4112011-06-03 20:08:23 +00001265 options.rietveld_url,
1266 options.rietveld_email,
1267 options.rietveld_password)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001268 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001269 with canned_check_filter(options.skip_canned):
1270 results = DoPresubmitChecks(
1271 change_class(options.name,
1272 options.description,
1273 options.root,
1274 files,
1275 options.issue,
1276 options.patchset,
1277 options.author),
1278 options.commit,
1279 options.verbose,
1280 sys.stdout,
1281 sys.stdin,
1282 options.default_presubmit,
1283 options.may_prompt,
1284 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001285 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001286 except NonexistantCannedCheckFilter, e:
1287 print >> sys.stderr, (
1288 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1289 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001290 except PresubmitFailure, e:
1291 print >> sys.stderr, e
1292 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1293 print >> sys.stderr, 'If all fails, contact maruel@'
1294 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001295
1296
1297if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001298 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001299 sys.exit(Main(None))