blob: 50fabb102595408982b467bfef021d55a28510d4 [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00002# Copyright (c) 2010 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.orgb1901a62010-06-16 00:18:47 +00009__version__ = '1.3.5'
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.
17import exceptions
18import fnmatch
19import glob
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000020import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import marshal # Exposed through the API.
22import optparse
23import os # Somewhat exposed through the API.
24import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000025import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import re # Exposed through the API.
27import subprocess # Exposed through the API.
28import sys # Parts exposed through API.
29import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000030import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000031import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000033import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000035from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000037try:
38 import simplejson as json
39except ImportError:
40 try:
41 import json
tony@chromium.org5e9f2ed2010-04-08 01:38:19 +000042 # Some versions of python2.5 have an incomplete json module. Check to make
43 # sure loads exists.
44 json.loads
45 except (ImportError, AttributeError):
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000046 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000047 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
48 import simplejson as json
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000049
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050# Local imports.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000051import gclient_utils
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000053import scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000054
55
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000056# Ask for feedback only once in program lifetime.
57_ASKED_FOR_FEEDBACK = False
58
59
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060class NotImplementedException(Exception):
61 """We're leaving placeholders in a bunch of places to remind us of the
62 design of the API, but we have not implemented all of it yet. Implement as
63 the need arises.
64 """
65 pass
66
67
68def normpath(path):
69 '''Version of os.path.normpath that also changes backward slashes to
70 forward slashes when not running on Windows.
71 '''
72 # This is safe to always do because the Windows version of os.path.normpath
73 # will replace forward slashes with backward slashes.
74 path = path.replace(os.sep, '/')
75 return os.path.normpath(path)
76
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000077
gspencer@google.comefb94502009-10-09 17:57:08 +000078def PromptYesNo(input_stream, output_stream, prompt):
79 output_stream.write(prompt)
80 response = input_stream.readline().strip().lower()
81 return response == 'y' or response == 'yes'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000082
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000083
84def _RightHandSideLinesImpl(affected_files):
85 """Implements RightHandSideLines for InputApi and GclChange."""
86 for af in affected_files:
87 lines = af.NewContents()
88 line_number = 0
89 for line in lines:
90 line_number += 1
91 yield (af, line_number, line)
92
93
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000094class OutputApi(object):
95 """This class (more like a module) gets passed to presubmit scripts so that
96 they can specify various types of results.
97 """
98
99 class PresubmitResult(object):
100 """Base class for result objects."""
101
102 def __init__(self, message, items=None, long_text=''):
103 """
104 message: A short one-line message to indicate errors.
105 items: A list of short strings to indicate where errors occurred.
106 long_text: multi-line text output, e.g. from another tool
107 """
108 self._message = message
109 self._items = []
110 if items:
111 self._items = items
112 self._long_text = long_text.rstrip()
113
114 def _Handle(self, output_stream, input_stream, may_prompt=True):
115 """Writes this result to the output stream.
116
117 Args:
118 output_stream: Where to write
119
120 Returns:
121 True if execution may continue, False otherwise.
122 """
123 output_stream.write(self._message)
124 output_stream.write('\n')
estade@chromium.org593258b2010-01-28 00:04:36 +0000125 if len(self._items) > 0:
126 output_stream.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000127 if self._long_text:
maruel@chromium.org310b3552010-11-01 13:23:35 +0000128 # Sometimes self._long_text is a ascii string, a codepage string
129 # (on windows), or a unicode object.
130 try:
131 long_text = self._long_text.decode()
132 except UnicodeDecodeError:
133 long_text = self._long_text.decode('ascii', 'replace')
134
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000135 output_stream.write('\n***************\n%s\n***************\n' %
maruel@chromium.org310b3552010-11-01 13:23:35 +0000136 long_text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000137
138 if self.ShouldPrompt() and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000139 if not PromptYesNo(input_stream, output_stream,
140 'Are you sure you want to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000141 return False
142
143 return not self.IsFatal()
144
145 def IsFatal(self):
146 """An error that is fatal stops g4 mail/submit immediately, i.e. before
147 other presubmit scripts are run.
148 """
149 return False
150
151 def ShouldPrompt(self):
152 """Whether this presubmit result should result in a prompt warning."""
153 return False
154
155 class PresubmitError(PresubmitResult):
156 """A hard presubmit error."""
157 def IsFatal(self):
158 return True
159
160 class PresubmitPromptWarning(PresubmitResult):
161 """An warning that prompts the user if they want to continue."""
162 def ShouldPrompt(self):
163 return True
164
165 class PresubmitNotifyResult(PresubmitResult):
166 """Just print something to the screen -- but it's not even a warning."""
167 pass
168
169 class MailTextResult(PresubmitResult):
170 """A warning that should be included in the review request email."""
171 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000172 super(OutputApi.MailTextResult, self).__init__()
173 raise NotImplementedException()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000174
175
176class InputApi(object):
177 """An instance of this object is passed to presubmit scripts so they can
178 know stuff about the change they're looking at.
179 """
180
maruel@chromium.org3410d912009-06-09 20:56:16 +0000181 # File extensions that are considered source files from a style guide
182 # perspective. Don't modify this list from a presubmit script!
183 DEFAULT_WHITE_LIST = (
184 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000185 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
186 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000187 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000188 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000189 # No extension at all, note that ALL CAPS files are black listed in
190 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000191 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000192 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000193 r".*\.java$", r".*\.mk$", r".*\.am$",
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 = (
199 r".*\bexperimental[\\\/].*",
200 r".*\bthird_party[\\\/].*",
201 # Output directories (just in case)
202 r".*\bDebug[\\\/].*",
203 r".*\bRelease[\\\/].*",
204 r".*\bxcodebuild[\\\/].*",
205 r".*\bsconsbuild[\\\/].*",
206 # All caps files like README and LICENCE.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000207 r".*\b[A-Z0-9_]+$",
208 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
209 r".*\.git[\\\/].*",
210 r".*\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000211 )
212
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000213 def __init__(self, change, presubmit_path, is_committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000214 """Builds an InputApi object.
215
216 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000217 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000218 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000219 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000220 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000221 # Version number of the presubmit_support script.
222 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000223 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000224 self.is_committing = is_committing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000225
226 # We expose various modules and functions as attributes of the input_api
227 # so that presubmit scripts don't have to import them.
228 self.basename = os.path.basename
229 self.cPickle = cPickle
230 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000231 self.json = json
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000232 self.os_path = os.path
233 self.pickle = pickle
234 self.marshal = marshal
235 self.re = re
236 self.subprocess = subprocess
237 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000238 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000239 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000240 self.urllib2 = urllib2
241
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000242 # To easily fork python.
243 self.python_executable = sys.executable
244 self.environ = os.environ
245
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000246 # InputApi.platform is the platform you're currently running on.
247 self.platform = sys.platform
248
249 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000250 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000251
252 # We carry the canned checks so presubmit scripts can easily use them.
253 self.canned_checks = presubmit_canned_checks
254
255 def PresubmitLocalPath(self):
256 """Returns the local path of the presubmit script currently being run.
257
258 This is useful if you don't want to hard-code absolute paths in the
259 presubmit script. For example, It can be used to find another file
260 relative to the PRESUBMIT.py script, so the whole tree can be branched and
261 the presubmit script still works, without editing its content.
262 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000263 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000264
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000265 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000266 """Translate a depot path to a local path (relative to client root).
267
268 Args:
269 Depot path as a string.
270
271 Returns:
272 The local path of the depot path under the user's current client, or None
273 if the file is not mapped.
274
275 Remember to check for the None case and show an appropriate error!
276 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000277 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000278 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000279 return local_path
280
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000281 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282 """Translate a local path to a depot path.
283
284 Args:
285 Local path (relative to current directory, or absolute) as a string.
286
287 Returns:
288 The depot path (SVN URL) of the file if mapped, otherwise None.
289 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000290 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000291 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000292 return depot_path
293
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000294 def AffectedFiles(self, include_dirs=False, include_deletes=True):
295 """Same as input_api.change.AffectedFiles() except only lists files
296 (and optionally directories) in the same directory as the current presubmit
297 script, or subdirectories thereof.
298 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000299 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000300 if len(dir_with_slash) == 1:
301 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000302 return filter(
303 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
304 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305
306 def LocalPaths(self, include_dirs=False):
307 """Returns local paths of input_api.AffectedFiles()."""
308 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
309
310 def AbsoluteLocalPaths(self, include_dirs=False):
311 """Returns absolute local paths of input_api.AffectedFiles()."""
312 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
313
314 def ServerPaths(self, include_dirs=False):
315 """Returns server paths of input_api.AffectedFiles()."""
316 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
317
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000318 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000319 """Same as input_api.change.AffectedTextFiles() except only lists files
320 in the same directory as the current presubmit script, or subdirectories
321 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000323 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000324 warn("AffectedTextFiles(include_deletes=%s)"
325 " is deprecated and ignored" % str(include_deletes),
326 category=DeprecationWarning,
327 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000328 return filter(lambda x: x.IsTextFile(),
329 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000330
maruel@chromium.org3410d912009-06-09 20:56:16 +0000331 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
332 """Filters out files that aren't considered "source file".
333
334 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
335 and InputApi.DEFAULT_BLACK_LIST is used respectively.
336
337 The lists will be compiled as regular expression and
338 AffectedFile.LocalPath() needs to pass both list.
339
340 Note: Copy-paste this function to suit your needs or use a lambda function.
341 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000342 def Find(affected_file, items):
343 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000344 local_path = affected_file.LocalPath()
345 if self.re.match(item, local_path):
346 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000347 return True
348 return False
349 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
350 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
351
352 def AffectedSourceFiles(self, source_file):
353 """Filter the list of AffectedTextFiles by the function source_file.
354
355 If source_file is None, InputApi.FilterSourceFile() is used.
356 """
357 if not source_file:
358 source_file = self.FilterSourceFile
359 return filter(source_file, self.AffectedTextFiles())
360
361 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000362 """An iterator over all text lines in "new" version of changed files.
363
364 Only lists lines from new or modified text files in the change that are
365 contained by the directory of the currently executing presubmit script.
366
367 This is useful for doing line-by-line regex checks, like checking for
368 trailing whitespace.
369
370 Yields:
371 a 3 tuple:
372 the AffectedFile instance of the current file;
373 integer line number (1-based); and
374 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000375
376 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000377 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000378 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000379 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000380
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000381 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000382 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000383
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000384 Deny reading anything outside the repository.
385 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000386 if isinstance(file_item, AffectedFile):
387 file_item = file_item.AbsoluteLocalPath()
388 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000389 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000390 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000391
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000392
393class AffectedFile(object):
394 """Representation of a file in a change."""
395
396 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000397 self._path = path
398 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000399 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000400 self._is_directory = None
401 self._properties = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402
403 def ServerPath(self):
404 """Returns a path string that identifies the file in the SCM system.
405
406 Returns the empty string if the file does not exist in SCM.
407 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000408 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000409
410 def LocalPath(self):
411 """Returns the path of this file on the local disk relative to client root.
412 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000413 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000414
415 def AbsoluteLocalPath(self):
416 """Returns the absolute path of this file on the local disk.
417 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000418 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000419
420 def IsDirectory(self):
421 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000422 if self._is_directory is None:
423 path = self.AbsoluteLocalPath()
424 self._is_directory = (os.path.exists(path) and
425 os.path.isdir(path))
426 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000427
428 def Action(self):
429 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000430 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
431 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000432 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000434 def Property(self, property_name):
435 """Returns the specified SCM property of this file, or None if no such
436 property.
437 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000438 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000439
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000440 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000441 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000442
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000443 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000444 raise NotImplementedError() # Implement when needed
445
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000446 def NewContents(self):
447 """Returns an iterator over the lines in the new version of file.
448
449 The new version is the file in the user's workspace, i.e. the "right hand
450 side".
451
452 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000453 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454 """
455 if self.IsDirectory():
456 return []
457 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000458 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
459 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000460
461 def OldContents(self):
462 """Returns an iterator over the lines in the old version of file.
463
464 The old version is the file in depot, i.e. the "left hand side".
465 """
466 raise NotImplementedError() # Implement when needed
467
468 def OldFileTempPath(self):
469 """Returns the path on local disk where the old contents resides.
470
471 The old version is the file in depot, i.e. the "left hand side".
472 This is a read-only cached copy of the old contents. *DO NOT* try to
473 modify this file.
474 """
475 raise NotImplementedError() # Implement if/when needed.
476
maruel@chromium.org5de13972009-06-10 18:16:06 +0000477 def __str__(self):
478 return self.LocalPath()
479
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000480
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000481class SvnAffectedFile(AffectedFile):
482 """Representation of a file in a change out of a Subversion checkout."""
483
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000484 def __init__(self, *args, **kwargs):
485 AffectedFile.__init__(self, *args, **kwargs)
486 self._server_path = None
487 self._is_text_file = None
488
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000489 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000490 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000491 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000492 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000493 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000494
495 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000496 if self._is_directory is None:
497 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000498 if os.path.exists(path):
499 # Retrieve directly from the file system; it is much faster than
500 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000501 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000502 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000503 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000504 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000505 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000506
507 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000508 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000509 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000510 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000511 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000512
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000513 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000514 if self._is_text_file is None:
515 if self.Action() == 'D':
516 # A deleted file is not a text file.
517 self._is_text_file = False
518 elif self.IsDirectory():
519 self._is_text_file = False
520 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000521 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
522 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000523 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
524 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000525
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000526
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000527class GitAffectedFile(AffectedFile):
528 """Representation of a file in a change out of a git checkout."""
529
530 def __init__(self, *args, **kwargs):
531 AffectedFile.__init__(self, *args, **kwargs)
532 self._server_path = None
533 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000534
535 def ServerPath(self):
536 if self._server_path is None:
537 raise NotImplementedException() # TODO(maruel) Implement.
538 return self._server_path
539
540 def IsDirectory(self):
541 if self._is_directory is None:
542 path = self.AbsoluteLocalPath()
543 if os.path.exists(path):
544 # Retrieve directly from the file system; it is much faster than
545 # querying subversion, especially on Windows.
546 self._is_directory = os.path.isdir(path)
547 else:
548 # raise NotImplementedException() # TODO(maruel) Implement.
549 self._is_directory = False
550 return self._is_directory
551
552 def Property(self, property_name):
553 if not property_name in self._properties:
554 raise NotImplementedException() # TODO(maruel) Implement.
555 return self._properties[property_name]
556
557 def IsTextFile(self):
558 if self._is_text_file is None:
559 if self.Action() == 'D':
560 # A deleted file is not a text file.
561 self._is_text_file = False
562 elif self.IsDirectory():
563 self._is_text_file = False
564 else:
565 # raise NotImplementedException() # TODO(maruel) Implement.
566 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
567 return self._is_text_file
568
569
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000570class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000571 """Describe a change.
572
573 Used directly by the presubmit scripts to query the current change being
574 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000575
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000576 Instance members:
577 tags: Dictionnary of KEY=VALUE pairs found in the change description.
578 self.KEY: equivalent to tags['KEY']
579 """
580
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000581 _AFFECTED_FILES = AffectedFile
582
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000583 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000584 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000585 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000586
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000587 def __init__(self, name, description, local_root, files, issue, patchset):
588 if files is None:
589 files = []
590 self._name = name
591 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000592 # Convert root into an absolute path.
593 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000594 self.issue = issue
595 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000596 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597
598 # From the description text, build up a dictionary of key/value pairs
599 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000600 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000601 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000602 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000603 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000604 if m:
605 self.tags[m.group('key')] = m.group('value')
606 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000607 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000608
609 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000610 self._description_without_tags = (
611 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000612
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000613 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000614 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
615 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000616 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000617
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000618 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000620 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000621
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000622 def DescriptionText(self):
623 """Returns the user-entered changelist description, minus tags.
624
625 Any line in the user-provided description starting with e.g. "FOO="
626 (whitespace permitted before and around) is considered a tag line. Such
627 lines are stripped out of the description this function returns.
628 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000629 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000630
631 def FullDescriptionText(self):
632 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000633 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000634
635 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000636 """Returns the repository (checkout) root directory for this change,
637 as an absolute path.
638 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000639 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000640
641 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000642 """Return tags directly as attributes on the object."""
643 if not re.match(r"^[A-Z_]*$", attr):
644 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000645 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000646
647 def AffectedFiles(self, include_dirs=False, include_deletes=True):
648 """Returns a list of AffectedFile instances for all files in the change.
649
650 Args:
651 include_deletes: If false, deleted files will be filtered out.
652 include_dirs: True to include directories in the list
653
654 Returns:
655 [AffectedFile(path, action), AffectedFile(path, action)]
656 """
657 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000658 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000659 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000660 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661
662 if include_deletes:
663 return affected
664 else:
665 return filter(lambda x: x.Action() != 'D', affected)
666
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000667 def AffectedTextFiles(self, include_deletes=None):
668 """Return a list of the existing text files in a change."""
669 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000670 warn("AffectedTextFiles(include_deletes=%s)"
671 " is deprecated and ignored" % str(include_deletes),
672 category=DeprecationWarning,
673 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000674 return filter(lambda x: x.IsTextFile(),
675 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676
677 def LocalPaths(self, include_dirs=False):
678 """Convenience function."""
679 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
680
681 def AbsoluteLocalPaths(self, include_dirs=False):
682 """Convenience function."""
683 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
684
685 def ServerPaths(self, include_dirs=False):
686 """Convenience function."""
687 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
688
689 def RightHandSideLines(self):
690 """An iterator over all text lines in "new" version of changed files.
691
692 Lists lines from new or modified text files in the change.
693
694 This is useful for doing line-by-line regex checks, like checking for
695 trailing whitespace.
696
697 Yields:
698 a 3 tuple:
699 the AffectedFile instance of the current file;
700 integer line number (1-based); and
701 the contents of the line as a string.
702 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000703 return _RightHandSideLinesImpl(
704 x for x in self.AffectedFiles(include_deletes=False)
705 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706
707
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000708class SvnChange(Change):
709 _AFFECTED_FILES = SvnAffectedFile
710
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000711 def __init__(self, *args, **kwargs):
712 Change.__init__(self, *args, **kwargs)
713 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000714 self._changelists = None
715
716 def _GetChangeLists(self):
717 """Get all change lists."""
718 if self._changelists == None:
719 previous_cwd = os.getcwd()
720 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000721 # Need to import here to avoid circular dependency.
722 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000723 self._changelists = gcl.GetModifiedFiles()
724 os.chdir(previous_cwd)
725 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000726
727 def GetAllModifiedFiles(self):
728 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000729 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000730 all_modified_files = []
731 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000732 all_modified_files.extend(
733 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000734 return all_modified_files
735
736 def GetModifiedFiles(self):
737 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000738 changelists = self._GetChangeLists()
739 return [os.path.join(self.RepositoryRoot(), f[1])
740 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000741
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000742
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000743class GitChange(Change):
744 _AFFECTED_FILES = GitAffectedFile
745
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000746 def __init__(self, *args, **kwargs):
747 Change.__init__(self, *args, **kwargs)
748 self.scm = 'git'
749
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000750
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000751def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000752 """Finds all presubmit files that apply to a given set of source files.
753
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000754 If inherit-review-settings-ok is present right under root, looks for
755 PRESUBMIT.py in directories enclosing root.
756
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000757 Args:
758 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000759 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000760
761 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000762 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000763 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000764 files = [normpath(os.path.join(root, f)) for f in files]
765
766 # List all the individual directories containing files.
767 directories = set([os.path.dirname(f) for f in files])
768
769 # Ignore root if inherit-review-settings-ok is present.
770 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
771 root = None
772
773 # Collect all unique directories that may contain PRESUBMIT.py.
774 candidates = set()
775 for directory in directories:
776 while True:
777 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000778 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000779 candidates.add(directory)
780 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000781 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000782 parent_dir = os.path.dirname(directory)
783 if parent_dir == directory:
784 # We hit the system root directory.
785 break
786 directory = parent_dir
787
788 # Look for PRESUBMIT.py in all candidate directories.
789 results = []
790 for directory in sorted(list(candidates)):
791 p = os.path.join(directory, 'PRESUBMIT.py')
792 if os.path.isfile(p):
793 results.append(p)
794
795 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000796
797
thestig@chromium.orgde243452009-10-06 21:02:56 +0000798class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000799 @staticmethod
800 def ExecPresubmitScript(script_text):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000801 """Executes GetPreferredTrySlaves() from a single presubmit script.
802
803 Args:
804 script_text: The text of the presubmit script.
805
806 Return:
807 A list of try slaves.
808 """
809 context = {}
810 exec script_text in context
811
812 function_name = 'GetPreferredTrySlaves'
813 if function_name in context:
814 result = eval(function_name + '()', context)
815 if not isinstance(result, types.ListType):
816 raise exceptions.RuntimeError(
817 'Presubmit functions must return a list, got a %s instead: %s' %
818 (type(result), str(result)))
819 for item in result:
820 if not isinstance(item, basestring):
821 raise exceptions.RuntimeError('All try slaves names must be strings.')
822 if item != item.strip():
823 raise exceptions.RuntimeError('Try slave names cannot start/end'
824 'with whitespace')
825 else:
826 result = []
827 return result
828
829
830def DoGetTrySlaves(changed_files,
831 repository_root,
832 default_presubmit,
833 verbose,
834 output_stream):
835 """Get the list of try servers from the presubmit scripts.
836
837 Args:
838 changed_files: List of modified files.
839 repository_root: The repository root.
840 default_presubmit: A default presubmit script to execute in any case.
841 verbose: Prints debug info.
842 output_stream: A stream to write debug output to.
843
844 Return:
845 List of try slaves
846 """
847 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
848 if not presubmit_files and verbose:
849 output_stream.write("Warning, no presubmit.py found.\n")
850 results = []
851 executer = GetTrySlavesExecuter()
852 if default_presubmit:
853 if verbose:
854 output_stream.write("Running default presubmit script.\n")
855 results += executer.ExecPresubmitScript(default_presubmit)
856 for filename in presubmit_files:
857 filename = os.path.abspath(filename)
858 if verbose:
859 output_stream.write("Running %s\n" % filename)
860 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000861 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000862 results += executer.ExecPresubmitScript(presubmit_script)
863
864 slaves = list(set(results))
865 if slaves and verbose:
866 output_stream.write(', '.join(slaves))
867 output_stream.write('\n')
868 return slaves
869
870
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000871class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000872 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000873 """
874 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000875 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000876 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
877 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000878 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000879 self.committing = committing
880
881 def ExecPresubmitScript(self, script_text, presubmit_path):
882 """Executes a single presubmit script.
883
884 Args:
885 script_text: The text of the presubmit script.
886 presubmit_path: The path to the presubmit file (this will be reported via
887 input_api.PresubmitLocalPath()).
888
889 Return:
890 A list of result objects, empty if no problems.
891 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000892
893 # Change to the presubmit file's directory to support local imports.
894 main_path = os.getcwd()
895 os.chdir(os.path.dirname(presubmit_path))
896
897 # Load the presubmit script into context.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000898 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000899 context = {}
900 exec script_text in context
901
902 # These function names must change if we make substantial changes to
903 # the presubmit API that are not backwards compatible.
904 if self.committing:
905 function_name = 'CheckChangeOnCommit'
906 else:
907 function_name = 'CheckChangeOnUpload'
908 if function_name in context:
909 context['__args'] = (input_api, OutputApi())
910 result = eval(function_name + '(*__args)', context)
911 if not (isinstance(result, types.TupleType) or
912 isinstance(result, types.ListType)):
913 raise exceptions.RuntimeError(
914 'Presubmit functions must return a tuple or list')
915 for item in result:
916 if not isinstance(item, OutputApi.PresubmitResult):
917 raise exceptions.RuntimeError(
918 'All presubmit results must be of types derived from '
919 'output_api.PresubmitResult')
920 else:
921 result = () # no error since the script doesn't care about current event.
922
chase@chromium.org8e416c82009-10-06 04:30:44 +0000923 # Return the process to the original working directory.
924 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000925 return result
926
927
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000928def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000929 committing,
930 verbose,
931 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000932 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000933 default_presubmit,
934 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935 """Runs all presubmit checks that apply to the files in the change.
936
937 This finds all PRESUBMIT.py files in directories enclosing the files in the
938 change (up to the repository root) and calls the relevant entrypoint function
939 depending on whether the change is being committed or uploaded.
940
941 Prints errors, warnings and notifications. Prompts the user for warnings
942 when needed.
943
944 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000945 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000946 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
947 verbose: Prints debug info.
948 output_stream: A stream to write output from presubmit tests to.
949 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000950 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000951 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000952
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000953 Warning:
954 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
955 SHOULD be sys.stdin.
956
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000957 Return:
958 True if execution can continue, False if not.
959 """
maruel@chromium.org8d195232010-10-05 12:58:49 +0000960 print "Running presubmit hooks..."
jam@chromium.org2a891dc2009-08-20 20:33:37 +0000961 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000962 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
963 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000965 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000966 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000967 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000968 if default_presubmit:
969 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000970 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000971 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000972 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000974 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000975 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000976 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000977 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000978 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979 results += executer.ExecPresubmitScript(presubmit_script, filename)
980
981 errors = []
982 notifications = []
983 warnings = []
984 for result in results:
985 if not result.IsFatal() and not result.ShouldPrompt():
986 notifications.append(result)
987 elif result.ShouldPrompt():
988 warnings.append(result)
989 else:
990 errors.append(result)
991
992 error_count = 0
993 for name, items in (('Messages', notifications),
994 ('Warnings', warnings),
995 ('ERRORS', errors)):
996 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000997 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 for item in items:
999 if not item._Handle(output_stream, input_stream,
1000 may_prompt=False):
1001 error_count += 1
1002 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001003
1004 total_time = time.time() - start_time
1005 if total_time > 1.0:
1006 print "Presubmit checks took %.1fs to calculate." % total_time
1007
maruel@chromium.org07bbc212009-06-11 02:08:41 +00001008 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +00001009 if not PromptYesNo(input_stream, output_stream,
1010 'There were presubmit warnings. '
1011 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001012 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001013
1014 global _ASKED_FOR_FEEDBACK
1015 # Ask for feedback one time out of 5.
1016 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1017 output_stream.write("Was the presubmit check useful? Please send feedback "
1018 "& hate mail to maruel@chromium.org!\n")
1019 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001020 return (error_count == 0)
1021
1022
1023def ScanSubDirs(mask, recursive):
1024 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001025 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 +00001026 else:
1027 results = []
1028 for root, dirs, files in os.walk('.'):
1029 if '.svn' in dirs:
1030 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001031 if '.git' in dirs:
1032 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033 for name in files:
1034 if fnmatch.fnmatch(name, mask):
1035 results.append(os.path.join(root, name))
1036 return results
1037
1038
1039def ParseFiles(args, recursive):
1040 files = []
1041 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001042 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001043 return files
1044
1045
1046def Main(argv):
1047 parser = optparse.OptionParser(usage="%prog [options]",
1048 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001049 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001050 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001051 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1052 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001053 parser.add_option("-r", "--recursive", action="store_true",
1054 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001055 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001056 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001057 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001058 parser.add_option("--name", default='no name')
1059 parser.add_option("--description", default='')
1060 parser.add_option("--issue", type='int', default=0)
1061 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001062 parser.add_option("--root", default=os.getcwd(),
1063 help="Search for PRESUBMIT.py up to this directory. "
1064 "If inherit-review-settings-ok is present in this "
1065 "directory, parent directories up to the root file "
1066 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001067 parser.add_option("--default_presubmit")
1068 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001069 options, args = parser.parse_args(argv[1:])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001070 if os.path.isdir(os.path.join(options.root, '.git')):
1071 change_class = GitChange
1072 if not options.files:
1073 if args:
1074 options.files = ParseFiles(args, options.recursive)
1075 else:
1076 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001077 options.files = scm.GIT.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001078 elif os.path.isdir(os.path.join(options.root, '.svn')):
1079 change_class = SvnChange
1080 if not options.files:
1081 if args:
1082 options.files = ParseFiles(args, options.recursive)
1083 else:
1084 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001085 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001086 else:
1087 # Doesn't seem under source control.
1088 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001089 if options.verbose:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001090 if len(options.files) != 1:
1091 print "Found %d files." % len(options.files)
1092 else:
1093 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001094 return not DoPresubmitChecks(change_class(options.name,
1095 options.description,
1096 options.root,
1097 options.files,
1098 options.issue,
1099 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001100 options.commit,
1101 options.verbose,
1102 sys.stdout,
1103 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001104 options.default_presubmit,
1105 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001106
1107
1108if __name__ == '__main__':
1109 sys.exit(Main(sys.argv))