blob: 9da419429ba4e548b702ecfbe5996a1428b4150e [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 +000037try:
38 import simplejson as json
39except ImportError:
40 try:
41 import json
42 except ImportError:
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000043 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000044 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
45 import simplejson as json
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000046
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000047# Local imports.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000048import gcl
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000049import gclient_utils
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000051import scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052
53
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000054# Ask for feedback only once in program lifetime.
55_ASKED_FOR_FEEDBACK = False
56
57
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000058class NotImplementedException(Exception):
59 """We're leaving placeholders in a bunch of places to remind us of the
60 design of the API, but we have not implemented all of it yet. Implement as
61 the need arises.
62 """
63 pass
64
65
66def normpath(path):
67 '''Version of os.path.normpath that also changes backward slashes to
68 forward slashes when not running on Windows.
69 '''
70 # This is safe to always do because the Windows version of os.path.normpath
71 # will replace forward slashes with backward slashes.
72 path = path.replace(os.sep, '/')
73 return os.path.normpath(path)
74
gspencer@google.comefb94502009-10-09 17:57:08 +000075def PromptYesNo(input_stream, output_stream, prompt):
76 output_stream.write(prompt)
77 response = input_stream.readline().strip().lower()
78 return response == 'y' or response == 'yes'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000079
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000080class OutputApi(object):
81 """This class (more like a module) gets passed to presubmit scripts so that
82 they can specify various types of results.
83 """
84
85 class PresubmitResult(object):
86 """Base class for result objects."""
87
88 def __init__(self, message, items=None, long_text=''):
89 """
90 message: A short one-line message to indicate errors.
91 items: A list of short strings to indicate where errors occurred.
92 long_text: multi-line text output, e.g. from another tool
93 """
94 self._message = message
95 self._items = []
96 if items:
97 self._items = items
98 self._long_text = long_text.rstrip()
99
100 def _Handle(self, output_stream, input_stream, may_prompt=True):
101 """Writes this result to the output stream.
102
103 Args:
104 output_stream: Where to write
105
106 Returns:
107 True if execution may continue, False otherwise.
108 """
109 output_stream.write(self._message)
110 output_stream.write('\n')
estade@chromium.org593258b2010-01-28 00:04:36 +0000111 if len(self._items) > 0:
112 output_stream.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000113 if self._long_text:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000114 output_stream.write('\n***************\n%s\n***************\n' %
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000115 self._long_text)
116
117 if self.ShouldPrompt() and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000118 if not PromptYesNo(input_stream, output_stream,
119 'Are you sure you want to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000120 return False
121
122 return not self.IsFatal()
123
124 def IsFatal(self):
125 """An error that is fatal stops g4 mail/submit immediately, i.e. before
126 other presubmit scripts are run.
127 """
128 return False
129
130 def ShouldPrompt(self):
131 """Whether this presubmit result should result in a prompt warning."""
132 return False
133
134 class PresubmitError(PresubmitResult):
135 """A hard presubmit error."""
136 def IsFatal(self):
137 return True
138
139 class PresubmitPromptWarning(PresubmitResult):
140 """An warning that prompts the user if they want to continue."""
141 def ShouldPrompt(self):
142 return True
143
144 class PresubmitNotifyResult(PresubmitResult):
145 """Just print something to the screen -- but it's not even a warning."""
146 pass
147
148 class MailTextResult(PresubmitResult):
149 """A warning that should be included in the review request email."""
150 def __init__(self, *args, **kwargs):
151 raise NotImplementedException() # TODO(joi) Implement.
152
153
154class InputApi(object):
155 """An instance of this object is passed to presubmit scripts so they can
156 know stuff about the change they're looking at.
157 """
158
maruel@chromium.org3410d912009-06-09 20:56:16 +0000159 # File extensions that are considered source files from a style guide
160 # perspective. Don't modify this list from a presubmit script!
161 DEFAULT_WHITE_LIST = (
162 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000163 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
164 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000165 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000166 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000167 # No extension at all, note that ALL CAPS files are black listed in
168 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000169 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000170 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000171 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000172 )
173
174 # Path regexp that should be excluded from being considered containing source
175 # files. Don't modify this list from a presubmit script!
176 DEFAULT_BLACK_LIST = (
177 r".*\bexperimental[\\\/].*",
178 r".*\bthird_party[\\\/].*",
179 # Output directories (just in case)
180 r".*\bDebug[\\\/].*",
181 r".*\bRelease[\\\/].*",
182 r".*\bxcodebuild[\\\/].*",
183 r".*\bsconsbuild[\\\/].*",
184 # All caps files like README and LICENCE.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000185 r".*\b[A-Z0-9_]+$",
186 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
187 r".*\.git[\\\/].*",
188 r".*\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000189 )
190
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000191 def __init__(self, change, presubmit_path, is_committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000192 """Builds an InputApi object.
193
194 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000195 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000196 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000197 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000198 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000199 # Version number of the presubmit_support script.
200 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000201 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000202 self.is_committing = is_committing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000203
204 # We expose various modules and functions as attributes of the input_api
205 # so that presubmit scripts don't have to import them.
206 self.basename = os.path.basename
207 self.cPickle = cPickle
208 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000209 self.json = json
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000210 self.os_path = os.path
211 self.pickle = pickle
212 self.marshal = marshal
213 self.re = re
214 self.subprocess = subprocess
215 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000216 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000217 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000218 self.urllib2 = urllib2
219
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000220 # To easily fork python.
221 self.python_executable = sys.executable
222 self.environ = os.environ
223
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000224 # InputApi.platform is the platform you're currently running on.
225 self.platform = sys.platform
226
227 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000228 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000229
230 # We carry the canned checks so presubmit scripts can easily use them.
231 self.canned_checks = presubmit_canned_checks
232
233 def PresubmitLocalPath(self):
234 """Returns the local path of the presubmit script currently being run.
235
236 This is useful if you don't want to hard-code absolute paths in the
237 presubmit script. For example, It can be used to find another file
238 relative to the PRESUBMIT.py script, so the whole tree can be branched and
239 the presubmit script still works, without editing its content.
240 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000241 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000242
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000243 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000244 """Translate a depot path to a local path (relative to client root).
245
246 Args:
247 Depot path as a string.
248
249 Returns:
250 The local path of the depot path under the user's current client, or None
251 if the file is not mapped.
252
253 Remember to check for the None case and show an appropriate error!
254 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000255 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000256 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000257 return local_path
258
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000259 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000260 """Translate a local path to a depot path.
261
262 Args:
263 Local path (relative to current directory, or absolute) as a string.
264
265 Returns:
266 The depot path (SVN URL) of the file if mapped, otherwise None.
267 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000268 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000269 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000270 return depot_path
271
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272 def AffectedFiles(self, include_dirs=False, include_deletes=True):
273 """Same as input_api.change.AffectedFiles() except only lists files
274 (and optionally directories) in the same directory as the current presubmit
275 script, or subdirectories thereof.
276 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000277 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000278 if len(dir_with_slash) == 1:
279 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000280 return filter(
281 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
282 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283
284 def LocalPaths(self, include_dirs=False):
285 """Returns local paths of input_api.AffectedFiles()."""
286 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
287
288 def AbsoluteLocalPaths(self, include_dirs=False):
289 """Returns absolute local paths of input_api.AffectedFiles()."""
290 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
291
292 def ServerPaths(self, include_dirs=False):
293 """Returns server paths of input_api.AffectedFiles()."""
294 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
295
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000296 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000297 """Same as input_api.change.AffectedTextFiles() except only lists files
298 in the same directory as the current presubmit script, or subdirectories
299 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000300 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000301 if include_deletes is not None:
302 warnings.warn("AffectedTextFiles(include_deletes=%s)"
303 " is deprecated and ignored" % str(include_deletes),
304 category=DeprecationWarning,
305 stacklevel=2)
306 return filter(lambda x: x.IsTextFile(),
307 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000308
maruel@chromium.org3410d912009-06-09 20:56:16 +0000309 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
310 """Filters out files that aren't considered "source file".
311
312 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
313 and InputApi.DEFAULT_BLACK_LIST is used respectively.
314
315 The lists will be compiled as regular expression and
316 AffectedFile.LocalPath() needs to pass both list.
317
318 Note: Copy-paste this function to suit your needs or use a lambda function.
319 """
320 def Find(affected_file, list):
321 for item in list:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000322 local_path = affected_file.LocalPath()
323 if self.re.match(item, local_path):
324 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000325 return True
326 return False
327 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
328 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
329
330 def AffectedSourceFiles(self, source_file):
331 """Filter the list of AffectedTextFiles by the function source_file.
332
333 If source_file is None, InputApi.FilterSourceFile() is used.
334 """
335 if not source_file:
336 source_file = self.FilterSourceFile
337 return filter(source_file, self.AffectedTextFiles())
338
339 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000340 """An iterator over all text lines in "new" version of changed files.
341
342 Only lists lines from new or modified text files in the change that are
343 contained by the directory of the currently executing presubmit script.
344
345 This is useful for doing line-by-line regex checks, like checking for
346 trailing whitespace.
347
348 Yields:
349 a 3 tuple:
350 the AffectedFile instance of the current file;
351 integer line number (1-based); and
352 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000353
354 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000355 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000356 files = self.AffectedSourceFiles(source_file_filter)
357 return InputApi._RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000358
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000359 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000360 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000361
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000362 Deny reading anything outside the repository.
363 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000364 if isinstance(file_item, AffectedFile):
365 file_item = file_item.AbsoluteLocalPath()
366 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000367 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000368 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000369
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000370 @staticmethod
371 def _RightHandSideLinesImpl(affected_files):
372 """Implements RightHandSideLines for InputApi and GclChange."""
373 for af in affected_files:
374 lines = af.NewContents()
375 line_number = 0
376 for line in lines:
377 line_number += 1
378 yield (af, line_number, line)
379
380
381class AffectedFile(object):
382 """Representation of a file in a change."""
383
384 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000385 self._path = path
386 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000387 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000388 self._is_directory = None
389 self._properties = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000390
391 def ServerPath(self):
392 """Returns a path string that identifies the file in the SCM system.
393
394 Returns the empty string if the file does not exist in SCM.
395 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000396 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000397
398 def LocalPath(self):
399 """Returns the path of this file on the local disk relative to client root.
400 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000401 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402
403 def AbsoluteLocalPath(self):
404 """Returns the absolute path of this file on the local disk.
405 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000406 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407
408 def IsDirectory(self):
409 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000410 if self._is_directory is None:
411 path = self.AbsoluteLocalPath()
412 self._is_directory = (os.path.exists(path) and
413 os.path.isdir(path))
414 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415
416 def Action(self):
417 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000418 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
419 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000420 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000421
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000422 def Property(self, property_name):
423 """Returns the specified SCM property of this file, or None if no such
424 property.
425 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000426 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000427
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000428 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000429 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000430
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000431 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000432 raise NotImplementedError() # Implement when needed
433
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000434 def NewContents(self):
435 """Returns an iterator over the lines in the new version of file.
436
437 The new version is the file in the user's workspace, i.e. the "right hand
438 side".
439
440 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000441 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000442 """
443 if self.IsDirectory():
444 return []
445 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000446 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
447 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448
449 def OldContents(self):
450 """Returns an iterator over the lines in the old version of file.
451
452 The old version is the file in depot, i.e. the "left hand side".
453 """
454 raise NotImplementedError() # Implement when needed
455
456 def OldFileTempPath(self):
457 """Returns the path on local disk where the old contents resides.
458
459 The old version is the file in depot, i.e. the "left hand side".
460 This is a read-only cached copy of the old contents. *DO NOT* try to
461 modify this file.
462 """
463 raise NotImplementedError() # Implement if/when needed.
464
maruel@chromium.org5de13972009-06-10 18:16:06 +0000465 def __str__(self):
466 return self.LocalPath()
467
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000468
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000469class SvnAffectedFile(AffectedFile):
470 """Representation of a file in a change out of a Subversion checkout."""
471
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000472 def __init__(self, *args, **kwargs):
473 AffectedFile.__init__(self, *args, **kwargs)
474 self._server_path = None
475 self._is_text_file = None
476
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000477 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000478 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000479 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000480 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000481 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000482
483 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000484 if self._is_directory is None:
485 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000486 if os.path.exists(path):
487 # Retrieve directly from the file system; it is much faster than
488 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000489 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000490 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000491 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000492 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000493 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000494
495 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000496 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000497 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000498 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000499 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000500
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000501 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000502 if self._is_text_file is None:
503 if self.Action() == 'D':
504 # A deleted file is not a text file.
505 self._is_text_file = False
506 elif self.IsDirectory():
507 self._is_text_file = False
508 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000509 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
510 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000511 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
512 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000513
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000514
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000515class GitAffectedFile(AffectedFile):
516 """Representation of a file in a change out of a git checkout."""
517
518 def __init__(self, *args, **kwargs):
519 AffectedFile.__init__(self, *args, **kwargs)
520 self._server_path = None
521 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000522
523 def ServerPath(self):
524 if self._server_path is None:
525 raise NotImplementedException() # TODO(maruel) Implement.
526 return self._server_path
527
528 def IsDirectory(self):
529 if self._is_directory is None:
530 path = self.AbsoluteLocalPath()
531 if os.path.exists(path):
532 # Retrieve directly from the file system; it is much faster than
533 # querying subversion, especially on Windows.
534 self._is_directory = os.path.isdir(path)
535 else:
536 # raise NotImplementedException() # TODO(maruel) Implement.
537 self._is_directory = False
538 return self._is_directory
539
540 def Property(self, property_name):
541 if not property_name in self._properties:
542 raise NotImplementedException() # TODO(maruel) Implement.
543 return self._properties[property_name]
544
545 def IsTextFile(self):
546 if self._is_text_file is None:
547 if self.Action() == 'D':
548 # A deleted file is not a text file.
549 self._is_text_file = False
550 elif self.IsDirectory():
551 self._is_text_file = False
552 else:
553 # raise NotImplementedException() # TODO(maruel) Implement.
554 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
555 return self._is_text_file
556
557
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000558class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000559 """Describe a change.
560
561 Used directly by the presubmit scripts to query the current change being
562 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000563
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000564 Instance members:
565 tags: Dictionnary of KEY=VALUE pairs found in the change description.
566 self.KEY: equivalent to tags['KEY']
567 """
568
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000569 _AFFECTED_FILES = AffectedFile
570
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000571 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000572 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000573 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000574
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000575 def __init__(self, name, description, local_root, files, issue, patchset):
576 if files is None:
577 files = []
578 self._name = name
579 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000580 # Convert root into an absolute path.
581 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000582 self.issue = issue
583 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000584 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000585
586 # From the description text, build up a dictionary of key/value pairs
587 # plus the description minus all key/value or "tag" lines.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000588 self._description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000589 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000590 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000591 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000592 if m:
593 self.tags[m.group('key')] = m.group('value')
594 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000595 self._description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000596
597 # Change back to text and remove whitespace at end.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000598 self._description_without_tags = '\n'.join(self._description_without_tags)
599 self._description_without_tags = self._description_without_tags.rstrip()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000600
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000601 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000602 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
603 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000604 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000605
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000606 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000607 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000608 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000609
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610 def DescriptionText(self):
611 """Returns the user-entered changelist description, minus tags.
612
613 Any line in the user-provided description starting with e.g. "FOO="
614 (whitespace permitted before and around) is considered a tag line. Such
615 lines are stripped out of the description this function returns.
616 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000617 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000618
619 def FullDescriptionText(self):
620 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000621 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000622
623 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000624 """Returns the repository (checkout) root directory for this change,
625 as an absolute path.
626 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000627 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000628
629 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000630 """Return tags directly as attributes on the object."""
631 if not re.match(r"^[A-Z_]*$", attr):
632 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000633 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000634
635 def AffectedFiles(self, include_dirs=False, include_deletes=True):
636 """Returns a list of AffectedFile instances for all files in the change.
637
638 Args:
639 include_deletes: If false, deleted files will be filtered out.
640 include_dirs: True to include directories in the list
641
642 Returns:
643 [AffectedFile(path, action), AffectedFile(path, action)]
644 """
645 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000646 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000648 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000649
650 if include_deletes:
651 return affected
652 else:
653 return filter(lambda x: x.Action() != 'D', affected)
654
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000655 def AffectedTextFiles(self, include_deletes=None):
656 """Return a list of the existing text files in a change."""
657 if include_deletes is not None:
658 warnings.warn("AffectedTextFiles(include_deletes=%s)"
659 " is deprecated and ignored" % str(include_deletes),
660 category=DeprecationWarning,
661 stacklevel=2)
662 return filter(lambda x: x.IsTextFile(),
663 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000664
665 def LocalPaths(self, include_dirs=False):
666 """Convenience function."""
667 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
668
669 def AbsoluteLocalPaths(self, include_dirs=False):
670 """Convenience function."""
671 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
672
673 def ServerPaths(self, include_dirs=False):
674 """Convenience function."""
675 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
676
677 def RightHandSideLines(self):
678 """An iterator over all text lines in "new" version of changed files.
679
680 Lists lines from new or modified text files in the change.
681
682 This is useful for doing line-by-line regex checks, like checking for
683 trailing whitespace.
684
685 Yields:
686 a 3 tuple:
687 the AffectedFile instance of the current file;
688 integer line number (1-based); and
689 the contents of the line as a string.
690 """
691 return InputApi._RightHandSideLinesImpl(
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000692 filter(lambda x: x.IsTextFile(),
693 self.AffectedFiles(include_deletes=False)))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694
695
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000696class SvnChange(Change):
697 _AFFECTED_FILES = SvnAffectedFile
698
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000699 def __init__(self, *args, **kwargs):
700 Change.__init__(self, *args, **kwargs)
701 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000702 self._changelists = None
703
704 def _GetChangeLists(self):
705 """Get all change lists."""
706 if self._changelists == None:
707 previous_cwd = os.getcwd()
708 os.chdir(self.RepositoryRoot())
709 self._changelists = gcl.GetModifiedFiles()
710 os.chdir(previous_cwd)
711 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000712
713 def GetAllModifiedFiles(self):
714 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000715 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000716 all_modified_files = []
717 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000718 all_modified_files.extend(
719 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000720 return all_modified_files
721
722 def GetModifiedFiles(self):
723 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000724 changelists = self._GetChangeLists()
725 return [os.path.join(self.RepositoryRoot(), f[1])
726 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000727
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000728
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000729class GitChange(Change):
730 _AFFECTED_FILES = GitAffectedFile
731
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000732 def __init__(self, *args, **kwargs):
733 Change.__init__(self, *args, **kwargs)
734 self.scm = 'git'
735
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000736
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000737def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000738 """Finds all presubmit files that apply to a given set of source files.
739
740 Args:
741 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000742 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000743
744 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000745 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000746 """
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000747 entries = []
748 for f in files:
749 f = normpath(os.path.join(root, f))
750 while f:
751 f = os.path.dirname(f)
752 if f in entries:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000753 break
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000754 entries.append(f)
755 if f == root:
756 break
757 entries.sort()
758 entries = map(lambda x: os.path.join(x, 'PRESUBMIT.py'), entries)
759 return filter(lambda x: os.path.isfile(x), entries)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000760
761
thestig@chromium.orgde243452009-10-06 21:02:56 +0000762class GetTrySlavesExecuter(object):
763 def ExecPresubmitScript(self, script_text):
764 """Executes GetPreferredTrySlaves() from a single presubmit script.
765
766 Args:
767 script_text: The text of the presubmit script.
768
769 Return:
770 A list of try slaves.
771 """
772 context = {}
773 exec script_text in context
774
775 function_name = 'GetPreferredTrySlaves'
776 if function_name in context:
777 result = eval(function_name + '()', context)
778 if not isinstance(result, types.ListType):
779 raise exceptions.RuntimeError(
780 'Presubmit functions must return a list, got a %s instead: %s' %
781 (type(result), str(result)))
782 for item in result:
783 if not isinstance(item, basestring):
784 raise exceptions.RuntimeError('All try slaves names must be strings.')
785 if item != item.strip():
786 raise exceptions.RuntimeError('Try slave names cannot start/end'
787 'with whitespace')
788 else:
789 result = []
790 return result
791
792
793def DoGetTrySlaves(changed_files,
794 repository_root,
795 default_presubmit,
796 verbose,
797 output_stream):
798 """Get the list of try servers from the presubmit scripts.
799
800 Args:
801 changed_files: List of modified files.
802 repository_root: The repository root.
803 default_presubmit: A default presubmit script to execute in any case.
804 verbose: Prints debug info.
805 output_stream: A stream to write debug output to.
806
807 Return:
808 List of try slaves
809 """
810 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
811 if not presubmit_files and verbose:
812 output_stream.write("Warning, no presubmit.py found.\n")
813 results = []
814 executer = GetTrySlavesExecuter()
815 if default_presubmit:
816 if verbose:
817 output_stream.write("Running default presubmit script.\n")
818 results += executer.ExecPresubmitScript(default_presubmit)
819 for filename in presubmit_files:
820 filename = os.path.abspath(filename)
821 if verbose:
822 output_stream.write("Running %s\n" % filename)
823 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000824 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000825 results += executer.ExecPresubmitScript(presubmit_script)
826
827 slaves = list(set(results))
828 if slaves and verbose:
829 output_stream.write(', '.join(slaves))
830 output_stream.write('\n')
831 return slaves
832
833
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000834class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000835 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836 """
837 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000838 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
840 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000841 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842 self.committing = committing
843
844 def ExecPresubmitScript(self, script_text, presubmit_path):
845 """Executes a single presubmit script.
846
847 Args:
848 script_text: The text of the presubmit script.
849 presubmit_path: The path to the presubmit file (this will be reported via
850 input_api.PresubmitLocalPath()).
851
852 Return:
853 A list of result objects, empty if no problems.
854 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000855
856 # Change to the presubmit file's directory to support local imports.
857 main_path = os.getcwd()
858 os.chdir(os.path.dirname(presubmit_path))
859
860 # Load the presubmit script into context.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000861 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000862 context = {}
863 exec script_text in context
864
865 # These function names must change if we make substantial changes to
866 # the presubmit API that are not backwards compatible.
867 if self.committing:
868 function_name = 'CheckChangeOnCommit'
869 else:
870 function_name = 'CheckChangeOnUpload'
871 if function_name in context:
872 context['__args'] = (input_api, OutputApi())
873 result = eval(function_name + '(*__args)', context)
874 if not (isinstance(result, types.TupleType) or
875 isinstance(result, types.ListType)):
876 raise exceptions.RuntimeError(
877 'Presubmit functions must return a tuple or list')
878 for item in result:
879 if not isinstance(item, OutputApi.PresubmitResult):
880 raise exceptions.RuntimeError(
881 'All presubmit results must be of types derived from '
882 'output_api.PresubmitResult')
883 else:
884 result = () # no error since the script doesn't care about current event.
885
chase@chromium.org8e416c82009-10-06 04:30:44 +0000886 # Return the process to the original working directory.
887 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000888 return result
889
890
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000891def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000892 committing,
893 verbose,
894 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000895 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000896 default_presubmit,
897 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000898 """Runs all presubmit checks that apply to the files in the change.
899
900 This finds all PRESUBMIT.py files in directories enclosing the files in the
901 change (up to the repository root) and calls the relevant entrypoint function
902 depending on whether the change is being committed or uploaded.
903
904 Prints errors, warnings and notifications. Prompts the user for warnings
905 when needed.
906
907 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000908 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000909 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
910 verbose: Prints debug info.
911 output_stream: A stream to write output from presubmit tests to.
912 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000913 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000914 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000915
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000916 Warning:
917 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
918 SHOULD be sys.stdin.
919
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000920 Return:
921 True if execution can continue, False if not.
922 """
jam@chromium.org2a891dc2009-08-20 20:33:37 +0000923 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000924 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
925 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000926 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000927 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000928 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000929 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000930 if default_presubmit:
931 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000932 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000933 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000934 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +0000936 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000937 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +0000938 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +0000939 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000940 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941 results += executer.ExecPresubmitScript(presubmit_script, filename)
942
943 errors = []
944 notifications = []
945 warnings = []
946 for result in results:
947 if not result.IsFatal() and not result.ShouldPrompt():
948 notifications.append(result)
949 elif result.ShouldPrompt():
950 warnings.append(result)
951 else:
952 errors.append(result)
953
954 error_count = 0
955 for name, items in (('Messages', notifications),
956 ('Warnings', warnings),
957 ('ERRORS', errors)):
958 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000959 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000960 for item in items:
961 if not item._Handle(output_stream, input_stream,
962 may_prompt=False):
963 error_count += 1
964 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +0000965
966 total_time = time.time() - start_time
967 if total_time > 1.0:
968 print "Presubmit checks took %.1fs to calculate." % total_time
969
maruel@chromium.org07bbc212009-06-11 02:08:41 +0000970 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000971 if not PromptYesNo(input_stream, output_stream,
972 'There were presubmit warnings. '
973 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000974 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000975
976 global _ASKED_FOR_FEEDBACK
977 # Ask for feedback one time out of 5.
978 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
979 output_stream.write("Was the presubmit check useful? Please send feedback "
980 "& hate mail to maruel@chromium.org!\n")
981 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000982 return (error_count == 0)
983
984
985def ScanSubDirs(mask, recursive):
986 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000987 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 +0000988 else:
989 results = []
990 for root, dirs, files in os.walk('.'):
991 if '.svn' in dirs:
992 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000993 if '.git' in dirs:
994 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995 for name in files:
996 if fnmatch.fnmatch(name, mask):
997 results.append(os.path.join(root, name))
998 return results
999
1000
1001def ParseFiles(args, recursive):
1002 files = []
1003 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001004 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001005 return files
1006
1007
1008def Main(argv):
1009 parser = optparse.OptionParser(usage="%prog [options]",
1010 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001011 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001012 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001013 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1014 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015 parser.add_option("-r", "--recursive", action="store_true",
1016 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001017 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001019 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001020 parser.add_option("--name", default='no name')
1021 parser.add_option("--description", default='')
1022 parser.add_option("--issue", type='int', default=0)
1023 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001024 parser.add_option("--root", default='')
1025 parser.add_option("--default_presubmit")
1026 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001027 options, args = parser.parse_args(argv[1:])
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001028 if not options.root:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001029 options.root = os.getcwd()
1030 if os.path.isdir(os.path.join(options.root, '.git')):
1031 change_class = GitChange
1032 if not options.files:
1033 if args:
1034 options.files = ParseFiles(args, options.recursive)
1035 else:
1036 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001037 options.files = scm.GIT.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001038 elif os.path.isdir(os.path.join(options.root, '.svn')):
1039 change_class = SvnChange
1040 if not options.files:
1041 if args:
1042 options.files = ParseFiles(args, options.recursive)
1043 else:
1044 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001045 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001046 else:
1047 # Doesn't seem under source control.
1048 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001049 if options.verbose:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001050 if len(options.files) != 1:
1051 print "Found %d files." % len(options.files)
1052 else:
1053 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001054 return not DoPresubmitChecks(change_class(options.name,
1055 options.description,
1056 options.root,
1057 options.files,
1058 options.issue,
1059 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001060 options.commit,
1061 options.verbose,
1062 sys.stdout,
1063 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001064 options.default_presubmit,
1065 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001066
1067
1068if __name__ == '__main__':
1069 sys.exit(Main(sys.argv))