blob: 5853e0b3d30f930011ac142196caa4f8764c0fff [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
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000153 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
154 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000155 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000156 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000157 # No extension at all, note that ALL CAPS files are black listed in
158 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000159 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000160 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000161 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000162 )
163
164 # Path regexp that should be excluded from being considered containing source
165 # files. Don't modify this list from a presubmit script!
166 DEFAULT_BLACK_LIST = (
167 r".*\bexperimental[\\\/].*",
168 r".*\bthird_party[\\\/].*",
169 # Output directories (just in case)
170 r".*\bDebug[\\\/].*",
171 r".*\bRelease[\\\/].*",
172 r".*\bxcodebuild[\\\/].*",
173 r".*\bsconsbuild[\\\/].*",
174 # All caps files like README and LICENCE.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000175 r".*\b[A-Z0-9_]+$",
176 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
177 r".*\.git[\\\/].*",
178 r".*\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000179 )
180
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000181 def __init__(self, change, presubmit_path, is_committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000182 """Builds an InputApi object.
183
184 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000185 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000186 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000187 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000188 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000189 # Version number of the presubmit_support script.
190 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000191 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000192 self.is_committing = is_committing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000193
194 # We expose various modules and functions as attributes of the input_api
195 # so that presubmit scripts don't have to import them.
196 self.basename = os.path.basename
197 self.cPickle = cPickle
198 self.cStringIO = cStringIO
199 self.os_path = os.path
200 self.pickle = pickle
201 self.marshal = marshal
202 self.re = re
203 self.subprocess = subprocess
204 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000205 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000206 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000207 self.urllib2 = urllib2
208
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000209 # To easily fork python.
210 self.python_executable = sys.executable
211 self.environ = os.environ
212
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000213 # InputApi.platform is the platform you're currently running on.
214 self.platform = sys.platform
215
216 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000217 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000218
219 # We carry the canned checks so presubmit scripts can easily use them.
220 self.canned_checks = presubmit_canned_checks
221
222 def PresubmitLocalPath(self):
223 """Returns the local path of the presubmit script currently being run.
224
225 This is useful if you don't want to hard-code absolute paths in the
226 presubmit script. For example, It can be used to find another file
227 relative to the PRESUBMIT.py script, so the whole tree can be branched and
228 the presubmit script still works, without editing its content.
229 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000230 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000231
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000232 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000233 """Translate a depot path to a local path (relative to client root).
234
235 Args:
236 Depot path as a string.
237
238 Returns:
239 The local path of the depot path under the user's current client, or None
240 if the file is not mapped.
241
242 Remember to check for the None case and show an appropriate error!
243 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000244 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000245 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000246 return local_path
247
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000248 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000249 """Translate a local path to a depot path.
250
251 Args:
252 Local path (relative to current directory, or absolute) as a string.
253
254 Returns:
255 The depot path (SVN URL) of the file if mapped, otherwise None.
256 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000257 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000258 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000259 return depot_path
260
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000261 def AffectedFiles(self, include_dirs=False, include_deletes=True):
262 """Same as input_api.change.AffectedFiles() except only lists files
263 (and optionally directories) in the same directory as the current presubmit
264 script, or subdirectories thereof.
265 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000266 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267 if len(dir_with_slash) == 1:
268 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000269 return filter(
270 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
271 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272
273 def LocalPaths(self, include_dirs=False):
274 """Returns local paths of input_api.AffectedFiles()."""
275 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
276
277 def AbsoluteLocalPaths(self, include_dirs=False):
278 """Returns absolute local paths of input_api.AffectedFiles()."""
279 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
280
281 def ServerPaths(self, include_dirs=False):
282 """Returns server paths of input_api.AffectedFiles()."""
283 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
284
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000285 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286 """Same as input_api.change.AffectedTextFiles() except only lists files
287 in the same directory as the current presubmit script, or subdirectories
288 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000290 if include_deletes is not None:
291 warnings.warn("AffectedTextFiles(include_deletes=%s)"
292 " is deprecated and ignored" % str(include_deletes),
293 category=DeprecationWarning,
294 stacklevel=2)
295 return filter(lambda x: x.IsTextFile(),
296 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000297
maruel@chromium.org3410d912009-06-09 20:56:16 +0000298 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
299 """Filters out files that aren't considered "source file".
300
301 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
302 and InputApi.DEFAULT_BLACK_LIST is used respectively.
303
304 The lists will be compiled as regular expression and
305 AffectedFile.LocalPath() needs to pass both list.
306
307 Note: Copy-paste this function to suit your needs or use a lambda function.
308 """
309 def Find(affected_file, list):
310 for item in list:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000311 local_path = affected_file.LocalPath()
312 if self.re.match(item, local_path):
313 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000314 return True
315 return False
316 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
317 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
318
319 def AffectedSourceFiles(self, source_file):
320 """Filter the list of AffectedTextFiles by the function source_file.
321
322 If source_file is None, InputApi.FilterSourceFile() is used.
323 """
324 if not source_file:
325 source_file = self.FilterSourceFile
326 return filter(source_file, self.AffectedTextFiles())
327
328 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000329 """An iterator over all text lines in "new" version of changed files.
330
331 Only lists lines from new or modified text files in the change that are
332 contained by the directory of the currently executing presubmit script.
333
334 This is useful for doing line-by-line regex checks, like checking for
335 trailing whitespace.
336
337 Yields:
338 a 3 tuple:
339 the AffectedFile instance of the current file;
340 integer line number (1-based); and
341 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000342
343 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000345 files = self.AffectedSourceFiles(source_file_filter)
346 return InputApi._RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000348 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000349 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000350
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000351 Deny reading anything outside the repository.
352 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000353 if isinstance(file_item, AffectedFile):
354 file_item = file_item.AbsoluteLocalPath()
355 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000356 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000357 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000358
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000359 @staticmethod
360 def _RightHandSideLinesImpl(affected_files):
361 """Implements RightHandSideLines for InputApi and GclChange."""
362 for af in affected_files:
363 lines = af.NewContents()
364 line_number = 0
365 for line in lines:
366 line_number += 1
367 yield (af, line_number, line)
368
369
370class AffectedFile(object):
371 """Representation of a file in a change."""
372
373 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000374 self._path = path
375 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000376 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000377 self._is_directory = None
378 self._properties = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000379
380 def ServerPath(self):
381 """Returns a path string that identifies the file in the SCM system.
382
383 Returns the empty string if the file does not exist in SCM.
384 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000385 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000386
387 def LocalPath(self):
388 """Returns the path of this file on the local disk relative to client root.
389 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000390 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000391
392 def AbsoluteLocalPath(self):
393 """Returns the absolute path of this file on the local disk.
394 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000395 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000396
397 def IsDirectory(self):
398 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000399 if self._is_directory is None:
400 path = self.AbsoluteLocalPath()
401 self._is_directory = (os.path.exists(path) and
402 os.path.isdir(path))
403 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000404
405 def Action(self):
406 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000407 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
408 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000409 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000410
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000411 def Property(self, property_name):
412 """Returns the specified SCM property of this file, or None if no such
413 property.
414 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000415 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000416
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000417 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000418 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000419
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000420 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000421 raise NotImplementedError() # Implement when needed
422
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000423 def NewContents(self):
424 """Returns an iterator over the lines in the new version of file.
425
426 The new version is the file in the user's workspace, i.e. the "right hand
427 side".
428
429 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000430 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431 """
432 if self.IsDirectory():
433 return []
434 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000435 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
436 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000437
438 def OldContents(self):
439 """Returns an iterator over the lines in the old version of file.
440
441 The old version is the file in depot, i.e. the "left hand side".
442 """
443 raise NotImplementedError() # Implement when needed
444
445 def OldFileTempPath(self):
446 """Returns the path on local disk where the old contents resides.
447
448 The old version is the file in depot, i.e. the "left hand side".
449 This is a read-only cached copy of the old contents. *DO NOT* try to
450 modify this file.
451 """
452 raise NotImplementedError() # Implement if/when needed.
453
maruel@chromium.org5de13972009-06-10 18:16:06 +0000454 def __str__(self):
455 return self.LocalPath()
456
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000457
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000458class SvnAffectedFile(AffectedFile):
459 """Representation of a file in a change out of a Subversion checkout."""
460
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000461 def __init__(self, *args, **kwargs):
462 AffectedFile.__init__(self, *args, **kwargs)
463 self._server_path = None
464 self._is_text_file = None
465
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000466 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000467 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000468 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000469 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000470 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000471
472 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000473 if self._is_directory is None:
474 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000475 if os.path.exists(path):
476 # Retrieve directly from the file system; it is much faster than
477 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000478 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000479 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000480 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000481 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000482 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000483
484 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000485 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000486 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000487 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000488 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000489
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000490 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000491 if self._is_text_file is None:
492 if self.Action() == 'D':
493 # A deleted file is not a text file.
494 self._is_text_file = False
495 elif self.IsDirectory():
496 self._is_text_file = False
497 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000498 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
499 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000500 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
501 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000502
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000503
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000504class GitAffectedFile(AffectedFile):
505 """Representation of a file in a change out of a git checkout."""
506
507 def __init__(self, *args, **kwargs):
508 AffectedFile.__init__(self, *args, **kwargs)
509 self._server_path = None
510 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000511
512 def ServerPath(self):
513 if self._server_path is None:
514 raise NotImplementedException() # TODO(maruel) Implement.
515 return self._server_path
516
517 def IsDirectory(self):
518 if self._is_directory is None:
519 path = self.AbsoluteLocalPath()
520 if os.path.exists(path):
521 # Retrieve directly from the file system; it is much faster than
522 # querying subversion, especially on Windows.
523 self._is_directory = os.path.isdir(path)
524 else:
525 # raise NotImplementedException() # TODO(maruel) Implement.
526 self._is_directory = False
527 return self._is_directory
528
529 def Property(self, property_name):
530 if not property_name in self._properties:
531 raise NotImplementedException() # TODO(maruel) Implement.
532 return self._properties[property_name]
533
534 def IsTextFile(self):
535 if self._is_text_file is None:
536 if self.Action() == 'D':
537 # A deleted file is not a text file.
538 self._is_text_file = False
539 elif self.IsDirectory():
540 self._is_text_file = False
541 else:
542 # raise NotImplementedException() # TODO(maruel) Implement.
543 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
544 return self._is_text_file
545
546
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000547class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000548 """Describe a change.
549
550 Used directly by the presubmit scripts to query the current change being
551 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000552
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000553 Instance members:
554 tags: Dictionnary of KEY=VALUE pairs found in the change description.
555 self.KEY: equivalent to tags['KEY']
556 """
557
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000558 _AFFECTED_FILES = AffectedFile
559
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000560 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000561 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000562 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000563
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000564 def __init__(self, name, description, local_root, files, issue, patchset):
565 if files is None:
566 files = []
567 self._name = name
568 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000569 # Convert root into an absolute path.
570 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000571 self.issue = issue
572 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000573 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000574
575 # From the description text, build up a dictionary of key/value pairs
576 # plus the description minus all key/value or "tag" lines.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000577 self._description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000578 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000579 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000580 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000581 if m:
582 self.tags[m.group('key')] = m.group('value')
583 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000584 self._description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000585
586 # Change back to text and remove whitespace at end.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000587 self._description_without_tags = '\n'.join(self._description_without_tags)
588 self._description_without_tags = self._description_without_tags.rstrip()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000589
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000590 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000591 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
592 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000593 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000594
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000595 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000596 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000597 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000598
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000599 def DescriptionText(self):
600 """Returns the user-entered changelist description, minus tags.
601
602 Any line in the user-provided description starting with e.g. "FOO="
603 (whitespace permitted before and around) is considered a tag line. Such
604 lines are stripped out of the description this function returns.
605 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000606 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000607
608 def FullDescriptionText(self):
609 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000610 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000611
612 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000613 """Returns the repository (checkout) root directory for this change,
614 as an absolute path.
615 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000616 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000617
618 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000619 """Return tags directly as attributes on the object."""
620 if not re.match(r"^[A-Z_]*$", attr):
621 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000622 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000623
624 def AffectedFiles(self, include_dirs=False, include_deletes=True):
625 """Returns a list of AffectedFile instances for all files in the change.
626
627 Args:
628 include_deletes: If false, deleted files will be filtered out.
629 include_dirs: True to include directories in the list
630
631 Returns:
632 [AffectedFile(path, action), AffectedFile(path, action)]
633 """
634 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000635 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000636 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000637 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000638
639 if include_deletes:
640 return affected
641 else:
642 return filter(lambda x: x.Action() != 'D', affected)
643
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000644 def AffectedTextFiles(self, include_deletes=None):
645 """Return a list of the existing text files in a change."""
646 if include_deletes is not None:
647 warnings.warn("AffectedTextFiles(include_deletes=%s)"
648 " is deprecated and ignored" % str(include_deletes),
649 category=DeprecationWarning,
650 stacklevel=2)
651 return filter(lambda x: x.IsTextFile(),
652 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000653
654 def LocalPaths(self, include_dirs=False):
655 """Convenience function."""
656 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
657
658 def AbsoluteLocalPaths(self, include_dirs=False):
659 """Convenience function."""
660 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
661
662 def ServerPaths(self, include_dirs=False):
663 """Convenience function."""
664 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
665
666 def RightHandSideLines(self):
667 """An iterator over all text lines in "new" version of changed files.
668
669 Lists lines from new or modified text files in the change.
670
671 This is useful for doing line-by-line regex checks, like checking for
672 trailing whitespace.
673
674 Yields:
675 a 3 tuple:
676 the AffectedFile instance of the current file;
677 integer line number (1-based); and
678 the contents of the line as a string.
679 """
680 return InputApi._RightHandSideLinesImpl(
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000681 filter(lambda x: x.IsTextFile(),
682 self.AffectedFiles(include_deletes=False)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683
684
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000685class SvnChange(Change):
686 _AFFECTED_FILES = SvnAffectedFile
687
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000688 def __init__(self, *args, **kwargs):
689 Change.__init__(self, *args, **kwargs)
690 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000691 self._changelists = None
692
693 def _GetChangeLists(self):
694 """Get all change lists."""
695 if self._changelists == None:
696 previous_cwd = os.getcwd()
697 os.chdir(self.RepositoryRoot())
698 self._changelists = gcl.GetModifiedFiles()
699 os.chdir(previous_cwd)
700 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000701
702 def GetAllModifiedFiles(self):
703 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000704 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000705 all_modified_files = []
706 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000707 all_modified_files.extend(
708 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000709 return all_modified_files
710
711 def GetModifiedFiles(self):
712 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000713 changelists = self._GetChangeLists()
714 return [os.path.join(self.RepositoryRoot(), f[1])
715 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000716
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000717
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000718class GitChange(Change):
719 _AFFECTED_FILES = GitAffectedFile
720
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000721 def __init__(self, *args, **kwargs):
722 Change.__init__(self, *args, **kwargs)
723 self.scm = 'git'
724
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000725
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000726def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727 """Finds all presubmit files that apply to a given set of source files.
728
729 Args:
730 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000731 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000732
733 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000734 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735 """
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000736 entries = []
737 for f in files:
738 f = normpath(os.path.join(root, f))
739 while f:
740 f = os.path.dirname(f)
741 if f in entries:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742 break
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000743 entries.append(f)
744 if f == root:
745 break
746 entries.sort()
747 entries = map(lambda x: os.path.join(x, 'PRESUBMIT.py'), entries)
748 return filter(lambda x: os.path.isfile(x), entries)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749
750
thestig@chromium.orgde243452009-10-06 21:02:56 +0000751class GetTrySlavesExecuter(object):
752 def ExecPresubmitScript(self, script_text):
753 """Executes GetPreferredTrySlaves() from a single presubmit script.
754
755 Args:
756 script_text: The text of the presubmit script.
757
758 Return:
759 A list of try slaves.
760 """
761 context = {}
762 exec script_text in context
763
764 function_name = 'GetPreferredTrySlaves'
765 if function_name in context:
766 result = eval(function_name + '()', context)
767 if not isinstance(result, types.ListType):
768 raise exceptions.RuntimeError(
769 'Presubmit functions must return a list, got a %s instead: %s' %
770 (type(result), str(result)))
771 for item in result:
772 if not isinstance(item, basestring):
773 raise exceptions.RuntimeError('All try slaves names must be strings.')
774 if item != item.strip():
775 raise exceptions.RuntimeError('Try slave names cannot start/end'
776 'with whitespace')
777 else:
778 result = []
779 return result
780
781
782def DoGetTrySlaves(changed_files,
783 repository_root,
784 default_presubmit,
785 verbose,
786 output_stream):
787 """Get the list of try servers from the presubmit scripts.
788
789 Args:
790 changed_files: List of modified files.
791 repository_root: The repository root.
792 default_presubmit: A default presubmit script to execute in any case.
793 verbose: Prints debug info.
794 output_stream: A stream to write debug output to.
795
796 Return:
797 List of try slaves
798 """
799 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
800 if not presubmit_files and verbose:
801 output_stream.write("Warning, no presubmit.py found.\n")
802 results = []
803 executer = GetTrySlavesExecuter()
804 if default_presubmit:
805 if verbose:
806 output_stream.write("Running default presubmit script.\n")
807 results += executer.ExecPresubmitScript(default_presubmit)
808 for filename in presubmit_files:
809 filename = os.path.abspath(filename)
810 if verbose:
811 output_stream.write("Running %s\n" % filename)
812 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000813 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000814 results += executer.ExecPresubmitScript(presubmit_script)
815
816 slaves = list(set(results))
817 if slaves and verbose:
818 output_stream.write(', '.join(slaves))
819 output_stream.write('\n')
820 return slaves
821
822
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000824 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825 """
826 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000827 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000828 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
829 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000830 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000831 self.committing = committing
832
833 def ExecPresubmitScript(self, script_text, presubmit_path):
834 """Executes a single presubmit script.
835
836 Args:
837 script_text: The text of the presubmit script.
838 presubmit_path: The path to the presubmit file (this will be reported via
839 input_api.PresubmitLocalPath()).
840
841 Return:
842 A list of result objects, empty if no problems.
843 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000844
845 # Change to the presubmit file's directory to support local imports.
846 main_path = os.getcwd()
847 os.chdir(os.path.dirname(presubmit_path))
848
849 # Load the presubmit script into context.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000850 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000851 context = {}
852 exec script_text in context
853
854 # These function names must change if we make substantial changes to
855 # the presubmit API that are not backwards compatible.
856 if self.committing:
857 function_name = 'CheckChangeOnCommit'
858 else:
859 function_name = 'CheckChangeOnUpload'
860 if function_name in context:
861 context['__args'] = (input_api, OutputApi())
862 result = eval(function_name + '(*__args)', context)
863 if not (isinstance(result, types.TupleType) or
864 isinstance(result, types.ListType)):
865 raise exceptions.RuntimeError(
866 'Presubmit functions must return a tuple or list')
867 for item in result:
868 if not isinstance(item, OutputApi.PresubmitResult):
869 raise exceptions.RuntimeError(
870 'All presubmit results must be of types derived from '
871 'output_api.PresubmitResult')
872 else:
873 result = () # no error since the script doesn't care about current event.
874
chase@chromium.org8e416c82009-10-06 04:30:44 +0000875 # Return the process to the original working directory.
876 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000877 return result
878
879
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000880def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881 committing,
882 verbose,
883 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000884 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000885 default_presubmit,
886 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000887 """Runs all presubmit checks that apply to the files in the change.
888
889 This finds all PRESUBMIT.py files in directories enclosing the files in the
890 change (up to the repository root) and calls the relevant entrypoint function
891 depending on whether the change is being committed or uploaded.
892
893 Prints errors, warnings and notifications. Prompts the user for warnings
894 when needed.
895
896 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000897 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000898 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
899 verbose: Prints debug info.
900 output_stream: A stream to write output from presubmit tests to.
901 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000902 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000903 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000904
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000905 Warning:
906 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
907 SHOULD be sys.stdin.
908
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000909 Return:
910 True if execution can continue, False if not.
911 """
jam@chromium.org2a891dc2009-08-20 20:33:37 +0000912 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000913 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
914 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000915 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000916 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000917 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000918 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000919 if default_presubmit:
920 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000921 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000922 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000923 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000924 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000925 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000926 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000927 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000928 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000929 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000930 results += executer.ExecPresubmitScript(presubmit_script, filename)
931
932 errors = []
933 notifications = []
934 warnings = []
935 for result in results:
936 if not result.IsFatal() and not result.ShouldPrompt():
937 notifications.append(result)
938 elif result.ShouldPrompt():
939 warnings.append(result)
940 else:
941 errors.append(result)
942
943 error_count = 0
944 for name, items in (('Messages', notifications),
945 ('Warnings', warnings),
946 ('ERRORS', errors)):
947 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000948 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000949 for item in items:
950 if not item._Handle(output_stream, input_stream,
951 may_prompt=False):
952 error_count += 1
953 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +0000954
955 total_time = time.time() - start_time
956 if total_time > 1.0:
957 print "Presubmit checks took %.1fs to calculate." % total_time
958
maruel@chromium.org07bbc212009-06-11 02:08:41 +0000959 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000960 if not PromptYesNo(input_stream, output_stream,
961 'There were presubmit warnings. '
962 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000963 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000964
965 global _ASKED_FOR_FEEDBACK
966 # Ask for feedback one time out of 5.
967 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
968 output_stream.write("Was the presubmit check useful? Please send feedback "
969 "& hate mail to maruel@chromium.org!\n")
970 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000971 return (error_count == 0)
972
973
974def ScanSubDirs(mask, recursive):
975 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000976 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 +0000977 else:
978 results = []
979 for root, dirs, files in os.walk('.'):
980 if '.svn' in dirs:
981 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000982 if '.git' in dirs:
983 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984 for name in files:
985 if fnmatch.fnmatch(name, mask):
986 results.append(os.path.join(root, name))
987 return results
988
989
990def ParseFiles(args, recursive):
991 files = []
992 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000993 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000994 return files
995
996
997def Main(argv):
998 parser = optparse.OptionParser(usage="%prog [options]",
999 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001000 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001001 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001002 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1003 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001004 parser.add_option("-r", "--recursive", action="store_true",
1005 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001006 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001008 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001009 parser.add_option("--name", default='no name')
1010 parser.add_option("--description", default='')
1011 parser.add_option("--issue", type='int', default=0)
1012 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001013 parser.add_option("--root", default='')
1014 parser.add_option("--default_presubmit")
1015 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001016 options, args = parser.parse_args(argv[1:])
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001017 if not options.root:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001018 options.root = os.getcwd()
1019 if os.path.isdir(os.path.join(options.root, '.git')):
1020 change_class = GitChange
1021 if not options.files:
1022 if args:
1023 options.files = ParseFiles(args, options.recursive)
1024 else:
1025 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001026 options.files = scm.GIT.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001027 elif os.path.isdir(os.path.join(options.root, '.svn')):
1028 change_class = SvnChange
1029 if not options.files:
1030 if args:
1031 options.files = ParseFiles(args, options.recursive)
1032 else:
1033 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001034 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001035 else:
1036 # Doesn't seem under source control.
1037 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 if options.verbose:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001039 if len(options.files) != 1:
1040 print "Found %d files." % len(options.files)
1041 else:
1042 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001043 return not DoPresubmitChecks(change_class(options.name,
1044 options.description,
1045 options.root,
1046 options.files,
1047 options.issue,
1048 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001049 options.commit,
1050 options.verbose,
1051 sys.stdout,
1052 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001053 options.default_presubmit,
1054 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001055
1056
1057if __name__ == '__main__':
1058 sys.exit(Main(sys.argv))