blob: fc1c7f23b0c09d666c88b2e03cb19eb50b828ba4 [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:
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000038 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000039except 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'))
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000050 import simplejson as json # pylint: disable=F0401
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
dpranke@chromium.org2a009622011-03-01 02:43:31 +000054import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000056import scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000057
58
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000059# Ask for feedback only once in program lifetime.
60_ASKED_FOR_FEEDBACK = False
61
62
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000063class NotImplementedException(Exception):
64 """We're leaving placeholders in a bunch of places to remind us of the
65 design of the API, but we have not implemented all of it yet. Implement as
66 the need arises.
67 """
68 pass
69
70
71def normpath(path):
72 '''Version of os.path.normpath that also changes backward slashes to
73 forward slashes when not running on Windows.
74 '''
75 # This is safe to always do because the Windows version of os.path.normpath
76 # will replace forward slashes with backward slashes.
77 path = path.replace(os.sep, '/')
78 return os.path.normpath(path)
79
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000080
gspencer@google.comefb94502009-10-09 17:57:08 +000081def PromptYesNo(input_stream, output_stream, prompt):
82 output_stream.write(prompt)
83 response = input_stream.readline().strip().lower()
84 return response == 'y' or response == 'yes'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000085
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000086
87def _RightHandSideLinesImpl(affected_files):
88 """Implements RightHandSideLines for InputApi and GclChange."""
89 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000090 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000091 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000092 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000093
94
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000095class OutputApi(object):
96 """This class (more like a module) gets passed to presubmit scripts so that
97 they can specify various types of results.
98 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +000099 # Method could be a function
100 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000101 class PresubmitResult(object):
102 """Base class for result objects."""
103
104 def __init__(self, message, items=None, long_text=''):
105 """
106 message: A short one-line message to indicate errors.
107 items: A list of short strings to indicate where errors occurred.
108 long_text: multi-line text output, e.g. from another tool
109 """
110 self._message = message
111 self._items = []
112 if items:
113 self._items = items
114 self._long_text = long_text.rstrip()
115
116 def _Handle(self, output_stream, input_stream, may_prompt=True):
117 """Writes this result to the output stream.
118
119 Args:
120 output_stream: Where to write
121
122 Returns:
123 True if execution may continue, False otherwise.
124 """
125 output_stream.write(self._message)
126 output_stream.write('\n')
estade@chromium.org593258b2010-01-28 00:04:36 +0000127 if len(self._items) > 0:
128 output_stream.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000129 if self._long_text:
maruel@chromium.org310b3552010-11-01 13:23:35 +0000130 # Sometimes self._long_text is a ascii string, a codepage string
131 # (on windows), or a unicode object.
132 try:
133 long_text = self._long_text.decode()
134 except UnicodeDecodeError:
135 long_text = self._long_text.decode('ascii', 'replace')
136
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000137 output_stream.write('\n***************\n%s\n***************\n' %
maruel@chromium.org310b3552010-11-01 13:23:35 +0000138 long_text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000139
140 if self.ShouldPrompt() and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +0000141 if not PromptYesNo(input_stream, output_stream,
142 'Are you sure you want to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000143 return False
144
145 return not self.IsFatal()
146
147 def IsFatal(self):
148 """An error that is fatal stops g4 mail/submit immediately, i.e. before
149 other presubmit scripts are run.
150 """
151 return False
152
153 def ShouldPrompt(self):
154 """Whether this presubmit result should result in a prompt warning."""
155 return False
156
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000157 class PresubmitAddText(PresubmitResult):
158 """Propagates a line of text back to the caller."""
159 def __init__(self, message, items=None, long_text=''):
160 super(OutputApi.PresubmitAddText, self).__init__("ADD: " + message,
161 items, long_text)
162
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000163 class PresubmitError(PresubmitResult):
164 """A hard presubmit error."""
165 def IsFatal(self):
166 return True
167
168 class PresubmitPromptWarning(PresubmitResult):
169 """An warning that prompts the user if they want to continue."""
170 def ShouldPrompt(self):
171 return True
172
173 class PresubmitNotifyResult(PresubmitResult):
174 """Just print something to the screen -- but it's not even a warning."""
175 pass
176
177 class MailTextResult(PresubmitResult):
178 """A warning that should be included in the review request email."""
179 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000180 super(OutputApi.MailTextResult, self).__init__()
181 raise NotImplementedException()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000182
183
184class InputApi(object):
185 """An instance of this object is passed to presubmit scripts so they can
186 know stuff about the change they're looking at.
187 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000188 # Method could be a function
189 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000190
maruel@chromium.org3410d912009-06-09 20:56:16 +0000191 # File extensions that are considered source files from a style guide
192 # perspective. Don't modify this list from a presubmit script!
193 DEFAULT_WHITE_LIST = (
194 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000195 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000196 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000197 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000198 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000199 # No extension at all, note that ALL CAPS files are black listed in
200 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000201 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000202 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000203 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000204 )
205
206 # Path regexp that should be excluded from being considered containing source
207 # files. Don't modify this list from a presubmit script!
208 DEFAULT_BLACK_LIST = (
209 r".*\bexperimental[\\\/].*",
210 r".*\bthird_party[\\\/].*",
211 # Output directories (just in case)
212 r".*\bDebug[\\\/].*",
213 r".*\bRelease[\\\/].*",
214 r".*\bxcodebuild[\\\/].*",
215 r".*\bsconsbuild[\\\/].*",
216 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000217 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000218 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000219 r"(|.*[\\\/])\.git[\\\/].*",
220 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000221 )
222
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000223 # TODO(dpranke): Update callers to pass in tbr, host_url, remove
224 # default arguments.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000225 def __init__(self, change, presubmit_path, is_committing, tbr, host_url=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226 """Builds an InputApi object.
227
228 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000229 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000231 is_committing: True if the change is about to be committed.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000232 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
233 host_url: scheme, host, and path of rietveld instance
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000234 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000235 # Version number of the presubmit_support script.
236 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000237 self.change = change
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000238 self.host_url = host_url
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000239 self.is_committing = is_committing
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000240 self.tbr = tbr
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000241 self.host_url = host_url or 'http://codereview.chromium.org'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000242
243 # We expose various modules and functions as attributes of the input_api
244 # so that presubmit scripts don't have to import them.
245 self.basename = os.path.basename
246 self.cPickle = cPickle
247 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000248 self.json = json
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000249 self.os_path = os.path
250 self.pickle = pickle
251 self.marshal = marshal
252 self.re = re
253 self.subprocess = subprocess
254 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000255 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000256 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000257 self.urllib2 = urllib2
258
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000259 # To easily fork python.
260 self.python_executable = sys.executable
261 self.environ = os.environ
262
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000263 # InputApi.platform is the platform you're currently running on.
264 self.platform = sys.platform
265
266 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000267 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268
269 # We carry the canned checks so presubmit scripts can easily use them.
270 self.canned_checks = presubmit_canned_checks
271
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000272 # TODO(dpranke): figure out a list of all approved owners for a repo
273 # in order to be able to handle wildcard OWNERS files?
274 self.owners_db = owners.Database(change.RepositoryRoot(),
275 fopen=file, os_path=self.os_path)
276
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000277 def PresubmitLocalPath(self):
278 """Returns the local path of the presubmit script currently being run.
279
280 This is useful if you don't want to hard-code absolute paths in the
281 presubmit script. For example, It can be used to find another file
282 relative to the PRESUBMIT.py script, so the whole tree can be branched and
283 the presubmit script still works, without editing its content.
284 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000285 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000287 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000288 """Translate a depot path to a local path (relative to client root).
289
290 Args:
291 Depot path as a string.
292
293 Returns:
294 The local path of the depot path under the user's current client, or None
295 if the file is not mapped.
296
297 Remember to check for the None case and show an appropriate error!
298 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000299 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000300 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000301 return local_path
302
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000303 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000304 """Translate a local path to a depot path.
305
306 Args:
307 Local path (relative to current directory, or absolute) as a string.
308
309 Returns:
310 The depot path (SVN URL) of the file if mapped, otherwise None.
311 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000312 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000313 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000314 return depot_path
315
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000316 def AffectedFiles(self, include_dirs=False, include_deletes=True):
317 """Same as input_api.change.AffectedFiles() except only lists files
318 (and optionally directories) in the same directory as the current presubmit
319 script, or subdirectories thereof.
320 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000321 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322 if len(dir_with_slash) == 1:
323 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000324 return filter(
325 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
326 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000327
328 def LocalPaths(self, include_dirs=False):
329 """Returns local paths of input_api.AffectedFiles()."""
330 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
331
332 def AbsoluteLocalPaths(self, include_dirs=False):
333 """Returns absolute local paths of input_api.AffectedFiles()."""
334 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
335
336 def ServerPaths(self, include_dirs=False):
337 """Returns server paths of input_api.AffectedFiles()."""
338 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
339
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000340 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000341 """Same as input_api.change.AffectedTextFiles() except only lists files
342 in the same directory as the current presubmit script, or subdirectories
343 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000345 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000346 warn("AffectedTextFiles(include_deletes=%s)"
347 " is deprecated and ignored" % str(include_deletes),
348 category=DeprecationWarning,
349 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000350 return filter(lambda x: x.IsTextFile(),
351 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000352
maruel@chromium.org3410d912009-06-09 20:56:16 +0000353 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
354 """Filters out files that aren't considered "source file".
355
356 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
357 and InputApi.DEFAULT_BLACK_LIST is used respectively.
358
359 The lists will be compiled as regular expression and
360 AffectedFile.LocalPath() needs to pass both list.
361
362 Note: Copy-paste this function to suit your needs or use a lambda function.
363 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000364 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000365 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000366 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000367 if self.re.match(item, local_path):
368 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000369 return True
370 return False
371 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
372 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
373
374 def AffectedSourceFiles(self, source_file):
375 """Filter the list of AffectedTextFiles by the function source_file.
376
377 If source_file is None, InputApi.FilterSourceFile() is used.
378 """
379 if not source_file:
380 source_file = self.FilterSourceFile
381 return filter(source_file, self.AffectedTextFiles())
382
383 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000384 """An iterator over all text lines in "new" version of changed files.
385
386 Only lists lines from new or modified text files in the change that are
387 contained by the directory of the currently executing presubmit script.
388
389 This is useful for doing line-by-line regex checks, like checking for
390 trailing whitespace.
391
392 Yields:
393 a 3 tuple:
394 the AffectedFile instance of the current file;
395 integer line number (1-based); and
396 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000397
398 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000399 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000400 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000401 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000403 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000404 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000405
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000406 Deny reading anything outside the repository.
407 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000408 if isinstance(file_item, AffectedFile):
409 file_item = file_item.AbsoluteLocalPath()
410 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000411 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000412 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000413
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000414
415class AffectedFile(object):
416 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000417 # Method could be a function
418 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000419 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000420 self._path = path
421 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000422 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000423 self._is_directory = None
424 self._properties = {}
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000425 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000426
427 def ServerPath(self):
428 """Returns a path string that identifies the file in the SCM system.
429
430 Returns the empty string if the file does not exist in SCM.
431 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000432 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433
434 def LocalPath(self):
435 """Returns the path of this file on the local disk relative to client root.
436 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000437 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438
439 def AbsoluteLocalPath(self):
440 """Returns the absolute path of this file on the local disk.
441 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000442 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000443
444 def IsDirectory(self):
445 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000446 if self._is_directory is None:
447 path = self.AbsoluteLocalPath()
448 self._is_directory = (os.path.exists(path) and
449 os.path.isdir(path))
450 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000451
452 def Action(self):
453 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000454 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
455 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000456 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000457
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000458 def Property(self, property_name):
459 """Returns the specified SCM property of this file, or None if no such
460 property.
461 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000462 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000463
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000464 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000465 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000466
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000467 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000468 raise NotImplementedError() # Implement when needed
469
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000470 def NewContents(self):
471 """Returns an iterator over the lines in the new version of file.
472
473 The new version is the file in the user's workspace, i.e. the "right hand
474 side".
475
476 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000477 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000478 """
479 if self.IsDirectory():
480 return []
481 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000482 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
483 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484
485 def OldContents(self):
486 """Returns an iterator over the lines in the old version of file.
487
488 The old version is the file in depot, i.e. the "left hand side".
489 """
490 raise NotImplementedError() # Implement when needed
491
492 def OldFileTempPath(self):
493 """Returns the path on local disk where the old contents resides.
494
495 The old version is the file in depot, i.e. the "left hand side".
496 This is a read-only cached copy of the old contents. *DO NOT* try to
497 modify this file.
498 """
499 raise NotImplementedError() # Implement if/when needed.
500
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000501 def ChangedContents(self):
502 """Returns a list of tuples (line number, line text) of all new lines.
503
504 This relies on the scm diff output describing each changed code section
505 with a line of the form
506
507 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
508 """
509 new_lines = []
510 line_num = 0
511
512 if self.IsDirectory():
513 return []
514
515 for line in self.GenerateScmDiff().splitlines():
516 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
517 if m:
518 line_num = int(m.groups(1)[0])
519 continue
520 if line.startswith('+') and not line.startswith('++'):
521 new_lines.append((line_num, line[1:]))
522 if not line.startswith('-'):
523 line_num += 1
524 return new_lines
525
maruel@chromium.org5de13972009-06-10 18:16:06 +0000526 def __str__(self):
527 return self.LocalPath()
528
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000529 def GenerateScmDiff(self):
530 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000531
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000532class SvnAffectedFile(AffectedFile):
533 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000534 # Method 'NNN' is abstract in class 'NNN' but is not overridden
535 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000536
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000537 def __init__(self, *args, **kwargs):
538 AffectedFile.__init__(self, *args, **kwargs)
539 self._server_path = None
540 self._is_text_file = None
541
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000542 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000543 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000544 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000545 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000546 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000547
548 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000549 if self._is_directory is None:
550 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000551 if os.path.exists(path):
552 # Retrieve directly from the file system; it is much faster than
553 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000554 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000555 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000556 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000557 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000558 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000559
560 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000561 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000562 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000563 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000564 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000565
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000566 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000567 if self._is_text_file is None:
568 if self.Action() == 'D':
569 # A deleted file is not a text file.
570 self._is_text_file = False
571 elif self.IsDirectory():
572 self._is_text_file = False
573 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000574 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
575 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000576 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
577 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000578
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000579 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000580 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
581
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000582
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000583class GitAffectedFile(AffectedFile):
584 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000585 # Method 'NNN' is abstract in class 'NNN' but is not overridden
586 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000587
588 def __init__(self, *args, **kwargs):
589 AffectedFile.__init__(self, *args, **kwargs)
590 self._server_path = None
591 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000592
593 def ServerPath(self):
594 if self._server_path is None:
595 raise NotImplementedException() # TODO(maruel) Implement.
596 return self._server_path
597
598 def IsDirectory(self):
599 if self._is_directory is None:
600 path = self.AbsoluteLocalPath()
601 if os.path.exists(path):
602 # Retrieve directly from the file system; it is much faster than
603 # querying subversion, especially on Windows.
604 self._is_directory = os.path.isdir(path)
605 else:
606 # raise NotImplementedException() # TODO(maruel) Implement.
607 self._is_directory = False
608 return self._is_directory
609
610 def Property(self, property_name):
611 if not property_name in self._properties:
612 raise NotImplementedException() # TODO(maruel) Implement.
613 return self._properties[property_name]
614
615 def IsTextFile(self):
616 if self._is_text_file is None:
617 if self.Action() == 'D':
618 # A deleted file is not a text file.
619 self._is_text_file = False
620 elif self.IsDirectory():
621 self._is_text_file = False
622 else:
623 # raise NotImplementedException() # TODO(maruel) Implement.
624 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
625 return self._is_text_file
626
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000627 def GenerateScmDiff(self):
628 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000629
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000630class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000631 """Describe a change.
632
633 Used directly by the presubmit scripts to query the current change being
634 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000635
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000636 Instance members:
637 tags: Dictionnary of KEY=VALUE pairs found in the change description.
638 self.KEY: equivalent to tags['KEY']
639 """
640
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000641 _AFFECTED_FILES = AffectedFile
642
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000643 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000644 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000645 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000646
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000647 def __init__(self, name, description, local_root, files, issue, patchset):
648 if files is None:
649 files = []
650 self._name = name
651 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000652 # Convert root into an absolute path.
653 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000654 self.issue = issue
655 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000656 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657
658 # From the description text, build up a dictionary of key/value pairs
659 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000660 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000662 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000663 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000664 if m:
665 self.tags[m.group('key')] = m.group('value')
666 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000667 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668
669 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000670 self._description_without_tags = (
671 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000673 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000674 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
675 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000676 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000677
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000678 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000680 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682 def DescriptionText(self):
683 """Returns the user-entered changelist description, minus tags.
684
685 Any line in the user-provided description starting with e.g. "FOO="
686 (whitespace permitted before and around) is considered a tag line. Such
687 lines are stripped out of the description this function returns.
688 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000689 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690
691 def FullDescriptionText(self):
692 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000693 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694
695 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000696 """Returns the repository (checkout) root directory for this change,
697 as an absolute path.
698 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000699 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700
701 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000702 """Return tags directly as attributes on the object."""
703 if not re.match(r"^[A-Z_]*$", attr):
704 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000705 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706
707 def AffectedFiles(self, include_dirs=False, include_deletes=True):
708 """Returns a list of AffectedFile instances for all files in the change.
709
710 Args:
711 include_deletes: If false, deleted files will be filtered out.
712 include_dirs: True to include directories in the list
713
714 Returns:
715 [AffectedFile(path, action), AffectedFile(path, action)]
716 """
717 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000718 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000720 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000721
722 if include_deletes:
723 return affected
724 else:
725 return filter(lambda x: x.Action() != 'D', affected)
726
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000727 def AffectedTextFiles(self, include_deletes=None):
728 """Return a list of the existing text files in a change."""
729 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000730 warn("AffectedTextFiles(include_deletes=%s)"
731 " is deprecated and ignored" % str(include_deletes),
732 category=DeprecationWarning,
733 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000734 return filter(lambda x: x.IsTextFile(),
735 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736
737 def LocalPaths(self, include_dirs=False):
738 """Convenience function."""
739 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
740
741 def AbsoluteLocalPaths(self, include_dirs=False):
742 """Convenience function."""
743 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
744
745 def ServerPaths(self, include_dirs=False):
746 """Convenience function."""
747 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
748
749 def RightHandSideLines(self):
750 """An iterator over all text lines in "new" version of changed files.
751
752 Lists lines from new or modified text files in the change.
753
754 This is useful for doing line-by-line regex checks, like checking for
755 trailing whitespace.
756
757 Yields:
758 a 3 tuple:
759 the AffectedFile instance of the current file;
760 integer line number (1-based); and
761 the contents of the line as a string.
762 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000763 return _RightHandSideLinesImpl(
764 x for x in self.AffectedFiles(include_deletes=False)
765 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000766
767
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000768class SvnChange(Change):
769 _AFFECTED_FILES = SvnAffectedFile
770
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000771 def __init__(self, *args, **kwargs):
772 Change.__init__(self, *args, **kwargs)
773 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000774 self._changelists = None
775
776 def _GetChangeLists(self):
777 """Get all change lists."""
778 if self._changelists == None:
779 previous_cwd = os.getcwd()
780 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000781 # Need to import here to avoid circular dependency.
782 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000783 self._changelists = gcl.GetModifiedFiles()
784 os.chdir(previous_cwd)
785 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000786
787 def GetAllModifiedFiles(self):
788 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000789 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000790 all_modified_files = []
791 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000792 all_modified_files.extend(
793 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000794 return all_modified_files
795
796 def GetModifiedFiles(self):
797 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000798 changelists = self._GetChangeLists()
799 return [os.path.join(self.RepositoryRoot(), f[1])
800 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000801
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000802
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000803class GitChange(Change):
804 _AFFECTED_FILES = GitAffectedFile
805
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000806 def __init__(self, *args, **kwargs):
807 Change.__init__(self, *args, **kwargs)
808 self.scm = 'git'
809
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000810
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000811def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000812 """Finds all presubmit files that apply to a given set of source files.
813
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000814 If inherit-review-settings-ok is present right under root, looks for
815 PRESUBMIT.py in directories enclosing root.
816
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817 Args:
818 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000819 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820
821 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000822 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000824 files = [normpath(os.path.join(root, f)) for f in files]
825
826 # List all the individual directories containing files.
827 directories = set([os.path.dirname(f) for f in files])
828
829 # Ignore root if inherit-review-settings-ok is present.
830 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
831 root = None
832
833 # Collect all unique directories that may contain PRESUBMIT.py.
834 candidates = set()
835 for directory in directories:
836 while True:
837 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000839 candidates.add(directory)
840 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000841 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000842 parent_dir = os.path.dirname(directory)
843 if parent_dir == directory:
844 # We hit the system root directory.
845 break
846 directory = parent_dir
847
848 # Look for PRESUBMIT.py in all candidate directories.
849 results = []
850 for directory in sorted(list(candidates)):
851 p = os.path.join(directory, 'PRESUBMIT.py')
852 if os.path.isfile(p):
853 results.append(p)
854
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000855 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000856 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000857
858
thestig@chromium.orgde243452009-10-06 21:02:56 +0000859class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000860 @staticmethod
861 def ExecPresubmitScript(script_text):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000862 """Executes GetPreferredTrySlaves() from a single presubmit script.
863
864 Args:
865 script_text: The text of the presubmit script.
866
867 Return:
868 A list of try slaves.
869 """
870 context = {}
871 exec script_text in context
872
873 function_name = 'GetPreferredTrySlaves'
874 if function_name in context:
875 result = eval(function_name + '()', context)
876 if not isinstance(result, types.ListType):
877 raise exceptions.RuntimeError(
878 'Presubmit functions must return a list, got a %s instead: %s' %
879 (type(result), str(result)))
880 for item in result:
881 if not isinstance(item, basestring):
882 raise exceptions.RuntimeError('All try slaves names must be strings.')
883 if item != item.strip():
884 raise exceptions.RuntimeError('Try slave names cannot start/end'
885 'with whitespace')
886 else:
887 result = []
888 return result
889
890
891def DoGetTrySlaves(changed_files,
892 repository_root,
893 default_presubmit,
894 verbose,
895 output_stream):
896 """Get the list of try servers from the presubmit scripts.
897
898 Args:
899 changed_files: List of modified files.
900 repository_root: The repository root.
901 default_presubmit: A default presubmit script to execute in any case.
902 verbose: Prints debug info.
903 output_stream: A stream to write debug output to.
904
905 Return:
906 List of try slaves
907 """
908 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
909 if not presubmit_files and verbose:
910 output_stream.write("Warning, no presubmit.py found.\n")
911 results = []
912 executer = GetTrySlavesExecuter()
913 if default_presubmit:
914 if verbose:
915 output_stream.write("Running default presubmit script.\n")
916 results += executer.ExecPresubmitScript(default_presubmit)
917 for filename in presubmit_files:
918 filename = os.path.abspath(filename)
919 if verbose:
920 output_stream.write("Running %s\n" % filename)
921 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000922 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000923 results += executer.ExecPresubmitScript(presubmit_script)
924
925 slaves = list(set(results))
926 if slaves and verbose:
927 output_stream.write(', '.join(slaves))
928 output_stream.write('\n')
929 return slaves
930
931
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000932class PresubmitExecuter(object):
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000933 def __init__(self, change, committing, tbr, host_url):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000934 """
935 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000936 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000937 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000938 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
939 host_url: scheme, host, and path of rietveld instance
940 (or None for default)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000942 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000943 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000944 self.tbr = tbr
945 self.host_url = host_url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000946
947 def ExecPresubmitScript(self, script_text, presubmit_path):
948 """Executes a single presubmit script.
949
950 Args:
951 script_text: The text of the presubmit script.
952 presubmit_path: The path to the presubmit file (this will be reported via
953 input_api.PresubmitLocalPath()).
954
955 Return:
956 A list of result objects, empty if no problems.
957 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000958
959 # Change to the presubmit file's directory to support local imports.
960 main_path = os.getcwd()
961 os.chdir(os.path.dirname(presubmit_path))
962
963 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000964 input_api = InputApi(self.change, presubmit_path, self.committing,
965 self.tbr, self.host_url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000966 context = {}
967 exec script_text in context
968
969 # These function names must change if we make substantial changes to
970 # the presubmit API that are not backwards compatible.
971 if self.committing:
972 function_name = 'CheckChangeOnCommit'
973 else:
974 function_name = 'CheckChangeOnUpload'
975 if function_name in context:
976 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000977 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000979 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980 if not (isinstance(result, types.TupleType) or
981 isinstance(result, types.ListType)):
982 raise exceptions.RuntimeError(
983 'Presubmit functions must return a tuple or list')
984 for item in result:
985 if not isinstance(item, OutputApi.PresubmitResult):
986 raise exceptions.RuntimeError(
987 'All presubmit results must be of types derived from '
988 'output_api.PresubmitResult')
989 else:
990 result = () # no error since the script doesn't care about current event.
991
chase@chromium.org8e416c82009-10-06 04:30:44 +0000992 # Return the process to the original working directory.
993 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000994 return result
995
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000996# TODO(dpranke): make all callers pass in tbr, host_url?
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000997def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 committing,
999 verbose,
1000 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001001 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001002 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001003 may_prompt,
1004 tbr=False,
1005 host_url=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 """Runs all presubmit checks that apply to the files in the change.
1007
1008 This finds all PRESUBMIT.py files in directories enclosing the files in the
1009 change (up to the repository root) and calls the relevant entrypoint function
1010 depending on whether the change is being committed or uploaded.
1011
1012 Prints errors, warnings and notifications. Prompts the user for warnings
1013 when needed.
1014
1015 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001016 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001017 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1018 verbose: Prints debug info.
1019 output_stream: A stream to write output from presubmit tests to.
1020 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001021 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001022 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001023 tbr: was --tbr specified to skip any reviewer/owner checks?
1024 host_url: scheme, host, and port of host to use for rietveld-related
1025 checks
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001026
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001027 Warning:
1028 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1029 SHOULD be sys.stdin.
1030
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001031 Return:
1032 True if execution can continue, False if not.
1033 """
maruel@chromium.org8d195232010-10-05 12:58:49 +00001034 print "Running presubmit hooks..."
jam@chromium.org2a891dc2009-08-20 20:33:37 +00001035 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001036 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
1037 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001039 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001040 results = []
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001041 executer = PresubmitExecuter(change, committing, tbr, host_url)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001042 if default_presubmit:
1043 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001044 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001045 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001046 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +00001048 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001049 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001050 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +00001051 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001052 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001053 results += executer.ExecPresubmitScript(presubmit_script, filename)
1054
1055 errors = []
1056 notifications = []
1057 warnings = []
1058 for result in results:
1059 if not result.IsFatal() and not result.ShouldPrompt():
1060 notifications.append(result)
1061 elif result.ShouldPrompt():
1062 warnings.append(result)
1063 else:
1064 errors.append(result)
1065
1066 error_count = 0
1067 for name, items in (('Messages', notifications),
1068 ('Warnings', warnings),
1069 ('ERRORS', errors)):
1070 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001071 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001072 for item in items:
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +00001073 # Access to a protected member XXX of a client class
1074 # pylint: disable=W0212
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001075 if not item._Handle(output_stream, input_stream,
1076 may_prompt=False):
1077 error_count += 1
1078 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001079
1080 total_time = time.time() - start_time
1081 if total_time > 1.0:
1082 print "Presubmit checks took %.1fs to calculate." % total_time
1083
maruel@chromium.org07bbc212009-06-11 02:08:41 +00001084 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +00001085 if not PromptYesNo(input_stream, output_stream,
1086 'There were presubmit warnings. '
1087 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001088 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001089
1090 global _ASKED_FOR_FEEDBACK
1091 # Ask for feedback one time out of 5.
1092 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1093 output_stream.write("Was the presubmit check useful? Please send feedback "
1094 "& hate mail to maruel@chromium.org!\n")
1095 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001096 return (error_count == 0)
1097
1098
1099def ScanSubDirs(mask, recursive):
1100 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001101 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 +00001102 else:
1103 results = []
1104 for root, dirs, files in os.walk('.'):
1105 if '.svn' in dirs:
1106 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001107 if '.git' in dirs:
1108 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001109 for name in files:
1110 if fnmatch.fnmatch(name, mask):
1111 results.append(os.path.join(root, name))
1112 return results
1113
1114
1115def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001116 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001117 files = []
1118 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001119 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001120 return files
1121
1122
1123def Main(argv):
1124 parser = optparse.OptionParser(usage="%prog [options]",
1125 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001126 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001127 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001128 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1129 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001130 parser.add_option("-r", "--recursive", action="store_true",
1131 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001132 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001133 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001134 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001135 parser.add_option("--name", default='no name')
1136 parser.add_option("--description", default='')
1137 parser.add_option("--issue", type='int', default=0)
1138 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001139 parser.add_option("--root", default=os.getcwd(),
1140 help="Search for PRESUBMIT.py up to this directory. "
1141 "If inherit-review-settings-ok is present in this "
1142 "directory, parent directories up to the root file "
1143 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001144 parser.add_option("--default_presubmit")
1145 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001146 options, args = parser.parse_args(argv[1:])
maruel@chromium.org7444c502011-02-09 14:02:11 +00001147 if options.verbose:
1148 logging.basicConfig(level=logging.DEBUG)
1149 if os.path.isdir(os.path.join(options.root, '.svn')):
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001150 change_class = SvnChange
1151 if not options.files:
1152 if args:
1153 options.files = ParseFiles(args, options.recursive)
1154 else:
1155 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001156 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001157 else:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001158 is_git = os.path.isdir(os.path.join(options.root, '.git'))
1159 if not is_git:
1160 is_git = (0 == subprocess.call(
1161 ['git', 'rev-parse', '--show-cdup'],
1162 stdout=subprocess.PIPE, cwd=options.root))
1163 if is_git:
1164 # Only look at the subdirectories below cwd.
1165 change_class = GitChange
1166 if not options.files:
1167 if args:
1168 options.files = ParseFiles(args, options.recursive)
1169 else:
1170 # Grab modified files.
1171 options.files = scm.GIT.CaptureStatus([options.root])
1172 else:
1173 logging.info('Doesn\'t seem under source control.')
1174 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001175 if options.verbose:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001176 if not options.files:
1177 print "Found no files."
1178 elif len(options.files) != 1:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001179 print "Found %d files." % len(options.files)
1180 else:
1181 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001182 return not DoPresubmitChecks(change_class(options.name,
1183 options.description,
1184 options.root,
1185 options.files,
1186 options.issue,
1187 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001188 options.commit,
1189 options.verbose,
1190 sys.stdout,
1191 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001192 options.default_presubmit,
1193 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001194
1195
1196if __name__ == '__main__':
1197 sys.exit(Main(sys.argv))