blob: 4c12912dc71e170672749107d95b3e5cd57112b5 [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.orgff5a87a2011-03-10 21:32:48 +0000157 def IsMessage(self):
158 """Whether this result contains anything needing to be displayed."""
159 return True
160
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000161 class PresubmitAddText(PresubmitResult):
162 """Propagates a line of text back to the caller."""
163 def __init__(self, message, items=None, long_text=''):
164 super(OutputApi.PresubmitAddText, self).__init__("ADD: " + message,
165 items, long_text)
166
dpranke@chromium.orgff5a87a2011-03-10 21:32:48 +0000167 def IsMessage(self):
168 return False
169
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000170 class PresubmitError(PresubmitResult):
171 """A hard presubmit error."""
172 def IsFatal(self):
173 return True
174
175 class PresubmitPromptWarning(PresubmitResult):
176 """An warning that prompts the user if they want to continue."""
177 def ShouldPrompt(self):
178 return True
179
180 class PresubmitNotifyResult(PresubmitResult):
181 """Just print something to the screen -- but it's not even a warning."""
182 pass
183
184 class MailTextResult(PresubmitResult):
185 """A warning that should be included in the review request email."""
186 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000187 super(OutputApi.MailTextResult, self).__init__()
188 raise NotImplementedException()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000189
190
191class InputApi(object):
192 """An instance of this object is passed to presubmit scripts so they can
193 know stuff about the change they're looking at.
194 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000195 # Method could be a function
196 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000197
maruel@chromium.org3410d912009-06-09 20:56:16 +0000198 # File extensions that are considered source files from a style guide
199 # perspective. Don't modify this list from a presubmit script!
200 DEFAULT_WHITE_LIST = (
201 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000202 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000203 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000204 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000205 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000206 # No extension at all, note that ALL CAPS files are black listed in
207 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000208 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000209 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000210 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000211 )
212
213 # Path regexp that should be excluded from being considered containing source
214 # files. Don't modify this list from a presubmit script!
215 DEFAULT_BLACK_LIST = (
216 r".*\bexperimental[\\\/].*",
217 r".*\bthird_party[\\\/].*",
218 # Output directories (just in case)
219 r".*\bDebug[\\\/].*",
220 r".*\bRelease[\\\/].*",
221 r".*\bxcodebuild[\\\/].*",
222 r".*\bsconsbuild[\\\/].*",
223 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000224 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000225 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000226 r"(|.*[\\\/])\.git[\\\/].*",
227 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000228 )
229
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000230 # TODO(dpranke): Update callers to pass in tbr, host_url, remove
231 # default arguments.
232 def __init__(self, change, presubmit_path, is_committing, tbr=False,
233 host_url='http://codereview.chromium.org'):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000234 """Builds an InputApi object.
235
236 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000237 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000238 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000239 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000240 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000241 # Version number of the presubmit_support script.
242 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000243 self.change = change
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000244 self.host_url = host_url
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000245 self.is_committing = is_committing
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000246 self.tbr = tbr
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000247
248 # We expose various modules and functions as attributes of the input_api
249 # so that presubmit scripts don't have to import them.
250 self.basename = os.path.basename
251 self.cPickle = cPickle
252 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000253 self.json = json
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000254 self.os_path = os.path
255 self.pickle = pickle
256 self.marshal = marshal
257 self.re = re
258 self.subprocess = subprocess
259 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000260 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000261 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000262 self.urllib2 = urllib2
263
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000264 # To easily fork python.
265 self.python_executable = sys.executable
266 self.environ = os.environ
267
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268 # InputApi.platform is the platform you're currently running on.
269 self.platform = sys.platform
270
271 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000272 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273
274 # We carry the canned checks so presubmit scripts can easily use them.
275 self.canned_checks = presubmit_canned_checks
276
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000277 # TODO(dpranke): figure out a list of all approved owners for a repo
278 # in order to be able to handle wildcard OWNERS files?
279 self.owners_db = owners.Database(change.RepositoryRoot(),
280 fopen=file, os_path=self.os_path)
281
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282 def PresubmitLocalPath(self):
283 """Returns the local path of the presubmit script currently being run.
284
285 This is useful if you don't want to hard-code absolute paths in the
286 presubmit script. For example, It can be used to find another file
287 relative to the PRESUBMIT.py script, so the whole tree can be branched and
288 the presubmit script still works, without editing its content.
289 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000290 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000291
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000292 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000293 """Translate a depot path to a local path (relative to client root).
294
295 Args:
296 Depot path as a string.
297
298 Returns:
299 The local path of the depot path under the user's current client, or None
300 if the file is not mapped.
301
302 Remember to check for the None case and show an appropriate error!
303 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000304 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000305 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000306 return local_path
307
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000308 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000309 """Translate a local path to a depot path.
310
311 Args:
312 Local path (relative to current directory, or absolute) as a string.
313
314 Returns:
315 The depot path (SVN URL) of the file if mapped, otherwise None.
316 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000317 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000318 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000319 return depot_path
320
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000321 def AffectedFiles(self, include_dirs=False, include_deletes=True):
322 """Same as input_api.change.AffectedFiles() except only lists files
323 (and optionally directories) in the same directory as the current presubmit
324 script, or subdirectories thereof.
325 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000326 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000327 if len(dir_with_slash) == 1:
328 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000329 return filter(
330 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
331 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000332
333 def LocalPaths(self, include_dirs=False):
334 """Returns local paths of input_api.AffectedFiles()."""
335 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
336
337 def AbsoluteLocalPaths(self, include_dirs=False):
338 """Returns absolute local paths of input_api.AffectedFiles()."""
339 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
340
341 def ServerPaths(self, include_dirs=False):
342 """Returns server paths of input_api.AffectedFiles()."""
343 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
344
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000345 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000346 """Same as input_api.change.AffectedTextFiles() except only lists files
347 in the same directory as the current presubmit script, or subdirectories
348 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000350 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000351 warn("AffectedTextFiles(include_deletes=%s)"
352 " is deprecated and ignored" % str(include_deletes),
353 category=DeprecationWarning,
354 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000355 return filter(lambda x: x.IsTextFile(),
356 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357
maruel@chromium.org3410d912009-06-09 20:56:16 +0000358 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
359 """Filters out files that aren't considered "source file".
360
361 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
362 and InputApi.DEFAULT_BLACK_LIST is used respectively.
363
364 The lists will be compiled as regular expression and
365 AffectedFile.LocalPath() needs to pass both list.
366
367 Note: Copy-paste this function to suit your needs or use a lambda function.
368 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000369 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000370 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000371 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000372 if self.re.match(item, local_path):
373 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000374 return True
375 return False
376 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
377 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
378
379 def AffectedSourceFiles(self, source_file):
380 """Filter the list of AffectedTextFiles by the function source_file.
381
382 If source_file is None, InputApi.FilterSourceFile() is used.
383 """
384 if not source_file:
385 source_file = self.FilterSourceFile
386 return filter(source_file, self.AffectedTextFiles())
387
388 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000389 """An iterator over all text lines in "new" version of changed files.
390
391 Only lists lines from new or modified text files in the change that are
392 contained by the directory of the currently executing presubmit script.
393
394 This is useful for doing line-by-line regex checks, like checking for
395 trailing whitespace.
396
397 Yields:
398 a 3 tuple:
399 the AffectedFile instance of the current file;
400 integer line number (1-based); and
401 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000402
403 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000404 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000405 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000406 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000408 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000409 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000410
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000411 Deny reading anything outside the repository.
412 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000413 if isinstance(file_item, AffectedFile):
414 file_item = file_item.AbsoluteLocalPath()
415 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000416 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000417 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000418
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000419
420class AffectedFile(object):
421 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000422 # Method could be a function
423 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000424 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000425 self._path = path
426 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000427 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000428 self._is_directory = None
429 self._properties = {}
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000430 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431
432 def ServerPath(self):
433 """Returns a path string that identifies the file in the SCM system.
434
435 Returns the empty string if the file does not exist in SCM.
436 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000437 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438
439 def LocalPath(self):
440 """Returns the path of this file on the local disk relative to client root.
441 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000442 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000443
444 def AbsoluteLocalPath(self):
445 """Returns the absolute path of this file on the local disk.
446 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000447 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448
449 def IsDirectory(self):
450 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000451 if self._is_directory is None:
452 path = self.AbsoluteLocalPath()
453 self._is_directory = (os.path.exists(path) and
454 os.path.isdir(path))
455 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000456
457 def Action(self):
458 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000459 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
460 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000461 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000462
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000463 def Property(self, property_name):
464 """Returns the specified SCM property of this file, or None if no such
465 property.
466 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000467 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000468
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000469 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000470 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000471
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000472 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000473 raise NotImplementedError() # Implement when needed
474
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000475 def NewContents(self):
476 """Returns an iterator over the lines in the new version of file.
477
478 The new version is the file in the user's workspace, i.e. the "right hand
479 side".
480
481 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000482 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000483 """
484 if self.IsDirectory():
485 return []
486 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000487 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
488 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000489
490 def OldContents(self):
491 """Returns an iterator over the lines in the old version of file.
492
493 The old version is the file in depot, i.e. the "left hand side".
494 """
495 raise NotImplementedError() # Implement when needed
496
497 def OldFileTempPath(self):
498 """Returns the path on local disk where the old contents resides.
499
500 The old version is the file in depot, i.e. the "left hand side".
501 This is a read-only cached copy of the old contents. *DO NOT* try to
502 modify this file.
503 """
504 raise NotImplementedError() # Implement if/when needed.
505
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000506 def ChangedContents(self):
507 """Returns a list of tuples (line number, line text) of all new lines.
508
509 This relies on the scm diff output describing each changed code section
510 with a line of the form
511
512 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
513 """
514 new_lines = []
515 line_num = 0
516
517 if self.IsDirectory():
518 return []
519
520 for line in self.GenerateScmDiff().splitlines():
521 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
522 if m:
523 line_num = int(m.groups(1)[0])
524 continue
525 if line.startswith('+') and not line.startswith('++'):
526 new_lines.append((line_num, line[1:]))
527 if not line.startswith('-'):
528 line_num += 1
529 return new_lines
530
maruel@chromium.org5de13972009-06-10 18:16:06 +0000531 def __str__(self):
532 return self.LocalPath()
533
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000534 def GenerateScmDiff(self):
535 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000536
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000537class SvnAffectedFile(AffectedFile):
538 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000539 # Method 'NNN' is abstract in class 'NNN' but is not overridden
540 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000541
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000542 def __init__(self, *args, **kwargs):
543 AffectedFile.__init__(self, *args, **kwargs)
544 self._server_path = None
545 self._is_text_file = None
546
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000547 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000548 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000549 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000550 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000551 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000552
553 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000554 if self._is_directory is None:
555 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000556 if os.path.exists(path):
557 # Retrieve directly from the file system; it is much faster than
558 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000559 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000560 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000561 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000562 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000563 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000564
565 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000566 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000567 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000568 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000569 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000570
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000571 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000572 if self._is_text_file is None:
573 if self.Action() == 'D':
574 # A deleted file is not a text file.
575 self._is_text_file = False
576 elif self.IsDirectory():
577 self._is_text_file = False
578 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000579 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
580 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000581 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
582 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000583
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000584 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000585 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
586
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000587
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000588class GitAffectedFile(AffectedFile):
589 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000590 # Method 'NNN' is abstract in class 'NNN' but is not overridden
591 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000592
593 def __init__(self, *args, **kwargs):
594 AffectedFile.__init__(self, *args, **kwargs)
595 self._server_path = None
596 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000597
598 def ServerPath(self):
599 if self._server_path is None:
600 raise NotImplementedException() # TODO(maruel) Implement.
601 return self._server_path
602
603 def IsDirectory(self):
604 if self._is_directory is None:
605 path = self.AbsoluteLocalPath()
606 if os.path.exists(path):
607 # Retrieve directly from the file system; it is much faster than
608 # querying subversion, especially on Windows.
609 self._is_directory = os.path.isdir(path)
610 else:
611 # raise NotImplementedException() # TODO(maruel) Implement.
612 self._is_directory = False
613 return self._is_directory
614
615 def Property(self, property_name):
616 if not property_name in self._properties:
617 raise NotImplementedException() # TODO(maruel) Implement.
618 return self._properties[property_name]
619
620 def IsTextFile(self):
621 if self._is_text_file is None:
622 if self.Action() == 'D':
623 # A deleted file is not a text file.
624 self._is_text_file = False
625 elif self.IsDirectory():
626 self._is_text_file = False
627 else:
628 # raise NotImplementedException() # TODO(maruel) Implement.
629 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
630 return self._is_text_file
631
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000632 def GenerateScmDiff(self):
633 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000634
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000635class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000636 """Describe a change.
637
638 Used directly by the presubmit scripts to query the current change being
639 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000640
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000641 Instance members:
642 tags: Dictionnary of KEY=VALUE pairs found in the change description.
643 self.KEY: equivalent to tags['KEY']
644 """
645
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000646 _AFFECTED_FILES = AffectedFile
647
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000648 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000649 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000650 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000651
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000652 def __init__(self, name, description, local_root, files, issue, patchset):
653 if files is None:
654 files = []
655 self._name = name
656 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000657 # Convert root into an absolute path.
658 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000659 self.issue = issue
660 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000661 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000662
663 # From the description text, build up a dictionary of key/value pairs
664 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000665 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000666 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000667 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000668 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000669 if m:
670 self.tags[m.group('key')] = m.group('value')
671 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000672 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000673
674 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000675 self._description_without_tags = (
676 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000677
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000678 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000679 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
680 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000681 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000683 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000685 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000686
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687 def DescriptionText(self):
688 """Returns the user-entered changelist description, minus tags.
689
690 Any line in the user-provided description starting with e.g. "FOO="
691 (whitespace permitted before and around) is considered a tag line. Such
692 lines are stripped out of the description this function returns.
693 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000694 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000695
696 def FullDescriptionText(self):
697 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000698 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000699
700 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000701 """Returns the repository (checkout) root directory for this change,
702 as an absolute path.
703 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000704 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000705
706 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000707 """Return tags directly as attributes on the object."""
708 if not re.match(r"^[A-Z_]*$", attr):
709 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000710 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000711
712 def AffectedFiles(self, include_dirs=False, include_deletes=True):
713 """Returns a list of AffectedFile instances for all files in the change.
714
715 Args:
716 include_deletes: If false, deleted files will be filtered out.
717 include_dirs: True to include directories in the list
718
719 Returns:
720 [AffectedFile(path, action), AffectedFile(path, action)]
721 """
722 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000723 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000724 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000725 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000726
727 if include_deletes:
728 return affected
729 else:
730 return filter(lambda x: x.Action() != 'D', affected)
731
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000732 def AffectedTextFiles(self, include_deletes=None):
733 """Return a list of the existing text files in a change."""
734 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000735 warn("AffectedTextFiles(include_deletes=%s)"
736 " is deprecated and ignored" % str(include_deletes),
737 category=DeprecationWarning,
738 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000739 return filter(lambda x: x.IsTextFile(),
740 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741
742 def LocalPaths(self, include_dirs=False):
743 """Convenience function."""
744 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
745
746 def AbsoluteLocalPaths(self, include_dirs=False):
747 """Convenience function."""
748 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
749
750 def ServerPaths(self, include_dirs=False):
751 """Convenience function."""
752 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
753
754 def RightHandSideLines(self):
755 """An iterator over all text lines in "new" version of changed files.
756
757 Lists lines from new or modified text files in the change.
758
759 This is useful for doing line-by-line regex checks, like checking for
760 trailing whitespace.
761
762 Yields:
763 a 3 tuple:
764 the AffectedFile instance of the current file;
765 integer line number (1-based); and
766 the contents of the line as a string.
767 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000768 return _RightHandSideLinesImpl(
769 x for x in self.AffectedFiles(include_deletes=False)
770 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000771
772
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000773class SvnChange(Change):
774 _AFFECTED_FILES = SvnAffectedFile
775
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000776 def __init__(self, *args, **kwargs):
777 Change.__init__(self, *args, **kwargs)
778 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000779 self._changelists = None
780
781 def _GetChangeLists(self):
782 """Get all change lists."""
783 if self._changelists == None:
784 previous_cwd = os.getcwd()
785 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000786 # Need to import here to avoid circular dependency.
787 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000788 self._changelists = gcl.GetModifiedFiles()
789 os.chdir(previous_cwd)
790 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000791
792 def GetAllModifiedFiles(self):
793 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000794 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000795 all_modified_files = []
796 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000797 all_modified_files.extend(
798 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000799 return all_modified_files
800
801 def GetModifiedFiles(self):
802 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000803 changelists = self._GetChangeLists()
804 return [os.path.join(self.RepositoryRoot(), f[1])
805 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000806
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000807
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000808class GitChange(Change):
809 _AFFECTED_FILES = GitAffectedFile
810
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000811 def __init__(self, *args, **kwargs):
812 Change.__init__(self, *args, **kwargs)
813 self.scm = 'git'
814
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000815
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000816def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817 """Finds all presubmit files that apply to a given set of source files.
818
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000819 If inherit-review-settings-ok is present right under root, looks for
820 PRESUBMIT.py in directories enclosing root.
821
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000822 Args:
823 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000824 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825
826 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000827 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000828 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000829 files = [normpath(os.path.join(root, f)) for f in files]
830
831 # List all the individual directories containing files.
832 directories = set([os.path.dirname(f) for f in files])
833
834 # Ignore root if inherit-review-settings-ok is present.
835 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
836 root = None
837
838 # Collect all unique directories that may contain PRESUBMIT.py.
839 candidates = set()
840 for directory in directories:
841 while True:
842 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000844 candidates.add(directory)
845 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000846 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000847 parent_dir = os.path.dirname(directory)
848 if parent_dir == directory:
849 # We hit the system root directory.
850 break
851 directory = parent_dir
852
853 # Look for PRESUBMIT.py in all candidate directories.
854 results = []
855 for directory in sorted(list(candidates)):
856 p = os.path.join(directory, 'PRESUBMIT.py')
857 if os.path.isfile(p):
858 results.append(p)
859
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000860 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000861 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000862
863
thestig@chromium.orgde243452009-10-06 21:02:56 +0000864class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000865 @staticmethod
866 def ExecPresubmitScript(script_text):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000867 """Executes GetPreferredTrySlaves() from a single presubmit script.
868
869 Args:
870 script_text: The text of the presubmit script.
871
872 Return:
873 A list of try slaves.
874 """
875 context = {}
876 exec script_text in context
877
878 function_name = 'GetPreferredTrySlaves'
879 if function_name in context:
880 result = eval(function_name + '()', context)
881 if not isinstance(result, types.ListType):
882 raise exceptions.RuntimeError(
883 'Presubmit functions must return a list, got a %s instead: %s' %
884 (type(result), str(result)))
885 for item in result:
886 if not isinstance(item, basestring):
887 raise exceptions.RuntimeError('All try slaves names must be strings.')
888 if item != item.strip():
889 raise exceptions.RuntimeError('Try slave names cannot start/end'
890 'with whitespace')
891 else:
892 result = []
893 return result
894
895
896def DoGetTrySlaves(changed_files,
897 repository_root,
898 default_presubmit,
899 verbose,
900 output_stream):
901 """Get the list of try servers from the presubmit scripts.
902
903 Args:
904 changed_files: List of modified files.
905 repository_root: The repository root.
906 default_presubmit: A default presubmit script to execute in any case.
907 verbose: Prints debug info.
908 output_stream: A stream to write debug output to.
909
910 Return:
911 List of try slaves
912 """
913 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
914 if not presubmit_files and verbose:
915 output_stream.write("Warning, no presubmit.py found.\n")
916 results = []
917 executer = GetTrySlavesExecuter()
918 if default_presubmit:
919 if verbose:
920 output_stream.write("Running default presubmit script.\n")
921 results += executer.ExecPresubmitScript(default_presubmit)
922 for filename in presubmit_files:
923 filename = os.path.abspath(filename)
924 if verbose:
925 output_stream.write("Running %s\n" % filename)
926 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000927 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000928 results += executer.ExecPresubmitScript(presubmit_script)
929
930 slaves = list(set(results))
931 if slaves and verbose:
932 output_stream.write(', '.join(slaves))
933 output_stream.write('\n')
934 return slaves
935
936
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000937class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000938 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000939 """
940 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000941 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000942 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
943 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000944 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000945 self.committing = committing
946
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.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000964 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000965 context = {}
966 exec script_text in context
967
968 # These function names must change if we make substantial changes to
969 # the presubmit API that are not backwards compatible.
970 if self.committing:
971 function_name = 'CheckChangeOnCommit'
972 else:
973 function_name = 'CheckChangeOnUpload'
974 if function_name in context:
975 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000976 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000977 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000978 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979 if not (isinstance(result, types.TupleType) or
980 isinstance(result, types.ListType)):
981 raise exceptions.RuntimeError(
982 'Presubmit functions must return a tuple or list')
983 for item in result:
984 if not isinstance(item, OutputApi.PresubmitResult):
985 raise exceptions.RuntimeError(
986 'All presubmit results must be of types derived from '
987 'output_api.PresubmitResult')
988 else:
989 result = () # no error since the script doesn't care about current event.
990
chase@chromium.org8e416c82009-10-06 04:30:44 +0000991 # Return the process to the original working directory.
992 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000993 return result
994
995
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000996def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000997 committing,
998 verbose,
999 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001000 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001001 default_presubmit,
1002 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001003 """Runs all presubmit checks that apply to the files in the change.
1004
1005 This finds all PRESUBMIT.py files in directories enclosing the files in the
1006 change (up to the repository root) and calls the relevant entrypoint function
1007 depending on whether the change is being committed or uploaded.
1008
1009 Prints errors, warnings and notifications. Prompts the user for warnings
1010 when needed.
1011
1012 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001013 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001014 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1015 verbose: Prints debug info.
1016 output_stream: A stream to write output from presubmit tests to.
1017 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001018 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001019 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001020
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001021 Warning:
1022 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1023 SHOULD be sys.stdin.
1024
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001025 Return:
1026 True if execution can continue, False if not.
1027 """
maruel@chromium.org8d195232010-10-05 12:58:49 +00001028 print "Running presubmit hooks..."
jam@chromium.org2a891dc2009-08-20 20:33:37 +00001029 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001030 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
1031 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001032 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001033 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001034 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001035 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001036 if default_presubmit:
1037 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001038 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001039 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001040 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001041 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +00001042 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001043 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001044 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +00001045 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001046 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047 results += executer.ExecPresubmitScript(presubmit_script, filename)
1048
1049 errors = []
1050 notifications = []
1051 warnings = []
1052 for result in results:
1053 if not result.IsFatal() and not result.ShouldPrompt():
1054 notifications.append(result)
1055 elif result.ShouldPrompt():
1056 warnings.append(result)
1057 else:
1058 errors.append(result)
1059
1060 error_count = 0
1061 for name, items in (('Messages', notifications),
1062 ('Warnings', warnings),
1063 ('ERRORS', errors)):
1064 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001065 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001066 for item in items:
dpranke@chromium.orgff5a87a2011-03-10 21:32:48 +00001067 if not item.IsMessage():
1068 continue
1069
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +00001070 # Access to a protected member XXX of a client class
1071 # pylint: disable=W0212
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001072 if not item._Handle(output_stream, input_stream,
1073 may_prompt=False):
1074 error_count += 1
1075 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001076
1077 total_time = time.time() - start_time
1078 if total_time > 1.0:
1079 print "Presubmit checks took %.1fs to calculate." % total_time
1080
maruel@chromium.org07bbc212009-06-11 02:08:41 +00001081 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +00001082 if not PromptYesNo(input_stream, output_stream,
1083 'There were presubmit warnings. '
1084 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001085 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001086
1087 global _ASKED_FOR_FEEDBACK
1088 # Ask for feedback one time out of 5.
1089 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1090 output_stream.write("Was the presubmit check useful? Please send feedback "
1091 "& hate mail to maruel@chromium.org!\n")
1092 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001093 return (error_count == 0)
1094
1095
1096def ScanSubDirs(mask, recursive):
1097 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001098 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 +00001099 else:
1100 results = []
1101 for root, dirs, files in os.walk('.'):
1102 if '.svn' in dirs:
1103 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001104 if '.git' in dirs:
1105 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001106 for name in files:
1107 if fnmatch.fnmatch(name, mask):
1108 results.append(os.path.join(root, name))
1109 return results
1110
1111
1112def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001113 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001114 files = []
1115 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001116 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001117 return files
1118
1119
1120def Main(argv):
1121 parser = optparse.OptionParser(usage="%prog [options]",
1122 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001123 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001124 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001125 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1126 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001127 parser.add_option("-r", "--recursive", action="store_true",
1128 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001129 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001130 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001131 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001132 parser.add_option("--name", default='no name')
1133 parser.add_option("--description", default='')
1134 parser.add_option("--issue", type='int', default=0)
1135 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001136 parser.add_option("--root", default=os.getcwd(),
1137 help="Search for PRESUBMIT.py up to this directory. "
1138 "If inherit-review-settings-ok is present in this "
1139 "directory, parent directories up to the root file "
1140 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001141 parser.add_option("--default_presubmit")
1142 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001143 options, args = parser.parse_args(argv[1:])
maruel@chromium.org7444c502011-02-09 14:02:11 +00001144 if options.verbose:
1145 logging.basicConfig(level=logging.DEBUG)
1146 if os.path.isdir(os.path.join(options.root, '.svn')):
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001147 change_class = SvnChange
1148 if not options.files:
1149 if args:
1150 options.files = ParseFiles(args, options.recursive)
1151 else:
1152 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001153 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001154 else:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001155 is_git = os.path.isdir(os.path.join(options.root, '.git'))
1156 if not is_git:
1157 is_git = (0 == subprocess.call(
1158 ['git', 'rev-parse', '--show-cdup'],
1159 stdout=subprocess.PIPE, cwd=options.root))
1160 if is_git:
1161 # Only look at the subdirectories below cwd.
1162 change_class = GitChange
1163 if not options.files:
1164 if args:
1165 options.files = ParseFiles(args, options.recursive)
1166 else:
1167 # Grab modified files.
1168 options.files = scm.GIT.CaptureStatus([options.root])
1169 else:
1170 logging.info('Doesn\'t seem under source control.')
1171 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001172 if options.verbose:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001173 if not options.files:
1174 print "Found no files."
1175 elif len(options.files) != 1:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001176 print "Found %d files." % len(options.files)
1177 else:
1178 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001179 return not DoPresubmitChecks(change_class(options.name,
1180 options.description,
1181 options.root,
1182 options.files,
1183 options.issue,
1184 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001185 options.commit,
1186 options.verbose,
1187 sys.stdout,
1188 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001189 options.default_presubmit,
1190 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001191
1192
1193if __name__ == '__main__':
1194 sys.exit(Main(sys.argv))