blob: 5a003eba0040c64f9c203f6551ece6c0651ff576 [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.org2b5ce562011-03-31 16:15:44 +00009__version__ = '1.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.org35625c72011-03-23 17:34:02 +000053import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000054import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000055import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000056import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000057import scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000058
59
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000060# Ask for feedback only once in program lifetime.
61_ASKED_FOR_FEEDBACK = False
62
63
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000064class NotImplementedException(Exception):
65 """We're leaving placeholders in a bunch of places to remind us of the
66 design of the API, but we have not implemented all of it yet. Implement as
67 the need arises.
68 """
69 pass
70
71
72def normpath(path):
73 '''Version of os.path.normpath that also changes backward slashes to
74 forward slashes when not running on Windows.
75 '''
76 # This is safe to always do because the Windows version of os.path.normpath
77 # will replace forward slashes with backward slashes.
78 path = path.replace(os.sep, '/')
79 return os.path.normpath(path)
80
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000081
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000082def _RightHandSideLinesImpl(affected_files):
83 """Implements RightHandSideLines for InputApi and GclChange."""
84 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000085 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000086 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000087 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000088
89
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000090class PresubmitOutput(object):
91 def __init__(self, input_stream=None, output_stream=None):
92 self.input_stream = input_stream
93 self.output_stream = output_stream
94 self.reviewers = []
95 self.written_output = []
96 self.error_count = 0
97
98 def prompt_yes_no(self, prompt_string):
99 self.write(prompt_string)
100 if self.input_stream:
101 response = self.input_stream.readline().strip().lower()
102 if response not in ('y', 'yes'):
103 self.fail()
104 else:
105 self.fail()
106
107 def fail(self):
108 self.error_count += 1
109
110 def should_continue(self):
111 return not self.error_count
112
113 def write(self, s):
114 self.written_output.append(s)
115 if self.output_stream:
116 self.output_stream.write(s)
117
118 def getvalue(self):
119 return ''.join(self.written_output)
120
121
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000122class OutputApi(object):
123 """This class (more like a module) gets passed to presubmit scripts so that
124 they can specify various types of results.
125 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000126 class PresubmitResult(object):
127 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000128 fatal = False
129 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000130
131 def __init__(self, message, items=None, long_text=''):
132 """
133 message: A short one-line message to indicate errors.
134 items: A list of short strings to indicate where errors occurred.
135 long_text: multi-line text output, e.g. from another tool
136 """
137 self._message = message
138 self._items = []
139 if items:
140 self._items = items
141 self._long_text = long_text.rstrip()
142
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000143 def handle(self, output):
144 output.write(self._message)
145 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000146 for index, item in enumerate(self._items):
147 output.write(' ')
148 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000149 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000150 if index < len(self._items) - 1:
151 output.write(' \\')
152 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000153 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000154 output.write('\n***************\n')
155 # Write separately in case it's unicode.
156 output.write(self._long_text)
157 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000158 if self.fatal:
159 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000160
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000161 class PresubmitAddReviewers(PresubmitResult):
162 """Add some suggested reviewers to the change."""
163 def __init__(self, reviewers):
164 super(OutputApi.PresubmitAddReviewers, self).__init__('')
165 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000166
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000167 def handle(self, output):
168 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000169
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000170 class PresubmitError(PresubmitResult):
171 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000172 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000173
174 class PresubmitPromptWarning(PresubmitResult):
175 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000176 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000177
178 class PresubmitNotifyResult(PresubmitResult):
179 """Just print something to the screen -- but it's not even a warning."""
180 pass
181
182 class MailTextResult(PresubmitResult):
183 """A warning that should be included in the review request email."""
184 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000185 super(OutputApi.MailTextResult, self).__init__()
186 raise NotImplementedException()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000187
188
189class InputApi(object):
190 """An instance of this object is passed to presubmit scripts so they can
191 know stuff about the change they're looking at.
192 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000193 # Method could be a function
194 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000195
maruel@chromium.org3410d912009-06-09 20:56:16 +0000196 # File extensions that are considered source files from a style guide
197 # perspective. Don't modify this list from a presubmit script!
198 DEFAULT_WHITE_LIST = (
199 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000200 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000201 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000202 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000203 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000204 # No extension at all, note that ALL CAPS files are black listed in
205 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000206 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000207 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000208 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000209 )
210
211 # Path regexp that should be excluded from being considered containing source
212 # files. Don't modify this list from a presubmit script!
213 DEFAULT_BLACK_LIST = (
214 r".*\bexperimental[\\\/].*",
215 r".*\bthird_party[\\\/].*",
216 # Output directories (just in case)
217 r".*\bDebug[\\\/].*",
218 r".*\bRelease[\\\/].*",
219 r".*\bxcodebuild[\\\/].*",
220 r".*\bsconsbuild[\\\/].*",
221 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000222 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000223 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000224 r"(|.*[\\\/])\.git[\\\/].*",
225 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000226 )
227
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000228 # TODO(dpranke): Update callers to pass in tbr, host_url, remove
229 # default arguments.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000230 def __init__(self, change, presubmit_path, is_committing, tbr, host_url=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000231 """Builds an InputApi object.
232
233 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000234 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000235 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000236 is_committing: True if the change is about to be committed.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000237 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
238 host_url: scheme, host, and path of rietveld instance
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000239 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000240 # Version number of the presubmit_support script.
241 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000242 self.change = change
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000243 self.host_url = host_url
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000244 self.is_committing = is_committing
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000245 self.tbr = tbr
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000246 self.host_url = host_url or 'http://codereview.chromium.org'
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@chromium.org2b5ce562011-03-31 16:15:44 +0000254 self.os_listdir = os.listdir
255 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256 self.os_path = os.path
257 self.pickle = pickle
258 self.marshal = marshal
259 self.re = re
260 self.subprocess = subprocess
261 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000262 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000263 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000264 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000265 self.urllib2 = urllib2
266
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000267 # To easily fork python.
268 self.python_executable = sys.executable
269 self.environ = os.environ
270
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271 # InputApi.platform is the platform you're currently running on.
272 self.platform = sys.platform
273
274 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000275 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276
277 # We carry the canned checks so presubmit scripts can easily use them.
278 self.canned_checks = presubmit_canned_checks
279
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000280 # TODO(dpranke): figure out a list of all approved owners for a repo
281 # in order to be able to handle wildcard OWNERS files?
282 self.owners_db = owners.Database(change.RepositoryRoot(),
283 fopen=file, os_path=self.os_path)
284
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000285 def PresubmitLocalPath(self):
286 """Returns the local path of the presubmit script currently being run.
287
288 This is useful if you don't want to hard-code absolute paths in the
289 presubmit script. For example, It can be used to find another file
290 relative to the PRESUBMIT.py script, so the whole tree can be branched and
291 the presubmit script still works, without editing its content.
292 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000293 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000294
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000295 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000296 """Translate a depot path to a local path (relative to client root).
297
298 Args:
299 Depot path as a string.
300
301 Returns:
302 The local path of the depot path under the user's current client, or None
303 if the file is not mapped.
304
305 Remember to check for the None case and show an appropriate error!
306 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000307 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000308 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000309 return local_path
310
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000311 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000312 """Translate a local path to a depot path.
313
314 Args:
315 Local path (relative to current directory, or absolute) as a string.
316
317 Returns:
318 The depot path (SVN URL) of the file if mapped, otherwise None.
319 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000320 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000321 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322 return depot_path
323
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000324 def AffectedFiles(self, include_dirs=False, include_deletes=True):
325 """Same as input_api.change.AffectedFiles() except only lists files
326 (and optionally directories) in the same directory as the current presubmit
327 script, or subdirectories thereof.
328 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000329 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000330 if len(dir_with_slash) == 1:
331 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000332 return filter(
333 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
334 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000335
336 def LocalPaths(self, include_dirs=False):
337 """Returns local paths of input_api.AffectedFiles()."""
338 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
339
340 def AbsoluteLocalPaths(self, include_dirs=False):
341 """Returns absolute local paths of input_api.AffectedFiles()."""
342 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
343
344 def ServerPaths(self, include_dirs=False):
345 """Returns server paths of input_api.AffectedFiles()."""
346 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
347
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000348 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349 """Same as input_api.change.AffectedTextFiles() except only lists files
350 in the same directory as the current presubmit script, or subdirectories
351 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000352 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000353 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000354 warn("AffectedTextFiles(include_deletes=%s)"
355 " is deprecated and ignored" % str(include_deletes),
356 category=DeprecationWarning,
357 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000358 return filter(lambda x: x.IsTextFile(),
359 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000360
maruel@chromium.org3410d912009-06-09 20:56:16 +0000361 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
362 """Filters out files that aren't considered "source file".
363
364 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
365 and InputApi.DEFAULT_BLACK_LIST is used respectively.
366
367 The lists will be compiled as regular expression and
368 AffectedFile.LocalPath() needs to pass both list.
369
370 Note: Copy-paste this function to suit your needs or use a lambda function.
371 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000372 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000373 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000374 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000375 if self.re.match(item, local_path):
376 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000377 return True
378 return False
379 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
380 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
381
382 def AffectedSourceFiles(self, source_file):
383 """Filter the list of AffectedTextFiles by the function source_file.
384
385 If source_file is None, InputApi.FilterSourceFile() is used.
386 """
387 if not source_file:
388 source_file = self.FilterSourceFile
389 return filter(source_file, self.AffectedTextFiles())
390
391 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000392 """An iterator over all text lines in "new" version of changed files.
393
394 Only lists lines from new or modified text files in the change that are
395 contained by the directory of the currently executing presubmit script.
396
397 This is useful for doing line-by-line regex checks, like checking for
398 trailing whitespace.
399
400 Yields:
401 a 3 tuple:
402 the AffectedFile instance of the current file;
403 integer line number (1-based); and
404 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000405
406 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000407 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000408 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000409 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000410
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000411 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000412 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000413
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000414 Deny reading anything outside the repository.
415 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000416 if isinstance(file_item, AffectedFile):
417 file_item = file_item.AbsoluteLocalPath()
418 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000419 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000420 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000421
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000422
423class AffectedFile(object):
424 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000425 # Method could be a function
426 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000427 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000428 self._path = path
429 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000430 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000431 self._is_directory = None
432 self._properties = {}
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000433 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000434
435 def ServerPath(self):
436 """Returns a path string that identifies the file in the SCM system.
437
438 Returns the empty string if the file does not exist in SCM.
439 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000440 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000441
442 def LocalPath(self):
443 """Returns the path of this file on the local disk relative to client root.
444 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000445 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000446
447 def AbsoluteLocalPath(self):
448 """Returns the absolute path of this file on the local disk.
449 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000450 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000451
452 def IsDirectory(self):
453 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000454 if self._is_directory is None:
455 path = self.AbsoluteLocalPath()
456 self._is_directory = (os.path.exists(path) and
457 os.path.isdir(path))
458 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000459
460 def Action(self):
461 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000462 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
463 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000464 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000465
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000466 def Property(self, property_name):
467 """Returns the specified SCM property of this file, or None if no such
468 property.
469 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000470 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000471
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000472 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000473 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000474
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000475 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000476 raise NotImplementedError() # Implement when needed
477
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000478 def NewContents(self):
479 """Returns an iterator over the lines in the new version of file.
480
481 The new version is the file in the user's workspace, i.e. the "right hand
482 side".
483
484 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000485 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000486 """
487 if self.IsDirectory():
488 return []
489 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000490 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
491 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000492
493 def OldContents(self):
494 """Returns an iterator over the lines in the old version of file.
495
496 The old version is the file in depot, i.e. the "left hand side".
497 """
498 raise NotImplementedError() # Implement when needed
499
500 def OldFileTempPath(self):
501 """Returns the path on local disk where the old contents resides.
502
503 The old version is the file in depot, i.e. the "left hand side".
504 This is a read-only cached copy of the old contents. *DO NOT* try to
505 modify this file.
506 """
507 raise NotImplementedError() # Implement if/when needed.
508
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000509 def ChangedContents(self):
510 """Returns a list of tuples (line number, line text) of all new lines.
511
512 This relies on the scm diff output describing each changed code section
513 with a line of the form
514
515 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
516 """
517 new_lines = []
518 line_num = 0
519
520 if self.IsDirectory():
521 return []
522
523 for line in self.GenerateScmDiff().splitlines():
524 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
525 if m:
526 line_num = int(m.groups(1)[0])
527 continue
528 if line.startswith('+') and not line.startswith('++'):
529 new_lines.append((line_num, line[1:]))
530 if not line.startswith('-'):
531 line_num += 1
532 return new_lines
533
maruel@chromium.org5de13972009-06-10 18:16:06 +0000534 def __str__(self):
535 return self.LocalPath()
536
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000537 def GenerateScmDiff(self):
538 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000539
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000540class SvnAffectedFile(AffectedFile):
541 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000542 # Method 'NNN' is abstract in class 'NNN' but is not overridden
543 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000544
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000545 def __init__(self, *args, **kwargs):
546 AffectedFile.__init__(self, *args, **kwargs)
547 self._server_path = None
548 self._is_text_file = None
549
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000550 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000551 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000552 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000553 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000554 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000555
556 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000557 if self._is_directory is None:
558 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000559 if os.path.exists(path):
560 # Retrieve directly from the file system; it is much faster than
561 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000562 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000563 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000564 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000565 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000566 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000567
568 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000569 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000570 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000571 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000572 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000573
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000574 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000575 if self._is_text_file is None:
576 if self.Action() == 'D':
577 # A deleted file is not a text file.
578 self._is_text_file = False
579 elif self.IsDirectory():
580 self._is_text_file = False
581 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000582 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
583 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000584 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
585 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000586
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000587 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000588 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
589
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000590
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000591class GitAffectedFile(AffectedFile):
592 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000593 # Method 'NNN' is abstract in class 'NNN' but is not overridden
594 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000595
596 def __init__(self, *args, **kwargs):
597 AffectedFile.__init__(self, *args, **kwargs)
598 self._server_path = None
599 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000600
601 def ServerPath(self):
602 if self._server_path is None:
603 raise NotImplementedException() # TODO(maruel) Implement.
604 return self._server_path
605
606 def IsDirectory(self):
607 if self._is_directory is None:
608 path = self.AbsoluteLocalPath()
609 if os.path.exists(path):
610 # Retrieve directly from the file system; it is much faster than
611 # querying subversion, especially on Windows.
612 self._is_directory = os.path.isdir(path)
613 else:
614 # raise NotImplementedException() # TODO(maruel) Implement.
615 self._is_directory = False
616 return self._is_directory
617
618 def Property(self, property_name):
619 if not property_name in self._properties:
620 raise NotImplementedException() # TODO(maruel) Implement.
621 return self._properties[property_name]
622
623 def IsTextFile(self):
624 if self._is_text_file is None:
625 if self.Action() == 'D':
626 # A deleted file is not a text file.
627 self._is_text_file = False
628 elif self.IsDirectory():
629 self._is_text_file = False
630 else:
631 # raise NotImplementedException() # TODO(maruel) Implement.
632 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
633 return self._is_text_file
634
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000635 def GenerateScmDiff(self):
636 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000637
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000638class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000639 """Describe a change.
640
641 Used directly by the presubmit scripts to query the current change being
642 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000643
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000644 Instance members:
645 tags: Dictionnary of KEY=VALUE pairs found in the change description.
646 self.KEY: equivalent to tags['KEY']
647 """
648
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000649 _AFFECTED_FILES = AffectedFile
650
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000651 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000652 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000653 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000654
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000655 def __init__(self, name, description, local_root, files, issue, patchset):
656 if files is None:
657 files = []
658 self._name = name
659 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000660 # Convert root into an absolute path.
661 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000662 self.issue = issue
663 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000664 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000665
666 # From the description text, build up a dictionary of key/value pairs
667 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000668 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000669 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000670 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000671 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672 if m:
673 self.tags[m.group('key')] = m.group('value')
674 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000675 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676
677 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000678 self._description_without_tags = (
679 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000681 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000682 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
683 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000684 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000686 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000688 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000689
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690 def DescriptionText(self):
691 """Returns the user-entered changelist description, minus tags.
692
693 Any line in the user-provided description starting with e.g. "FOO="
694 (whitespace permitted before and around) is considered a tag line. Such
695 lines are stripped out of the description this function returns.
696 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000697 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698
699 def FullDescriptionText(self):
700 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000701 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702
703 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000704 """Returns the repository (checkout) root directory for this change,
705 as an absolute path.
706 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000707 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000708
709 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000710 """Return tags directly as attributes on the object."""
711 if not re.match(r"^[A-Z_]*$", attr):
712 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000713 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714
715 def AffectedFiles(self, include_dirs=False, include_deletes=True):
716 """Returns a list of AffectedFile instances for all files in the change.
717
718 Args:
719 include_deletes: If false, deleted files will be filtered out.
720 include_dirs: True to include directories in the list
721
722 Returns:
723 [AffectedFile(path, action), AffectedFile(path, action)]
724 """
725 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000726 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000728 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729
730 if include_deletes:
731 return affected
732 else:
733 return filter(lambda x: x.Action() != 'D', affected)
734
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000735 def AffectedTextFiles(self, include_deletes=None):
736 """Return a list of the existing text files in a change."""
737 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000738 warn("AffectedTextFiles(include_deletes=%s)"
739 " is deprecated and ignored" % str(include_deletes),
740 category=DeprecationWarning,
741 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000742 return filter(lambda x: x.IsTextFile(),
743 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000744
745 def LocalPaths(self, include_dirs=False):
746 """Convenience function."""
747 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
748
749 def AbsoluteLocalPaths(self, include_dirs=False):
750 """Convenience function."""
751 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
752
753 def ServerPaths(self, include_dirs=False):
754 """Convenience function."""
755 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
756
757 def RightHandSideLines(self):
758 """An iterator over all text lines in "new" version of changed files.
759
760 Lists lines from new or modified text files in the change.
761
762 This is useful for doing line-by-line regex checks, like checking for
763 trailing whitespace.
764
765 Yields:
766 a 3 tuple:
767 the AffectedFile instance of the current file;
768 integer line number (1-based); and
769 the contents of the line as a string.
770 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000771 return _RightHandSideLinesImpl(
772 x for x in self.AffectedFiles(include_deletes=False)
773 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000774
775
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000776class SvnChange(Change):
777 _AFFECTED_FILES = SvnAffectedFile
778
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000779 def __init__(self, *args, **kwargs):
780 Change.__init__(self, *args, **kwargs)
781 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000782 self._changelists = None
783
784 def _GetChangeLists(self):
785 """Get all change lists."""
786 if self._changelists == None:
787 previous_cwd = os.getcwd()
788 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000789 # Need to import here to avoid circular dependency.
790 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000791 self._changelists = gcl.GetModifiedFiles()
792 os.chdir(previous_cwd)
793 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000794
795 def GetAllModifiedFiles(self):
796 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000797 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000798 all_modified_files = []
799 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000800 all_modified_files.extend(
801 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000802 return all_modified_files
803
804 def GetModifiedFiles(self):
805 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000806 changelists = self._GetChangeLists()
807 return [os.path.join(self.RepositoryRoot(), f[1])
808 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000809
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000810
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000811class GitChange(Change):
812 _AFFECTED_FILES = GitAffectedFile
813
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000814 def __init__(self, *args, **kwargs):
815 Change.__init__(self, *args, **kwargs)
816 self.scm = 'git'
817
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000818
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000819def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820 """Finds all presubmit files that apply to a given set of source files.
821
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000822 If inherit-review-settings-ok is present right under root, looks for
823 PRESUBMIT.py in directories enclosing root.
824
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825 Args:
826 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000827 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000828
829 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000830 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000831 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000832 files = [normpath(os.path.join(root, f)) for f in files]
833
834 # List all the individual directories containing files.
835 directories = set([os.path.dirname(f) for f in files])
836
837 # Ignore root if inherit-review-settings-ok is present.
838 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
839 root = None
840
841 # Collect all unique directories that may contain PRESUBMIT.py.
842 candidates = set()
843 for directory in directories:
844 while True:
845 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000846 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000847 candidates.add(directory)
848 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000849 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000850 parent_dir = os.path.dirname(directory)
851 if parent_dir == directory:
852 # We hit the system root directory.
853 break
854 directory = parent_dir
855
856 # Look for PRESUBMIT.py in all candidate directories.
857 results = []
858 for directory in sorted(list(candidates)):
859 p = os.path.join(directory, 'PRESUBMIT.py')
860 if os.path.isfile(p):
861 results.append(p)
862
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000863 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000864 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000865
866
thestig@chromium.orgde243452009-10-06 21:02:56 +0000867class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000868 @staticmethod
869 def ExecPresubmitScript(script_text):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000870 """Executes GetPreferredTrySlaves() from a single presubmit script.
871
872 Args:
873 script_text: The text of the presubmit script.
874
875 Return:
876 A list of try slaves.
877 """
878 context = {}
879 exec script_text in context
880
881 function_name = 'GetPreferredTrySlaves'
882 if function_name in context:
883 result = eval(function_name + '()', context)
884 if not isinstance(result, types.ListType):
885 raise exceptions.RuntimeError(
886 'Presubmit functions must return a list, got a %s instead: %s' %
887 (type(result), str(result)))
888 for item in result:
889 if not isinstance(item, basestring):
890 raise exceptions.RuntimeError('All try slaves names must be strings.')
891 if item != item.strip():
892 raise exceptions.RuntimeError('Try slave names cannot start/end'
893 'with whitespace')
894 else:
895 result = []
896 return result
897
898
899def DoGetTrySlaves(changed_files,
900 repository_root,
901 default_presubmit,
902 verbose,
903 output_stream):
904 """Get the list of try servers from the presubmit scripts.
905
906 Args:
907 changed_files: List of modified files.
908 repository_root: The repository root.
909 default_presubmit: A default presubmit script to execute in any case.
910 verbose: Prints debug info.
911 output_stream: A stream to write debug output to.
912
913 Return:
914 List of try slaves
915 """
916 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
917 if not presubmit_files and verbose:
918 output_stream.write("Warning, no presubmit.py found.\n")
919 results = []
920 executer = GetTrySlavesExecuter()
921 if default_presubmit:
922 if verbose:
923 output_stream.write("Running default presubmit script.\n")
924 results += executer.ExecPresubmitScript(default_presubmit)
925 for filename in presubmit_files:
926 filename = os.path.abspath(filename)
927 if verbose:
928 output_stream.write("Running %s\n" % filename)
929 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000930 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000931 results += executer.ExecPresubmitScript(presubmit_script)
932
933 slaves = list(set(results))
934 if slaves and verbose:
935 output_stream.write(', '.join(slaves))
936 output_stream.write('\n')
937 return slaves
938
939
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940class PresubmitExecuter(object):
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000941 def __init__(self, change, committing, tbr, host_url):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000942 """
943 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000944 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000945 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000946 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
947 host_url: scheme, host, and path of rietveld instance
948 (or None for default)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000949 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000950 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000951 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000952 self.tbr = tbr
953 self.host_url = host_url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000954
955 def ExecPresubmitScript(self, script_text, presubmit_path):
956 """Executes a single presubmit script.
957
958 Args:
959 script_text: The text of the presubmit script.
960 presubmit_path: The path to the presubmit file (this will be reported via
961 input_api.PresubmitLocalPath()).
962
963 Return:
964 A list of result objects, empty if no problems.
965 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000966
967 # Change to the presubmit file's directory to support local imports.
968 main_path = os.getcwd()
969 os.chdir(os.path.dirname(presubmit_path))
970
971 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000972 input_api = InputApi(self.change, presubmit_path, self.committing,
973 self.tbr, self.host_url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000974 context = {}
975 exec script_text in context
976
977 # These function names must change if we make substantial changes to
978 # the presubmit API that are not backwards compatible.
979 if self.committing:
980 function_name = 'CheckChangeOnCommit'
981 else:
982 function_name = 'CheckChangeOnUpload'
983 if function_name in context:
984 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000985 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000986 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000987 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000988 if not (isinstance(result, types.TupleType) or
989 isinstance(result, types.ListType)):
990 raise exceptions.RuntimeError(
991 'Presubmit functions must return a tuple or list')
992 for item in result:
993 if not isinstance(item, OutputApi.PresubmitResult):
994 raise exceptions.RuntimeError(
995 'All presubmit results must be of types derived from '
996 'output_api.PresubmitResult')
997 else:
998 result = () # no error since the script doesn't care about current event.
999
chase@chromium.org8e416c82009-10-06 04:30:44 +00001000 # Return the process to the original working directory.
1001 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002 return result
1003
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001004
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001005# TODO(dpranke): make all callers pass in tbr, host_url?
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001006def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 committing,
1008 verbose,
1009 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001010 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001011 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001012 may_prompt,
1013 tbr=False,
1014 host_url=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015 """Runs all presubmit checks that apply to the files in the change.
1016
1017 This finds all PRESUBMIT.py files in directories enclosing the files in the
1018 change (up to the repository root) and calls the relevant entrypoint function
1019 depending on whether the change is being committed or uploaded.
1020
1021 Prints errors, warnings and notifications. Prompts the user for warnings
1022 when needed.
1023
1024 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001025 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001026 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1027 verbose: Prints debug info.
1028 output_stream: A stream to write output from presubmit tests to.
1029 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001030 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001031 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001032 tbr: was --tbr specified to skip any reviewer/owner checks?
1033 host_url: scheme, host, and port of host to use for rietveld-related
1034 checks
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001036 Warning:
1037 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1038 SHOULD be sys.stdin.
1039
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001040 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001041 A PresubmitOutput object. Use output.should_continue() to figure out
1042 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001043 """
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001044 output = PresubmitOutput(input_stream, output_stream)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001045 if committing:
1046 output.write("Running presubmit commit checks ...\n")
1047 else:
1048 output.write("Running presubmit upload checks ...\n")
jam@chromium.org2a891dc2009-08-20 20:33:37 +00001049 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001050 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
1051 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001052 if not presubmit_files and verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001053 output.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001054 results = []
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001055 executer = PresubmitExecuter(change, committing, tbr, host_url)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001056 if default_presubmit:
1057 if verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001058 output.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001059 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001060 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001061 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +00001062 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001063 if verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001064 output.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +00001065 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001066 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001067 results += executer.ExecPresubmitScript(presubmit_script, filename)
1068
1069 errors = []
1070 notifications = []
1071 warnings = []
1072 for result in results:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001073 if result.fatal:
1074 errors.append(result)
1075 elif result.should_prompt:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001076 warnings.append(result)
1077 else:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001078 notifications.append(result)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001079
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001080 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001081 for name, items in (('Messages', notifications),
1082 ('Warnings', warnings),
1083 ('ERRORS', errors)):
1084 if items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001085 output.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001086 for item in items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001087 item.handle(output)
1088 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001089
1090 total_time = time.time() - start_time
1091 if total_time > 1.0:
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001092 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001093
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001094 if not errors:
1095 if not warnings:
1096 output.write('Presubmit checks passed.\n')
1097 elif may_prompt:
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001098 output.prompt_yes_no('There were presubmit warnings. '
1099 'Are you sure you wish to continue? (y/N): ')
1100 else:
1101 output.fail()
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001102
1103 global _ASKED_FOR_FEEDBACK
1104 # Ask for feedback one time out of 5.
1105 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001106 output.write("Was the presubmit check useful? Please send feedback "
1107 "& hate mail to maruel@chromium.org!\n")
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001108 _ASKED_FOR_FEEDBACK = True
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001109 return output
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001110
1111
1112def ScanSubDirs(mask, recursive):
1113 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001114 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 +00001115 else:
1116 results = []
1117 for root, dirs, files in os.walk('.'):
1118 if '.svn' in dirs:
1119 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001120 if '.git' in dirs:
1121 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001122 for name in files:
1123 if fnmatch.fnmatch(name, mask):
1124 results.append(os.path.join(root, name))
1125 return results
1126
1127
1128def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001129 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001130 files = []
1131 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001132 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001133 return files
1134
1135
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001136def load_files(options, args):
1137 """Tries to determine the SCM."""
1138 change_scm = scm.determine_scm(options.root)
1139 files = []
1140 if change_scm == 'svn':
1141 change_class = SvnChange
1142 status_fn = scm.SVN.CaptureStatus
1143 elif change_scm == 'git':
1144 change_class = GitChange
1145 status_fn = scm.GIT.CaptureStatus
1146 else:
1147 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1148 if not args:
1149 return None, None
1150 change_class = Change
1151 if args:
1152 files = ParseFiles(args, options.recursive)
1153 else:
1154 # Grab modified files.
1155 files = status_fn([options.root])
1156 return change_class, files
1157
1158
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001159def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001160 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001162 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001163 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001164 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1165 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001166 parser.add_option("-r", "--recursive", action="store_true",
1167 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001168 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001169 help="Verbose output")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001170 parser.add_option("--name", default='no name')
1171 parser.add_option("--description", default='')
1172 parser.add_option("--issue", type='int', default=0)
1173 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001174 parser.add_option("--root", default=os.getcwd(),
1175 help="Search for PRESUBMIT.py up to this directory. "
1176 "If inherit-review-settings-ok is present in this "
1177 "directory, parent directories up to the root file "
1178 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001179 parser.add_option("--default_presubmit")
1180 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001181 options, args = parser.parse_args(argv)
maruel@chromium.org7444c502011-02-09 14:02:11 +00001182 if options.verbose:
1183 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001184 change_class, files = load_files(options, args)
1185 if not change_class:
1186 parser.error('For unversioned directory, <files> is not optional.')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001187 if options.verbose:
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001188 print "Found %d file(s)." % len(files)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001189 results = DoPresubmitChecks(change_class(options.name,
1190 options.description,
1191 options.root,
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001192 files,
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001193 options.issue,
1194 options.patchset),
1195 options.commit,
1196 options.verbose,
1197 sys.stdout,
1198 sys.stdin,
1199 options.default_presubmit,
1200 options.may_prompt)
1201 return not results.should_continue()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001202
1203
1204if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001205 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001206 sys.exit(Main(None))