blob: e15b634f8adeb75e19f002da1dac3bcb5f0752b0 [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.org5c8c6de2011-03-18 16:20:18 +00009__version__ = '1.4'
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@google.comfb2b8eb2009-04-23 21:03:42 +0000254 self.os_path = os.path
255 self.pickle = pickle
256 self.marshal = marshal
257 self.re = re
258 self.subprocess = subprocess
259 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000260 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000261 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000262 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000263 self.urllib2 = urllib2
264
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000265 # To easily fork python.
266 self.python_executable = sys.executable
267 self.environ = os.environ
268
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269 # InputApi.platform is the platform you're currently running on.
270 self.platform = sys.platform
271
272 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000273 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000274
275 # We carry the canned checks so presubmit scripts can easily use them.
276 self.canned_checks = presubmit_canned_checks
277
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000278 # TODO(dpranke): figure out a list of all approved owners for a repo
279 # in order to be able to handle wildcard OWNERS files?
280 self.owners_db = owners.Database(change.RepositoryRoot(),
281 fopen=file, os_path=self.os_path)
282
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283 def PresubmitLocalPath(self):
284 """Returns the local path of the presubmit script currently being run.
285
286 This is useful if you don't want to hard-code absolute paths in the
287 presubmit script. For example, It can be used to find another file
288 relative to the PRESUBMIT.py script, so the whole tree can be branched and
289 the presubmit script still works, without editing its content.
290 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000291 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000292
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000293 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000294 """Translate a depot path to a local path (relative to client root).
295
296 Args:
297 Depot path as a string.
298
299 Returns:
300 The local path of the depot path under the user's current client, or None
301 if the file is not mapped.
302
303 Remember to check for the None case and show an appropriate error!
304 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000305 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000306 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000307 return local_path
308
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000309 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000310 """Translate a local path to a depot path.
311
312 Args:
313 Local path (relative to current directory, or absolute) as a string.
314
315 Returns:
316 The depot path (SVN URL) of the file if mapped, otherwise None.
317 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000318 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000319 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000320 return depot_path
321
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322 def AffectedFiles(self, include_dirs=False, include_deletes=True):
323 """Same as input_api.change.AffectedFiles() except only lists files
324 (and optionally directories) in the same directory as the current presubmit
325 script, or subdirectories thereof.
326 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000327 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000328 if len(dir_with_slash) == 1:
329 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000330 return filter(
331 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
332 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000333
334 def LocalPaths(self, include_dirs=False):
335 """Returns local paths of input_api.AffectedFiles()."""
336 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
337
338 def AbsoluteLocalPaths(self, include_dirs=False):
339 """Returns absolute local paths of input_api.AffectedFiles()."""
340 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
341
342 def ServerPaths(self, include_dirs=False):
343 """Returns server paths of input_api.AffectedFiles()."""
344 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
345
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000346 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 """Same as input_api.change.AffectedTextFiles() except only lists files
348 in the same directory as the current presubmit script, or subdirectories
349 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000351 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000352 warn("AffectedTextFiles(include_deletes=%s)"
353 " is deprecated and ignored" % str(include_deletes),
354 category=DeprecationWarning,
355 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000356 return filter(lambda x: x.IsTextFile(),
357 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000358
maruel@chromium.org3410d912009-06-09 20:56:16 +0000359 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
360 """Filters out files that aren't considered "source file".
361
362 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
363 and InputApi.DEFAULT_BLACK_LIST is used respectively.
364
365 The lists will be compiled as regular expression and
366 AffectedFile.LocalPath() needs to pass both list.
367
368 Note: Copy-paste this function to suit your needs or use a lambda function.
369 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000370 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000371 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000372 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000373 if self.re.match(item, local_path):
374 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000375 return True
376 return False
377 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
378 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
379
380 def AffectedSourceFiles(self, source_file):
381 """Filter the list of AffectedTextFiles by the function source_file.
382
383 If source_file is None, InputApi.FilterSourceFile() is used.
384 """
385 if not source_file:
386 source_file = self.FilterSourceFile
387 return filter(source_file, self.AffectedTextFiles())
388
389 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000390 """An iterator over all text lines in "new" version of changed files.
391
392 Only lists lines from new or modified text files in the change that are
393 contained by the directory of the currently executing presubmit script.
394
395 This is useful for doing line-by-line regex checks, like checking for
396 trailing whitespace.
397
398 Yields:
399 a 3 tuple:
400 the AffectedFile instance of the current file;
401 integer line number (1-based); and
402 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000403
404 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000406 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000407 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000408
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000409 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000410 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000411
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000412 Deny reading anything outside the repository.
413 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000414 if isinstance(file_item, AffectedFile):
415 file_item = file_item.AbsoluteLocalPath()
416 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000417 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000418 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000419
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000420
421class AffectedFile(object):
422 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000423 # Method could be a function
424 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000425 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000426 self._path = path
427 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000428 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000429 self._is_directory = None
430 self._properties = {}
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000431 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000432
433 def ServerPath(self):
434 """Returns a path string that identifies the file in the SCM system.
435
436 Returns the empty string if the file does not exist in SCM.
437 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000438 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000439
440 def LocalPath(self):
441 """Returns the path of this file on the local disk relative to client root.
442 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000443 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000444
445 def AbsoluteLocalPath(self):
446 """Returns the absolute path of this file on the local disk.
447 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000448 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000449
450 def IsDirectory(self):
451 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000452 if self._is_directory is None:
453 path = self.AbsoluteLocalPath()
454 self._is_directory = (os.path.exists(path) and
455 os.path.isdir(path))
456 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000457
458 def Action(self):
459 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000460 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
461 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000462 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000463
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000464 def Property(self, property_name):
465 """Returns the specified SCM property of this file, or None if no such
466 property.
467 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000468 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000469
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000470 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000471 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000472
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000473 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000474 raise NotImplementedError() # Implement when needed
475
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000476 def NewContents(self):
477 """Returns an iterator over the lines in the new version of file.
478
479 The new version is the file in the user's workspace, i.e. the "right hand
480 side".
481
482 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000483 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484 """
485 if self.IsDirectory():
486 return []
487 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000488 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
489 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000490
491 def OldContents(self):
492 """Returns an iterator over the lines in the old version of file.
493
494 The old version is the file in depot, i.e. the "left hand side".
495 """
496 raise NotImplementedError() # Implement when needed
497
498 def OldFileTempPath(self):
499 """Returns the path on local disk where the old contents resides.
500
501 The old version is the file in depot, i.e. the "left hand side".
502 This is a read-only cached copy of the old contents. *DO NOT* try to
503 modify this file.
504 """
505 raise NotImplementedError() # Implement if/when needed.
506
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000507 def ChangedContents(self):
508 """Returns a list of tuples (line number, line text) of all new lines.
509
510 This relies on the scm diff output describing each changed code section
511 with a line of the form
512
513 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
514 """
515 new_lines = []
516 line_num = 0
517
518 if self.IsDirectory():
519 return []
520
521 for line in self.GenerateScmDiff().splitlines():
522 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
523 if m:
524 line_num = int(m.groups(1)[0])
525 continue
526 if line.startswith('+') and not line.startswith('++'):
527 new_lines.append((line_num, line[1:]))
528 if not line.startswith('-'):
529 line_num += 1
530 return new_lines
531
maruel@chromium.org5de13972009-06-10 18:16:06 +0000532 def __str__(self):
533 return self.LocalPath()
534
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000535 def GenerateScmDiff(self):
536 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000537
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000538class SvnAffectedFile(AffectedFile):
539 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000540 # Method 'NNN' is abstract in class 'NNN' but is not overridden
541 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000542
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000543 def __init__(self, *args, **kwargs):
544 AffectedFile.__init__(self, *args, **kwargs)
545 self._server_path = None
546 self._is_text_file = None
547
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000548 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000549 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000550 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000551 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000552 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000553
554 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000555 if self._is_directory is None:
556 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000557 if os.path.exists(path):
558 # Retrieve directly from the file system; it is much faster than
559 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000560 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000561 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000562 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000563 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000564 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000565
566 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000567 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000568 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000569 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000570 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000571
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000572 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000573 if self._is_text_file is None:
574 if self.Action() == 'D':
575 # A deleted file is not a text file.
576 self._is_text_file = False
577 elif self.IsDirectory():
578 self._is_text_file = False
579 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000580 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
581 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000582 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
583 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000584
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000585 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000586 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
587
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000588
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000589class GitAffectedFile(AffectedFile):
590 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000591 # Method 'NNN' is abstract in class 'NNN' but is not overridden
592 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000593
594 def __init__(self, *args, **kwargs):
595 AffectedFile.__init__(self, *args, **kwargs)
596 self._server_path = None
597 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000598
599 def ServerPath(self):
600 if self._server_path is None:
601 raise NotImplementedException() # TODO(maruel) Implement.
602 return self._server_path
603
604 def IsDirectory(self):
605 if self._is_directory is None:
606 path = self.AbsoluteLocalPath()
607 if os.path.exists(path):
608 # Retrieve directly from the file system; it is much faster than
609 # querying subversion, especially on Windows.
610 self._is_directory = os.path.isdir(path)
611 else:
612 # raise NotImplementedException() # TODO(maruel) Implement.
613 self._is_directory = False
614 return self._is_directory
615
616 def Property(self, property_name):
617 if not property_name in self._properties:
618 raise NotImplementedException() # TODO(maruel) Implement.
619 return self._properties[property_name]
620
621 def IsTextFile(self):
622 if self._is_text_file is None:
623 if self.Action() == 'D':
624 # A deleted file is not a text file.
625 self._is_text_file = False
626 elif self.IsDirectory():
627 self._is_text_file = False
628 else:
629 # raise NotImplementedException() # TODO(maruel) Implement.
630 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
631 return self._is_text_file
632
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000633 def GenerateScmDiff(self):
634 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000635
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000636class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000637 """Describe a change.
638
639 Used directly by the presubmit scripts to query the current change being
640 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000641
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000642 Instance members:
643 tags: Dictionnary of KEY=VALUE pairs found in the change description.
644 self.KEY: equivalent to tags['KEY']
645 """
646
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000647 _AFFECTED_FILES = AffectedFile
648
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000649 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000650 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000651 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000652
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000653 def __init__(self, name, description, local_root, files, issue, patchset):
654 if files is None:
655 files = []
656 self._name = name
657 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000658 # Convert root into an absolute path.
659 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000660 self.issue = issue
661 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000662 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000663
664 # From the description text, build up a dictionary of key/value pairs
665 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000666 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000667 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000668 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000669 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670 if m:
671 self.tags[m.group('key')] = m.group('value')
672 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000673 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674
675 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000676 self._description_without_tags = (
677 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000679 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000680 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
681 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000682 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000684 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000686 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688 def DescriptionText(self):
689 """Returns the user-entered changelist description, minus tags.
690
691 Any line in the user-provided description starting with e.g. "FOO="
692 (whitespace permitted before and around) is considered a tag line. Such
693 lines are stripped out of the description this function returns.
694 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000695 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000696
697 def FullDescriptionText(self):
698 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000699 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700
701 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000702 """Returns the repository (checkout) root directory for this change,
703 as an absolute path.
704 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000705 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706
707 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000708 """Return tags directly as attributes on the object."""
709 if not re.match(r"^[A-Z_]*$", attr):
710 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000711 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000712
713 def AffectedFiles(self, include_dirs=False, include_deletes=True):
714 """Returns a list of AffectedFile instances for all files in the change.
715
716 Args:
717 include_deletes: If false, deleted files will be filtered out.
718 include_dirs: True to include directories in the list
719
720 Returns:
721 [AffectedFile(path, action), AffectedFile(path, action)]
722 """
723 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000724 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000726 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727
728 if include_deletes:
729 return affected
730 else:
731 return filter(lambda x: x.Action() != 'D', affected)
732
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000733 def AffectedTextFiles(self, include_deletes=None):
734 """Return a list of the existing text files in a change."""
735 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000736 warn("AffectedTextFiles(include_deletes=%s)"
737 " is deprecated and ignored" % str(include_deletes),
738 category=DeprecationWarning,
739 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000740 return filter(lambda x: x.IsTextFile(),
741 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742
743 def LocalPaths(self, include_dirs=False):
744 """Convenience function."""
745 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
746
747 def AbsoluteLocalPaths(self, include_dirs=False):
748 """Convenience function."""
749 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
750
751 def ServerPaths(self, include_dirs=False):
752 """Convenience function."""
753 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
754
755 def RightHandSideLines(self):
756 """An iterator over all text lines in "new" version of changed files.
757
758 Lists lines from new or modified text files in the change.
759
760 This is useful for doing line-by-line regex checks, like checking for
761 trailing whitespace.
762
763 Yields:
764 a 3 tuple:
765 the AffectedFile instance of the current file;
766 integer line number (1-based); and
767 the contents of the line as a string.
768 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000769 return _RightHandSideLinesImpl(
770 x for x in self.AffectedFiles(include_deletes=False)
771 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000772
773
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000774class SvnChange(Change):
775 _AFFECTED_FILES = SvnAffectedFile
776
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000777 def __init__(self, *args, **kwargs):
778 Change.__init__(self, *args, **kwargs)
779 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000780 self._changelists = None
781
782 def _GetChangeLists(self):
783 """Get all change lists."""
784 if self._changelists == None:
785 previous_cwd = os.getcwd()
786 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000787 # Need to import here to avoid circular dependency.
788 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000789 self._changelists = gcl.GetModifiedFiles()
790 os.chdir(previous_cwd)
791 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000792
793 def GetAllModifiedFiles(self):
794 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000795 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000796 all_modified_files = []
797 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000798 all_modified_files.extend(
799 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000800 return all_modified_files
801
802 def GetModifiedFiles(self):
803 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000804 changelists = self._GetChangeLists()
805 return [os.path.join(self.RepositoryRoot(), f[1])
806 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000807
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000808
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000809class GitChange(Change):
810 _AFFECTED_FILES = GitAffectedFile
811
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000812 def __init__(self, *args, **kwargs):
813 Change.__init__(self, *args, **kwargs)
814 self.scm = 'git'
815
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000816
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000817def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000818 """Finds all presubmit files that apply to a given set of source files.
819
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000820 If inherit-review-settings-ok is present right under root, looks for
821 PRESUBMIT.py in directories enclosing root.
822
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823 Args:
824 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000825 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000826
827 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000828 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000829 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000830 files = [normpath(os.path.join(root, f)) for f in files]
831
832 # List all the individual directories containing files.
833 directories = set([os.path.dirname(f) for f in files])
834
835 # Ignore root if inherit-review-settings-ok is present.
836 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
837 root = None
838
839 # Collect all unique directories that may contain PRESUBMIT.py.
840 candidates = set()
841 for directory in directories:
842 while True:
843 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000844 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000845 candidates.add(directory)
846 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000847 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000848 parent_dir = os.path.dirname(directory)
849 if parent_dir == directory:
850 # We hit the system root directory.
851 break
852 directory = parent_dir
853
854 # Look for PRESUBMIT.py in all candidate directories.
855 results = []
856 for directory in sorted(list(candidates)):
857 p = os.path.join(directory, 'PRESUBMIT.py')
858 if os.path.isfile(p):
859 results.append(p)
860
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000861 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000862 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000863
864
thestig@chromium.orgde243452009-10-06 21:02:56 +0000865class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000866 @staticmethod
867 def ExecPresubmitScript(script_text):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000868 """Executes GetPreferredTrySlaves() from a single presubmit script.
869
870 Args:
871 script_text: The text of the presubmit script.
872
873 Return:
874 A list of try slaves.
875 """
876 context = {}
877 exec script_text in context
878
879 function_name = 'GetPreferredTrySlaves'
880 if function_name in context:
881 result = eval(function_name + '()', context)
882 if not isinstance(result, types.ListType):
883 raise exceptions.RuntimeError(
884 'Presubmit functions must return a list, got a %s instead: %s' %
885 (type(result), str(result)))
886 for item in result:
887 if not isinstance(item, basestring):
888 raise exceptions.RuntimeError('All try slaves names must be strings.')
889 if item != item.strip():
890 raise exceptions.RuntimeError('Try slave names cannot start/end'
891 'with whitespace')
892 else:
893 result = []
894 return result
895
896
897def DoGetTrySlaves(changed_files,
898 repository_root,
899 default_presubmit,
900 verbose,
901 output_stream):
902 """Get the list of try servers from the presubmit scripts.
903
904 Args:
905 changed_files: List of modified files.
906 repository_root: The repository root.
907 default_presubmit: A default presubmit script to execute in any case.
908 verbose: Prints debug info.
909 output_stream: A stream to write debug output to.
910
911 Return:
912 List of try slaves
913 """
914 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
915 if not presubmit_files and verbose:
916 output_stream.write("Warning, no presubmit.py found.\n")
917 results = []
918 executer = GetTrySlavesExecuter()
919 if default_presubmit:
920 if verbose:
921 output_stream.write("Running default presubmit script.\n")
922 results += executer.ExecPresubmitScript(default_presubmit)
923 for filename in presubmit_files:
924 filename = os.path.abspath(filename)
925 if verbose:
926 output_stream.write("Running %s\n" % filename)
927 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000928 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000929 results += executer.ExecPresubmitScript(presubmit_script)
930
931 slaves = list(set(results))
932 if slaves and verbose:
933 output_stream.write(', '.join(slaves))
934 output_stream.write('\n')
935 return slaves
936
937
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000938class PresubmitExecuter(object):
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000939 def __init__(self, change, committing, tbr, host_url):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940 """
941 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000942 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000943 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000944 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
945 host_url: scheme, host, and path of rietveld instance
946 (or None for default)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000947 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000948 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000949 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000950 self.tbr = tbr
951 self.host_url = host_url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000952
953 def ExecPresubmitScript(self, script_text, presubmit_path):
954 """Executes a single presubmit script.
955
956 Args:
957 script_text: The text of the presubmit script.
958 presubmit_path: The path to the presubmit file (this will be reported via
959 input_api.PresubmitLocalPath()).
960
961 Return:
962 A list of result objects, empty if no problems.
963 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000964
965 # Change to the presubmit file's directory to support local imports.
966 main_path = os.getcwd()
967 os.chdir(os.path.dirname(presubmit_path))
968
969 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000970 input_api = InputApi(self.change, presubmit_path, self.committing,
971 self.tbr, self.host_url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000972 context = {}
973 exec script_text in context
974
975 # These function names must change if we make substantial changes to
976 # the presubmit API that are not backwards compatible.
977 if self.committing:
978 function_name = 'CheckChangeOnCommit'
979 else:
980 function_name = 'CheckChangeOnUpload'
981 if function_name in context:
982 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000983 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000985 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000986 if not (isinstance(result, types.TupleType) or
987 isinstance(result, types.ListType)):
988 raise exceptions.RuntimeError(
989 'Presubmit functions must return a tuple or list')
990 for item in result:
991 if not isinstance(item, OutputApi.PresubmitResult):
992 raise exceptions.RuntimeError(
993 'All presubmit results must be of types derived from '
994 'output_api.PresubmitResult')
995 else:
996 result = () # no error since the script doesn't care about current event.
997
chase@chromium.org8e416c82009-10-06 04:30:44 +0000998 # Return the process to the original working directory.
999 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000 return result
1001
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001002
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001003# TODO(dpranke): make all callers pass in tbr, host_url?
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001004def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001005 committing,
1006 verbose,
1007 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001008 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001009 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001010 may_prompt,
1011 tbr=False,
1012 host_url=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013 """Runs all presubmit checks that apply to the files in the change.
1014
1015 This finds all PRESUBMIT.py files in directories enclosing the files in the
1016 change (up to the repository root) and calls the relevant entrypoint function
1017 depending on whether the change is being committed or uploaded.
1018
1019 Prints errors, warnings and notifications. Prompts the user for warnings
1020 when needed.
1021
1022 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001023 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1025 verbose: Prints debug info.
1026 output_stream: A stream to write output from presubmit tests to.
1027 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001028 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001029 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001030 tbr: was --tbr specified to skip any reviewer/owner checks?
1031 host_url: scheme, host, and port of host to use for rietveld-related
1032 checks
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001034 Warning:
1035 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1036 SHOULD be sys.stdin.
1037
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001039 A PresubmitOutput object. Use output.should_continue() to figure out
1040 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001041 """
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001042 output = PresubmitOutput(input_stream, output_stream)
1043 output.write("Running presubmit hooks...\n")
jam@chromium.org2a891dc2009-08-20 20:33:37 +00001044 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001045 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
1046 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047 if not presubmit_files and verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001048 output.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001049 results = []
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001050 executer = PresubmitExecuter(change, committing, tbr, host_url)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001051 if default_presubmit:
1052 if verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001053 output.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001054 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001055 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001056 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +00001057 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001058 if verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001059 output.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +00001060 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001061 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001062 results += executer.ExecPresubmitScript(presubmit_script, filename)
1063
1064 errors = []
1065 notifications = []
1066 warnings = []
1067 for result in results:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001068 if result.fatal:
1069 errors.append(result)
1070 elif result.should_prompt:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001071 warnings.append(result)
1072 else:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001073 notifications.append(result)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001074
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001075 for name, items in (('Messages', notifications),
1076 ('Warnings', warnings),
1077 ('ERRORS', errors)):
1078 if items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001079 output.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001080 for item in items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001081 item.handle(output)
1082 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001083
1084 total_time = time.time() - start_time
1085 if total_time > 1.0:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001086 output.write("Presubmit checks took %.1fs to calculate.\n" % total_time)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001087
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001088 if not errors and warnings:
1089 if may_prompt:
1090 output.prompt_yes_no('There were presubmit warnings. '
1091 'Are you sure you wish to continue? (y/N): ')
1092 else:
1093 output.fail()
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001094
1095 global _ASKED_FOR_FEEDBACK
1096 # Ask for feedback one time out of 5.
1097 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001098 output.write("Was the presubmit check useful? Please send feedback "
1099 "& hate mail to maruel@chromium.org!\n")
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001100 _ASKED_FOR_FEEDBACK = True
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001101 return output
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001102
1103
1104def ScanSubDirs(mask, recursive):
1105 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001106 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 +00001107 else:
1108 results = []
1109 for root, dirs, files in os.walk('.'):
1110 if '.svn' in dirs:
1111 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001112 if '.git' in dirs:
1113 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001114 for name in files:
1115 if fnmatch.fnmatch(name, mask):
1116 results.append(os.path.join(root, name))
1117 return results
1118
1119
1120def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001121 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001122 files = []
1123 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001124 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001125 return files
1126
1127
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001128def load_files(options, args):
1129 """Tries to determine the SCM."""
1130 change_scm = scm.determine_scm(options.root)
1131 files = []
1132 if change_scm == 'svn':
1133 change_class = SvnChange
1134 status_fn = scm.SVN.CaptureStatus
1135 elif change_scm == 'git':
1136 change_class = GitChange
1137 status_fn = scm.GIT.CaptureStatus
1138 else:
1139 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1140 if not args:
1141 return None, None
1142 change_class = Change
1143 if args:
1144 files = ParseFiles(args, options.recursive)
1145 else:
1146 # Grab modified files.
1147 files = status_fn([options.root])
1148 return change_class, files
1149
1150
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001151def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001152 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001153 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001154 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001155 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001156 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1157 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001158 parser.add_option("-r", "--recursive", action="store_true",
1159 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001160 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161 help="Verbose output")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001162 parser.add_option("--name", default='no name')
1163 parser.add_option("--description", default='')
1164 parser.add_option("--issue", type='int', default=0)
1165 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001166 parser.add_option("--root", default=os.getcwd(),
1167 help="Search for PRESUBMIT.py up to this directory. "
1168 "If inherit-review-settings-ok is present in this "
1169 "directory, parent directories up to the root file "
1170 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001171 parser.add_option("--default_presubmit")
1172 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001173 options, args = parser.parse_args(argv)
maruel@chromium.org7444c502011-02-09 14:02:11 +00001174 if options.verbose:
1175 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001176 change_class, files = load_files(options, args)
1177 if not change_class:
1178 parser.error('For unversioned directory, <files> is not optional.')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001179 if options.verbose:
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001180 print "Found %d file(s)." % len(files)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001181 results = DoPresubmitChecks(change_class(options.name,
1182 options.description,
1183 options.root,
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001184 files,
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001185 options.issue,
1186 options.patchset),
1187 options.commit,
1188 options.verbose,
1189 sys.stdout,
1190 sys.stdin,
1191 options.default_presubmit,
1192 options.may_prompt)
1193 return not results.should_continue()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001194
1195
1196if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001197 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001198 sys.exit(Main(None))