blob: d1c54f52cdb66675e954f562021f434fd7d023f2 [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
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000037# json may not be available.
38try:
39 import simplejson as json
40except ImportError:
41 try:
42 import json
43 except ImportError:
44 json = None
45
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000046# Local imports.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000047import gcl
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000048import gclient_utils
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000050import scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000051
52
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000053# Ask for feedback only once in program lifetime.
54_ASKED_FOR_FEEDBACK = False
55
56
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000057class NotImplementedException(Exception):
58 """We're leaving placeholders in a bunch of places to remind us of the
59 design of the API, but we have not implemented all of it yet. Implement as
60 the need arises.
61 """
62 pass
63
64
65def normpath(path):
66 '''Version of os.path.normpath that also changes backward slashes to
67 forward slashes when not running on Windows.
68 '''
69 # This is safe to always do because the Windows version of os.path.normpath
70 # will replace forward slashes with backward slashes.
71 path = path.replace(os.sep, '/')
72 return os.path.normpath(path)
73
gspencer@google.comefb94502009-10-09 17:57:08 +000074def PromptYesNo(input_stream, output_stream, prompt):
75 output_stream.write(prompt)
76 response = input_stream.readline().strip().lower()
77 return response == 'y' or response == 'yes'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000078
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000079class OutputApi(object):
80 """This class (more like a module) gets passed to presubmit scripts so that
81 they can specify various types of results.
82 """
83
84 class PresubmitResult(object):
85 """Base class for result objects."""
86
87 def __init__(self, message, items=None, long_text=''):
88 """
89 message: A short one-line message to indicate errors.
90 items: A list of short strings to indicate where errors occurred.
91 long_text: multi-line text output, e.g. from another tool
92 """
93 self._message = message
94 self._items = []
95 if items:
96 self._items = items
97 self._long_text = long_text.rstrip()
98
99 def _Handle(self, output_stream, input_stream, may_prompt=True):
100 """Writes this result to the output stream.
101
102 Args:
103 output_stream: Where to write
104
105 Returns:
106 True if execution may continue, False otherwise.
107 """
108 output_stream.write(self._message)
109 output_stream.write('\n')
estade@chromium.org593258b2010-01-28 00:04:36 +0000110 if len(self._items) > 0:
111 output_stream.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000112 if self._long_text:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000113 output_stream.write('\n***************\n%s\n***************\n' %
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000114 self._long_text)
115
116 if self.ShouldPrompt() and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000117 if not PromptYesNo(input_stream, output_stream,
118 'Are you sure you want to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000119 return False
120
121 return not self.IsFatal()
122
123 def IsFatal(self):
124 """An error that is fatal stops g4 mail/submit immediately, i.e. before
125 other presubmit scripts are run.
126 """
127 return False
128
129 def ShouldPrompt(self):
130 """Whether this presubmit result should result in a prompt warning."""
131 return False
132
133 class PresubmitError(PresubmitResult):
134 """A hard presubmit error."""
135 def IsFatal(self):
136 return True
137
138 class PresubmitPromptWarning(PresubmitResult):
139 """An warning that prompts the user if they want to continue."""
140 def ShouldPrompt(self):
141 return True
142
143 class PresubmitNotifyResult(PresubmitResult):
144 """Just print something to the screen -- but it's not even a warning."""
145 pass
146
147 class MailTextResult(PresubmitResult):
148 """A warning that should be included in the review request email."""
149 def __init__(self, *args, **kwargs):
150 raise NotImplementedException() # TODO(joi) Implement.
151
152
153class InputApi(object):
154 """An instance of this object is passed to presubmit scripts so they can
155 know stuff about the change they're looking at.
156 """
157
maruel@chromium.org3410d912009-06-09 20:56:16 +0000158 # File extensions that are considered source files from a style guide
159 # perspective. Don't modify this list from a presubmit script!
160 DEFAULT_WHITE_LIST = (
161 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000162 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
163 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000164 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000165 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000166 # No extension at all, note that ALL CAPS files are black listed in
167 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000168 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000169 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000170 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000171 )
172
173 # Path regexp that should be excluded from being considered containing source
174 # files. Don't modify this list from a presubmit script!
175 DEFAULT_BLACK_LIST = (
176 r".*\bexperimental[\\\/].*",
177 r".*\bthird_party[\\\/].*",
178 # Output directories (just in case)
179 r".*\bDebug[\\\/].*",
180 r".*\bRelease[\\\/].*",
181 r".*\bxcodebuild[\\\/].*",
182 r".*\bsconsbuild[\\\/].*",
183 # All caps files like README and LICENCE.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000184 r".*\b[A-Z0-9_]+$",
185 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
186 r".*\.git[\\\/].*",
187 r".*\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000188 )
189
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000190 def __init__(self, change, presubmit_path, is_committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000191 """Builds an InputApi object.
192
193 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000194 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000195 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000196 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000197 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000198 # Version number of the presubmit_support script.
199 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000200 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000201 self.is_committing = is_committing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000202
203 # We expose various modules and functions as attributes of the input_api
204 # so that presubmit scripts don't have to import them.
205 self.basename = os.path.basename
206 self.cPickle = cPickle
207 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000208 self.json = json
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000209 self.os_path = os.path
210 self.pickle = pickle
211 self.marshal = marshal
212 self.re = re
213 self.subprocess = subprocess
214 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000215 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000216 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000217 self.urllib2 = urllib2
218
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000219 # To easily fork python.
220 self.python_executable = sys.executable
221 self.environ = os.environ
222
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000223 # InputApi.platform is the platform you're currently running on.
224 self.platform = sys.platform
225
226 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000227 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228
229 # We carry the canned checks so presubmit scripts can easily use them.
230 self.canned_checks = presubmit_canned_checks
231
232 def PresubmitLocalPath(self):
233 """Returns the local path of the presubmit script currently being run.
234
235 This is useful if you don't want to hard-code absolute paths in the
236 presubmit script. For example, It can be used to find another file
237 relative to the PRESUBMIT.py script, so the whole tree can be branched and
238 the presubmit script still works, without editing its content.
239 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000240 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000241
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000242 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000243 """Translate a depot path to a local path (relative to client root).
244
245 Args:
246 Depot path as a string.
247
248 Returns:
249 The local path of the depot path under the user's current client, or None
250 if the file is not mapped.
251
252 Remember to check for the None case and show an appropriate error!
253 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000254 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000255 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256 return local_path
257
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000258 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000259 """Translate a local path to a depot path.
260
261 Args:
262 Local path (relative to current directory, or absolute) as a string.
263
264 Returns:
265 The depot path (SVN URL) of the file if mapped, otherwise None.
266 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000267 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000268 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269 return depot_path
270
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271 def AffectedFiles(self, include_dirs=False, include_deletes=True):
272 """Same as input_api.change.AffectedFiles() except only lists files
273 (and optionally directories) in the same directory as the current presubmit
274 script, or subdirectories thereof.
275 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000276 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000277 if len(dir_with_slash) == 1:
278 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000279 return filter(
280 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
281 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282
283 def LocalPaths(self, include_dirs=False):
284 """Returns local paths of input_api.AffectedFiles()."""
285 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
286
287 def AbsoluteLocalPaths(self, include_dirs=False):
288 """Returns absolute local paths of input_api.AffectedFiles()."""
289 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
290
291 def ServerPaths(self, include_dirs=False):
292 """Returns server paths of input_api.AffectedFiles()."""
293 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
294
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000295 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000296 """Same as input_api.change.AffectedTextFiles() except only lists files
297 in the same directory as the current presubmit script, or subdirectories
298 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000299 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000300 if include_deletes is not None:
301 warnings.warn("AffectedTextFiles(include_deletes=%s)"
302 " is deprecated and ignored" % str(include_deletes),
303 category=DeprecationWarning,
304 stacklevel=2)
305 return filter(lambda x: x.IsTextFile(),
306 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000307
maruel@chromium.org3410d912009-06-09 20:56:16 +0000308 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
309 """Filters out files that aren't considered "source file".
310
311 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
312 and InputApi.DEFAULT_BLACK_LIST is used respectively.
313
314 The lists will be compiled as regular expression and
315 AffectedFile.LocalPath() needs to pass both list.
316
317 Note: Copy-paste this function to suit your needs or use a lambda function.
318 """
319 def Find(affected_file, list):
320 for item in list:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000321 local_path = affected_file.LocalPath()
322 if self.re.match(item, local_path):
323 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000324 return True
325 return False
326 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
327 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
328
329 def AffectedSourceFiles(self, source_file):
330 """Filter the list of AffectedTextFiles by the function source_file.
331
332 If source_file is None, InputApi.FilterSourceFile() is used.
333 """
334 if not source_file:
335 source_file = self.FilterSourceFile
336 return filter(source_file, self.AffectedTextFiles())
337
338 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000339 """An iterator over all text lines in "new" version of changed files.
340
341 Only lists lines from new or modified text files in the change that are
342 contained by the directory of the currently executing presubmit script.
343
344 This is useful for doing line-by-line regex checks, like checking for
345 trailing whitespace.
346
347 Yields:
348 a 3 tuple:
349 the AffectedFile instance of the current file;
350 integer line number (1-based); and
351 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000352
353 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000354 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000355 files = self.AffectedSourceFiles(source_file_filter)
356 return InputApi._RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000358 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000359 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000360
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000361 Deny reading anything outside the repository.
362 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000363 if isinstance(file_item, AffectedFile):
364 file_item = file_item.AbsoluteLocalPath()
365 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000366 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000367 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000368
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000369 @staticmethod
370 def _RightHandSideLinesImpl(affected_files):
371 """Implements RightHandSideLines for InputApi and GclChange."""
372 for af in affected_files:
373 lines = af.NewContents()
374 line_number = 0
375 for line in lines:
376 line_number += 1
377 yield (af, line_number, line)
378
379
380class AffectedFile(object):
381 """Representation of a file in a change."""
382
383 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000384 self._path = path
385 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000386 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000387 self._is_directory = None
388 self._properties = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000389
390 def ServerPath(self):
391 """Returns a path string that identifies the file in the SCM system.
392
393 Returns the empty string if the file does not exist in SCM.
394 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000395 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000396
397 def LocalPath(self):
398 """Returns the path of this file on the local disk relative to client root.
399 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000400 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000401
402 def AbsoluteLocalPath(self):
403 """Returns the absolute path of this file on the local disk.
404 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000405 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000406
407 def IsDirectory(self):
408 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000409 if self._is_directory is None:
410 path = self.AbsoluteLocalPath()
411 self._is_directory = (os.path.exists(path) and
412 os.path.isdir(path))
413 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000414
415 def Action(self):
416 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000417 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
418 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000419 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000420
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000421 def Property(self, property_name):
422 """Returns the specified SCM property of this file, or None if no such
423 property.
424 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000425 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000426
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000427 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000428 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000429
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000430 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000431 raise NotImplementedError() # Implement when needed
432
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433 def NewContents(self):
434 """Returns an iterator over the lines in the new version of file.
435
436 The new version is the file in the user's workspace, i.e. the "right hand
437 side".
438
439 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000440 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000441 """
442 if self.IsDirectory():
443 return []
444 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000445 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
446 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000447
448 def OldContents(self):
449 """Returns an iterator over the lines in the old version of file.
450
451 The old version is the file in depot, i.e. the "left hand side".
452 """
453 raise NotImplementedError() # Implement when needed
454
455 def OldFileTempPath(self):
456 """Returns the path on local disk where the old contents resides.
457
458 The old version is the file in depot, i.e. the "left hand side".
459 This is a read-only cached copy of the old contents. *DO NOT* try to
460 modify this file.
461 """
462 raise NotImplementedError() # Implement if/when needed.
463
maruel@chromium.org5de13972009-06-10 18:16:06 +0000464 def __str__(self):
465 return self.LocalPath()
466
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000467
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000468class SvnAffectedFile(AffectedFile):
469 """Representation of a file in a change out of a Subversion checkout."""
470
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000471 def __init__(self, *args, **kwargs):
472 AffectedFile.__init__(self, *args, **kwargs)
473 self._server_path = None
474 self._is_text_file = None
475
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000476 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000477 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000478 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000479 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000480 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000481
482 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000483 if self._is_directory is None:
484 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000485 if os.path.exists(path):
486 # Retrieve directly from the file system; it is much faster than
487 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000488 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000489 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000490 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000491 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000492 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000493
494 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000495 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000496 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000497 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000498 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000499
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000500 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000501 if self._is_text_file is None:
502 if self.Action() == 'D':
503 # A deleted file is not a text file.
504 self._is_text_file = False
505 elif self.IsDirectory():
506 self._is_text_file = False
507 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000508 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
509 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000510 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
511 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000512
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000513
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000514class GitAffectedFile(AffectedFile):
515 """Representation of a file in a change out of a git checkout."""
516
517 def __init__(self, *args, **kwargs):
518 AffectedFile.__init__(self, *args, **kwargs)
519 self._server_path = None
520 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000521
522 def ServerPath(self):
523 if self._server_path is None:
524 raise NotImplementedException() # TODO(maruel) Implement.
525 return self._server_path
526
527 def IsDirectory(self):
528 if self._is_directory is None:
529 path = self.AbsoluteLocalPath()
530 if os.path.exists(path):
531 # Retrieve directly from the file system; it is much faster than
532 # querying subversion, especially on Windows.
533 self._is_directory = os.path.isdir(path)
534 else:
535 # raise NotImplementedException() # TODO(maruel) Implement.
536 self._is_directory = False
537 return self._is_directory
538
539 def Property(self, property_name):
540 if not property_name in self._properties:
541 raise NotImplementedException() # TODO(maruel) Implement.
542 return self._properties[property_name]
543
544 def IsTextFile(self):
545 if self._is_text_file is None:
546 if self.Action() == 'D':
547 # A deleted file is not a text file.
548 self._is_text_file = False
549 elif self.IsDirectory():
550 self._is_text_file = False
551 else:
552 # raise NotImplementedException() # TODO(maruel) Implement.
553 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
554 return self._is_text_file
555
556
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000557class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000558 """Describe a change.
559
560 Used directly by the presubmit scripts to query the current change being
561 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000562
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000563 Instance members:
564 tags: Dictionnary of KEY=VALUE pairs found in the change description.
565 self.KEY: equivalent to tags['KEY']
566 """
567
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000568 _AFFECTED_FILES = AffectedFile
569
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000570 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000571 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000572 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000573
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000574 def __init__(self, name, description, local_root, files, issue, patchset):
575 if files is None:
576 files = []
577 self._name = name
578 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000579 # Convert root into an absolute path.
580 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000581 self.issue = issue
582 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000583 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000584
585 # From the description text, build up a dictionary of key/value pairs
586 # plus the description minus all key/value or "tag" lines.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000587 self._description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000588 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000589 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000590 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000591 if m:
592 self.tags[m.group('key')] = m.group('value')
593 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000594 self._description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000595
596 # Change back to text and remove whitespace at end.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000597 self._description_without_tags = '\n'.join(self._description_without_tags)
598 self._description_without_tags = self._description_without_tags.rstrip()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000599
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000600 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000601 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
602 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000603 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000604
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000605 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000606 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000607 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000608
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000609 def DescriptionText(self):
610 """Returns the user-entered changelist description, minus tags.
611
612 Any line in the user-provided description starting with e.g. "FOO="
613 (whitespace permitted before and around) is considered a tag line. Such
614 lines are stripped out of the description this function returns.
615 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000616 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000617
618 def FullDescriptionText(self):
619 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000620 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000621
622 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000623 """Returns the repository (checkout) root directory for this change,
624 as an absolute path.
625 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000626 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000627
628 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000629 """Return tags directly as attributes on the object."""
630 if not re.match(r"^[A-Z_]*$", attr):
631 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000632 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000633
634 def AffectedFiles(self, include_dirs=False, include_deletes=True):
635 """Returns a list of AffectedFile instances for all files in the change.
636
637 Args:
638 include_deletes: If false, deleted files will be filtered out.
639 include_dirs: True to include directories in the list
640
641 Returns:
642 [AffectedFile(path, action), AffectedFile(path, action)]
643 """
644 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000645 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000646 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000647 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000648
649 if include_deletes:
650 return affected
651 else:
652 return filter(lambda x: x.Action() != 'D', affected)
653
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000654 def AffectedTextFiles(self, include_deletes=None):
655 """Return a list of the existing text files in a change."""
656 if include_deletes is not None:
657 warnings.warn("AffectedTextFiles(include_deletes=%s)"
658 " is deprecated and ignored" % str(include_deletes),
659 category=DeprecationWarning,
660 stacklevel=2)
661 return filter(lambda x: x.IsTextFile(),
662 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000663
664 def LocalPaths(self, include_dirs=False):
665 """Convenience function."""
666 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
667
668 def AbsoluteLocalPaths(self, include_dirs=False):
669 """Convenience function."""
670 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
671
672 def ServerPaths(self, include_dirs=False):
673 """Convenience function."""
674 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
675
676 def RightHandSideLines(self):
677 """An iterator over all text lines in "new" version of changed files.
678
679 Lists lines from new or modified text files in the change.
680
681 This is useful for doing line-by-line regex checks, like checking for
682 trailing whitespace.
683
684 Yields:
685 a 3 tuple:
686 the AffectedFile instance of the current file;
687 integer line number (1-based); and
688 the contents of the line as a string.
689 """
690 return InputApi._RightHandSideLinesImpl(
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000691 filter(lambda x: x.IsTextFile(),
692 self.AffectedFiles(include_deletes=False)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693
694
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000695class SvnChange(Change):
696 _AFFECTED_FILES = SvnAffectedFile
697
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000698 def __init__(self, *args, **kwargs):
699 Change.__init__(self, *args, **kwargs)
700 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000701 self._changelists = None
702
703 def _GetChangeLists(self):
704 """Get all change lists."""
705 if self._changelists == None:
706 previous_cwd = os.getcwd()
707 os.chdir(self.RepositoryRoot())
708 self._changelists = gcl.GetModifiedFiles()
709 os.chdir(previous_cwd)
710 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000711
712 def GetAllModifiedFiles(self):
713 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000714 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000715 all_modified_files = []
716 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000717 all_modified_files.extend(
718 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000719 return all_modified_files
720
721 def GetModifiedFiles(self):
722 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000723 changelists = self._GetChangeLists()
724 return [os.path.join(self.RepositoryRoot(), f[1])
725 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000726
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000727
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000728class GitChange(Change):
729 _AFFECTED_FILES = GitAffectedFile
730
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000731 def __init__(self, *args, **kwargs):
732 Change.__init__(self, *args, **kwargs)
733 self.scm = 'git'
734
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000735
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000736def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000737 """Finds all presubmit files that apply to a given set of source files.
738
739 Args:
740 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000741 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742
743 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000744 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000745 """
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000746 entries = []
747 for f in files:
748 f = normpath(os.path.join(root, f))
749 while f:
750 f = os.path.dirname(f)
751 if f in entries:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000752 break
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000753 entries.append(f)
754 if f == root:
755 break
756 entries.sort()
757 entries = map(lambda x: os.path.join(x, 'PRESUBMIT.py'), entries)
758 return filter(lambda x: os.path.isfile(x), entries)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000759
760
thestig@chromium.orgde243452009-10-06 21:02:56 +0000761class GetTrySlavesExecuter(object):
762 def ExecPresubmitScript(self, script_text):
763 """Executes GetPreferredTrySlaves() from a single presubmit script.
764
765 Args:
766 script_text: The text of the presubmit script.
767
768 Return:
769 A list of try slaves.
770 """
771 context = {}
772 exec script_text in context
773
774 function_name = 'GetPreferredTrySlaves'
775 if function_name in context:
776 result = eval(function_name + '()', context)
777 if not isinstance(result, types.ListType):
778 raise exceptions.RuntimeError(
779 'Presubmit functions must return a list, got a %s instead: %s' %
780 (type(result), str(result)))
781 for item in result:
782 if not isinstance(item, basestring):
783 raise exceptions.RuntimeError('All try slaves names must be strings.')
784 if item != item.strip():
785 raise exceptions.RuntimeError('Try slave names cannot start/end'
786 'with whitespace')
787 else:
788 result = []
789 return result
790
791
792def DoGetTrySlaves(changed_files,
793 repository_root,
794 default_presubmit,
795 verbose,
796 output_stream):
797 """Get the list of try servers from the presubmit scripts.
798
799 Args:
800 changed_files: List of modified files.
801 repository_root: The repository root.
802 default_presubmit: A default presubmit script to execute in any case.
803 verbose: Prints debug info.
804 output_stream: A stream to write debug output to.
805
806 Return:
807 List of try slaves
808 """
809 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
810 if not presubmit_files and verbose:
811 output_stream.write("Warning, no presubmit.py found.\n")
812 results = []
813 executer = GetTrySlavesExecuter()
814 if default_presubmit:
815 if verbose:
816 output_stream.write("Running default presubmit script.\n")
817 results += executer.ExecPresubmitScript(default_presubmit)
818 for filename in presubmit_files:
819 filename = os.path.abspath(filename)
820 if verbose:
821 output_stream.write("Running %s\n" % filename)
822 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000823 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000824 results += executer.ExecPresubmitScript(presubmit_script)
825
826 slaves = list(set(results))
827 if slaves and verbose:
828 output_stream.write(', '.join(slaves))
829 output_stream.write('\n')
830 return slaves
831
832
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000833class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000834 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000835 """
836 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000837 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
839 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000840 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000841 self.committing = committing
842
843 def ExecPresubmitScript(self, script_text, presubmit_path):
844 """Executes a single presubmit script.
845
846 Args:
847 script_text: The text of the presubmit script.
848 presubmit_path: The path to the presubmit file (this will be reported via
849 input_api.PresubmitLocalPath()).
850
851 Return:
852 A list of result objects, empty if no problems.
853 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000854
855 # Change to the presubmit file's directory to support local imports.
856 main_path = os.getcwd()
857 os.chdir(os.path.dirname(presubmit_path))
858
859 # Load the presubmit script into context.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000860 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000861 context = {}
862 exec script_text in context
863
864 # These function names must change if we make substantial changes to
865 # the presubmit API that are not backwards compatible.
866 if self.committing:
867 function_name = 'CheckChangeOnCommit'
868 else:
869 function_name = 'CheckChangeOnUpload'
870 if function_name in context:
871 context['__args'] = (input_api, OutputApi())
872 result = eval(function_name + '(*__args)', context)
873 if not (isinstance(result, types.TupleType) or
874 isinstance(result, types.ListType)):
875 raise exceptions.RuntimeError(
876 'Presubmit functions must return a tuple or list')
877 for item in result:
878 if not isinstance(item, OutputApi.PresubmitResult):
879 raise exceptions.RuntimeError(
880 'All presubmit results must be of types derived from '
881 'output_api.PresubmitResult')
882 else:
883 result = () # no error since the script doesn't care about current event.
884
chase@chromium.org8e416c82009-10-06 04:30:44 +0000885 # Return the process to the original working directory.
886 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000887 return result
888
889
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000890def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000891 committing,
892 verbose,
893 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000894 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000895 default_presubmit,
896 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000897 """Runs all presubmit checks that apply to the files in the change.
898
899 This finds all PRESUBMIT.py files in directories enclosing the files in the
900 change (up to the repository root) and calls the relevant entrypoint function
901 depending on whether the change is being committed or uploaded.
902
903 Prints errors, warnings and notifications. Prompts the user for warnings
904 when needed.
905
906 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000907 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000908 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
909 verbose: Prints debug info.
910 output_stream: A stream to write output from presubmit tests to.
911 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000912 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000913 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000914
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000915 Warning:
916 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
917 SHOULD be sys.stdin.
918
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000919 Return:
920 True if execution can continue, False if not.
921 """
jam@chromium.org2a891dc2009-08-20 20:33:37 +0000922 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000923 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
924 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000925 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000926 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000927 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000928 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000929 if default_presubmit:
930 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000931 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000932 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000933 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000934 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000935 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000936 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000937 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000938 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000939 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940 results += executer.ExecPresubmitScript(presubmit_script, filename)
941
942 errors = []
943 notifications = []
944 warnings = []
945 for result in results:
946 if not result.IsFatal() and not result.ShouldPrompt():
947 notifications.append(result)
948 elif result.ShouldPrompt():
949 warnings.append(result)
950 else:
951 errors.append(result)
952
953 error_count = 0
954 for name, items in (('Messages', notifications),
955 ('Warnings', warnings),
956 ('ERRORS', errors)):
957 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000958 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000959 for item in items:
960 if not item._Handle(output_stream, input_stream,
961 may_prompt=False):
962 error_count += 1
963 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +0000964
965 total_time = time.time() - start_time
966 if total_time > 1.0:
967 print "Presubmit checks took %.1fs to calculate." % total_time
968
maruel@chromium.org07bbc212009-06-11 02:08:41 +0000969 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000970 if not PromptYesNo(input_stream, output_stream,
971 'There were presubmit warnings. '
972 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000974
975 global _ASKED_FOR_FEEDBACK
976 # Ask for feedback one time out of 5.
977 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
978 output_stream.write("Was the presubmit check useful? Please send feedback "
979 "& hate mail to maruel@chromium.org!\n")
980 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981 return (error_count == 0)
982
983
984def ScanSubDirs(mask, recursive):
985 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000986 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 +0000987 else:
988 results = []
989 for root, dirs, files in os.walk('.'):
990 if '.svn' in dirs:
991 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000992 if '.git' in dirs:
993 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000994 for name in files:
995 if fnmatch.fnmatch(name, mask):
996 results.append(os.path.join(root, name))
997 return results
998
999
1000def ParseFiles(args, recursive):
1001 files = []
1002 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001003 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001004 return files
1005
1006
1007def Main(argv):
1008 parser = optparse.OptionParser(usage="%prog [options]",
1009 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001010 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001011 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001012 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1013 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001014 parser.add_option("-r", "--recursive", action="store_true",
1015 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001016 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001017 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001018 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001019 parser.add_option("--name", default='no name')
1020 parser.add_option("--description", default='')
1021 parser.add_option("--issue", type='int', default=0)
1022 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001023 parser.add_option("--root", default='')
1024 parser.add_option("--default_presubmit")
1025 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001026 options, args = parser.parse_args(argv[1:])
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001027 if not options.root:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001028 options.root = os.getcwd()
1029 if os.path.isdir(os.path.join(options.root, '.git')):
1030 change_class = GitChange
1031 if not options.files:
1032 if args:
1033 options.files = ParseFiles(args, options.recursive)
1034 else:
1035 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001036 options.files = scm.GIT.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001037 elif os.path.isdir(os.path.join(options.root, '.svn')):
1038 change_class = SvnChange
1039 if not options.files:
1040 if args:
1041 options.files = ParseFiles(args, options.recursive)
1042 else:
1043 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001044 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001045 else:
1046 # Doesn't seem under source control.
1047 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001048 if options.verbose:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001049 if len(options.files) != 1:
1050 print "Found %d files." % len(options.files)
1051 else:
1052 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001053 return not DoPresubmitChecks(change_class(options.name,
1054 options.description,
1055 options.root,
1056 options.files,
1057 options.issue,
1058 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001059 options.commit,
1060 options.verbose,
1061 sys.stdout,
1062 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001063 options.default_presubmit,
1064 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001065
1066
1067if __name__ == '__main__':
1068 sys.exit(Main(sys.argv))