blob: 8ab01daae39dcd2ebc21fa522c3d798b79df5d35 [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
2# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
3# 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.org1e08c002009-05-28 19:09:33 +000035import warnings
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@google.comfb2b8eb2009-04-23 21:03:42 +000051import gcl
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000052import gclient_utils
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000054import scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055
56
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000057# Ask for feedback only once in program lifetime.
58_ASKED_FOR_FEEDBACK = False
59
60
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000061class NotImplementedException(Exception):
62 """We're leaving placeholders in a bunch of places to remind us of the
63 design of the API, but we have not implemented all of it yet. Implement as
64 the need arises.
65 """
66 pass
67
68
69def normpath(path):
70 '''Version of os.path.normpath that also changes backward slashes to
71 forward slashes when not running on Windows.
72 '''
73 # This is safe to always do because the Windows version of os.path.normpath
74 # will replace forward slashes with backward slashes.
75 path = path.replace(os.sep, '/')
76 return os.path.normpath(path)
77
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@google.comfb2b8eb2009-04-23 21:03:42 +000083class OutputApi(object):
84 """This class (more like a module) gets passed to presubmit scripts so that
85 they can specify various types of results.
86 """
87
88 class PresubmitResult(object):
89 """Base class for result objects."""
90
91 def __init__(self, message, items=None, long_text=''):
92 """
93 message: A short one-line message to indicate errors.
94 items: A list of short strings to indicate where errors occurred.
95 long_text: multi-line text output, e.g. from another tool
96 """
97 self._message = message
98 self._items = []
99 if items:
100 self._items = items
101 self._long_text = long_text.rstrip()
102
103 def _Handle(self, output_stream, input_stream, may_prompt=True):
104 """Writes this result to the output stream.
105
106 Args:
107 output_stream: Where to write
108
109 Returns:
110 True if execution may continue, False otherwise.
111 """
112 output_stream.write(self._message)
113 output_stream.write('\n')
estade@chromium.org593258b2010-01-28 00:04:36 +0000114 if len(self._items) > 0:
115 output_stream.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000116 if self._long_text:
maruel@chromium.org310b3552010-11-01 13:23:35 +0000117 # Sometimes self._long_text is a ascii string, a codepage string
118 # (on windows), or a unicode object.
119 try:
120 long_text = self._long_text.decode()
121 except UnicodeDecodeError:
122 long_text = self._long_text.decode('ascii', 'replace')
123
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000124 output_stream.write('\n***************\n%s\n***************\n' %
maruel@chromium.org310b3552010-11-01 13:23:35 +0000125 long_text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000126
127 if self.ShouldPrompt() and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000128 if not PromptYesNo(input_stream, output_stream,
129 'Are you sure you want to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000130 return False
131
132 return not self.IsFatal()
133
134 def IsFatal(self):
135 """An error that is fatal stops g4 mail/submit immediately, i.e. before
136 other presubmit scripts are run.
137 """
138 return False
139
140 def ShouldPrompt(self):
141 """Whether this presubmit result should result in a prompt warning."""
142 return False
143
144 class PresubmitError(PresubmitResult):
145 """A hard presubmit error."""
146 def IsFatal(self):
147 return True
148
149 class PresubmitPromptWarning(PresubmitResult):
150 """An warning that prompts the user if they want to continue."""
151 def ShouldPrompt(self):
152 return True
153
154 class PresubmitNotifyResult(PresubmitResult):
155 """Just print something to the screen -- but it's not even a warning."""
156 pass
157
158 class MailTextResult(PresubmitResult):
159 """A warning that should be included in the review request email."""
160 def __init__(self, *args, **kwargs):
161 raise NotImplementedException() # TODO(joi) Implement.
162
163
164class InputApi(object):
165 """An instance of this object is passed to presubmit scripts so they can
166 know stuff about the change they're looking at.
167 """
168
maruel@chromium.org3410d912009-06-09 20:56:16 +0000169 # File extensions that are considered source files from a style guide
170 # perspective. Don't modify this list from a presubmit script!
171 DEFAULT_WHITE_LIST = (
172 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000173 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
174 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000175 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000176 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000177 # No extension at all, note that ALL CAPS files are black listed in
178 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000179 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000180 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000181 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000182 )
183
184 # Path regexp that should be excluded from being considered containing source
185 # files. Don't modify this list from a presubmit script!
186 DEFAULT_BLACK_LIST = (
187 r".*\bexperimental[\\\/].*",
188 r".*\bthird_party[\\\/].*",
189 # Output directories (just in case)
190 r".*\bDebug[\\\/].*",
191 r".*\bRelease[\\\/].*",
192 r".*\bxcodebuild[\\\/].*",
193 r".*\bsconsbuild[\\\/].*",
194 # All caps files like README and LICENCE.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000195 r".*\b[A-Z0-9_]+$",
196 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
197 r".*\.git[\\\/].*",
198 r".*\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000199 )
200
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000201 def __init__(self, change, presubmit_path, is_committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000202 """Builds an InputApi object.
203
204 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000205 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000206 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000207 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000208 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000209 # Version number of the presubmit_support script.
210 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000211 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000212 self.is_committing = is_committing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000213
214 # We expose various modules and functions as attributes of the input_api
215 # so that presubmit scripts don't have to import them.
216 self.basename = os.path.basename
217 self.cPickle = cPickle
218 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000219 self.json = json
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000220 self.os_path = os.path
221 self.pickle = pickle
222 self.marshal = marshal
223 self.re = re
224 self.subprocess = subprocess
225 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000226 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000227 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228 self.urllib2 = urllib2
229
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000230 # To easily fork python.
231 self.python_executable = sys.executable
232 self.environ = os.environ
233
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000234 # InputApi.platform is the platform you're currently running on.
235 self.platform = sys.platform
236
237 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000238 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000239
240 # We carry the canned checks so presubmit scripts can easily use them.
241 self.canned_checks = presubmit_canned_checks
242
243 def PresubmitLocalPath(self):
244 """Returns the local path of the presubmit script currently being run.
245
246 This is useful if you don't want to hard-code absolute paths in the
247 presubmit script. For example, It can be used to find another file
248 relative to the PRESUBMIT.py script, so the whole tree can be branched and
249 the presubmit script still works, without editing its content.
250 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000251 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000252
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000253 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000254 """Translate a depot path to a local path (relative to client root).
255
256 Args:
257 Depot path as a string.
258
259 Returns:
260 The local path of the depot path under the user's current client, or None
261 if the file is not mapped.
262
263 Remember to check for the None case and show an appropriate error!
264 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000265 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000266 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267 return local_path
268
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000269 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000270 """Translate a local path to a depot path.
271
272 Args:
273 Local path (relative to current directory, or absolute) as a string.
274
275 Returns:
276 The depot path (SVN URL) of the file if mapped, otherwise None.
277 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000278 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000279 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000280 return depot_path
281
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282 def AffectedFiles(self, include_dirs=False, include_deletes=True):
283 """Same as input_api.change.AffectedFiles() except only lists files
284 (and optionally directories) in the same directory as the current presubmit
285 script, or subdirectories thereof.
286 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000287 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000288 if len(dir_with_slash) == 1:
289 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000290 return filter(
291 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
292 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000293
294 def LocalPaths(self, include_dirs=False):
295 """Returns local paths of input_api.AffectedFiles()."""
296 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
297
298 def AbsoluteLocalPaths(self, include_dirs=False):
299 """Returns absolute local paths of input_api.AffectedFiles()."""
300 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
301
302 def ServerPaths(self, include_dirs=False):
303 """Returns server paths of input_api.AffectedFiles()."""
304 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
305
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000306 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000307 """Same as input_api.change.AffectedTextFiles() except only lists files
308 in the same directory as the current presubmit script, or subdirectories
309 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000310 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000311 if include_deletes is not None:
312 warnings.warn("AffectedTextFiles(include_deletes=%s)"
313 " is deprecated and ignored" % str(include_deletes),
314 category=DeprecationWarning,
315 stacklevel=2)
316 return filter(lambda x: x.IsTextFile(),
317 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000318
maruel@chromium.org3410d912009-06-09 20:56:16 +0000319 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
320 """Filters out files that aren't considered "source file".
321
322 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
323 and InputApi.DEFAULT_BLACK_LIST is used respectively.
324
325 The lists will be compiled as regular expression and
326 AffectedFile.LocalPath() needs to pass both list.
327
328 Note: Copy-paste this function to suit your needs or use a lambda function.
329 """
330 def Find(affected_file, list):
331 for item in list:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000332 local_path = affected_file.LocalPath()
333 if self.re.match(item, local_path):
334 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000335 return True
336 return False
337 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
338 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
339
340 def AffectedSourceFiles(self, source_file):
341 """Filter the list of AffectedTextFiles by the function source_file.
342
343 If source_file is None, InputApi.FilterSourceFile() is used.
344 """
345 if not source_file:
346 source_file = self.FilterSourceFile
347 return filter(source_file, self.AffectedTextFiles())
348
349 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350 """An iterator over all text lines in "new" version of changed files.
351
352 Only lists lines from new or modified text files in the change that are
353 contained by the directory of the currently executing presubmit script.
354
355 This is useful for doing line-by-line regex checks, like checking for
356 trailing whitespace.
357
358 Yields:
359 a 3 tuple:
360 the AffectedFile instance of the current file;
361 integer line number (1-based); and
362 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000363
364 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000365 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000366 files = self.AffectedSourceFiles(source_file_filter)
367 return InputApi._RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000368
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000369 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000370 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000371
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000372 Deny reading anything outside the repository.
373 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000374 if isinstance(file_item, AffectedFile):
375 file_item = file_item.AbsoluteLocalPath()
376 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000377 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000378 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000379
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000380 @staticmethod
381 def _RightHandSideLinesImpl(affected_files):
382 """Implements RightHandSideLines for InputApi and GclChange."""
383 for af in affected_files:
384 lines = af.NewContents()
385 line_number = 0
386 for line in lines:
387 line_number += 1
388 yield (af, line_number, line)
389
390
391class AffectedFile(object):
392 """Representation of a file in a change."""
393
394 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000395 self._path = path
396 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000397 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000398 self._is_directory = None
399 self._properties = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000400
401 def ServerPath(self):
402 """Returns a path string that identifies the file in the SCM system.
403
404 Returns the empty string if the file does not exist in SCM.
405 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000406 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407
408 def LocalPath(self):
409 """Returns the path of this file on the local disk relative to client root.
410 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000411 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000412
413 def AbsoluteLocalPath(self):
414 """Returns the absolute path of this file on the local disk.
415 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000416 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417
418 def IsDirectory(self):
419 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000420 if self._is_directory is None:
421 path = self.AbsoluteLocalPath()
422 self._is_directory = (os.path.exists(path) and
423 os.path.isdir(path))
424 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000425
426 def Action(self):
427 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000428 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
429 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000430 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000432 def Property(self, property_name):
433 """Returns the specified SCM property of this file, or None if no such
434 property.
435 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000436 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000437
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000438 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000439 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000440
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000441 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000442 raise NotImplementedError() # Implement when needed
443
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000444 def NewContents(self):
445 """Returns an iterator over the lines in the new version of file.
446
447 The new version is the file in the user's workspace, i.e. the "right hand
448 side".
449
450 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000451 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000452 """
453 if self.IsDirectory():
454 return []
455 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000456 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
457 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458
459 def OldContents(self):
460 """Returns an iterator over the lines in the old version of file.
461
462 The old version is the file in depot, i.e. the "left hand side".
463 """
464 raise NotImplementedError() # Implement when needed
465
466 def OldFileTempPath(self):
467 """Returns the path on local disk where the old contents resides.
468
469 The old version is the file in depot, i.e. the "left hand side".
470 This is a read-only cached copy of the old contents. *DO NOT* try to
471 modify this file.
472 """
473 raise NotImplementedError() # Implement if/when needed.
474
maruel@chromium.org5de13972009-06-10 18:16:06 +0000475 def __str__(self):
476 return self.LocalPath()
477
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000478
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000479class SvnAffectedFile(AffectedFile):
480 """Representation of a file in a change out of a Subversion checkout."""
481
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000482 def __init__(self, *args, **kwargs):
483 AffectedFile.__init__(self, *args, **kwargs)
484 self._server_path = None
485 self._is_text_file = None
486
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000487 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000488 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000489 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000490 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000491 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000492
493 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000494 if self._is_directory is None:
495 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000496 if os.path.exists(path):
497 # Retrieve directly from the file system; it is much faster than
498 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000499 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000500 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000501 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000502 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000503 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000504
505 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000506 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000507 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000508 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000509 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000510
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000511 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000512 if self._is_text_file is None:
513 if self.Action() == 'D':
514 # A deleted file is not a text file.
515 self._is_text_file = False
516 elif self.IsDirectory():
517 self._is_text_file = False
518 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000519 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
520 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000521 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
522 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000523
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000524
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000525class GitAffectedFile(AffectedFile):
526 """Representation of a file in a change out of a git checkout."""
527
528 def __init__(self, *args, **kwargs):
529 AffectedFile.__init__(self, *args, **kwargs)
530 self._server_path = None
531 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000532
533 def ServerPath(self):
534 if self._server_path is None:
535 raise NotImplementedException() # TODO(maruel) Implement.
536 return self._server_path
537
538 def IsDirectory(self):
539 if self._is_directory is None:
540 path = self.AbsoluteLocalPath()
541 if os.path.exists(path):
542 # Retrieve directly from the file system; it is much faster than
543 # querying subversion, especially on Windows.
544 self._is_directory = os.path.isdir(path)
545 else:
546 # raise NotImplementedException() # TODO(maruel) Implement.
547 self._is_directory = False
548 return self._is_directory
549
550 def Property(self, property_name):
551 if not property_name in self._properties:
552 raise NotImplementedException() # TODO(maruel) Implement.
553 return self._properties[property_name]
554
555 def IsTextFile(self):
556 if self._is_text_file is None:
557 if self.Action() == 'D':
558 # A deleted file is not a text file.
559 self._is_text_file = False
560 elif self.IsDirectory():
561 self._is_text_file = False
562 else:
563 # raise NotImplementedException() # TODO(maruel) Implement.
564 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
565 return self._is_text_file
566
567
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000568class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000569 """Describe a change.
570
571 Used directly by the presubmit scripts to query the current change being
572 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000573
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000574 Instance members:
575 tags: Dictionnary of KEY=VALUE pairs found in the change description.
576 self.KEY: equivalent to tags['KEY']
577 """
578
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000579 _AFFECTED_FILES = AffectedFile
580
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000581 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000582 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000583 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000584
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000585 def __init__(self, name, description, local_root, files, issue, patchset):
586 if files is None:
587 files = []
588 self._name = name
589 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000590 # Convert root into an absolute path.
591 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000592 self.issue = issue
593 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000594 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000595
596 # From the description text, build up a dictionary of key/value pairs
597 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000598 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000599 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000600 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000601 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000602 if m:
603 self.tags[m.group('key')] = m.group('value')
604 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000605 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000606
607 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000608 self._description_without_tags = (
609 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000611 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000612 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
613 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000614 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000615
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000616 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000617 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000618 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000620 def DescriptionText(self):
621 """Returns the user-entered changelist description, minus tags.
622
623 Any line in the user-provided description starting with e.g. "FOO="
624 (whitespace permitted before and around) is considered a tag line. Such
625 lines are stripped out of the description this function returns.
626 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000627 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000628
629 def FullDescriptionText(self):
630 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000631 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000632
633 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000634 """Returns the repository (checkout) root directory for this change,
635 as an absolute path.
636 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000637 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000638
639 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000640 """Return tags directly as attributes on the object."""
641 if not re.match(r"^[A-Z_]*$", attr):
642 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000643 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000644
645 def AffectedFiles(self, include_dirs=False, include_deletes=True):
646 """Returns a list of AffectedFile instances for all files in the change.
647
648 Args:
649 include_deletes: If false, deleted files will be filtered out.
650 include_dirs: True to include directories in the list
651
652 Returns:
653 [AffectedFile(path, action), AffectedFile(path, action)]
654 """
655 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000656 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000658 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000659
660 if include_deletes:
661 return affected
662 else:
663 return filter(lambda x: x.Action() != 'D', affected)
664
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000665 def AffectedTextFiles(self, include_deletes=None):
666 """Return a list of the existing text files in a change."""
667 if include_deletes is not None:
668 warnings.warn("AffectedTextFiles(include_deletes=%s)"
669 " is deprecated and ignored" % str(include_deletes),
670 category=DeprecationWarning,
671 stacklevel=2)
672 return filter(lambda x: x.IsTextFile(),
673 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674
675 def LocalPaths(self, include_dirs=False):
676 """Convenience function."""
677 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
678
679 def AbsoluteLocalPaths(self, include_dirs=False):
680 """Convenience function."""
681 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
682
683 def ServerPaths(self, include_dirs=False):
684 """Convenience function."""
685 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
686
687 def RightHandSideLines(self):
688 """An iterator over all text lines in "new" version of changed files.
689
690 Lists lines from new or modified text files in the change.
691
692 This is useful for doing line-by-line regex checks, like checking for
693 trailing whitespace.
694
695 Yields:
696 a 3 tuple:
697 the AffectedFile instance of the current file;
698 integer line number (1-based); and
699 the contents of the line as a string.
700 """
701 return InputApi._RightHandSideLinesImpl(
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000702 filter(lambda x: x.IsTextFile(),
703 self.AffectedFiles(include_deletes=False)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704
705
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000706class SvnChange(Change):
707 _AFFECTED_FILES = SvnAffectedFile
708
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000709 def __init__(self, *args, **kwargs):
710 Change.__init__(self, *args, **kwargs)
711 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000712 self._changelists = None
713
714 def _GetChangeLists(self):
715 """Get all change lists."""
716 if self._changelists == None:
717 previous_cwd = os.getcwd()
718 os.chdir(self.RepositoryRoot())
719 self._changelists = gcl.GetModifiedFiles()
720 os.chdir(previous_cwd)
721 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000722
723 def GetAllModifiedFiles(self):
724 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000725 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000726 all_modified_files = []
727 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000728 all_modified_files.extend(
729 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000730 return all_modified_files
731
732 def GetModifiedFiles(self):
733 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000734 changelists = self._GetChangeLists()
735 return [os.path.join(self.RepositoryRoot(), f[1])
736 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000737
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000738
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000739class GitChange(Change):
740 _AFFECTED_FILES = GitAffectedFile
741
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000742 def __init__(self, *args, **kwargs):
743 Change.__init__(self, *args, **kwargs)
744 self.scm = 'git'
745
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000746
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000747def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748 """Finds all presubmit files that apply to a given set of source files.
749
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000750 If inherit-review-settings-ok is present right under root, looks for
751 PRESUBMIT.py in directories enclosing root.
752
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000753 Args:
754 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000755 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000756
757 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000758 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000759 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000760 files = [normpath(os.path.join(root, f)) for f in files]
761
762 # List all the individual directories containing files.
763 directories = set([os.path.dirname(f) for f in files])
764
765 # Ignore root if inherit-review-settings-ok is present.
766 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
767 root = None
768
769 # Collect all unique directories that may contain PRESUBMIT.py.
770 candidates = set()
771 for directory in directories:
772 while True:
773 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000774 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000775 candidates.add(directory)
776 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000777 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000778 parent_dir = os.path.dirname(directory)
779 if parent_dir == directory:
780 # We hit the system root directory.
781 break
782 directory = parent_dir
783
784 # Look for PRESUBMIT.py in all candidate directories.
785 results = []
786 for directory in sorted(list(candidates)):
787 p = os.path.join(directory, 'PRESUBMIT.py')
788 if os.path.isfile(p):
789 results.append(p)
790
791 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000792
793
thestig@chromium.orgde243452009-10-06 21:02:56 +0000794class GetTrySlavesExecuter(object):
795 def ExecPresubmitScript(self, script_text):
796 """Executes GetPreferredTrySlaves() from a single presubmit script.
797
798 Args:
799 script_text: The text of the presubmit script.
800
801 Return:
802 A list of try slaves.
803 """
804 context = {}
805 exec script_text in context
806
807 function_name = 'GetPreferredTrySlaves'
808 if function_name in context:
809 result = eval(function_name + '()', context)
810 if not isinstance(result, types.ListType):
811 raise exceptions.RuntimeError(
812 'Presubmit functions must return a list, got a %s instead: %s' %
813 (type(result), str(result)))
814 for item in result:
815 if not isinstance(item, basestring):
816 raise exceptions.RuntimeError('All try slaves names must be strings.')
817 if item != item.strip():
818 raise exceptions.RuntimeError('Try slave names cannot start/end'
819 'with whitespace')
820 else:
821 result = []
822 return result
823
824
825def DoGetTrySlaves(changed_files,
826 repository_root,
827 default_presubmit,
828 verbose,
829 output_stream):
830 """Get the list of try servers from the presubmit scripts.
831
832 Args:
833 changed_files: List of modified files.
834 repository_root: The repository root.
835 default_presubmit: A default presubmit script to execute in any case.
836 verbose: Prints debug info.
837 output_stream: A stream to write debug output to.
838
839 Return:
840 List of try slaves
841 """
842 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
843 if not presubmit_files and verbose:
844 output_stream.write("Warning, no presubmit.py found.\n")
845 results = []
846 executer = GetTrySlavesExecuter()
847 if default_presubmit:
848 if verbose:
849 output_stream.write("Running default presubmit script.\n")
850 results += executer.ExecPresubmitScript(default_presubmit)
851 for filename in presubmit_files:
852 filename = os.path.abspath(filename)
853 if verbose:
854 output_stream.write("Running %s\n" % filename)
855 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000856 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000857 results += executer.ExecPresubmitScript(presubmit_script)
858
859 slaves = list(set(results))
860 if slaves and verbose:
861 output_stream.write(', '.join(slaves))
862 output_stream.write('\n')
863 return slaves
864
865
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000866class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000867 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000868 """
869 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000870 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000871 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
872 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000873 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000874 self.committing = committing
875
876 def ExecPresubmitScript(self, script_text, presubmit_path):
877 """Executes a single presubmit script.
878
879 Args:
880 script_text: The text of the presubmit script.
881 presubmit_path: The path to the presubmit file (this will be reported via
882 input_api.PresubmitLocalPath()).
883
884 Return:
885 A list of result objects, empty if no problems.
886 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000887
888 # Change to the presubmit file's directory to support local imports.
889 main_path = os.getcwd()
890 os.chdir(os.path.dirname(presubmit_path))
891
892 # Load the presubmit script into context.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000893 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000894 context = {}
895 exec script_text in context
896
897 # These function names must change if we make substantial changes to
898 # the presubmit API that are not backwards compatible.
899 if self.committing:
900 function_name = 'CheckChangeOnCommit'
901 else:
902 function_name = 'CheckChangeOnUpload'
903 if function_name in context:
904 context['__args'] = (input_api, OutputApi())
905 result = eval(function_name + '(*__args)', context)
906 if not (isinstance(result, types.TupleType) or
907 isinstance(result, types.ListType)):
908 raise exceptions.RuntimeError(
909 'Presubmit functions must return a tuple or list')
910 for item in result:
911 if not isinstance(item, OutputApi.PresubmitResult):
912 raise exceptions.RuntimeError(
913 'All presubmit results must be of types derived from '
914 'output_api.PresubmitResult')
915 else:
916 result = () # no error since the script doesn't care about current event.
917
chase@chromium.org8e416c82009-10-06 04:30:44 +0000918 # Return the process to the original working directory.
919 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000920 return result
921
922
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000923def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000924 committing,
925 verbose,
926 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000927 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000928 default_presubmit,
929 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000930 """Runs all presubmit checks that apply to the files in the change.
931
932 This finds all PRESUBMIT.py files in directories enclosing the files in the
933 change (up to the repository root) and calls the relevant entrypoint function
934 depending on whether the change is being committed or uploaded.
935
936 Prints errors, warnings and notifications. Prompts the user for warnings
937 when needed.
938
939 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000940 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
942 verbose: Prints debug info.
943 output_stream: A stream to write output from presubmit tests to.
944 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000945 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000946 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000947
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000948 Warning:
949 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
950 SHOULD be sys.stdin.
951
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000952 Return:
953 True if execution can continue, False if not.
954 """
maruel@chromium.org8d195232010-10-05 12:58:49 +0000955 print "Running presubmit hooks..."
jam@chromium.org2a891dc2009-08-20 20:33:37 +0000956 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000957 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
958 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000959 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000960 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000961 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000962 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000963 if default_presubmit:
964 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000965 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000966 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000967 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000968 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000969 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000970 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000971 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000972 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000973 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000974 results += executer.ExecPresubmitScript(presubmit_script, filename)
975
976 errors = []
977 notifications = []
978 warnings = []
979 for result in results:
980 if not result.IsFatal() and not result.ShouldPrompt():
981 notifications.append(result)
982 elif result.ShouldPrompt():
983 warnings.append(result)
984 else:
985 errors.append(result)
986
987 error_count = 0
988 for name, items in (('Messages', notifications),
989 ('Warnings', warnings),
990 ('ERRORS', errors)):
991 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000992 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000993 for item in items:
994 if not item._Handle(output_stream, input_stream,
995 may_prompt=False):
996 error_count += 1
997 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +0000998
999 total_time = time.time() - start_time
1000 if total_time > 1.0:
1001 print "Presubmit checks took %.1fs to calculate." % total_time
1002
maruel@chromium.org07bbc212009-06-11 02:08:41 +00001003 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +00001004 if not PromptYesNo(input_stream, output_stream,
1005 'There were presubmit warnings. '
1006 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001008
1009 global _ASKED_FOR_FEEDBACK
1010 # Ask for feedback one time out of 5.
1011 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1012 output_stream.write("Was the presubmit check useful? Please send feedback "
1013 "& hate mail to maruel@chromium.org!\n")
1014 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015 return (error_count == 0)
1016
1017
1018def ScanSubDirs(mask, recursive):
1019 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001020 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 +00001021 else:
1022 results = []
1023 for root, dirs, files in os.walk('.'):
1024 if '.svn' in dirs:
1025 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001026 if '.git' in dirs:
1027 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001028 for name in files:
1029 if fnmatch.fnmatch(name, mask):
1030 results.append(os.path.join(root, name))
1031 return results
1032
1033
1034def ParseFiles(args, recursive):
1035 files = []
1036 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001037 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 return files
1039
1040
1041def Main(argv):
1042 parser = optparse.OptionParser(usage="%prog [options]",
1043 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001044 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001045 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001046 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1047 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001048 parser.add_option("-r", "--recursive", action="store_true",
1049 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001050 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001051 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001052 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001053 parser.add_option("--name", default='no name')
1054 parser.add_option("--description", default='')
1055 parser.add_option("--issue", type='int', default=0)
1056 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001057 parser.add_option("--root", default=os.getcwd(),
1058 help="Search for PRESUBMIT.py up to this directory. "
1059 "If inherit-review-settings-ok is present in this "
1060 "directory, parent directories up to the root file "
1061 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001062 parser.add_option("--default_presubmit")
1063 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001064 options, args = parser.parse_args(argv[1:])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001065 if os.path.isdir(os.path.join(options.root, '.git')):
1066 change_class = GitChange
1067 if not options.files:
1068 if args:
1069 options.files = ParseFiles(args, options.recursive)
1070 else:
1071 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001072 options.files = scm.GIT.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001073 elif os.path.isdir(os.path.join(options.root, '.svn')):
1074 change_class = SvnChange
1075 if not options.files:
1076 if args:
1077 options.files = ParseFiles(args, options.recursive)
1078 else:
1079 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001080 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001081 else:
1082 # Doesn't seem under source control.
1083 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001084 if options.verbose:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001085 if len(options.files) != 1:
1086 print "Found %d files." % len(options.files)
1087 else:
1088 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001089 return not DoPresubmitChecks(change_class(options.name,
1090 options.description,
1091 options.root,
1092 options.files,
1093 options.issue,
1094 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001095 options.commit,
1096 options.verbose,
1097 sys.stdout,
1098 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001099 options.default_presubmit,
1100 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001101
1102
1103if __name__ == '__main__':
1104 sys.exit(Main(sys.argv))