blob: 6be415dce140a70e9e26cb334b91d9a6d4160a49 [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00002# Copyright (c) 2010 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# 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.orgcb2985f2010-11-03 14:08:31 +000035from warnings import warn
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.
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +000044 # Statement seems to have no effect
45 # pylint: disable=W0104
tony@chromium.org5e9f2ed2010-04-08 01:38:19 +000046 json.loads
47 except (ImportError, AttributeError):
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000048 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000049 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
50 import simplejson as json
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000051
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052# Local imports.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000053import gclient_utils
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000054import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000055import scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000056
57
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000058# Ask for feedback only once in program lifetime.
59_ASKED_FOR_FEEDBACK = False
60
61
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000062class NotImplementedException(Exception):
63 """We're leaving placeholders in a bunch of places to remind us of the
64 design of the API, but we have not implemented all of it yet. Implement as
65 the need arises.
66 """
67 pass
68
69
70def normpath(path):
71 '''Version of os.path.normpath that also changes backward slashes to
72 forward slashes when not running on Windows.
73 '''
74 # This is safe to always do because the Windows version of os.path.normpath
75 # will replace forward slashes with backward slashes.
76 path = path.replace(os.sep, '/')
77 return os.path.normpath(path)
78
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000079
gspencer@google.comefb94502009-10-09 17:57:08 +000080def PromptYesNo(input_stream, output_stream, prompt):
81 output_stream.write(prompt)
82 response = input_stream.readline().strip().lower()
83 return response == 'y' or response == 'yes'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000084
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000085
86def _RightHandSideLinesImpl(affected_files):
87 """Implements RightHandSideLines for InputApi and GclChange."""
88 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000089 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000090 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000091 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000092
93
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000094class OutputApi(object):
95 """This class (more like a module) gets passed to presubmit scripts so that
96 they can specify various types of results.
97 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +000098 # Method could be a function
99 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000100 class PresubmitResult(object):
101 """Base class for result objects."""
102
103 def __init__(self, message, items=None, long_text=''):
104 """
105 message: A short one-line message to indicate errors.
106 items: A list of short strings to indicate where errors occurred.
107 long_text: multi-line text output, e.g. from another tool
108 """
109 self._message = message
110 self._items = []
111 if items:
112 self._items = items
113 self._long_text = long_text.rstrip()
114
115 def _Handle(self, output_stream, input_stream, may_prompt=True):
116 """Writes this result to the output stream.
117
118 Args:
119 output_stream: Where to write
120
121 Returns:
122 True if execution may continue, False otherwise.
123 """
124 output_stream.write(self._message)
125 output_stream.write('\n')
estade@chromium.org593258b2010-01-28 00:04:36 +0000126 if len(self._items) > 0:
127 output_stream.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000128 if self._long_text:
maruel@chromium.org310b3552010-11-01 13:23:35 +0000129 # Sometimes self._long_text is a ascii string, a codepage string
130 # (on windows), or a unicode object.
131 try:
132 long_text = self._long_text.decode()
133 except UnicodeDecodeError:
134 long_text = self._long_text.decode('ascii', 'replace')
135
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000136 output_stream.write('\n***************\n%s\n***************\n' %
maruel@chromium.org310b3552010-11-01 13:23:35 +0000137 long_text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000138
139 if self.ShouldPrompt() and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000140 if not PromptYesNo(input_stream, output_stream,
141 'Are you sure you want to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000142 return False
143
144 return not self.IsFatal()
145
146 def IsFatal(self):
147 """An error that is fatal stops g4 mail/submit immediately, i.e. before
148 other presubmit scripts are run.
149 """
150 return False
151
152 def ShouldPrompt(self):
153 """Whether this presubmit result should result in a prompt warning."""
154 return False
155
156 class PresubmitError(PresubmitResult):
157 """A hard presubmit error."""
158 def IsFatal(self):
159 return True
160
161 class PresubmitPromptWarning(PresubmitResult):
162 """An warning that prompts the user if they want to continue."""
163 def ShouldPrompt(self):
164 return True
165
166 class PresubmitNotifyResult(PresubmitResult):
167 """Just print something to the screen -- but it's not even a warning."""
168 pass
169
170 class MailTextResult(PresubmitResult):
171 """A warning that should be included in the review request email."""
172 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000173 super(OutputApi.MailTextResult, self).__init__()
174 raise NotImplementedException()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000175
176
177class InputApi(object):
178 """An instance of this object is passed to presubmit scripts so they can
179 know stuff about the change they're looking at.
180 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000181 # Method could be a function
182 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000183
maruel@chromium.org3410d912009-06-09 20:56:16 +0000184 # File extensions that are considered source files from a style guide
185 # perspective. Don't modify this list from a presubmit script!
186 DEFAULT_WHITE_LIST = (
187 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000188 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000189 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000190 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000191 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000192 # No extension at all, note that ALL CAPS files are black listed in
193 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000194 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000195 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000196 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000197 )
198
199 # Path regexp that should be excluded from being considered containing source
200 # files. Don't modify this list from a presubmit script!
201 DEFAULT_BLACK_LIST = (
202 r".*\bexperimental[\\\/].*",
203 r".*\bthird_party[\\\/].*",
204 # Output directories (just in case)
205 r".*\bDebug[\\\/].*",
206 r".*\bRelease[\\\/].*",
207 r".*\bxcodebuild[\\\/].*",
208 r".*\bsconsbuild[\\\/].*",
209 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000210 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000211 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000212 r"(|.*[\\\/])\.git[\\\/].*",
213 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000214 )
215
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000216 def __init__(self, change, presubmit_path, is_committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000217 """Builds an InputApi object.
218
219 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000220 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000221 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000222 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000223 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000224 # Version number of the presubmit_support script.
225 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000227 self.is_committing = is_committing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228
229 # We expose various modules and functions as attributes of the input_api
230 # so that presubmit scripts don't have to import them.
231 self.basename = os.path.basename
232 self.cPickle = cPickle
233 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000234 self.json = json
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000235 self.os_path = os.path
236 self.pickle = pickle
237 self.marshal = marshal
238 self.re = re
239 self.subprocess = subprocess
240 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000241 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000242 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000243 self.urllib2 = urllib2
244
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000245 # To easily fork python.
246 self.python_executable = sys.executable
247 self.environ = os.environ
248
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000249 # InputApi.platform is the platform you're currently running on.
250 self.platform = sys.platform
251
252 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000253 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000254
255 # We carry the canned checks so presubmit scripts can easily use them.
256 self.canned_checks = presubmit_canned_checks
257
258 def PresubmitLocalPath(self):
259 """Returns the local path of the presubmit script currently being run.
260
261 This is useful if you don't want to hard-code absolute paths in the
262 presubmit script. For example, It can be used to find another file
263 relative to the PRESUBMIT.py script, so the whole tree can be branched and
264 the presubmit script still works, without editing its content.
265 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000266 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000268 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269 """Translate a depot path to a local path (relative to client root).
270
271 Args:
272 Depot path as a string.
273
274 Returns:
275 The local path of the depot path under the user's current client, or None
276 if the file is not mapped.
277
278 Remember to check for the None case and show an appropriate error!
279 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000280 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000281 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282 return local_path
283
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000284 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000285 """Translate a local path to a depot path.
286
287 Args:
288 Local path (relative to current directory, or absolute) as a string.
289
290 Returns:
291 The depot path (SVN URL) of the file if mapped, otherwise None.
292 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000293 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000294 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000295 return depot_path
296
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000297 def AffectedFiles(self, include_dirs=False, include_deletes=True):
298 """Same as input_api.change.AffectedFiles() except only lists files
299 (and optionally directories) in the same directory as the current presubmit
300 script, or subdirectories thereof.
301 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000302 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000303 if len(dir_with_slash) == 1:
304 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000305 return filter(
306 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
307 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000308
309 def LocalPaths(self, include_dirs=False):
310 """Returns local paths of input_api.AffectedFiles()."""
311 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
312
313 def AbsoluteLocalPaths(self, include_dirs=False):
314 """Returns absolute local paths of input_api.AffectedFiles()."""
315 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
316
317 def ServerPaths(self, include_dirs=False):
318 """Returns server paths of input_api.AffectedFiles()."""
319 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
320
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000321 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322 """Same as input_api.change.AffectedTextFiles() except only lists files
323 in the same directory as the current presubmit script, or subdirectories
324 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000325 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000326 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000327 warn("AffectedTextFiles(include_deletes=%s)"
328 " is deprecated and ignored" % str(include_deletes),
329 category=DeprecationWarning,
330 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000331 return filter(lambda x: x.IsTextFile(),
332 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000333
maruel@chromium.org3410d912009-06-09 20:56:16 +0000334 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
335 """Filters out files that aren't considered "source file".
336
337 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
338 and InputApi.DEFAULT_BLACK_LIST is used respectively.
339
340 The lists will be compiled as regular expression and
341 AffectedFile.LocalPath() needs to pass both list.
342
343 Note: Copy-paste this function to suit your needs or use a lambda function.
344 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000345 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000346 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000347 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000348 if self.re.match(item, local_path):
349 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000350 return True
351 return False
352 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
353 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
354
355 def AffectedSourceFiles(self, source_file):
356 """Filter the list of AffectedTextFiles by the function source_file.
357
358 If source_file is None, InputApi.FilterSourceFile() is used.
359 """
360 if not source_file:
361 source_file = self.FilterSourceFile
362 return filter(source_file, self.AffectedTextFiles())
363
364 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000365 """An iterator over all text lines in "new" version of changed files.
366
367 Only lists lines from new or modified text files in the change that are
368 contained by the directory of the currently executing presubmit script.
369
370 This is useful for doing line-by-line regex checks, like checking for
371 trailing whitespace.
372
373 Yields:
374 a 3 tuple:
375 the AffectedFile instance of the current file;
376 integer line number (1-based); and
377 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000378
379 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000380 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000381 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000382 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000383
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000384 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000385 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000386
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000387 Deny reading anything outside the repository.
388 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000389 if isinstance(file_item, AffectedFile):
390 file_item = file_item.AbsoluteLocalPath()
391 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000392 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000393 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000394
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000395
396class AffectedFile(object):
397 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000398 # Method could be a function
399 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000400 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000401 self._path = path
402 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000403 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000404 self._is_directory = None
405 self._properties = {}
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000406 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407
408 def ServerPath(self):
409 """Returns a path string that identifies the file in the SCM system.
410
411 Returns the empty string if the file does not exist in SCM.
412 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000413 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000414
415 def LocalPath(self):
416 """Returns the path of this file on the local disk relative to client root.
417 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000418 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000419
420 def AbsoluteLocalPath(self):
421 """Returns the absolute path of this file on the local disk.
422 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000423 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000424
425 def IsDirectory(self):
426 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000427 if self._is_directory is None:
428 path = self.AbsoluteLocalPath()
429 self._is_directory = (os.path.exists(path) and
430 os.path.isdir(path))
431 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000432
433 def Action(self):
434 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000435 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
436 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000437 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000439 def Property(self, property_name):
440 """Returns the specified SCM property of this file, or None if no such
441 property.
442 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000443 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000444
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000445 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000446 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000447
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000448 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000449 raise NotImplementedError() # Implement when needed
450
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000451 def NewContents(self):
452 """Returns an iterator over the lines in the new version of file.
453
454 The new version is the file in the user's workspace, i.e. the "right hand
455 side".
456
457 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000458 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000459 """
460 if self.IsDirectory():
461 return []
462 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000463 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
464 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000465
466 def OldContents(self):
467 """Returns an iterator over the lines in the old version of file.
468
469 The old version is the file in depot, i.e. the "left hand side".
470 """
471 raise NotImplementedError() # Implement when needed
472
473 def OldFileTempPath(self):
474 """Returns the path on local disk where the old contents resides.
475
476 The old version is the file in depot, i.e. the "left hand side".
477 This is a read-only cached copy of the old contents. *DO NOT* try to
478 modify this file.
479 """
480 raise NotImplementedError() # Implement if/when needed.
481
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000482 def ChangedContents(self):
483 """Returns a list of tuples (line number, line text) of all new lines.
484
485 This relies on the scm diff output describing each changed code section
486 with a line of the form
487
488 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
489 """
490 new_lines = []
491 line_num = 0
492
493 if self.IsDirectory():
494 return []
495
496 for line in self.GenerateScmDiff().splitlines():
497 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
498 if m:
499 line_num = int(m.groups(1)[0])
500 continue
501 if line.startswith('+') and not line.startswith('++'):
502 new_lines.append((line_num, line[1:]))
503 if not line.startswith('-'):
504 line_num += 1
505 return new_lines
506
maruel@chromium.org5de13972009-06-10 18:16:06 +0000507 def __str__(self):
508 return self.LocalPath()
509
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000510 def GenerateScmDiff(self):
511 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000512
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000513class SvnAffectedFile(AffectedFile):
514 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000515 # Method 'NNN' is abstract in class 'NNN' but is not overridden
516 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000517
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000518 def __init__(self, *args, **kwargs):
519 AffectedFile.__init__(self, *args, **kwargs)
520 self._server_path = None
521 self._is_text_file = None
522
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000523 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000524 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000525 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000526 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000527 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000528
529 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000530 if self._is_directory is None:
531 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000532 if os.path.exists(path):
533 # Retrieve directly from the file system; it is much faster than
534 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000535 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000536 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000537 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000538 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000539 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000540
541 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000542 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000543 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000544 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000545 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000546
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000547 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000548 if self._is_text_file is None:
549 if self.Action() == 'D':
550 # A deleted file is not a text file.
551 self._is_text_file = False
552 elif self.IsDirectory():
553 self._is_text_file = False
554 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000555 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
556 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000557 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
558 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000559
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000560 def GenerateScmDiff(self):
561 return scm.SVN.GenerateDiff(self.AbsoluteLocalPath())
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000562
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000563class GitAffectedFile(AffectedFile):
564 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000565 # Method 'NNN' is abstract in class 'NNN' but is not overridden
566 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000567
568 def __init__(self, *args, **kwargs):
569 AffectedFile.__init__(self, *args, **kwargs)
570 self._server_path = None
571 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000572
573 def ServerPath(self):
574 if self._server_path is None:
575 raise NotImplementedException() # TODO(maruel) Implement.
576 return self._server_path
577
578 def IsDirectory(self):
579 if self._is_directory is None:
580 path = self.AbsoluteLocalPath()
581 if os.path.exists(path):
582 # Retrieve directly from the file system; it is much faster than
583 # querying subversion, especially on Windows.
584 self._is_directory = os.path.isdir(path)
585 else:
586 # raise NotImplementedException() # TODO(maruel) Implement.
587 self._is_directory = False
588 return self._is_directory
589
590 def Property(self, property_name):
591 if not property_name in self._properties:
592 raise NotImplementedException() # TODO(maruel) Implement.
593 return self._properties[property_name]
594
595 def IsTextFile(self):
596 if self._is_text_file is None:
597 if self.Action() == 'D':
598 # A deleted file is not a text file.
599 self._is_text_file = False
600 elif self.IsDirectory():
601 self._is_text_file = False
602 else:
603 # raise NotImplementedException() # TODO(maruel) Implement.
604 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
605 return self._is_text_file
606
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000607 def GenerateScmDiff(self):
608 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000609
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000610class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000611 """Describe a change.
612
613 Used directly by the presubmit scripts to query the current change being
614 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000615
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000616 Instance members:
617 tags: Dictionnary of KEY=VALUE pairs found in the change description.
618 self.KEY: equivalent to tags['KEY']
619 """
620
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000621 _AFFECTED_FILES = AffectedFile
622
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000623 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000624 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000625 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000626
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000627 def __init__(self, name, description, local_root, files, issue, patchset):
628 if files is None:
629 files = []
630 self._name = name
631 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000632 # Convert root into an absolute path.
633 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000634 self.issue = issue
635 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000636 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000637
638 # From the description text, build up a dictionary of key/value pairs
639 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000640 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000641 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000642 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000643 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000644 if m:
645 self.tags[m.group('key')] = m.group('value')
646 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000647 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000648
649 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000650 self._description_without_tags = (
651 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000652
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000653 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000654 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
655 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000656 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000658 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000659 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000660 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000662 def DescriptionText(self):
663 """Returns the user-entered changelist description, minus tags.
664
665 Any line in the user-provided description starting with e.g. "FOO="
666 (whitespace permitted before and around) is considered a tag line. Such
667 lines are stripped out of the description this function returns.
668 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000669 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670
671 def FullDescriptionText(self):
672 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000673 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674
675 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000676 """Returns the repository (checkout) root directory for this change,
677 as an absolute path.
678 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000679 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680
681 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000682 """Return tags directly as attributes on the object."""
683 if not re.match(r"^[A-Z_]*$", attr):
684 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000685 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000686
687 def AffectedFiles(self, include_dirs=False, include_deletes=True):
688 """Returns a list of AffectedFile instances for all files in the change.
689
690 Args:
691 include_deletes: If false, deleted files will be filtered out.
692 include_dirs: True to include directories in the list
693
694 Returns:
695 [AffectedFile(path, action), AffectedFile(path, action)]
696 """
697 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000698 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000699 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000700 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000701
702 if include_deletes:
703 return affected
704 else:
705 return filter(lambda x: x.Action() != 'D', affected)
706
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000707 def AffectedTextFiles(self, include_deletes=None):
708 """Return a list of the existing text files in a change."""
709 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000710 warn("AffectedTextFiles(include_deletes=%s)"
711 " is deprecated and ignored" % str(include_deletes),
712 category=DeprecationWarning,
713 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000714 return filter(lambda x: x.IsTextFile(),
715 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000716
717 def LocalPaths(self, include_dirs=False):
718 """Convenience function."""
719 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
720
721 def AbsoluteLocalPaths(self, include_dirs=False):
722 """Convenience function."""
723 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
724
725 def ServerPaths(self, include_dirs=False):
726 """Convenience function."""
727 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
728
729 def RightHandSideLines(self):
730 """An iterator over all text lines in "new" version of changed files.
731
732 Lists lines from new or modified text files in the change.
733
734 This is useful for doing line-by-line regex checks, like checking for
735 trailing whitespace.
736
737 Yields:
738 a 3 tuple:
739 the AffectedFile instance of the current file;
740 integer line number (1-based); and
741 the contents of the line as a string.
742 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000743 return _RightHandSideLinesImpl(
744 x for x in self.AffectedFiles(include_deletes=False)
745 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000746
747
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000748class SvnChange(Change):
749 _AFFECTED_FILES = SvnAffectedFile
750
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000751 def __init__(self, *args, **kwargs):
752 Change.__init__(self, *args, **kwargs)
753 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000754 self._changelists = None
755
756 def _GetChangeLists(self):
757 """Get all change lists."""
758 if self._changelists == None:
759 previous_cwd = os.getcwd()
760 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000761 # Need to import here to avoid circular dependency.
762 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000763 self._changelists = gcl.GetModifiedFiles()
764 os.chdir(previous_cwd)
765 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000766
767 def GetAllModifiedFiles(self):
768 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000769 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000770 all_modified_files = []
771 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000772 all_modified_files.extend(
773 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000774 return all_modified_files
775
776 def GetModifiedFiles(self):
777 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000778 changelists = self._GetChangeLists()
779 return [os.path.join(self.RepositoryRoot(), f[1])
780 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000781
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000782
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000783class GitChange(Change):
784 _AFFECTED_FILES = GitAffectedFile
785
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000786 def __init__(self, *args, **kwargs):
787 Change.__init__(self, *args, **kwargs)
788 self.scm = 'git'
789
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000790
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000791def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000792 """Finds all presubmit files that apply to a given set of source files.
793
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000794 If inherit-review-settings-ok is present right under root, looks for
795 PRESUBMIT.py in directories enclosing root.
796
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000797 Args:
798 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000799 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000800
801 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000802 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000803 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000804 files = [normpath(os.path.join(root, f)) for f in files]
805
806 # List all the individual directories containing files.
807 directories = set([os.path.dirname(f) for f in files])
808
809 # Ignore root if inherit-review-settings-ok is present.
810 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
811 root = None
812
813 # Collect all unique directories that may contain PRESUBMIT.py.
814 candidates = set()
815 for directory in directories:
816 while True:
817 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000818 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000819 candidates.add(directory)
820 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000821 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000822 parent_dir = os.path.dirname(directory)
823 if parent_dir == directory:
824 # We hit the system root directory.
825 break
826 directory = parent_dir
827
828 # Look for PRESUBMIT.py in all candidate directories.
829 results = []
830 for directory in sorted(list(candidates)):
831 p = os.path.join(directory, 'PRESUBMIT.py')
832 if os.path.isfile(p):
833 results.append(p)
834
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000835 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000836 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000837
838
thestig@chromium.orgde243452009-10-06 21:02:56 +0000839class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000840 @staticmethod
841 def ExecPresubmitScript(script_text):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000842 """Executes GetPreferredTrySlaves() from a single presubmit script.
843
844 Args:
845 script_text: The text of the presubmit script.
846
847 Return:
848 A list of try slaves.
849 """
850 context = {}
851 exec script_text in context
852
853 function_name = 'GetPreferredTrySlaves'
854 if function_name in context:
855 result = eval(function_name + '()', context)
856 if not isinstance(result, types.ListType):
857 raise exceptions.RuntimeError(
858 'Presubmit functions must return a list, got a %s instead: %s' %
859 (type(result), str(result)))
860 for item in result:
861 if not isinstance(item, basestring):
862 raise exceptions.RuntimeError('All try slaves names must be strings.')
863 if item != item.strip():
864 raise exceptions.RuntimeError('Try slave names cannot start/end'
865 'with whitespace')
866 else:
867 result = []
868 return result
869
870
871def DoGetTrySlaves(changed_files,
872 repository_root,
873 default_presubmit,
874 verbose,
875 output_stream):
876 """Get the list of try servers from the presubmit scripts.
877
878 Args:
879 changed_files: List of modified files.
880 repository_root: The repository root.
881 default_presubmit: A default presubmit script to execute in any case.
882 verbose: Prints debug info.
883 output_stream: A stream to write debug output to.
884
885 Return:
886 List of try slaves
887 """
888 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
889 if not presubmit_files and verbose:
890 output_stream.write("Warning, no presubmit.py found.\n")
891 results = []
892 executer = GetTrySlavesExecuter()
893 if default_presubmit:
894 if verbose:
895 output_stream.write("Running default presubmit script.\n")
896 results += executer.ExecPresubmitScript(default_presubmit)
897 for filename in presubmit_files:
898 filename = os.path.abspath(filename)
899 if verbose:
900 output_stream.write("Running %s\n" % filename)
901 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000902 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000903 results += executer.ExecPresubmitScript(presubmit_script)
904
905 slaves = list(set(results))
906 if slaves and verbose:
907 output_stream.write(', '.join(slaves))
908 output_stream.write('\n')
909 return slaves
910
911
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000912class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000913 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000914 """
915 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000916 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000917 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
918 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000919 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000920 self.committing = committing
921
922 def ExecPresubmitScript(self, script_text, presubmit_path):
923 """Executes a single presubmit script.
924
925 Args:
926 script_text: The text of the presubmit script.
927 presubmit_path: The path to the presubmit file (this will be reported via
928 input_api.PresubmitLocalPath()).
929
930 Return:
931 A list of result objects, empty if no problems.
932 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000933
934 # Change to the presubmit file's directory to support local imports.
935 main_path = os.getcwd()
936 os.chdir(os.path.dirname(presubmit_path))
937
938 # Load the presubmit script into context.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000939 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940 context = {}
941 exec script_text in context
942
943 # These function names must change if we make substantial changes to
944 # the presubmit API that are not backwards compatible.
945 if self.committing:
946 function_name = 'CheckChangeOnCommit'
947 else:
948 function_name = 'CheckChangeOnUpload'
949 if function_name in context:
950 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000951 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000952 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000953 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000954 if not (isinstance(result, types.TupleType) or
955 isinstance(result, types.ListType)):
956 raise exceptions.RuntimeError(
957 'Presubmit functions must return a tuple or list')
958 for item in result:
959 if not isinstance(item, OutputApi.PresubmitResult):
960 raise exceptions.RuntimeError(
961 'All presubmit results must be of types derived from '
962 'output_api.PresubmitResult')
963 else:
964 result = () # no error since the script doesn't care about current event.
965
chase@chromium.org8e416c82009-10-06 04:30:44 +0000966 # Return the process to the original working directory.
967 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000968 return result
969
970
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000971def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000972 committing,
973 verbose,
974 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000975 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000976 default_presubmit,
977 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978 """Runs all presubmit checks that apply to the files in the change.
979
980 This finds all PRESUBMIT.py files in directories enclosing the files in the
981 change (up to the repository root) and calls the relevant entrypoint function
982 depending on whether the change is being committed or uploaded.
983
984 Prints errors, warnings and notifications. Prompts the user for warnings
985 when needed.
986
987 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000988 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000989 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
990 verbose: Prints debug info.
991 output_stream: A stream to write output from presubmit tests to.
992 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000993 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000994 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000996 Warning:
997 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
998 SHOULD be sys.stdin.
999
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000 Return:
1001 True if execution can continue, False if not.
1002 """
maruel@chromium.org8d195232010-10-05 12:58:49 +00001003 print "Running presubmit hooks..."
jam@chromium.org2a891dc2009-08-20 20:33:37 +00001004 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001005 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
1006 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001008 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001009 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001010 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001011 if default_presubmit:
1012 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001013 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001014 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001015 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001016 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +00001017 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001019 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +00001020 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001021 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001022 results += executer.ExecPresubmitScript(presubmit_script, filename)
1023
1024 errors = []
1025 notifications = []
1026 warnings = []
1027 for result in results:
1028 if not result.IsFatal() and not result.ShouldPrompt():
1029 notifications.append(result)
1030 elif result.ShouldPrompt():
1031 warnings.append(result)
1032 else:
1033 errors.append(result)
1034
1035 error_count = 0
1036 for name, items in (('Messages', notifications),
1037 ('Warnings', warnings),
1038 ('ERRORS', errors)):
1039 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001040 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001041 for item in items:
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +00001042 # Access to a protected member XXX of a client class
1043 # pylint: disable=W0212
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001044 if not item._Handle(output_stream, input_stream,
1045 may_prompt=False):
1046 error_count += 1
1047 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001048
1049 total_time = time.time() - start_time
1050 if total_time > 1.0:
1051 print "Presubmit checks took %.1fs to calculate." % total_time
1052
maruel@chromium.org07bbc212009-06-11 02:08:41 +00001053 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +00001054 if not PromptYesNo(input_stream, output_stream,
1055 'There were presubmit warnings. '
1056 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001057 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001058
1059 global _ASKED_FOR_FEEDBACK
1060 # Ask for feedback one time out of 5.
1061 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1062 output_stream.write("Was the presubmit check useful? Please send feedback "
1063 "& hate mail to maruel@chromium.org!\n")
1064 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001065 return (error_count == 0)
1066
1067
1068def ScanSubDirs(mask, recursive):
1069 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001070 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 +00001071 else:
1072 results = []
1073 for root, dirs, files in os.walk('.'):
1074 if '.svn' in dirs:
1075 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001076 if '.git' in dirs:
1077 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001078 for name in files:
1079 if fnmatch.fnmatch(name, mask):
1080 results.append(os.path.join(root, name))
1081 return results
1082
1083
1084def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001085 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001086 files = []
1087 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001088 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001089 return files
1090
1091
1092def Main(argv):
1093 parser = optparse.OptionParser(usage="%prog [options]",
1094 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001095 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001096 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001097 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1098 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001099 parser.add_option("-r", "--recursive", action="store_true",
1100 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001101 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001102 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001103 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001104 parser.add_option("--name", default='no name')
1105 parser.add_option("--description", default='')
1106 parser.add_option("--issue", type='int', default=0)
1107 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001108 parser.add_option("--root", default=os.getcwd(),
1109 help="Search for PRESUBMIT.py up to this directory. "
1110 "If inherit-review-settings-ok is present in this "
1111 "directory, parent directories up to the root file "
1112 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001113 parser.add_option("--default_presubmit")
1114 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001115 options, args = parser.parse_args(argv[1:])
maruel@chromium.org7444c502011-02-09 14:02:11 +00001116 if options.verbose:
1117 logging.basicConfig(level=logging.DEBUG)
1118 if os.path.isdir(os.path.join(options.root, '.svn')):
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001119 change_class = SvnChange
1120 if not options.files:
1121 if args:
1122 options.files = ParseFiles(args, options.recursive)
1123 else:
1124 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001125 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001126 else:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001127 is_git = os.path.isdir(os.path.join(options.root, '.git'))
1128 if not is_git:
1129 is_git = (0 == subprocess.call(
1130 ['git', 'rev-parse', '--show-cdup'],
1131 stdout=subprocess.PIPE, cwd=options.root))
1132 if is_git:
1133 # Only look at the subdirectories below cwd.
1134 change_class = GitChange
1135 if not options.files:
1136 if args:
1137 options.files = ParseFiles(args, options.recursive)
1138 else:
1139 # Grab modified files.
1140 options.files = scm.GIT.CaptureStatus([options.root])
1141 else:
1142 logging.info('Doesn\'t seem under source control.')
1143 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001144 if options.verbose:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001145 if not options.files:
1146 print "Found no files."
1147 elif len(options.files) != 1:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001148 print "Found %d files." % len(options.files)
1149 else:
1150 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001151 return not DoPresubmitChecks(change_class(options.name,
1152 options.description,
1153 options.root,
1154 options.files,
1155 options.issue,
1156 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001157 options.commit,
1158 options.verbose,
1159 sys.stdout,
1160 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001161 options.default_presubmit,
1162 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001163
1164
1165if __name__ == '__main__':
1166 sys.exit(Main(sys.argv))