blob: 38d29902752bc176f7175cd3fbc27cd190f3156f [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):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000561 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
562
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000563
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000564class GitAffectedFile(AffectedFile):
565 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000566 # Method 'NNN' is abstract in class 'NNN' but is not overridden
567 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000568
569 def __init__(self, *args, **kwargs):
570 AffectedFile.__init__(self, *args, **kwargs)
571 self._server_path = None
572 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000573
574 def ServerPath(self):
575 if self._server_path is None:
576 raise NotImplementedException() # TODO(maruel) Implement.
577 return self._server_path
578
579 def IsDirectory(self):
580 if self._is_directory is None:
581 path = self.AbsoluteLocalPath()
582 if os.path.exists(path):
583 # Retrieve directly from the file system; it is much faster than
584 # querying subversion, especially on Windows.
585 self._is_directory = os.path.isdir(path)
586 else:
587 # raise NotImplementedException() # TODO(maruel) Implement.
588 self._is_directory = False
589 return self._is_directory
590
591 def Property(self, property_name):
592 if not property_name in self._properties:
593 raise NotImplementedException() # TODO(maruel) Implement.
594 return self._properties[property_name]
595
596 def IsTextFile(self):
597 if self._is_text_file is None:
598 if self.Action() == 'D':
599 # A deleted file is not a text file.
600 self._is_text_file = False
601 elif self.IsDirectory():
602 self._is_text_file = False
603 else:
604 # raise NotImplementedException() # TODO(maruel) Implement.
605 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
606 return self._is_text_file
607
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000608 def GenerateScmDiff(self):
609 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000610
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000611class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000612 """Describe a change.
613
614 Used directly by the presubmit scripts to query the current change being
615 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000616
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000617 Instance members:
618 tags: Dictionnary of KEY=VALUE pairs found in the change description.
619 self.KEY: equivalent to tags['KEY']
620 """
621
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000622 _AFFECTED_FILES = AffectedFile
623
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000624 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000625 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000626 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000627
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000628 def __init__(self, name, description, local_root, files, issue, patchset):
629 if files is None:
630 files = []
631 self._name = name
632 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000633 # Convert root into an absolute path.
634 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000635 self.issue = issue
636 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000637 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000638
639 # From the description text, build up a dictionary of key/value pairs
640 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000641 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000642 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000643 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000644 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000645 if m:
646 self.tags[m.group('key')] = m.group('value')
647 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000648 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000649
650 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000651 self._description_without_tags = (
652 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000653
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000654 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000655 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
656 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000657 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000658
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000659 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000660 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000661 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000662
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000663 def DescriptionText(self):
664 """Returns the user-entered changelist description, minus tags.
665
666 Any line in the user-provided description starting with e.g. "FOO="
667 (whitespace permitted before and around) is considered a tag line. Such
668 lines are stripped out of the description this function returns.
669 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000670 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000671
672 def FullDescriptionText(self):
673 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000674 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000675
676 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000677 """Returns the repository (checkout) root directory for this change,
678 as an absolute path.
679 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000680 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681
682 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000683 """Return tags directly as attributes on the object."""
684 if not re.match(r"^[A-Z_]*$", attr):
685 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000686 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
688 def AffectedFiles(self, include_dirs=False, include_deletes=True):
689 """Returns a list of AffectedFile instances for all files in the change.
690
691 Args:
692 include_deletes: If false, deleted files will be filtered out.
693 include_dirs: True to include directories in the list
694
695 Returns:
696 [AffectedFile(path, action), AffectedFile(path, action)]
697 """
698 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000699 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000701 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702
703 if include_deletes:
704 return affected
705 else:
706 return filter(lambda x: x.Action() != 'D', affected)
707
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000708 def AffectedTextFiles(self, include_deletes=None):
709 """Return a list of the existing text files in a change."""
710 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000711 warn("AffectedTextFiles(include_deletes=%s)"
712 " is deprecated and ignored" % str(include_deletes),
713 category=DeprecationWarning,
714 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000715 return filter(lambda x: x.IsTextFile(),
716 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717
718 def LocalPaths(self, include_dirs=False):
719 """Convenience function."""
720 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
721
722 def AbsoluteLocalPaths(self, include_dirs=False):
723 """Convenience function."""
724 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
725
726 def ServerPaths(self, include_dirs=False):
727 """Convenience function."""
728 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
729
730 def RightHandSideLines(self):
731 """An iterator over all text lines in "new" version of changed files.
732
733 Lists lines from new or modified text files in the change.
734
735 This is useful for doing line-by-line regex checks, like checking for
736 trailing whitespace.
737
738 Yields:
739 a 3 tuple:
740 the AffectedFile instance of the current file;
741 integer line number (1-based); and
742 the contents of the line as a string.
743 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000744 return _RightHandSideLinesImpl(
745 x for x in self.AffectedFiles(include_deletes=False)
746 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747
748
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000749class SvnChange(Change):
750 _AFFECTED_FILES = SvnAffectedFile
751
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000752 def __init__(self, *args, **kwargs):
753 Change.__init__(self, *args, **kwargs)
754 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000755 self._changelists = None
756
757 def _GetChangeLists(self):
758 """Get all change lists."""
759 if self._changelists == None:
760 previous_cwd = os.getcwd()
761 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000762 # Need to import here to avoid circular dependency.
763 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000764 self._changelists = gcl.GetModifiedFiles()
765 os.chdir(previous_cwd)
766 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000767
768 def GetAllModifiedFiles(self):
769 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000770 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000771 all_modified_files = []
772 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000773 all_modified_files.extend(
774 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000775 return all_modified_files
776
777 def GetModifiedFiles(self):
778 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000779 changelists = self._GetChangeLists()
780 return [os.path.join(self.RepositoryRoot(), f[1])
781 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000782
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000783
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000784class GitChange(Change):
785 _AFFECTED_FILES = GitAffectedFile
786
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000787 def __init__(self, *args, **kwargs):
788 Change.__init__(self, *args, **kwargs)
789 self.scm = 'git'
790
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000791
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000792def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000793 """Finds all presubmit files that apply to a given set of source files.
794
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000795 If inherit-review-settings-ok is present right under root, looks for
796 PRESUBMIT.py in directories enclosing root.
797
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000798 Args:
799 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000800 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000801
802 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000803 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000804 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000805 files = [normpath(os.path.join(root, f)) for f in files]
806
807 # List all the individual directories containing files.
808 directories = set([os.path.dirname(f) for f in files])
809
810 # Ignore root if inherit-review-settings-ok is present.
811 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
812 root = None
813
814 # Collect all unique directories that may contain PRESUBMIT.py.
815 candidates = set()
816 for directory in directories:
817 while True:
818 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000819 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000820 candidates.add(directory)
821 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000822 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000823 parent_dir = os.path.dirname(directory)
824 if parent_dir == directory:
825 # We hit the system root directory.
826 break
827 directory = parent_dir
828
829 # Look for PRESUBMIT.py in all candidate directories.
830 results = []
831 for directory in sorted(list(candidates)):
832 p = os.path.join(directory, 'PRESUBMIT.py')
833 if os.path.isfile(p):
834 results.append(p)
835
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000836 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000837 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838
839
thestig@chromium.orgde243452009-10-06 21:02:56 +0000840class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000841 @staticmethod
842 def ExecPresubmitScript(script_text):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000843 """Executes GetPreferredTrySlaves() from a single presubmit script.
844
845 Args:
846 script_text: The text of the presubmit script.
847
848 Return:
849 A list of try slaves.
850 """
851 context = {}
852 exec script_text in context
853
854 function_name = 'GetPreferredTrySlaves'
855 if function_name in context:
856 result = eval(function_name + '()', context)
857 if not isinstance(result, types.ListType):
858 raise exceptions.RuntimeError(
859 'Presubmit functions must return a list, got a %s instead: %s' %
860 (type(result), str(result)))
861 for item in result:
862 if not isinstance(item, basestring):
863 raise exceptions.RuntimeError('All try slaves names must be strings.')
864 if item != item.strip():
865 raise exceptions.RuntimeError('Try slave names cannot start/end'
866 'with whitespace')
867 else:
868 result = []
869 return result
870
871
872def DoGetTrySlaves(changed_files,
873 repository_root,
874 default_presubmit,
875 verbose,
876 output_stream):
877 """Get the list of try servers from the presubmit scripts.
878
879 Args:
880 changed_files: List of modified files.
881 repository_root: The repository root.
882 default_presubmit: A default presubmit script to execute in any case.
883 verbose: Prints debug info.
884 output_stream: A stream to write debug output to.
885
886 Return:
887 List of try slaves
888 """
889 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
890 if not presubmit_files and verbose:
891 output_stream.write("Warning, no presubmit.py found.\n")
892 results = []
893 executer = GetTrySlavesExecuter()
894 if default_presubmit:
895 if verbose:
896 output_stream.write("Running default presubmit script.\n")
897 results += executer.ExecPresubmitScript(default_presubmit)
898 for filename in presubmit_files:
899 filename = os.path.abspath(filename)
900 if verbose:
901 output_stream.write("Running %s\n" % filename)
902 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000903 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000904 results += executer.ExecPresubmitScript(presubmit_script)
905
906 slaves = list(set(results))
907 if slaves and verbose:
908 output_stream.write(', '.join(slaves))
909 output_stream.write('\n')
910 return slaves
911
912
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000913class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000914 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000915 """
916 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000917 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000918 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
919 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000920 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000921 self.committing = committing
922
923 def ExecPresubmitScript(self, script_text, presubmit_path):
924 """Executes a single presubmit script.
925
926 Args:
927 script_text: The text of the presubmit script.
928 presubmit_path: The path to the presubmit file (this will be reported via
929 input_api.PresubmitLocalPath()).
930
931 Return:
932 A list of result objects, empty if no problems.
933 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000934
935 # Change to the presubmit file's directory to support local imports.
936 main_path = os.getcwd()
937 os.chdir(os.path.dirname(presubmit_path))
938
939 # Load the presubmit script into context.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000940 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941 context = {}
942 exec script_text in context
943
944 # These function names must change if we make substantial changes to
945 # the presubmit API that are not backwards compatible.
946 if self.committing:
947 function_name = 'CheckChangeOnCommit'
948 else:
949 function_name = 'CheckChangeOnUpload'
950 if function_name in context:
951 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000952 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000953 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000954 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000955 if not (isinstance(result, types.TupleType) or
956 isinstance(result, types.ListType)):
957 raise exceptions.RuntimeError(
958 'Presubmit functions must return a tuple or list')
959 for item in result:
960 if not isinstance(item, OutputApi.PresubmitResult):
961 raise exceptions.RuntimeError(
962 'All presubmit results must be of types derived from '
963 'output_api.PresubmitResult')
964 else:
965 result = () # no error since the script doesn't care about current event.
966
chase@chromium.org8e416c82009-10-06 04:30:44 +0000967 # Return the process to the original working directory.
968 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000969 return result
970
971
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000972def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973 committing,
974 verbose,
975 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000976 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000977 default_presubmit,
978 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979 """Runs all presubmit checks that apply to the files in the change.
980
981 This finds all PRESUBMIT.py files in directories enclosing the files in the
982 change (up to the repository root) and calls the relevant entrypoint function
983 depending on whether the change is being committed or uploaded.
984
985 Prints errors, warnings and notifications. Prompts the user for warnings
986 when needed.
987
988 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000989 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000990 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
991 verbose: Prints debug info.
992 output_stream: A stream to write output from presubmit tests to.
993 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000994 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000995 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +0000997 Warning:
998 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
999 SHOULD be sys.stdin.
1000
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001001 Return:
1002 True if execution can continue, False if not.
1003 """
maruel@chromium.org8d195232010-10-05 12:58:49 +00001004 print "Running presubmit hooks..."
jam@chromium.org2a891dc2009-08-20 20:33:37 +00001005 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001006 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
1007 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001009 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001011 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001012 if default_presubmit:
1013 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001014 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001015 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001016 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001017 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +00001018 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001019 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001020 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +00001021 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001022 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001023 results += executer.ExecPresubmitScript(presubmit_script, filename)
1024
1025 errors = []
1026 notifications = []
1027 warnings = []
1028 for result in results:
1029 if not result.IsFatal() and not result.ShouldPrompt():
1030 notifications.append(result)
1031 elif result.ShouldPrompt():
1032 warnings.append(result)
1033 else:
1034 errors.append(result)
1035
1036 error_count = 0
1037 for name, items in (('Messages', notifications),
1038 ('Warnings', warnings),
1039 ('ERRORS', errors)):
1040 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001041 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001042 for item in items:
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +00001043 # Access to a protected member XXX of a client class
1044 # pylint: disable=W0212
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001045 if not item._Handle(output_stream, input_stream,
1046 may_prompt=False):
1047 error_count += 1
1048 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001049
1050 total_time = time.time() - start_time
1051 if total_time > 1.0:
1052 print "Presubmit checks took %.1fs to calculate." % total_time
1053
maruel@chromium.org07bbc212009-06-11 02:08:41 +00001054 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +00001055 if not PromptYesNo(input_stream, output_stream,
1056 'There were presubmit warnings. '
1057 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001058 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001059
1060 global _ASKED_FOR_FEEDBACK
1061 # Ask for feedback one time out of 5.
1062 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1063 output_stream.write("Was the presubmit check useful? Please send feedback "
1064 "& hate mail to maruel@chromium.org!\n")
1065 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001066 return (error_count == 0)
1067
1068
1069def ScanSubDirs(mask, recursive):
1070 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001071 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 +00001072 else:
1073 results = []
1074 for root, dirs, files in os.walk('.'):
1075 if '.svn' in dirs:
1076 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001077 if '.git' in dirs:
1078 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001079 for name in files:
1080 if fnmatch.fnmatch(name, mask):
1081 results.append(os.path.join(root, name))
1082 return results
1083
1084
1085def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001086 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001087 files = []
1088 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001089 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001090 return files
1091
1092
1093def Main(argv):
1094 parser = optparse.OptionParser(usage="%prog [options]",
1095 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001096 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001097 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001098 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1099 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001100 parser.add_option("-r", "--recursive", action="store_true",
1101 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001102 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001103 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001104 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001105 parser.add_option("--name", default='no name')
1106 parser.add_option("--description", default='')
1107 parser.add_option("--issue", type='int', default=0)
1108 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001109 parser.add_option("--root", default=os.getcwd(),
1110 help="Search for PRESUBMIT.py up to this directory. "
1111 "If inherit-review-settings-ok is present in this "
1112 "directory, parent directories up to the root file "
1113 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001114 parser.add_option("--default_presubmit")
1115 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001116 options, args = parser.parse_args(argv[1:])
maruel@chromium.org7444c502011-02-09 14:02:11 +00001117 if options.verbose:
1118 logging.basicConfig(level=logging.DEBUG)
1119 if os.path.isdir(os.path.join(options.root, '.svn')):
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001120 change_class = SvnChange
1121 if not options.files:
1122 if args:
1123 options.files = ParseFiles(args, options.recursive)
1124 else:
1125 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001126 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001127 else:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001128 is_git = os.path.isdir(os.path.join(options.root, '.git'))
1129 if not is_git:
1130 is_git = (0 == subprocess.call(
1131 ['git', 'rev-parse', '--show-cdup'],
1132 stdout=subprocess.PIPE, cwd=options.root))
1133 if is_git:
1134 # Only look at the subdirectories below cwd.
1135 change_class = GitChange
1136 if not options.files:
1137 if args:
1138 options.files = ParseFiles(args, options.recursive)
1139 else:
1140 # Grab modified files.
1141 options.files = scm.GIT.CaptureStatus([options.root])
1142 else:
1143 logging.info('Doesn\'t seem under source control.')
1144 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001145 if options.verbose:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001146 if not options.files:
1147 print "Found no files."
1148 elif len(options.files) != 1:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001149 print "Found %d files." % len(options.files)
1150 else:
1151 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001152 return not DoPresubmitChecks(change_class(options.name,
1153 options.description,
1154 options.root,
1155 options.files,
1156 options.issue,
1157 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001158 options.commit,
1159 options.verbose,
1160 sys.stdout,
1161 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001162 options.default_presubmit,
1163 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001164
1165
1166if __name__ == '__main__':
1167 sys.exit(Main(sys.argv))