blob: 83ba5eaf659d8c0a687e2eb8aab664507426fc77 [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.org5aeb7dd2009-11-17 18:09:01 +00009__version__ = '1.3.4'
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
37# Local imports.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000038import gcl
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000039import gclient_utils
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000040import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000041import scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000042
43
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000044# Ask for feedback only once in program lifetime.
45_ASKED_FOR_FEEDBACK = False
46
47
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000048class NotImplementedException(Exception):
49 """We're leaving placeholders in a bunch of places to remind us of the
50 design of the API, but we have not implemented all of it yet. Implement as
51 the need arises.
52 """
53 pass
54
55
56def normpath(path):
57 '''Version of os.path.normpath that also changes backward slashes to
58 forward slashes when not running on Windows.
59 '''
60 # This is safe to always do because the Windows version of os.path.normpath
61 # will replace forward slashes with backward slashes.
62 path = path.replace(os.sep, '/')
63 return os.path.normpath(path)
64
gspencer@google.comefb94502009-10-09 17:57:08 +000065def PromptYesNo(input_stream, output_stream, prompt):
66 output_stream.write(prompt)
67 response = input_stream.readline().strip().lower()
68 return response == 'y' or response == 'yes'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000069
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000070class OutputApi(object):
71 """This class (more like a module) gets passed to presubmit scripts so that
72 they can specify various types of results.
73 """
74
75 class PresubmitResult(object):
76 """Base class for result objects."""
77
78 def __init__(self, message, items=None, long_text=''):
79 """
80 message: A short one-line message to indicate errors.
81 items: A list of short strings to indicate where errors occurred.
82 long_text: multi-line text output, e.g. from another tool
83 """
84 self._message = message
85 self._items = []
86 if items:
87 self._items = items
88 self._long_text = long_text.rstrip()
89
90 def _Handle(self, output_stream, input_stream, may_prompt=True):
91 """Writes this result to the output stream.
92
93 Args:
94 output_stream: Where to write
95
96 Returns:
97 True if execution may continue, False otherwise.
98 """
99 output_stream.write(self._message)
100 output_stream.write('\n')
estade@chromium.org593258b2010-01-28 00:04:36 +0000101 if len(self._items) > 0:
102 output_stream.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000103 if self._long_text:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000104 output_stream.write('\n***************\n%s\n***************\n' %
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000105 self._long_text)
106
107 if self.ShouldPrompt() and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000108 if not PromptYesNo(input_stream, output_stream,
109 'Are you sure you want to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000110 return False
111
112 return not self.IsFatal()
113
114 def IsFatal(self):
115 """An error that is fatal stops g4 mail/submit immediately, i.e. before
116 other presubmit scripts are run.
117 """
118 return False
119
120 def ShouldPrompt(self):
121 """Whether this presubmit result should result in a prompt warning."""
122 return False
123
124 class PresubmitError(PresubmitResult):
125 """A hard presubmit error."""
126 def IsFatal(self):
127 return True
128
129 class PresubmitPromptWarning(PresubmitResult):
130 """An warning that prompts the user if they want to continue."""
131 def ShouldPrompt(self):
132 return True
133
134 class PresubmitNotifyResult(PresubmitResult):
135 """Just print something to the screen -- but it's not even a warning."""
136 pass
137
138 class MailTextResult(PresubmitResult):
139 """A warning that should be included in the review request email."""
140 def __init__(self, *args, **kwargs):
141 raise NotImplementedException() # TODO(joi) Implement.
142
143
144class InputApi(object):
145 """An instance of this object is passed to presubmit scripts so they can
146 know stuff about the change they're looking at.
147 """
148
maruel@chromium.org3410d912009-06-09 20:56:16 +0000149 # File extensions that are considered source files from a style guide
150 # perspective. Don't modify this list from a presubmit script!
151 DEFAULT_WHITE_LIST = (
152 # C++ and friends
153 r".*\.c", r".*\.cc", r".*\.cpp", r".*\.h", r".*\.m", r".*\.mm",
154 r".*\.inl", r".*\.asm", r".*\.hxx", r".*\.hpp",
155 # Scripts
156 r".*\.js", r".*\.py", r".*\.json", r".*\.sh", r".*\.rb",
157 # No extension at all
158 r"(^|.*[\\\/])[^.]+$",
159 # Other
maruel@chromium.orgd59982a2009-08-24 15:48:47 +0000160 r".*\.java", r".*\.mk", r".*\.am", r".*\.txt",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000161 )
162
163 # Path regexp that should be excluded from being considered containing source
164 # files. Don't modify this list from a presubmit script!
165 DEFAULT_BLACK_LIST = (
166 r".*\bexperimental[\\\/].*",
167 r".*\bthird_party[\\\/].*",
168 # Output directories (just in case)
169 r".*\bDebug[\\\/].*",
170 r".*\bRelease[\\\/].*",
171 r".*\bxcodebuild[\\\/].*",
172 r".*\bsconsbuild[\\\/].*",
173 # All caps files like README and LICENCE.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000174 r".*\b[A-Z0-9_]+$",
175 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
176 r".*\.git[\\\/].*",
177 r".*\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000178 )
179
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000180 def __init__(self, change, presubmit_path, is_committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000181 """Builds an InputApi object.
182
183 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000184 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000185 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000186 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000187 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000188 # Version number of the presubmit_support script.
189 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000190 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000191 self.is_committing = is_committing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000192
193 # We expose various modules and functions as attributes of the input_api
194 # so that presubmit scripts don't have to import them.
195 self.basename = os.path.basename
196 self.cPickle = cPickle
197 self.cStringIO = cStringIO
198 self.os_path = os.path
199 self.pickle = pickle
200 self.marshal = marshal
201 self.re = re
202 self.subprocess = subprocess
203 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000204 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000205 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000206 self.urllib2 = urllib2
207
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000208 # To easily fork python.
209 self.python_executable = sys.executable
210 self.environ = os.environ
211
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000212 # InputApi.platform is the platform you're currently running on.
213 self.platform = sys.platform
214
215 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000216 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000217
218 # We carry the canned checks so presubmit scripts can easily use them.
219 self.canned_checks = presubmit_canned_checks
220
221 def PresubmitLocalPath(self):
222 """Returns the local path of the presubmit script currently being run.
223
224 This is useful if you don't want to hard-code absolute paths in the
225 presubmit script. For example, It can be used to find another file
226 relative to the PRESUBMIT.py script, so the whole tree can be branched and
227 the presubmit script still works, without editing its content.
228 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000229 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000231 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000232 """Translate a depot path to a local path (relative to client root).
233
234 Args:
235 Depot path as a string.
236
237 Returns:
238 The local path of the depot path under the user's current client, or None
239 if the file is not mapped.
240
241 Remember to check for the None case and show an appropriate error!
242 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000243 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000244 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000245 return local_path
246
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000247 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000248 """Translate a local path to a depot path.
249
250 Args:
251 Local path (relative to current directory, or absolute) as a string.
252
253 Returns:
254 The depot path (SVN URL) of the file if mapped, otherwise None.
255 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000256 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000257 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000258 return depot_path
259
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000260 def AffectedFiles(self, include_dirs=False, include_deletes=True):
261 """Same as input_api.change.AffectedFiles() except only lists files
262 (and optionally directories) in the same directory as the current presubmit
263 script, or subdirectories thereof.
264 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000265 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000266 if len(dir_with_slash) == 1:
267 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000268 return filter(
269 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
270 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271
272 def LocalPaths(self, include_dirs=False):
273 """Returns local paths of input_api.AffectedFiles()."""
274 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
275
276 def AbsoluteLocalPaths(self, include_dirs=False):
277 """Returns absolute local paths of input_api.AffectedFiles()."""
278 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
279
280 def ServerPaths(self, include_dirs=False):
281 """Returns server paths of input_api.AffectedFiles()."""
282 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
283
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000284 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000285 """Same as input_api.change.AffectedTextFiles() except only lists files
286 in the same directory as the current presubmit script, or subdirectories
287 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000288 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000289 if include_deletes is not None:
290 warnings.warn("AffectedTextFiles(include_deletes=%s)"
291 " is deprecated and ignored" % str(include_deletes),
292 category=DeprecationWarning,
293 stacklevel=2)
294 return filter(lambda x: x.IsTextFile(),
295 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000296
maruel@chromium.org3410d912009-06-09 20:56:16 +0000297 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
298 """Filters out files that aren't considered "source file".
299
300 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
301 and InputApi.DEFAULT_BLACK_LIST is used respectively.
302
303 The lists will be compiled as regular expression and
304 AffectedFile.LocalPath() needs to pass both list.
305
306 Note: Copy-paste this function to suit your needs or use a lambda function.
307 """
308 def Find(affected_file, list):
309 for item in list:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000310 local_path = affected_file.LocalPath()
311 if self.re.match(item, local_path):
312 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000313 return True
314 return False
315 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
316 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
317
318 def AffectedSourceFiles(self, source_file):
319 """Filter the list of AffectedTextFiles by the function source_file.
320
321 If source_file is None, InputApi.FilterSourceFile() is used.
322 """
323 if not source_file:
324 source_file = self.FilterSourceFile
325 return filter(source_file, self.AffectedTextFiles())
326
327 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000328 """An iterator over all text lines in "new" version of changed files.
329
330 Only lists lines from new or modified text files in the change that are
331 contained by the directory of the currently executing presubmit script.
332
333 This is useful for doing line-by-line regex checks, like checking for
334 trailing whitespace.
335
336 Yields:
337 a 3 tuple:
338 the AffectedFile instance of the current file;
339 integer line number (1-based); and
340 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000341
342 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000343 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000344 files = self.AffectedSourceFiles(source_file_filter)
345 return InputApi._RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000346
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000347 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000348 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000349
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000350 Deny reading anything outside the repository.
351 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000352 if isinstance(file_item, AffectedFile):
353 file_item = file_item.AbsoluteLocalPath()
354 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000355 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000356 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000357
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000358 @staticmethod
359 def _RightHandSideLinesImpl(affected_files):
360 """Implements RightHandSideLines for InputApi and GclChange."""
361 for af in affected_files:
362 lines = af.NewContents()
363 line_number = 0
364 for line in lines:
365 line_number += 1
366 yield (af, line_number, line)
367
368
369class AffectedFile(object):
370 """Representation of a file in a change."""
371
372 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000373 self._path = path
374 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000375 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000376 self._is_directory = None
377 self._properties = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000378
379 def ServerPath(self):
380 """Returns a path string that identifies the file in the SCM system.
381
382 Returns the empty string if the file does not exist in SCM.
383 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000384 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000385
386 def LocalPath(self):
387 """Returns the path of this file on the local disk relative to client root.
388 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000389 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000390
391 def AbsoluteLocalPath(self):
392 """Returns the absolute path of this file on the local disk.
393 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000394 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000395
396 def IsDirectory(self):
397 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000398 if self._is_directory is None:
399 path = self.AbsoluteLocalPath()
400 self._is_directory = (os.path.exists(path) and
401 os.path.isdir(path))
402 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000403
404 def Action(self):
405 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000406 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
407 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000408 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000409
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000410 def Property(self, property_name):
411 """Returns the specified SCM property of this file, or None if no such
412 property.
413 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000414 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000415
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000416 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000417 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000418
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000419 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000420 raise NotImplementedError() # Implement when needed
421
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000422 def NewContents(self):
423 """Returns an iterator over the lines in the new version of file.
424
425 The new version is the file in the user's workspace, i.e. the "right hand
426 side".
427
428 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000429 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000430 """
431 if self.IsDirectory():
432 return []
433 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000434 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
435 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000436
437 def OldContents(self):
438 """Returns an iterator over the lines in the old version of file.
439
440 The old version is the file in depot, i.e. the "left hand side".
441 """
442 raise NotImplementedError() # Implement when needed
443
444 def OldFileTempPath(self):
445 """Returns the path on local disk where the old contents resides.
446
447 The old version is the file in depot, i.e. the "left hand side".
448 This is a read-only cached copy of the old contents. *DO NOT* try to
449 modify this file.
450 """
451 raise NotImplementedError() # Implement if/when needed.
452
maruel@chromium.org5de13972009-06-10 18:16:06 +0000453 def __str__(self):
454 return self.LocalPath()
455
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000456
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000457class SvnAffectedFile(AffectedFile):
458 """Representation of a file in a change out of a Subversion checkout."""
459
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000460 def __init__(self, *args, **kwargs):
461 AffectedFile.__init__(self, *args, **kwargs)
462 self._server_path = None
463 self._is_text_file = None
464
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000465 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000466 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000467 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000468 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000469 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000470
471 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000472 if self._is_directory is None:
473 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000474 if os.path.exists(path):
475 # Retrieve directly from the file system; it is much faster than
476 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000477 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000478 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000479 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000480 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000481 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000482
483 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000484 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000485 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000486 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000487 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000488
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000489 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000490 if self._is_text_file is None:
491 if self.Action() == 'D':
492 # A deleted file is not a text file.
493 self._is_text_file = False
494 elif self.IsDirectory():
495 self._is_text_file = False
496 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000497 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
498 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000499 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
500 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000501
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000502
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000503class GitAffectedFile(AffectedFile):
504 """Representation of a file in a change out of a git checkout."""
505
506 def __init__(self, *args, **kwargs):
507 AffectedFile.__init__(self, *args, **kwargs)
508 self._server_path = None
509 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000510
511 def ServerPath(self):
512 if self._server_path is None:
513 raise NotImplementedException() # TODO(maruel) Implement.
514 return self._server_path
515
516 def IsDirectory(self):
517 if self._is_directory is None:
518 path = self.AbsoluteLocalPath()
519 if os.path.exists(path):
520 # Retrieve directly from the file system; it is much faster than
521 # querying subversion, especially on Windows.
522 self._is_directory = os.path.isdir(path)
523 else:
524 # raise NotImplementedException() # TODO(maruel) Implement.
525 self._is_directory = False
526 return self._is_directory
527
528 def Property(self, property_name):
529 if not property_name in self._properties:
530 raise NotImplementedException() # TODO(maruel) Implement.
531 return self._properties[property_name]
532
533 def IsTextFile(self):
534 if self._is_text_file is None:
535 if self.Action() == 'D':
536 # A deleted file is not a text file.
537 self._is_text_file = False
538 elif self.IsDirectory():
539 self._is_text_file = False
540 else:
541 # raise NotImplementedException() # TODO(maruel) Implement.
542 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
543 return self._is_text_file
544
545
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000546class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000547 """Describe a change.
548
549 Used directly by the presubmit scripts to query the current change being
550 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000551
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000552 Instance members:
553 tags: Dictionnary of KEY=VALUE pairs found in the change description.
554 self.KEY: equivalent to tags['KEY']
555 """
556
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000557 _AFFECTED_FILES = AffectedFile
558
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000559 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000560 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000561 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000562
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000563 def __init__(self, name, description, local_root, files, issue, patchset):
564 if files is None:
565 files = []
566 self._name = name
567 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000568 # Convert root into an absolute path.
569 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000570 self.issue = issue
571 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000572 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000573
574 # From the description text, build up a dictionary of key/value pairs
575 # plus the description minus all key/value or "tag" lines.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000576 self._description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000577 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000578 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000579 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000580 if m:
581 self.tags[m.group('key')] = m.group('value')
582 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000583 self._description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000584
585 # Change back to text and remove whitespace at end.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000586 self._description_without_tags = '\n'.join(self._description_without_tags)
587 self._description_without_tags = self._description_without_tags.rstrip()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000588
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000589 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000590 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
591 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000592 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000593
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000594 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000595 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000596 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000598 def DescriptionText(self):
599 """Returns the user-entered changelist description, minus tags.
600
601 Any line in the user-provided description starting with e.g. "FOO="
602 (whitespace permitted before and around) is considered a tag line. Such
603 lines are stripped out of the description this function returns.
604 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000605 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000606
607 def FullDescriptionText(self):
608 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000609 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610
611 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000612 """Returns the repository (checkout) root directory for this change,
613 as an absolute path.
614 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000615 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000616
617 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000618 """Return tags directly as attributes on the object."""
619 if not re.match(r"^[A-Z_]*$", attr):
620 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000621 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000622
623 def AffectedFiles(self, include_dirs=False, include_deletes=True):
624 """Returns a list of AffectedFile instances for all files in the change.
625
626 Args:
627 include_deletes: If false, deleted files will be filtered out.
628 include_dirs: True to include directories in the list
629
630 Returns:
631 [AffectedFile(path, action), AffectedFile(path, action)]
632 """
633 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000634 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000635 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000636 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000637
638 if include_deletes:
639 return affected
640 else:
641 return filter(lambda x: x.Action() != 'D', affected)
642
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000643 def AffectedTextFiles(self, include_deletes=None):
644 """Return a list of the existing text files in a change."""
645 if include_deletes is not None:
646 warnings.warn("AffectedTextFiles(include_deletes=%s)"
647 " is deprecated and ignored" % str(include_deletes),
648 category=DeprecationWarning,
649 stacklevel=2)
650 return filter(lambda x: x.IsTextFile(),
651 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000652
653 def LocalPaths(self, include_dirs=False):
654 """Convenience function."""
655 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
656
657 def AbsoluteLocalPaths(self, include_dirs=False):
658 """Convenience function."""
659 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
660
661 def ServerPaths(self, include_dirs=False):
662 """Convenience function."""
663 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
664
665 def RightHandSideLines(self):
666 """An iterator over all text lines in "new" version of changed files.
667
668 Lists lines from new or modified text files in the change.
669
670 This is useful for doing line-by-line regex checks, like checking for
671 trailing whitespace.
672
673 Yields:
674 a 3 tuple:
675 the AffectedFile instance of the current file;
676 integer line number (1-based); and
677 the contents of the line as a string.
678 """
679 return InputApi._RightHandSideLinesImpl(
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000680 filter(lambda x: x.IsTextFile(),
681 self.AffectedFiles(include_deletes=False)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682
683
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000684class SvnChange(Change):
685 _AFFECTED_FILES = SvnAffectedFile
686
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000687 def __init__(self, *args, **kwargs):
688 Change.__init__(self, *args, **kwargs)
689 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000690 self._changelists = None
691
692 def _GetChangeLists(self):
693 """Get all change lists."""
694 if self._changelists == None:
695 previous_cwd = os.getcwd()
696 os.chdir(self.RepositoryRoot())
697 self._changelists = gcl.GetModifiedFiles()
698 os.chdir(previous_cwd)
699 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000700
701 def GetAllModifiedFiles(self):
702 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000703 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000704 all_modified_files = []
705 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000706 all_modified_files.extend(
707 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000708 return all_modified_files
709
710 def GetModifiedFiles(self):
711 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000712 changelists = self._GetChangeLists()
713 return [os.path.join(self.RepositoryRoot(), f[1])
714 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000715
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000716
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000717class GitChange(Change):
718 _AFFECTED_FILES = GitAffectedFile
719
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000720 def __init__(self, *args, **kwargs):
721 Change.__init__(self, *args, **kwargs)
722 self.scm = 'git'
723
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000724
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000725def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000726 """Finds all presubmit files that apply to a given set of source files.
727
728 Args:
729 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000730 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731
732 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000733 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000734 """
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000735 entries = []
736 for f in files:
737 f = normpath(os.path.join(root, f))
738 while f:
739 f = os.path.dirname(f)
740 if f in entries:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741 break
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000742 entries.append(f)
743 if f == root:
744 break
745 entries.sort()
746 entries = map(lambda x: os.path.join(x, 'PRESUBMIT.py'), entries)
747 return filter(lambda x: os.path.isfile(x), entries)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748
749
thestig@chromium.orgde243452009-10-06 21:02:56 +0000750class GetTrySlavesExecuter(object):
751 def ExecPresubmitScript(self, script_text):
752 """Executes GetPreferredTrySlaves() from a single presubmit script.
753
754 Args:
755 script_text: The text of the presubmit script.
756
757 Return:
758 A list of try slaves.
759 """
760 context = {}
761 exec script_text in context
762
763 function_name = 'GetPreferredTrySlaves'
764 if function_name in context:
765 result = eval(function_name + '()', context)
766 if not isinstance(result, types.ListType):
767 raise exceptions.RuntimeError(
768 'Presubmit functions must return a list, got a %s instead: %s' %
769 (type(result), str(result)))
770 for item in result:
771 if not isinstance(item, basestring):
772 raise exceptions.RuntimeError('All try slaves names must be strings.')
773 if item != item.strip():
774 raise exceptions.RuntimeError('Try slave names cannot start/end'
775 'with whitespace')
776 else:
777 result = []
778 return result
779
780
781def DoGetTrySlaves(changed_files,
782 repository_root,
783 default_presubmit,
784 verbose,
785 output_stream):
786 """Get the list of try servers from the presubmit scripts.
787
788 Args:
789 changed_files: List of modified files.
790 repository_root: The repository root.
791 default_presubmit: A default presubmit script to execute in any case.
792 verbose: Prints debug info.
793 output_stream: A stream to write debug output to.
794
795 Return:
796 List of try slaves
797 """
798 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
799 if not presubmit_files and verbose:
800 output_stream.write("Warning, no presubmit.py found.\n")
801 results = []
802 executer = GetTrySlavesExecuter()
803 if default_presubmit:
804 if verbose:
805 output_stream.write("Running default presubmit script.\n")
806 results += executer.ExecPresubmitScript(default_presubmit)
807 for filename in presubmit_files:
808 filename = os.path.abspath(filename)
809 if verbose:
810 output_stream.write("Running %s\n" % filename)
811 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000812 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000813 results += executer.ExecPresubmitScript(presubmit_script)
814
815 slaves = list(set(results))
816 if slaves and verbose:
817 output_stream.write(', '.join(slaves))
818 output_stream.write('\n')
819 return slaves
820
821
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000822class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000823 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000824 """
825 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000826 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
828 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000829 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 self.committing = committing
831
832 def ExecPresubmitScript(self, script_text, presubmit_path):
833 """Executes a single presubmit script.
834
835 Args:
836 script_text: The text of the presubmit script.
837 presubmit_path: The path to the presubmit file (this will be reported via
838 input_api.PresubmitLocalPath()).
839
840 Return:
841 A list of result objects, empty if no problems.
842 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000843
844 # Change to the presubmit file's directory to support local imports.
845 main_path = os.getcwd()
846 os.chdir(os.path.dirname(presubmit_path))
847
848 # Load the presubmit script into context.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000849 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000850 context = {}
851 exec script_text in context
852
853 # These function names must change if we make substantial changes to
854 # the presubmit API that are not backwards compatible.
855 if self.committing:
856 function_name = 'CheckChangeOnCommit'
857 else:
858 function_name = 'CheckChangeOnUpload'
859 if function_name in context:
860 context['__args'] = (input_api, OutputApi())
861 result = eval(function_name + '(*__args)', context)
862 if not (isinstance(result, types.TupleType) or
863 isinstance(result, types.ListType)):
864 raise exceptions.RuntimeError(
865 'Presubmit functions must return a tuple or list')
866 for item in result:
867 if not isinstance(item, OutputApi.PresubmitResult):
868 raise exceptions.RuntimeError(
869 'All presubmit results must be of types derived from '
870 'output_api.PresubmitResult')
871 else:
872 result = () # no error since the script doesn't care about current event.
873
chase@chromium.org8e416c82009-10-06 04:30:44 +0000874 # Return the process to the original working directory.
875 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000876 return result
877
878
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000879def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000880 committing,
881 verbose,
882 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000883 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000884 default_presubmit,
885 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000886 """Runs all presubmit checks that apply to the files in the change.
887
888 This finds all PRESUBMIT.py files in directories enclosing the files in the
889 change (up to the repository root) and calls the relevant entrypoint function
890 depending on whether the change is being committed or uploaded.
891
892 Prints errors, warnings and notifications. Prompts the user for warnings
893 when needed.
894
895 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000896 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000897 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
898 verbose: Prints debug info.
899 output_stream: A stream to write output from presubmit tests to.
900 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000901 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000902 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000903
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000904 Warning:
905 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
906 SHOULD be sys.stdin.
907
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000908 Return:
909 True if execution can continue, False if not.
910 """
jam@chromium.org2a891dc2009-08-20 20:33:37 +0000911 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000912 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
913 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000914 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000915 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000916 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000917 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000918 if default_presubmit:
919 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000920 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000921 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000922 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000923 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000924 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000925 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000926 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000927 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000928 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000929 results += executer.ExecPresubmitScript(presubmit_script, filename)
930
931 errors = []
932 notifications = []
933 warnings = []
934 for result in results:
935 if not result.IsFatal() and not result.ShouldPrompt():
936 notifications.append(result)
937 elif result.ShouldPrompt():
938 warnings.append(result)
939 else:
940 errors.append(result)
941
942 error_count = 0
943 for name, items in (('Messages', notifications),
944 ('Warnings', warnings),
945 ('ERRORS', errors)):
946 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000947 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000948 for item in items:
949 if not item._Handle(output_stream, input_stream,
950 may_prompt=False):
951 error_count += 1
952 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +0000953
954 total_time = time.time() - start_time
955 if total_time > 1.0:
956 print "Presubmit checks took %.1fs to calculate." % total_time
957
maruel@chromium.org07bbc212009-06-11 02:08:41 +0000958 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000959 if not PromptYesNo(input_stream, output_stream,
960 'There were presubmit warnings. '
961 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000962 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000963
964 global _ASKED_FOR_FEEDBACK
965 # Ask for feedback one time out of 5.
966 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
967 output_stream.write("Was the presubmit check useful? Please send feedback "
968 "& hate mail to maruel@chromium.org!\n")
969 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000970 return (error_count == 0)
971
972
973def ScanSubDirs(mask, recursive):
974 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000975 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 +0000976 else:
977 results = []
978 for root, dirs, files in os.walk('.'):
979 if '.svn' in dirs:
980 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000981 if '.git' in dirs:
982 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983 for name in files:
984 if fnmatch.fnmatch(name, mask):
985 results.append(os.path.join(root, name))
986 return results
987
988
989def ParseFiles(args, recursive):
990 files = []
991 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000992 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000993 return files
994
995
996def Main(argv):
997 parser = optparse.OptionParser(usage="%prog [options]",
998 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000999 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001001 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1002 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001003 parser.add_option("-r", "--recursive", action="store_true",
1004 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001005 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001007 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001008 parser.add_option("--name", default='no name')
1009 parser.add_option("--description", default='')
1010 parser.add_option("--issue", type='int', default=0)
1011 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001012 parser.add_option("--root", default='')
1013 parser.add_option("--default_presubmit")
1014 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015 options, args = parser.parse_args(argv[1:])
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001016 if not options.root:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001017 options.root = os.getcwd()
1018 if os.path.isdir(os.path.join(options.root, '.git')):
1019 change_class = GitChange
1020 if not options.files:
1021 if args:
1022 options.files = ParseFiles(args, options.recursive)
1023 else:
1024 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001025 options.files = scm.GIT.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001026 elif os.path.isdir(os.path.join(options.root, '.svn')):
1027 change_class = SvnChange
1028 if not options.files:
1029 if args:
1030 options.files = ParseFiles(args, options.recursive)
1031 else:
1032 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001033 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001034 else:
1035 # Doesn't seem under source control.
1036 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037 if options.verbose:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001038 if len(options.files) != 1:
1039 print "Found %d files." % len(options.files)
1040 else:
1041 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001042 return not DoPresubmitChecks(change_class(options.name,
1043 options.description,
1044 options.root,
1045 options.files,
1046 options.issue,
1047 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001048 options.commit,
1049 options.verbose,
1050 sys.stdout,
1051 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001052 options.default_presubmit,
1053 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001054
1055
1056if __name__ == '__main__':
1057 sys.exit(Main(sys.argv))