blob: a62948d9fafbc39fdcfdb832fe6d7b01de73cb9f [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00002# Copyright (c) 2010 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00009__version__ = '1.3.5'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
15import cPickle # Exposed through the API.
16import cStringIO # Exposed through the API.
17import exceptions
18import fnmatch
19import glob
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000020import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import marshal # Exposed through the API.
22import optparse
23import os # Somewhat exposed through the API.
24import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000025import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import re # Exposed through the API.
27import subprocess # Exposed through the API.
28import sys # Parts exposed through API.
29import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000030import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000031import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000033import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000035from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000037try:
38 import simplejson as json
39except ImportError:
40 try:
41 import json
tony@chromium.org5e9f2ed2010-04-08 01:38:19 +000042 # Some versions of python2.5 have an incomplete json module. Check to make
43 # sure loads exists.
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +000044 # Statement seems to have no effect
45 # pylint: disable=W0104
tony@chromium.org5e9f2ed2010-04-08 01:38:19 +000046 json.loads
47 except (ImportError, AttributeError):
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000048 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000049 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
50 import simplejson as json
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000051
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052# Local imports.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000053import gclient_utils
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
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000223 def __init__(self, change, presubmit_path, is_committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000224 """Builds an InputApi object.
225
226 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000227 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000229 is_committing: True if the change is about to be committed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000231 # Version number of the presubmit_support script.
232 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000233 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000234 self.is_committing = is_committing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000235
236 # We expose various modules and functions as attributes of the input_api
237 # so that presubmit scripts don't have to import them.
238 self.basename = os.path.basename
239 self.cPickle = cPickle
240 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000241 self.json = json
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000242 self.os_path = os.path
243 self.pickle = pickle
244 self.marshal = marshal
245 self.re = re
246 self.subprocess = subprocess
247 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000248 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000249 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000250 self.urllib2 = urllib2
251
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000252 # To easily fork python.
253 self.python_executable = sys.executable
254 self.environ = os.environ
255
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256 # InputApi.platform is the platform you're currently running on.
257 self.platform = sys.platform
258
259 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000260 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000261
262 # We carry the canned checks so presubmit scripts can easily use them.
263 self.canned_checks = presubmit_canned_checks
264
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000265 # TODO(dpranke): figure out a list of all approved owners for a repo
266 # in order to be able to handle wildcard OWNERS files?
267 self.owners_db = owners.Database(change.RepositoryRoot(),
268 fopen=file, os_path=self.os_path)
269
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000270 def PresubmitLocalPath(self):
271 """Returns the local path of the presubmit script currently being run.
272
273 This is useful if you don't want to hard-code absolute paths in the
274 presubmit script. For example, It can be used to find another file
275 relative to the PRESUBMIT.py script, so the whole tree can be branched and
276 the presubmit script still works, without editing its content.
277 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000278 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000279
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000280 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000281 """Translate a depot path to a local path (relative to client root).
282
283 Args:
284 Depot path as a string.
285
286 Returns:
287 The local path of the depot path under the user's current client, or None
288 if the file is not mapped.
289
290 Remember to check for the None case and show an appropriate error!
291 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000292 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000293 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000294 return local_path
295
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000296 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000297 """Translate a local path to a depot path.
298
299 Args:
300 Local path (relative to current directory, or absolute) as a string.
301
302 Returns:
303 The depot path (SVN URL) of the file if mapped, otherwise None.
304 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000305 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000306 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000307 return depot_path
308
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000309 def AffectedFiles(self, include_dirs=False, include_deletes=True):
310 """Same as input_api.change.AffectedFiles() except only lists files
311 (and optionally directories) in the same directory as the current presubmit
312 script, or subdirectories thereof.
313 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000314 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000315 if len(dir_with_slash) == 1:
316 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000317 return filter(
318 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
319 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000320
321 def LocalPaths(self, include_dirs=False):
322 """Returns local paths of input_api.AffectedFiles()."""
323 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
324
325 def AbsoluteLocalPaths(self, include_dirs=False):
326 """Returns absolute local paths of input_api.AffectedFiles()."""
327 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
328
329 def ServerPaths(self, include_dirs=False):
330 """Returns server paths of input_api.AffectedFiles()."""
331 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
332
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000333 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000334 """Same as input_api.change.AffectedTextFiles() except only lists files
335 in the same directory as the current presubmit script, or subdirectories
336 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000337 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000338 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000339 warn("AffectedTextFiles(include_deletes=%s)"
340 " is deprecated and ignored" % str(include_deletes),
341 category=DeprecationWarning,
342 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000343 return filter(lambda x: x.IsTextFile(),
344 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345
maruel@chromium.org3410d912009-06-09 20:56:16 +0000346 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
347 """Filters out files that aren't considered "source file".
348
349 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
350 and InputApi.DEFAULT_BLACK_LIST is used respectively.
351
352 The lists will be compiled as regular expression and
353 AffectedFile.LocalPath() needs to pass both list.
354
355 Note: Copy-paste this function to suit your needs or use a lambda function.
356 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000357 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000358 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000359 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000360 if self.re.match(item, local_path):
361 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000362 return True
363 return False
364 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
365 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
366
367 def AffectedSourceFiles(self, source_file):
368 """Filter the list of AffectedTextFiles by the function source_file.
369
370 If source_file is None, InputApi.FilterSourceFile() is used.
371 """
372 if not source_file:
373 source_file = self.FilterSourceFile
374 return filter(source_file, self.AffectedTextFiles())
375
376 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000377 """An iterator over all text lines in "new" version of changed files.
378
379 Only lists lines from new or modified text files in the change that are
380 contained by the directory of the currently executing presubmit script.
381
382 This is useful for doing line-by-line regex checks, like checking for
383 trailing whitespace.
384
385 Yields:
386 a 3 tuple:
387 the AffectedFile instance of the current file;
388 integer line number (1-based); and
389 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000390
391 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000392 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000393 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000394 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000395
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000396 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000397 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000398
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000399 Deny reading anything outside the repository.
400 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000401 if isinstance(file_item, AffectedFile):
402 file_item = file_item.AbsoluteLocalPath()
403 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000404 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000405 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000406
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407
408class AffectedFile(object):
409 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000410 # Method could be a function
411 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000412 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000413 self._path = path
414 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000415 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000416 self._is_directory = None
417 self._properties = {}
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000418 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000419
420 def ServerPath(self):
421 """Returns a path string that identifies the file in the SCM system.
422
423 Returns the empty string if the file does not exist in SCM.
424 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000425 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000426
427 def LocalPath(self):
428 """Returns the path of this file on the local disk relative to client root.
429 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000430 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431
432 def AbsoluteLocalPath(self):
433 """Returns the absolute path of this file on the local disk.
434 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000435 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000436
437 def IsDirectory(self):
438 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000439 if self._is_directory is None:
440 path = self.AbsoluteLocalPath()
441 self._is_directory = (os.path.exists(path) and
442 os.path.isdir(path))
443 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000444
445 def Action(self):
446 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000447 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
448 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000449 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000450
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000451 def Property(self, property_name):
452 """Returns the specified SCM property of this file, or None if no such
453 property.
454 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000455 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000456
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000457 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000458 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000459
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000460 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000461 raise NotImplementedError() # Implement when needed
462
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000463 def NewContents(self):
464 """Returns an iterator over the lines in the new version of file.
465
466 The new version is the file in the user's workspace, i.e. the "right hand
467 side".
468
469 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000470 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000471 """
472 if self.IsDirectory():
473 return []
474 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000475 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
476 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000477
478 def OldContents(self):
479 """Returns an iterator over the lines in the old version of file.
480
481 The old version is the file in depot, i.e. the "left hand side".
482 """
483 raise NotImplementedError() # Implement when needed
484
485 def OldFileTempPath(self):
486 """Returns the path on local disk where the old contents resides.
487
488 The old version is the file in depot, i.e. the "left hand side".
489 This is a read-only cached copy of the old contents. *DO NOT* try to
490 modify this file.
491 """
492 raise NotImplementedError() # Implement if/when needed.
493
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000494 def ChangedContents(self):
495 """Returns a list of tuples (line number, line text) of all new lines.
496
497 This relies on the scm diff output describing each changed code section
498 with a line of the form
499
500 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
501 """
502 new_lines = []
503 line_num = 0
504
505 if self.IsDirectory():
506 return []
507
508 for line in self.GenerateScmDiff().splitlines():
509 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
510 if m:
511 line_num = int(m.groups(1)[0])
512 continue
513 if line.startswith('+') and not line.startswith('++'):
514 new_lines.append((line_num, line[1:]))
515 if not line.startswith('-'):
516 line_num += 1
517 return new_lines
518
maruel@chromium.org5de13972009-06-10 18:16:06 +0000519 def __str__(self):
520 return self.LocalPath()
521
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000522 def GenerateScmDiff(self):
523 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000524
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000525class SvnAffectedFile(AffectedFile):
526 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000527 # Method 'NNN' is abstract in class 'NNN' but is not overridden
528 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000529
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000530 def __init__(self, *args, **kwargs):
531 AffectedFile.__init__(self, *args, **kwargs)
532 self._server_path = None
533 self._is_text_file = None
534
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000535 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000536 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000537 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000538 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000539 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000540
541 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000542 if self._is_directory is None:
543 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000544 if os.path.exists(path):
545 # Retrieve directly from the file system; it is much faster than
546 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000547 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000548 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000549 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000550 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000551 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000552
553 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000554 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000555 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000556 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000557 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000558
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000559 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000560 if self._is_text_file is None:
561 if self.Action() == 'D':
562 # A deleted file is not a text file.
563 self._is_text_file = False
564 elif self.IsDirectory():
565 self._is_text_file = False
566 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000567 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
568 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000569 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
570 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000571
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000572 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000573 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
574
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000575
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000576class GitAffectedFile(AffectedFile):
577 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000578 # Method 'NNN' is abstract in class 'NNN' but is not overridden
579 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000580
581 def __init__(self, *args, **kwargs):
582 AffectedFile.__init__(self, *args, **kwargs)
583 self._server_path = None
584 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000585
586 def ServerPath(self):
587 if self._server_path is None:
588 raise NotImplementedException() # TODO(maruel) Implement.
589 return self._server_path
590
591 def IsDirectory(self):
592 if self._is_directory is None:
593 path = self.AbsoluteLocalPath()
594 if os.path.exists(path):
595 # Retrieve directly from the file system; it is much faster than
596 # querying subversion, especially on Windows.
597 self._is_directory = os.path.isdir(path)
598 else:
599 # raise NotImplementedException() # TODO(maruel) Implement.
600 self._is_directory = False
601 return self._is_directory
602
603 def Property(self, property_name):
604 if not property_name in self._properties:
605 raise NotImplementedException() # TODO(maruel) Implement.
606 return self._properties[property_name]
607
608 def IsTextFile(self):
609 if self._is_text_file is None:
610 if self.Action() == 'D':
611 # A deleted file is not a text file.
612 self._is_text_file = False
613 elif self.IsDirectory():
614 self._is_text_file = False
615 else:
616 # raise NotImplementedException() # TODO(maruel) Implement.
617 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
618 return self._is_text_file
619
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000620 def GenerateScmDiff(self):
621 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000622
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000623class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000624 """Describe a change.
625
626 Used directly by the presubmit scripts to query the current change being
627 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000628
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000629 Instance members:
630 tags: Dictionnary of KEY=VALUE pairs found in the change description.
631 self.KEY: equivalent to tags['KEY']
632 """
633
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000634 _AFFECTED_FILES = AffectedFile
635
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000636 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000637 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000638 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000639
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000640 def __init__(self, name, description, local_root, files, issue, patchset):
641 if files is None:
642 files = []
643 self._name = name
644 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000645 # Convert root into an absolute path.
646 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000647 self.issue = issue
648 self.patchset = patchset
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000649
650 # TODO(dpranke): implement - get from the patchset?
651 self.approvers = set()
652
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000653 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000654
655 # From the description text, build up a dictionary of key/value pairs
656 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000657 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000658 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000659 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000660 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661 if m:
662 self.tags[m.group('key')] = m.group('value')
663 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000664 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000665
666 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000667 self._description_without_tags = (
668 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000669
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000670 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000671 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
672 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000673 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000675 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000677 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679 def DescriptionText(self):
680 """Returns the user-entered changelist description, minus tags.
681
682 Any line in the user-provided description starting with e.g. "FOO="
683 (whitespace permitted before and around) is considered a tag line. Such
684 lines are stripped out of the description this function returns.
685 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000686 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
688 def FullDescriptionText(self):
689 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000690 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000691
692 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000693 """Returns the repository (checkout) root directory for this change,
694 as an absolute path.
695 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000696 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697
698 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000699 """Return tags directly as attributes on the object."""
700 if not re.match(r"^[A-Z_]*$", attr):
701 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000702 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703
704 def AffectedFiles(self, include_dirs=False, include_deletes=True):
705 """Returns a list of AffectedFile instances for all files in the change.
706
707 Args:
708 include_deletes: If false, deleted files will be filtered out.
709 include_dirs: True to include directories in the list
710
711 Returns:
712 [AffectedFile(path, action), AffectedFile(path, action)]
713 """
714 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000715 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000716 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000717 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000718
719 if include_deletes:
720 return affected
721 else:
722 return filter(lambda x: x.Action() != 'D', affected)
723
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000724 def AffectedTextFiles(self, include_deletes=None):
725 """Return a list of the existing text files in a change."""
726 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000727 warn("AffectedTextFiles(include_deletes=%s)"
728 " is deprecated and ignored" % str(include_deletes),
729 category=DeprecationWarning,
730 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000731 return filter(lambda x: x.IsTextFile(),
732 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000733
734 def LocalPaths(self, include_dirs=False):
735 """Convenience function."""
736 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
737
738 def AbsoluteLocalPaths(self, include_dirs=False):
739 """Convenience function."""
740 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
741
742 def ServerPaths(self, include_dirs=False):
743 """Convenience function."""
744 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
745
746 def RightHandSideLines(self):
747 """An iterator over all text lines in "new" version of changed files.
748
749 Lists lines from new or modified text files in the change.
750
751 This is useful for doing line-by-line regex checks, like checking for
752 trailing whitespace.
753
754 Yields:
755 a 3 tuple:
756 the AffectedFile instance of the current file;
757 integer line number (1-based); and
758 the contents of the line as a string.
759 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000760 return _RightHandSideLinesImpl(
761 x for x in self.AffectedFiles(include_deletes=False)
762 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000763
764
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000765class SvnChange(Change):
766 _AFFECTED_FILES = SvnAffectedFile
767
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000768 def __init__(self, *args, **kwargs):
769 Change.__init__(self, *args, **kwargs)
770 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000771 self._changelists = None
772
773 def _GetChangeLists(self):
774 """Get all change lists."""
775 if self._changelists == None:
776 previous_cwd = os.getcwd()
777 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000778 # Need to import here to avoid circular dependency.
779 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000780 self._changelists = gcl.GetModifiedFiles()
781 os.chdir(previous_cwd)
782 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000783
784 def GetAllModifiedFiles(self):
785 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000786 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000787 all_modified_files = []
788 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000789 all_modified_files.extend(
790 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000791 return all_modified_files
792
793 def GetModifiedFiles(self):
794 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000795 changelists = self._GetChangeLists()
796 return [os.path.join(self.RepositoryRoot(), f[1])
797 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000798
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000799
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000800class GitChange(Change):
801 _AFFECTED_FILES = GitAffectedFile
802
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000803 def __init__(self, *args, **kwargs):
804 Change.__init__(self, *args, **kwargs)
805 self.scm = 'git'
806
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000807
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000808def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000809 """Finds all presubmit files that apply to a given set of source files.
810
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000811 If inherit-review-settings-ok is present right under root, looks for
812 PRESUBMIT.py in directories enclosing root.
813
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000814 Args:
815 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000816 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817
818 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000819 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000821 files = [normpath(os.path.join(root, f)) for f in files]
822
823 # List all the individual directories containing files.
824 directories = set([os.path.dirname(f) for f in files])
825
826 # Ignore root if inherit-review-settings-ok is present.
827 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
828 root = None
829
830 # Collect all unique directories that may contain PRESUBMIT.py.
831 candidates = set()
832 for directory in directories:
833 while True:
834 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000835 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000836 candidates.add(directory)
837 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000838 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000839 parent_dir = os.path.dirname(directory)
840 if parent_dir == directory:
841 # We hit the system root directory.
842 break
843 directory = parent_dir
844
845 # Look for PRESUBMIT.py in all candidate directories.
846 results = []
847 for directory in sorted(list(candidates)):
848 p = os.path.join(directory, 'PRESUBMIT.py')
849 if os.path.isfile(p):
850 results.append(p)
851
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000852 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000853 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000854
855
thestig@chromium.orgde243452009-10-06 21:02:56 +0000856class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000857 @staticmethod
858 def ExecPresubmitScript(script_text):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000859 """Executes GetPreferredTrySlaves() from a single presubmit script.
860
861 Args:
862 script_text: The text of the presubmit script.
863
864 Return:
865 A list of try slaves.
866 """
867 context = {}
868 exec script_text in context
869
870 function_name = 'GetPreferredTrySlaves'
871 if function_name in context:
872 result = eval(function_name + '()', context)
873 if not isinstance(result, types.ListType):
874 raise exceptions.RuntimeError(
875 'Presubmit functions must return a list, got a %s instead: %s' %
876 (type(result), str(result)))
877 for item in result:
878 if not isinstance(item, basestring):
879 raise exceptions.RuntimeError('All try slaves names must be strings.')
880 if item != item.strip():
881 raise exceptions.RuntimeError('Try slave names cannot start/end'
882 'with whitespace')
883 else:
884 result = []
885 return result
886
887
888def DoGetTrySlaves(changed_files,
889 repository_root,
890 default_presubmit,
891 verbose,
892 output_stream):
893 """Get the list of try servers from the presubmit scripts.
894
895 Args:
896 changed_files: List of modified files.
897 repository_root: The repository root.
898 default_presubmit: A default presubmit script to execute in any case.
899 verbose: Prints debug info.
900 output_stream: A stream to write debug output to.
901
902 Return:
903 List of try slaves
904 """
905 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
906 if not presubmit_files and verbose:
907 output_stream.write("Warning, no presubmit.py found.\n")
908 results = []
909 executer = GetTrySlavesExecuter()
910 if default_presubmit:
911 if verbose:
912 output_stream.write("Running default presubmit script.\n")
913 results += executer.ExecPresubmitScript(default_presubmit)
914 for filename in presubmit_files:
915 filename = os.path.abspath(filename)
916 if verbose:
917 output_stream.write("Running %s\n" % filename)
918 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000919 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000920 results += executer.ExecPresubmitScript(presubmit_script)
921
922 slaves = list(set(results))
923 if slaves and verbose:
924 output_stream.write(', '.join(slaves))
925 output_stream.write('\n')
926 return slaves
927
928
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000929class PresubmitExecuter(object):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000930 def __init__(self, change, committing):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000931 """
932 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000933 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000934 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
935 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000936 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000937 self.committing = committing
938
939 def ExecPresubmitScript(self, script_text, presubmit_path):
940 """Executes a single presubmit script.
941
942 Args:
943 script_text: The text of the presubmit script.
944 presubmit_path: The path to the presubmit file (this will be reported via
945 input_api.PresubmitLocalPath()).
946
947 Return:
948 A list of result objects, empty if no problems.
949 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000950
951 # Change to the presubmit file's directory to support local imports.
952 main_path = os.getcwd()
953 os.chdir(os.path.dirname(presubmit_path))
954
955 # Load the presubmit script into context.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000956 input_api = InputApi(self.change, presubmit_path, self.committing)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000957 context = {}
958 exec script_text in context
959
960 # These function names must change if we make substantial changes to
961 # the presubmit API that are not backwards compatible.
962 if self.committing:
963 function_name = 'CheckChangeOnCommit'
964 else:
965 function_name = 'CheckChangeOnUpload'
966 if function_name in context:
967 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000968 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000969 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000970 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000971 if not (isinstance(result, types.TupleType) or
972 isinstance(result, types.ListType)):
973 raise exceptions.RuntimeError(
974 'Presubmit functions must return a tuple or list')
975 for item in result:
976 if not isinstance(item, OutputApi.PresubmitResult):
977 raise exceptions.RuntimeError(
978 'All presubmit results must be of types derived from '
979 'output_api.PresubmitResult')
980 else:
981 result = () # no error since the script doesn't care about current event.
982
chase@chromium.org8e416c82009-10-06 04:30:44 +0000983 # Return the process to the original working directory.
984 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000985 return result
986
987
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000988def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000989 committing,
990 verbose,
991 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +0000992 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +0000993 default_presubmit,
994 may_prompt):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995 """Runs all presubmit checks that apply to the files in the change.
996
997 This finds all PRESUBMIT.py files in directories enclosing the files in the
998 change (up to the repository root) and calls the relevant entrypoint function
999 depending on whether the change is being committed or uploaded.
1000
1001 Prints errors, warnings and notifications. Prompts the user for warnings
1002 when needed.
1003
1004 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001005 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1007 verbose: Prints debug info.
1008 output_stream: A stream to write output from presubmit tests to.
1009 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001010 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001011 may_prompt: Enable (y/n) questions on warning or error.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001012
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001013 Warning:
1014 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1015 SHOULD be sys.stdin.
1016
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001017 Return:
1018 True if execution can continue, False if not.
1019 """
maruel@chromium.org8d195232010-10-05 12:58:49 +00001020 print "Running presubmit hooks..."
jam@chromium.org2a891dc2009-08-20 20:33:37 +00001021 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001022 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
1023 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 if not presubmit_files and verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001025 output_stream.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001026 results = []
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001027 executer = PresubmitExecuter(change, committing)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001028 if default_presubmit:
1029 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001030 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001031 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001032 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +00001034 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035 if verbose:
maruel@chromium.orgf3eee562009-05-27 00:51:10 +00001036 output_stream.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +00001037 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001038 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001039 results += executer.ExecPresubmitScript(presubmit_script, filename)
1040
1041 errors = []
1042 notifications = []
1043 warnings = []
1044 for result in results:
1045 if not result.IsFatal() and not result.ShouldPrompt():
1046 notifications.append(result)
1047 elif result.ShouldPrompt():
1048 warnings.append(result)
1049 else:
1050 errors.append(result)
1051
1052 error_count = 0
1053 for name, items in (('Messages', notifications),
1054 ('Warnings', warnings),
1055 ('ERRORS', errors)):
1056 if items:
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001057 output_stream.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001058 for item in items:
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +00001059 # Access to a protected member XXX of a client class
1060 # pylint: disable=W0212
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001061 if not item._Handle(output_stream, input_stream,
1062 may_prompt=False):
1063 error_count += 1
1064 output_stream.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001065
1066 total_time = time.time() - start_time
1067 if total_time > 1.0:
1068 print "Presubmit checks took %.1fs to calculate." % total_time
1069
maruel@chromium.org07bbc212009-06-11 02:08:41 +00001070 if not errors and warnings and may_prompt:
gspencer@google.comefb94502009-10-09 17:57:08 +00001071 if not PromptYesNo(input_stream, output_stream,
1072 'There were presubmit warnings. '
1073 'Are you sure you wish to continue? (y/N): '):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001074 error_count += 1
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001075
1076 global _ASKED_FOR_FEEDBACK
1077 # Ask for feedback one time out of 5.
1078 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1079 output_stream.write("Was the presubmit check useful? Please send feedback "
1080 "& hate mail to maruel@chromium.org!\n")
1081 _ASKED_FOR_FEEDBACK = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001082 return (error_count == 0)
1083
1084
1085def ScanSubDirs(mask, recursive):
1086 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001087 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 +00001088 else:
1089 results = []
1090 for root, dirs, files in os.walk('.'):
1091 if '.svn' in dirs:
1092 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001093 if '.git' in dirs:
1094 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001095 for name in files:
1096 if fnmatch.fnmatch(name, mask):
1097 results.append(os.path.join(root, name))
1098 return results
1099
1100
1101def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001102 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001103 files = []
1104 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001105 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001106 return files
1107
1108
1109def Main(argv):
1110 parser = optparse.OptionParser(usage="%prog [options]",
1111 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001112 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001113 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001114 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1115 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001116 parser.add_option("-r", "--recursive", action="store_true",
1117 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001118 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001119 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001120 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001121 parser.add_option("--name", default='no name')
1122 parser.add_option("--description", default='')
1123 parser.add_option("--issue", type='int', default=0)
1124 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001125 parser.add_option("--root", default=os.getcwd(),
1126 help="Search for PRESUBMIT.py up to this directory. "
1127 "If inherit-review-settings-ok is present in this "
1128 "directory, parent directories up to the root file "
1129 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001130 parser.add_option("--default_presubmit")
1131 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001132 options, args = parser.parse_args(argv[1:])
maruel@chromium.org7444c502011-02-09 14:02:11 +00001133 if options.verbose:
1134 logging.basicConfig(level=logging.DEBUG)
1135 if os.path.isdir(os.path.join(options.root, '.svn')):
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001136 change_class = SvnChange
1137 if not options.files:
1138 if args:
1139 options.files = ParseFiles(args, options.recursive)
1140 else:
1141 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001142 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001143 else:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001144 is_git = os.path.isdir(os.path.join(options.root, '.git'))
1145 if not is_git:
1146 is_git = (0 == subprocess.call(
1147 ['git', 'rev-parse', '--show-cdup'],
1148 stdout=subprocess.PIPE, cwd=options.root))
1149 if is_git:
1150 # Only look at the subdirectories below cwd.
1151 change_class = GitChange
1152 if not options.files:
1153 if args:
1154 options.files = ParseFiles(args, options.recursive)
1155 else:
1156 # Grab modified files.
1157 options.files = scm.GIT.CaptureStatus([options.root])
1158 else:
1159 logging.info('Doesn\'t seem under source control.')
1160 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161 if options.verbose:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001162 if not options.files:
1163 print "Found no files."
1164 elif len(options.files) != 1:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001165 print "Found %d files." % len(options.files)
1166 else:
1167 print "Found 1 file."
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001168 return not DoPresubmitChecks(change_class(options.name,
1169 options.description,
1170 options.root,
1171 options.files,
1172 options.issue,
1173 options.patchset),
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001174 options.commit,
1175 options.verbose,
1176 sys.stdout,
1177 sys.stdin,
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001178 options.default_presubmit,
1179 options.may_prompt)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001180
1181
1182if __name__ == '__main__':
1183 sys.exit(Main(sys.argv))