blob: f6216432f76c282e90234426eb32aa2da32dfd97 [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
2# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00009__version__ = '1.3.5'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
15import cPickle # Exposed through the API.
16import cStringIO # Exposed through the API.
17import exceptions
18import fnmatch
19import glob
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000020import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import marshal # Exposed through the API.
22import optparse
23import os # Somewhat exposed through the API.
24import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000025import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import re # Exposed through the API.
27import subprocess # Exposed through the API.
28import sys # Parts exposed through API.
29import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000030import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000031import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000033import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import urllib2 # Exposed through the API.
maruel@chromium.org1e08c002009-05-28 19:09:33 +000035import warnings
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000037try:
38 import simplejson as json
39except ImportError:
40 try:
41 import json
tony@chromium.org5e9f2ed2010-04-08 01:38:19 +000042 # Some versions of python2.5 have an incomplete json module. Check to make
43 # sure loads exists.
44 json.loads
45 except (ImportError, AttributeError):
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000046 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000047 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
48 import simplejson as json
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000049
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050# Local imports.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000051import gcl
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000052import gclient_utils
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000054import scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055
56
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000057# Ask for feedback only once in program lifetime.
58_ASKED_FOR_FEEDBACK = False
59
60
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000061class NotImplementedException(Exception):
62 """We're leaving placeholders in a bunch of places to remind us of the
63 design of the API, but we have not implemented all of it yet. Implement as
64 the need arises.
65 """
66 pass
67
68
69def normpath(path):
70 '''Version of os.path.normpath that also changes backward slashes to
71 forward slashes when not running on Windows.
72 '''
73 # This is safe to always do because the Windows version of os.path.normpath
74 # will replace forward slashes with backward slashes.
75 path = path.replace(os.sep, '/')
76 return os.path.normpath(path)
77
gspencer@google.comefb94502009-10-09 17:57:08 +000078def PromptYesNo(input_stream, output_stream, prompt):
79 output_stream.write(prompt)
80 response = input_stream.readline().strip().lower()
81 return response == 'y' or response == 'yes'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000082
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000083class OutputApi(object):
84 """This class (more like a module) gets passed to presubmit scripts so that
85 they can specify various types of results.
86 """
87
88 class PresubmitResult(object):
89 """Base class for result objects."""
90
91 def __init__(self, message, items=None, long_text=''):
92 """
93 message: A short one-line message to indicate errors.
94 items: A list of short strings to indicate where errors occurred.
95 long_text: multi-line text output, e.g. from another tool
96 """
97 self._message = message
98 self._items = []
99 if items:
100 self._items = items
101 self._long_text = long_text.rstrip()
102
103 def _Handle(self, output_stream, input_stream, may_prompt=True):
104 """Writes this result to the output stream.
105
106 Args:
107 output_stream: Where to write
108
109 Returns:
110 True if execution may continue, False otherwise.
111 """
112 output_stream.write(self._message)
113 output_stream.write('\n')
estade@chromium.org593258b2010-01-28 00:04:36 +0000114 if len(self._items) > 0:
115 output_stream.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000116 if self._long_text:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000117 output_stream.write('\n***************\n%s\n***************\n' %
maruel@chromium.org35db5092010-09-20 19:58:37 +0000118 self._long_text.encode('ascii', 'replace'))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000119
120 if self.ShouldPrompt() and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000121 if not PromptYesNo(input_stream, output_stream,
122 'Are you sure you want to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000123 return False
124
125 return not self.IsFatal()
126
127 def IsFatal(self):
128 """An error that is fatal stops g4 mail/submit immediately, i.e. before
129 other presubmit scripts are run.
130 """
131 return False
132
133 def ShouldPrompt(self):
134 """Whether this presubmit result should result in a prompt warning."""
135 return False
136
137 class PresubmitError(PresubmitResult):
138 """A hard presubmit error."""
139 def IsFatal(self):
140 return True
141
142 class PresubmitPromptWarning(PresubmitResult):
143 """An warning that prompts the user if they want to continue."""
144 def ShouldPrompt(self):
145 return True
146
147 class PresubmitNotifyResult(PresubmitResult):
148 """Just print something to the screen -- but it's not even a warning."""
149 pass
150
151 class MailTextResult(PresubmitResult):
152 """A warning that should be included in the review request email."""
153 def __init__(self, *args, **kwargs):
154 raise NotImplementedException() # TODO(joi) Implement.
155
156
157class InputApi(object):
158 """An instance of this object is passed to presubmit scripts so they can
159 know stuff about the change they're looking at.
160 """
161
maruel@chromium.org3410d912009-06-09 20:56:16 +0000162 # File extensions that are considered source files from a style guide
163 # perspective. Don't modify this list from a presubmit script!
164 DEFAULT_WHITE_LIST = (
165 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000166 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
167 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000168 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000169 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000170 # No extension at all, note that ALL CAPS files are black listed in
171 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000172 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000173 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000174 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000175 )
176
177 # Path regexp that should be excluded from being considered containing source
178 # files. Don't modify this list from a presubmit script!
179 DEFAULT_BLACK_LIST = (
180 r".*\bexperimental[\\\/].*",
181 r".*\bthird_party[\\\/].*",
182 # Output directories (just in case)
183 r".*\bDebug[\\\/].*",
184 r".*\bRelease[\\\/].*",
185 r".*\bxcodebuild[\\\/].*",
186 r".*\bsconsbuild[\\\/].*",
187 # All caps files like README and LICENCE.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000188 r".*\b[A-Z0-9_]+$",
189 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
190 r".*\.git[\\\/].*",
191 r".*\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000192 )
193
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000194 def __init__(self, change, presubmit_path, is_committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000195 """Builds an InputApi object.
196
197 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000198 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000199 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000200 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000201 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000202 # Version number of the presubmit_support script.
203 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000204 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000205 self.is_committing = is_committing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000206
207 # We expose various modules and functions as attributes of the input_api
208 # so that presubmit scripts don't have to import them.
209 self.basename = os.path.basename
210 self.cPickle = cPickle
211 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000212 self.json = json
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000213 self.os_path = os.path
214 self.pickle = pickle
215 self.marshal = marshal
216 self.re = re
217 self.subprocess = subprocess
218 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000219 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000220 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000221 self.urllib2 = urllib2
222
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000223 # To easily fork python.
224 self.python_executable = sys.executable
225 self.environ = os.environ
226
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000227 # InputApi.platform is the platform you're currently running on.
228 self.platform = sys.platform
229
230 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000231 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000232
233 # We carry the canned checks so presubmit scripts can easily use them.
234 self.canned_checks = presubmit_canned_checks
235
236 def PresubmitLocalPath(self):
237 """Returns the local path of the presubmit script currently being run.
238
239 This is useful if you don't want to hard-code absolute paths in the
240 presubmit script. For example, It can be used to find another file
241 relative to the PRESUBMIT.py script, so the whole tree can be branched and
242 the presubmit script still works, without editing its content.
243 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000244 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000245
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000246 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000247 """Translate a depot path to a local path (relative to client root).
248
249 Args:
250 Depot path as a string.
251
252 Returns:
253 The local path of the depot path under the user's current client, or None
254 if the file is not mapped.
255
256 Remember to check for the None case and show an appropriate error!
257 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000258 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000259 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000260 return local_path
261
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000262 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000263 """Translate a local path to a depot path.
264
265 Args:
266 Local path (relative to current directory, or absolute) as a string.
267
268 Returns:
269 The depot path (SVN URL) of the file if mapped, otherwise None.
270 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000271 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000272 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273 return depot_path
274
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000275 def AffectedFiles(self, include_dirs=False, include_deletes=True):
276 """Same as input_api.change.AffectedFiles() except only lists files
277 (and optionally directories) in the same directory as the current presubmit
278 script, or subdirectories thereof.
279 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000280 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000281 if len(dir_with_slash) == 1:
282 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000283 return filter(
284 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
285 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286
287 def LocalPaths(self, include_dirs=False):
288 """Returns local paths of input_api.AffectedFiles()."""
289 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
290
291 def AbsoluteLocalPaths(self, include_dirs=False):
292 """Returns absolute local paths of input_api.AffectedFiles()."""
293 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
294
295 def ServerPaths(self, include_dirs=False):
296 """Returns server paths of input_api.AffectedFiles()."""
297 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
298
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000299 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000300 """Same as input_api.change.AffectedTextFiles() except only lists files
301 in the same directory as the current presubmit script, or subdirectories
302 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000303 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000304 if include_deletes is not None:
305 warnings.warn("AffectedTextFiles(include_deletes=%s)"
306 " is deprecated and ignored" % str(include_deletes),
307 category=DeprecationWarning,
308 stacklevel=2)
309 return filter(lambda x: x.IsTextFile(),
310 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000311
maruel@chromium.org3410d912009-06-09 20:56:16 +0000312 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
313 """Filters out files that aren't considered "source file".
314
315 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
316 and InputApi.DEFAULT_BLACK_LIST is used respectively.
317
318 The lists will be compiled as regular expression and
319 AffectedFile.LocalPath() needs to pass both list.
320
321 Note: Copy-paste this function to suit your needs or use a lambda function.
322 """
323 def Find(affected_file, list):
324 for item in list:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000325 local_path = affected_file.LocalPath()
326 if self.re.match(item, local_path):
327 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000328 return True
329 return False
330 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
331 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
332
333 def AffectedSourceFiles(self, source_file):
334 """Filter the list of AffectedTextFiles by the function source_file.
335
336 If source_file is None, InputApi.FilterSourceFile() is used.
337 """
338 if not source_file:
339 source_file = self.FilterSourceFile
340 return filter(source_file, self.AffectedTextFiles())
341
342 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000343 """An iterator over all text lines in "new" version of changed files.
344
345 Only lists lines from new or modified text files in the change that are
346 contained by the directory of the currently executing presubmit script.
347
348 This is useful for doing line-by-line regex checks, like checking for
349 trailing whitespace.
350
351 Yields:
352 a 3 tuple:
353 the AffectedFile instance of the current file;
354 integer line number (1-based); and
355 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000356
357 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000358 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000359 files = self.AffectedSourceFiles(source_file_filter)
360 return InputApi._RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000361
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000362 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000363 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000364
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000365 Deny reading anything outside the repository.
366 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000367 if isinstance(file_item, AffectedFile):
368 file_item = file_item.AbsoluteLocalPath()
369 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000370 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000371 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000372
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000373 @staticmethod
374 def _RightHandSideLinesImpl(affected_files):
375 """Implements RightHandSideLines for InputApi and GclChange."""
376 for af in affected_files:
377 lines = af.NewContents()
378 line_number = 0
379 for line in lines:
380 line_number += 1
381 yield (af, line_number, line)
382
383
384class AffectedFile(object):
385 """Representation of a file in a change."""
386
387 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000388 self._path = path
389 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000390 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000391 self._is_directory = None
392 self._properties = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000393
394 def ServerPath(self):
395 """Returns a path string that identifies the file in the SCM system.
396
397 Returns the empty string if the file does not exist in SCM.
398 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000399 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000400
401 def LocalPath(self):
402 """Returns the path of this file on the local disk relative to client root.
403 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000404 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405
406 def AbsoluteLocalPath(self):
407 """Returns the absolute path of this file on the local disk.
408 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000409 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000410
411 def IsDirectory(self):
412 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000413 if self._is_directory is None:
414 path = self.AbsoluteLocalPath()
415 self._is_directory = (os.path.exists(path) and
416 os.path.isdir(path))
417 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000418
419 def Action(self):
420 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000421 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
422 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000423 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000424
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000425 def Property(self, property_name):
426 """Returns the specified SCM property of this file, or None if no such
427 property.
428 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000429 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000430
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000431 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000432 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000433
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000434 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000435 raise NotImplementedError() # Implement when needed
436
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000437 def NewContents(self):
438 """Returns an iterator over the lines in the new version of file.
439
440 The new version is the file in the user's workspace, i.e. the "right hand
441 side".
442
443 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000444 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000445 """
446 if self.IsDirectory():
447 return []
448 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000449 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
450 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000451
452 def OldContents(self):
453 """Returns an iterator over the lines in the old version of file.
454
455 The old version is the file in depot, i.e. the "left hand side".
456 """
457 raise NotImplementedError() # Implement when needed
458
459 def OldFileTempPath(self):
460 """Returns the path on local disk where the old contents resides.
461
462 The old version is the file in depot, i.e. the "left hand side".
463 This is a read-only cached copy of the old contents. *DO NOT* try to
464 modify this file.
465 """
466 raise NotImplementedError() # Implement if/when needed.
467
maruel@chromium.org5de13972009-06-10 18:16:06 +0000468 def __str__(self):
469 return self.LocalPath()
470
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000471
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000472class SvnAffectedFile(AffectedFile):
473 """Representation of a file in a change out of a Subversion checkout."""
474
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000475 def __init__(self, *args, **kwargs):
476 AffectedFile.__init__(self, *args, **kwargs)
477 self._server_path = None
478 self._is_text_file = None
479
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000480 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000481 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000482 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000483 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000484 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000485
486 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000487 if self._is_directory is None:
488 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000489 if os.path.exists(path):
490 # Retrieve directly from the file system; it is much faster than
491 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000492 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000493 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000494 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000495 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000496 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000497
498 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000499 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000500 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000501 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000502 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000503
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000504 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000505 if self._is_text_file is None:
506 if self.Action() == 'D':
507 # A deleted file is not a text file.
508 self._is_text_file = False
509 elif self.IsDirectory():
510 self._is_text_file = False
511 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000512 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
513 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000514 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
515 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000516
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000517
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000518class GitAffectedFile(AffectedFile):
519 """Representation of a file in a change out of a git checkout."""
520
521 def __init__(self, *args, **kwargs):
522 AffectedFile.__init__(self, *args, **kwargs)
523 self._server_path = None
524 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000525
526 def ServerPath(self):
527 if self._server_path is None:
528 raise NotImplementedException() # TODO(maruel) Implement.
529 return self._server_path
530
531 def IsDirectory(self):
532 if self._is_directory is None:
533 path = self.AbsoluteLocalPath()
534 if os.path.exists(path):
535 # Retrieve directly from the file system; it is much faster than
536 # querying subversion, especially on Windows.
537 self._is_directory = os.path.isdir(path)
538 else:
539 # raise NotImplementedException() # TODO(maruel) Implement.
540 self._is_directory = False
541 return self._is_directory
542
543 def Property(self, property_name):
544 if not property_name in self._properties:
545 raise NotImplementedException() # TODO(maruel) Implement.
546 return self._properties[property_name]
547
548 def IsTextFile(self):
549 if self._is_text_file is None:
550 if self.Action() == 'D':
551 # A deleted file is not a text file.
552 self._is_text_file = False
553 elif self.IsDirectory():
554 self._is_text_file = False
555 else:
556 # raise NotImplementedException() # TODO(maruel) Implement.
557 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
558 return self._is_text_file
559
560
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000561class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000562 """Describe a change.
563
564 Used directly by the presubmit scripts to query the current change being
565 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000566
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000567 Instance members:
568 tags: Dictionnary of KEY=VALUE pairs found in the change description.
569 self.KEY: equivalent to tags['KEY']
570 """
571
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000572 _AFFECTED_FILES = AffectedFile
573
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000574 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000575 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000576 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000577
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000578 def __init__(self, name, description, local_root, files, issue, patchset):
579 if files is None:
580 files = []
581 self._name = name
582 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000583 # Convert root into an absolute path.
584 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000585 self.issue = issue
586 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000587 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000588
589 # From the description text, build up a dictionary of key/value pairs
590 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000591 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000592 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000593 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000594 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000595 if m:
596 self.tags[m.group('key')] = m.group('value')
597 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000598 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000599
600 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000601 self._description_without_tags = (
602 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000603
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000604 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000605 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
606 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000607 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000608
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000609 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000611 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000612
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000613 def DescriptionText(self):
614 """Returns the user-entered changelist description, minus tags.
615
616 Any line in the user-provided description starting with e.g. "FOO="
617 (whitespace permitted before and around) is considered a tag line. Such
618 lines are stripped out of the description this function returns.
619 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000620 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000621
622 def FullDescriptionText(self):
623 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000624 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000625
626 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000627 """Returns the repository (checkout) root directory for this change,
628 as an absolute path.
629 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000630 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000631
632 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000633 """Return tags directly as attributes on the object."""
634 if not re.match(r"^[A-Z_]*$", attr):
635 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000636 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000637
638 def AffectedFiles(self, include_dirs=False, include_deletes=True):
639 """Returns a list of AffectedFile instances for all files in the change.
640
641 Args:
642 include_deletes: If false, deleted files will be filtered out.
643 include_dirs: True to include directories in the list
644
645 Returns:
646 [AffectedFile(path, action), AffectedFile(path, action)]
647 """
648 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000649 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000650 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000651 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000652
653 if include_deletes:
654 return affected
655 else:
656 return filter(lambda x: x.Action() != 'D', affected)
657
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000658 def AffectedTextFiles(self, include_deletes=None):
659 """Return a list of the existing text files in a change."""
660 if include_deletes is not None:
661 warnings.warn("AffectedTextFiles(include_deletes=%s)"
662 " is deprecated and ignored" % str(include_deletes),
663 category=DeprecationWarning,
664 stacklevel=2)
665 return filter(lambda x: x.IsTextFile(),
666 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000667
668 def LocalPaths(self, include_dirs=False):
669 """Convenience function."""
670 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
671
672 def AbsoluteLocalPaths(self, include_dirs=False):
673 """Convenience function."""
674 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
675
676 def ServerPaths(self, include_dirs=False):
677 """Convenience function."""
678 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
679
680 def RightHandSideLines(self):
681 """An iterator over all text lines in "new" version of changed files.
682
683 Lists lines from new or modified text files in the change.
684
685 This is useful for doing line-by-line regex checks, like checking for
686 trailing whitespace.
687
688 Yields:
689 a 3 tuple:
690 the AffectedFile instance of the current file;
691 integer line number (1-based); and
692 the contents of the line as a string.
693 """
694 return InputApi._RightHandSideLinesImpl(
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000695 filter(lambda x: x.IsTextFile(),
696 self.AffectedFiles(include_deletes=False)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697
698
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000699class SvnChange(Change):
700 _AFFECTED_FILES = SvnAffectedFile
701
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000702 def __init__(self, *args, **kwargs):
703 Change.__init__(self, *args, **kwargs)
704 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000705 self._changelists = None
706
707 def _GetChangeLists(self):
708 """Get all change lists."""
709 if self._changelists == None:
710 previous_cwd = os.getcwd()
711 os.chdir(self.RepositoryRoot())
712 self._changelists = gcl.GetModifiedFiles()
713 os.chdir(previous_cwd)
714 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000715
716 def GetAllModifiedFiles(self):
717 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000718 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000719 all_modified_files = []
720 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000721 all_modified_files.extend(
722 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000723 return all_modified_files
724
725 def GetModifiedFiles(self):
726 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000727 changelists = self._GetChangeLists()
728 return [os.path.join(self.RepositoryRoot(), f[1])
729 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000730
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000731
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000732class GitChange(Change):
733 _AFFECTED_FILES = GitAffectedFile
734
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000735 def __init__(self, *args, **kwargs):
736 Change.__init__(self, *args, **kwargs)
737 self.scm = 'git'
738
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000739
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000740def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741 """Finds all presubmit files that apply to a given set of source files.
742
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000743 If inherit-review-settings-ok is present right under root, looks for
744 PRESUBMIT.py in directories enclosing root.
745
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000746 Args:
747 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000748 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749
750 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000751 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000752 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000753 files = [normpath(os.path.join(root, f)) for f in files]
754
755 # List all the individual directories containing files.
756 directories = set([os.path.dirname(f) for f in files])
757
758 # Ignore root if inherit-review-settings-ok is present.
759 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
760 root = None
761
762 # Collect all unique directories that may contain PRESUBMIT.py.
763 candidates = set()
764 for directory in directories:
765 while True:
766 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000767 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000768 candidates.add(directory)
769 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000770 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000771 parent_dir = os.path.dirname(directory)
772 if parent_dir == directory:
773 # We hit the system root directory.
774 break
775 directory = parent_dir
776
777 # Look for PRESUBMIT.py in all candidate directories.
778 results = []
779 for directory in sorted(list(candidates)):
780 p = os.path.join(directory, 'PRESUBMIT.py')
781 if os.path.isfile(p):
782 results.append(p)
783
784 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000785
786
thestig@chromium.orgde243452009-10-06 21:02:56 +0000787class GetTrySlavesExecuter(object):
788 def ExecPresubmitScript(self, script_text):
789 """Executes GetPreferredTrySlaves() from a single presubmit script.
790
791 Args:
792 script_text: The text of the presubmit script.
793
794 Return:
795 A list of try slaves.
796 """
797 context = {}
798 exec script_text in context
799
800 function_name = 'GetPreferredTrySlaves'
801 if function_name in context:
802 result = eval(function_name + '()', context)
803 if not isinstance(result, types.ListType):
804 raise exceptions.RuntimeError(
805 'Presubmit functions must return a list, got a %s instead: %s' %
806 (type(result), str(result)))
807 for item in result:
808 if not isinstance(item, basestring):
809 raise exceptions.RuntimeError('All try slaves names must be strings.')
810 if item != item.strip():
811 raise exceptions.RuntimeError('Try slave names cannot start/end'
812 'with whitespace')
813 else:
814 result = []
815 return result
816
817
818def DoGetTrySlaves(changed_files,
819 repository_root,
820 default_presubmit,
821 verbose,
822 output_stream):
823 """Get the list of try servers from the presubmit scripts.
824
825 Args:
826 changed_files: List of modified files.
827 repository_root: The repository root.
828 default_presubmit: A default presubmit script to execute in any case.
829 verbose: Prints debug info.
830 output_stream: A stream to write debug output to.
831
832 Return:
833 List of try slaves
834 """
835 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
836 if not presubmit_files and verbose:
837 output_stream.write("Warning, no presubmit.py found.\n")
838 results = []
839 executer = GetTrySlavesExecuter()
840 if default_presubmit:
841 if verbose:
842 output_stream.write("Running default presubmit script.\n")
843 results += executer.ExecPresubmitScript(default_presubmit)
844 for filename in presubmit_files:
845 filename = os.path.abspath(filename)
846 if verbose:
847 output_stream.write("Running %s\n" % filename)
848 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000849 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000850 results += executer.ExecPresubmitScript(presubmit_script)
851
852 slaves = list(set(results))
853 if slaves and verbose:
854 output_stream.write(', '.join(slaves))
855 output_stream.write('\n')
856 return slaves
857
858
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000859class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000860 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000861 """
862 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000863 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000864 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
865 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000866 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000867 self.committing = committing
868
869 def ExecPresubmitScript(self, script_text, presubmit_path):
870 """Executes a single presubmit script.
871
872 Args:
873 script_text: The text of the presubmit script.
874 presubmit_path: The path to the presubmit file (this will be reported via
875 input_api.PresubmitLocalPath()).
876
877 Return:
878 A list of result objects, empty if no problems.
879 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000880
881 # Change to the presubmit file's directory to support local imports.
882 main_path = os.getcwd()
883 os.chdir(os.path.dirname(presubmit_path))
884
885 # Load the presubmit script into context.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000886 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000887 context = {}
888 exec script_text in context
889
890 # These function names must change if we make substantial changes to
891 # the presubmit API that are not backwards compatible.
892 if self.committing:
893 function_name = 'CheckChangeOnCommit'
894 else:
895 function_name = 'CheckChangeOnUpload'
896 if function_name in context:
897 context['__args'] = (input_api, OutputApi())
898 result = eval(function_name + '(*__args)', context)
899 if not (isinstance(result, types.TupleType) or
900 isinstance(result, types.ListType)):
901 raise exceptions.RuntimeError(
902 'Presubmit functions must return a tuple or list')
903 for item in result:
904 if not isinstance(item, OutputApi.PresubmitResult):
905 raise exceptions.RuntimeError(
906 'All presubmit results must be of types derived from '
907 'output_api.PresubmitResult')
908 else:
909 result = () # no error since the script doesn't care about current event.
910
chase@chromium.org8e416c82009-10-06 04:30:44 +0000911 # Return the process to the original working directory.
912 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000913 return result
914
915
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000916def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000917 committing,
918 verbose,
919 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000920 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000921 default_presubmit,
922 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000923 """Runs all presubmit checks that apply to the files in the change.
924
925 This finds all PRESUBMIT.py files in directories enclosing the files in the
926 change (up to the repository root) and calls the relevant entrypoint function
927 depending on whether the change is being committed or uploaded.
928
929 Prints errors, warnings and notifications. Prompts the user for warnings
930 when needed.
931
932 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000933 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000934 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
935 verbose: Prints debug info.
936 output_stream: A stream to write output from presubmit tests to.
937 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000938 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000939 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000941 Warning:
942 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
943 SHOULD be sys.stdin.
944
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000945 Return:
946 True if execution can continue, False if not.
947 """
maruel@chromium.org8d195232010-10-05 12:58:49 +0000948 print "Running presubmit hooks..."
jam@chromium.org2a891dc2009-08-20 20:33:37 +0000949 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000950 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
951 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000952 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000953 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000954 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000955 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000956 if default_presubmit:
957 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000958 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000959 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000960 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000961 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000962 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000963 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000964 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000965 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000966 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000967 results += executer.ExecPresubmitScript(presubmit_script, filename)
968
969 errors = []
970 notifications = []
971 warnings = []
972 for result in results:
973 if not result.IsFatal() and not result.ShouldPrompt():
974 notifications.append(result)
975 elif result.ShouldPrompt():
976 warnings.append(result)
977 else:
978 errors.append(result)
979
980 error_count = 0
981 for name, items in (('Messages', notifications),
982 ('Warnings', warnings),
983 ('ERRORS', errors)):
984 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000985 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000986 for item in items:
987 if not item._Handle(output_stream, input_stream,
988 may_prompt=False):
989 error_count += 1
990 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +0000991
992 total_time = time.time() - start_time
993 if total_time > 1.0:
994 print "Presubmit checks took %.1fs to calculate." % total_time
995
maruel@chromium.org07bbc212009-06-11 02:08:41 +0000996 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000997 if not PromptYesNo(input_stream, output_stream,
998 'There were presubmit warnings. '
999 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001001
1002 global _ASKED_FOR_FEEDBACK
1003 # Ask for feedback one time out of 5.
1004 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1005 output_stream.write("Was the presubmit check useful? Please send feedback "
1006 "& hate mail to maruel@chromium.org!\n")
1007 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008 return (error_count == 0)
1009
1010
1011def ScanSubDirs(mask, recursive):
1012 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001013 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 +00001014 else:
1015 results = []
1016 for root, dirs, files in os.walk('.'):
1017 if '.svn' in dirs:
1018 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001019 if '.git' in dirs:
1020 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001021 for name in files:
1022 if fnmatch.fnmatch(name, mask):
1023 results.append(os.path.join(root, name))
1024 return results
1025
1026
1027def ParseFiles(args, recursive):
1028 files = []
1029 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001030 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001031 return files
1032
1033
1034def Main(argv):
1035 parser = optparse.OptionParser(usage="%prog [options]",
1036 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001037 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001039 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1040 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001041 parser.add_option("-r", "--recursive", action="store_true",
1042 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001043 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001044 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001045 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001046 parser.add_option("--name", default='no name')
1047 parser.add_option("--description", default='')
1048 parser.add_option("--issue", type='int', default=0)
1049 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001050 parser.add_option("--root", default=os.getcwd(),
1051 help="Search for PRESUBMIT.py up to this directory. "
1052 "If inherit-review-settings-ok is present in this "
1053 "directory, parent directories up to the root file "
1054 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001055 parser.add_option("--default_presubmit")
1056 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001057 options, args = parser.parse_args(argv[1:])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001058 if os.path.isdir(os.path.join(options.root, '.git')):
1059 change_class = GitChange
1060 if not options.files:
1061 if args:
1062 options.files = ParseFiles(args, options.recursive)
1063 else:
1064 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001065 options.files = scm.GIT.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001066 elif os.path.isdir(os.path.join(options.root, '.svn')):
1067 change_class = SvnChange
1068 if not options.files:
1069 if args:
1070 options.files = ParseFiles(args, options.recursive)
1071 else:
1072 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001073 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001074 else:
1075 # Doesn't seem under source control.
1076 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001077 if options.verbose:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001078 if len(options.files) != 1:
1079 print "Found %d files." % len(options.files)
1080 else:
1081 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001082 return not DoPresubmitChecks(change_class(options.name,
1083 options.description,
1084 options.root,
1085 options.files,
1086 options.issue,
1087 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001088 options.commit,
1089 options.verbose,
1090 sys.stdout,
1091 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001092 options.default_presubmit,
1093 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001094
1095
1096if __name__ == '__main__':
1097 sys.exit(Main(sys.argv))