blob: 7ee2b0d1326856a9e8343c7ba1545a3916a31a99 [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00002# Copyright (c) 2010 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00009__version__ = '1.3.5'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
15import cPickle # Exposed through the API.
16import cStringIO # Exposed through the API.
17import exceptions
18import fnmatch
19import glob
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000020import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import marshal # Exposed through the API.
22import optparse
23import os # Somewhat exposed through the API.
24import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000025import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import re # Exposed through the API.
27import subprocess # Exposed through the API.
28import sys # Parts exposed through API.
29import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000030import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000031import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000033import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000035from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000037try:
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000038 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000039except ImportError:
40 try:
41 import json
tony@chromium.org5e9f2ed2010-04-08 01:38:19 +000042 # Some versions of python2.5 have an incomplete json module. Check to make
43 # sure loads exists.
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +000044 # Statement seems to have no effect
45 # pylint: disable=W0104
tony@chromium.org5e9f2ed2010-04-08 01:38:19 +000046 json.loads
47 except (ImportError, AttributeError):
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000048 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000049 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000050 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000051
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052# Local imports.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000053import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000054import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000056import scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000057
58
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000059# Ask for feedback only once in program lifetime.
60_ASKED_FOR_FEEDBACK = False
61
62
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000063class NotImplementedException(Exception):
64 """We're leaving placeholders in a bunch of places to remind us of the
65 design of the API, but we have not implemented all of it yet. Implement as
66 the need arises.
67 """
68 pass
69
70
71def normpath(path):
72 '''Version of os.path.normpath that also changes backward slashes to
73 forward slashes when not running on Windows.
74 '''
75 # This is safe to always do because the Windows version of os.path.normpath
76 # will replace forward slashes with backward slashes.
77 path = path.replace(os.sep, '/')
78 return os.path.normpath(path)
79
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000080
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000081def _RightHandSideLinesImpl(affected_files):
82 """Implements RightHandSideLines for InputApi and GclChange."""
83 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000084 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000085 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000086 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000087
88
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000089class PresubmitOutput(object):
90 def __init__(self, input_stream=None, output_stream=None):
91 self.input_stream = input_stream
92 self.output_stream = output_stream
93 self.reviewers = []
94 self.written_output = []
95 self.error_count = 0
96
97 def prompt_yes_no(self, prompt_string):
98 self.write(prompt_string)
99 if self.input_stream:
100 response = self.input_stream.readline().strip().lower()
101 if response not in ('y', 'yes'):
102 self.fail()
103 else:
104 self.fail()
105
106 def fail(self):
107 self.error_count += 1
108
109 def should_continue(self):
110 return not self.error_count
111
112 def write(self, s):
113 self.written_output.append(s)
114 if self.output_stream:
115 self.output_stream.write(s)
116
117 def getvalue(self):
118 return ''.join(self.written_output)
119
120
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000121class OutputApi(object):
122 """This class (more like a module) gets passed to presubmit scripts so that
123 they can specify various types of results.
124 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000125 class PresubmitResult(object):
126 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000127 fatal = False
128 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000129
130 def __init__(self, message, items=None, long_text=''):
131 """
132 message: A short one-line message to indicate errors.
133 items: A list of short strings to indicate where errors occurred.
134 long_text: multi-line text output, e.g. from another tool
135 """
136 self._message = message
137 self._items = []
138 if items:
139 self._items = items
140 self._long_text = long_text.rstrip()
141
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000142 def handle(self, output):
143 output.write(self._message)
144 output.write('\n')
estade@chromium.org593258b2010-01-28 00:04:36 +0000145 if len(self._items) > 0:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000146 output.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000147 if self._long_text:
maruel@chromium.org310b3552010-11-01 13:23:35 +0000148 # Sometimes self._long_text is a ascii string, a codepage string
149 # (on windows), or a unicode object.
150 try:
151 long_text = self._long_text.decode()
152 except UnicodeDecodeError:
153 long_text = self._long_text.decode('ascii', 'replace')
154
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000155 output.write('\n***************\n%s\n***************\n' %
maruel@chromium.org310b3552010-11-01 13:23:35 +0000156 long_text)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000157 if self.fatal:
158 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000159
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000160 class PresubmitAddReviewers(PresubmitResult):
161 """Add some suggested reviewers to the change."""
162 def __init__(self, reviewers):
163 super(OutputApi.PresubmitAddReviewers, self).__init__('')
164 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000165
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000166 def handle(self, output):
167 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000168
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000169 class PresubmitError(PresubmitResult):
170 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000171 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000172
173 class PresubmitPromptWarning(PresubmitResult):
174 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000175 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000176
177 class PresubmitNotifyResult(PresubmitResult):
178 """Just print something to the screen -- but it's not even a warning."""
179 pass
180
181 class MailTextResult(PresubmitResult):
182 """A warning that should be included in the review request email."""
183 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000184 super(OutputApi.MailTextResult, self).__init__()
185 raise NotImplementedException()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000186
187
188class InputApi(object):
189 """An instance of this object is passed to presubmit scripts so they can
190 know stuff about the change they're looking at.
191 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000192 # Method could be a function
193 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000194
maruel@chromium.org3410d912009-06-09 20:56:16 +0000195 # File extensions that are considered source files from a style guide
196 # perspective. Don't modify this list from a presubmit script!
197 DEFAULT_WHITE_LIST = (
198 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000199 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000200 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000201 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000202 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000203 # No extension at all, note that ALL CAPS files are black listed in
204 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000205 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000206 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000207 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000208 )
209
210 # Path regexp that should be excluded from being considered containing source
211 # files. Don't modify this list from a presubmit script!
212 DEFAULT_BLACK_LIST = (
213 r".*\bexperimental[\\\/].*",
214 r".*\bthird_party[\\\/].*",
215 # Output directories (just in case)
216 r".*\bDebug[\\\/].*",
217 r".*\bRelease[\\\/].*",
218 r".*\bxcodebuild[\\\/].*",
219 r".*\bsconsbuild[\\\/].*",
220 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000221 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000222 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000223 r"(|.*[\\\/])\.git[\\\/].*",
224 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000225 )
226
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000227 # TODO(dpranke): Update callers to pass in tbr, host_url, remove
228 # default arguments.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000229 def __init__(self, change, presubmit_path, is_committing, tbr, host_url=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 """Builds an InputApi object.
231
232 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000233 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000234 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000235 is_committing: True if the change is about to be committed.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000236 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
237 host_url: scheme, host, and path of rietveld instance
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000238 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000239 # Version number of the presubmit_support script.
240 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000241 self.change = change
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000242 self.host_url = host_url
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000243 self.is_committing = is_committing
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000244 self.tbr = tbr
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000245 self.host_url = host_url or 'http://codereview.chromium.org'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000246
247 # We expose various modules and functions as attributes of the input_api
248 # so that presubmit scripts don't have to import them.
249 self.basename = os.path.basename
250 self.cPickle = cPickle
251 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000252 self.json = json
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000253 self.os_path = os.path
254 self.pickle = pickle
255 self.marshal = marshal
256 self.re = re
257 self.subprocess = subprocess
258 self.tempfile = tempfile
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000259 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000260 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000261 self.urllib2 = urllib2
262
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000263 # To easily fork python.
264 self.python_executable = sys.executable
265 self.environ = os.environ
266
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267 # InputApi.platform is the platform you're currently running on.
268 self.platform = sys.platform
269
270 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000271 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272
273 # We carry the canned checks so presubmit scripts can easily use them.
274 self.canned_checks = presubmit_canned_checks
275
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000276 # TODO(dpranke): figure out a list of all approved owners for a repo
277 # in order to be able to handle wildcard OWNERS files?
278 self.owners_db = owners.Database(change.RepositoryRoot(),
279 fopen=file, os_path=self.os_path)
280
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000281 def PresubmitLocalPath(self):
282 """Returns the local path of the presubmit script currently being run.
283
284 This is useful if you don't want to hard-code absolute paths in the
285 presubmit script. For example, It can be used to find another file
286 relative to the PRESUBMIT.py script, so the whole tree can be branched and
287 the presubmit script still works, without editing its content.
288 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000289 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000290
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000291 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000292 """Translate a depot path to a local path (relative to client root).
293
294 Args:
295 Depot path as a string.
296
297 Returns:
298 The local path of the depot path under the user's current client, or None
299 if the file is not mapped.
300
301 Remember to check for the None case and show an appropriate error!
302 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000303 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000304 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305 return local_path
306
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000307 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000308 """Translate a local path to a depot path.
309
310 Args:
311 Local path (relative to current directory, or absolute) as a string.
312
313 Returns:
314 The depot path (SVN URL) of the file if mapped, otherwise None.
315 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000316 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000317 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000318 return depot_path
319
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000320 def AffectedFiles(self, include_dirs=False, include_deletes=True):
321 """Same as input_api.change.AffectedFiles() except only lists files
322 (and optionally directories) in the same directory as the current presubmit
323 script, or subdirectories thereof.
324 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000325 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000326 if len(dir_with_slash) == 1:
327 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000328 return filter(
329 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
330 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000331
332 def LocalPaths(self, include_dirs=False):
333 """Returns local paths of input_api.AffectedFiles()."""
334 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
335
336 def AbsoluteLocalPaths(self, include_dirs=False):
337 """Returns absolute local paths of input_api.AffectedFiles()."""
338 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
339
340 def ServerPaths(self, include_dirs=False):
341 """Returns server paths of input_api.AffectedFiles()."""
342 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
343
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000344 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345 """Same as input_api.change.AffectedTextFiles() except only lists files
346 in the same directory as the current presubmit script, or subdirectories
347 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000348 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000349 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000350 warn("AffectedTextFiles(include_deletes=%s)"
351 " is deprecated and ignored" % str(include_deletes),
352 category=DeprecationWarning,
353 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000354 return filter(lambda x: x.IsTextFile(),
355 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000356
maruel@chromium.org3410d912009-06-09 20:56:16 +0000357 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
358 """Filters out files that aren't considered "source file".
359
360 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
361 and InputApi.DEFAULT_BLACK_LIST is used respectively.
362
363 The lists will be compiled as regular expression and
364 AffectedFile.LocalPath() needs to pass both list.
365
366 Note: Copy-paste this function to suit your needs or use a lambda function.
367 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000368 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000369 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000370 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000371 if self.re.match(item, local_path):
372 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000373 return True
374 return False
375 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
376 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
377
378 def AffectedSourceFiles(self, source_file):
379 """Filter the list of AffectedTextFiles by the function source_file.
380
381 If source_file is None, InputApi.FilterSourceFile() is used.
382 """
383 if not source_file:
384 source_file = self.FilterSourceFile
385 return filter(source_file, self.AffectedTextFiles())
386
387 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000388 """An iterator over all text lines in "new" version of changed files.
389
390 Only lists lines from new or modified text files in the change that are
391 contained by the directory of the currently executing presubmit script.
392
393 This is useful for doing line-by-line regex checks, like checking for
394 trailing whitespace.
395
396 Yields:
397 a 3 tuple:
398 the AffectedFile instance of the current file;
399 integer line number (1-based); and
400 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000401
402 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000403 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000404 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000405 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000406
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000407 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000408 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000409
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000410 Deny reading anything outside the repository.
411 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000412 if isinstance(file_item, AffectedFile):
413 file_item = file_item.AbsoluteLocalPath()
414 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000415 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000416 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000417
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000418
419class AffectedFile(object):
420 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000421 # Method could be a function
422 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000423 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000424 self._path = path
425 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000426 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000427 self._is_directory = None
428 self._properties = {}
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000429 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000430
431 def ServerPath(self):
432 """Returns a path string that identifies the file in the SCM system.
433
434 Returns the empty string if the file does not exist in SCM.
435 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000436 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000437
438 def LocalPath(self):
439 """Returns the path of this file on the local disk relative to client root.
440 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000441 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000442
443 def AbsoluteLocalPath(self):
444 """Returns the absolute path of this file on the local disk.
445 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000446 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000447
448 def IsDirectory(self):
449 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000450 if self._is_directory is None:
451 path = self.AbsoluteLocalPath()
452 self._is_directory = (os.path.exists(path) and
453 os.path.isdir(path))
454 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455
456 def Action(self):
457 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000458 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
459 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000460 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000461
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000462 def Property(self, property_name):
463 """Returns the specified SCM property of this file, or None if no such
464 property.
465 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000466 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000467
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000468 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000469 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000470
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000471 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000472 raise NotImplementedError() # Implement when needed
473
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000474 def NewContents(self):
475 """Returns an iterator over the lines in the new version of file.
476
477 The new version is the file in the user's workspace, i.e. the "right hand
478 side".
479
480 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000481 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000482 """
483 if self.IsDirectory():
484 return []
485 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000486 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
487 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000488
489 def OldContents(self):
490 """Returns an iterator over the lines in the old version of file.
491
492 The old version is the file in depot, i.e. the "left hand side".
493 """
494 raise NotImplementedError() # Implement when needed
495
496 def OldFileTempPath(self):
497 """Returns the path on local disk where the old contents resides.
498
499 The old version is the file in depot, i.e. the "left hand side".
500 This is a read-only cached copy of the old contents. *DO NOT* try to
501 modify this file.
502 """
503 raise NotImplementedError() # Implement if/when needed.
504
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000505 def ChangedContents(self):
506 """Returns a list of tuples (line number, line text) of all new lines.
507
508 This relies on the scm diff output describing each changed code section
509 with a line of the form
510
511 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
512 """
513 new_lines = []
514 line_num = 0
515
516 if self.IsDirectory():
517 return []
518
519 for line in self.GenerateScmDiff().splitlines():
520 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
521 if m:
522 line_num = int(m.groups(1)[0])
523 continue
524 if line.startswith('+') and not line.startswith('++'):
525 new_lines.append((line_num, line[1:]))
526 if not line.startswith('-'):
527 line_num += 1
528 return new_lines
529
maruel@chromium.org5de13972009-06-10 18:16:06 +0000530 def __str__(self):
531 return self.LocalPath()
532
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000533 def GenerateScmDiff(self):
534 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000535
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000536class SvnAffectedFile(AffectedFile):
537 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000538 # Method 'NNN' is abstract in class 'NNN' but is not overridden
539 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000540
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000541 def __init__(self, *args, **kwargs):
542 AffectedFile.__init__(self, *args, **kwargs)
543 self._server_path = None
544 self._is_text_file = None
545
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000546 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000547 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000548 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000549 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000550 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000551
552 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000553 if self._is_directory is None:
554 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000555 if os.path.exists(path):
556 # Retrieve directly from the file system; it is much faster than
557 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000558 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000559 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000560 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000561 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000562 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000563
564 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000565 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000566 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000567 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000568 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000569
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000570 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000571 if self._is_text_file is None:
572 if self.Action() == 'D':
573 # A deleted file is not a text file.
574 self._is_text_file = False
575 elif self.IsDirectory():
576 self._is_text_file = False
577 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000578 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
579 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000580 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
581 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000582
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000583 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000584 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
585
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000586
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000587class GitAffectedFile(AffectedFile):
588 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000589 # Method 'NNN' is abstract in class 'NNN' but is not overridden
590 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000591
592 def __init__(self, *args, **kwargs):
593 AffectedFile.__init__(self, *args, **kwargs)
594 self._server_path = None
595 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000596
597 def ServerPath(self):
598 if self._server_path is None:
599 raise NotImplementedException() # TODO(maruel) Implement.
600 return self._server_path
601
602 def IsDirectory(self):
603 if self._is_directory is None:
604 path = self.AbsoluteLocalPath()
605 if os.path.exists(path):
606 # Retrieve directly from the file system; it is much faster than
607 # querying subversion, especially on Windows.
608 self._is_directory = os.path.isdir(path)
609 else:
610 # raise NotImplementedException() # TODO(maruel) Implement.
611 self._is_directory = False
612 return self._is_directory
613
614 def Property(self, property_name):
615 if not property_name in self._properties:
616 raise NotImplementedException() # TODO(maruel) Implement.
617 return self._properties[property_name]
618
619 def IsTextFile(self):
620 if self._is_text_file is None:
621 if self.Action() == 'D':
622 # A deleted file is not a text file.
623 self._is_text_file = False
624 elif self.IsDirectory():
625 self._is_text_file = False
626 else:
627 # raise NotImplementedException() # TODO(maruel) Implement.
628 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
629 return self._is_text_file
630
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000631 def GenerateScmDiff(self):
632 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000633
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000634class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000635 """Describe a change.
636
637 Used directly by the presubmit scripts to query the current change being
638 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000639
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000640 Instance members:
641 tags: Dictionnary of KEY=VALUE pairs found in the change description.
642 self.KEY: equivalent to tags['KEY']
643 """
644
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000645 _AFFECTED_FILES = AffectedFile
646
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000647 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000648 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000649 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000650
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000651 def __init__(self, name, description, local_root, files, issue, patchset):
652 if files is None:
653 files = []
654 self._name = name
655 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000656 # Convert root into an absolute path.
657 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000658 self.issue = issue
659 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000660 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661
662 # From the description text, build up a dictionary of key/value pairs
663 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000664 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000665 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000666 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000667 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668 if m:
669 self.tags[m.group('key')] = m.group('value')
670 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000671 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672
673 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000674 self._description_without_tags = (
675 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000677 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000678 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
679 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000680 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000682 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000684 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000686 def DescriptionText(self):
687 """Returns the user-entered changelist description, minus tags.
688
689 Any line in the user-provided description starting with e.g. "FOO="
690 (whitespace permitted before and around) is considered a tag line. Such
691 lines are stripped out of the description this function returns.
692 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000693 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694
695 def FullDescriptionText(self):
696 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000697 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698
699 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000700 """Returns the repository (checkout) root directory for this change,
701 as an absolute path.
702 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000703 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704
705 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000706 """Return tags directly as attributes on the object."""
707 if not re.match(r"^[A-Z_]*$", attr):
708 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000709 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000710
711 def AffectedFiles(self, include_dirs=False, include_deletes=True):
712 """Returns a list of AffectedFile instances for all files in the change.
713
714 Args:
715 include_deletes: If false, deleted files will be filtered out.
716 include_dirs: True to include directories in the list
717
718 Returns:
719 [AffectedFile(path, action), AffectedFile(path, action)]
720 """
721 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000722 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000723 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000724 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725
726 if include_deletes:
727 return affected
728 else:
729 return filter(lambda x: x.Action() != 'D', affected)
730
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000731 def AffectedTextFiles(self, include_deletes=None):
732 """Return a list of the existing text files in a change."""
733 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000734 warn("AffectedTextFiles(include_deletes=%s)"
735 " is deprecated and ignored" % str(include_deletes),
736 category=DeprecationWarning,
737 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000738 return filter(lambda x: x.IsTextFile(),
739 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000740
741 def LocalPaths(self, include_dirs=False):
742 """Convenience function."""
743 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
744
745 def AbsoluteLocalPaths(self, include_dirs=False):
746 """Convenience function."""
747 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
748
749 def ServerPaths(self, include_dirs=False):
750 """Convenience function."""
751 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
752
753 def RightHandSideLines(self):
754 """An iterator over all text lines in "new" version of changed files.
755
756 Lists lines from new or modified text files in the change.
757
758 This is useful for doing line-by-line regex checks, like checking for
759 trailing whitespace.
760
761 Yields:
762 a 3 tuple:
763 the AffectedFile instance of the current file;
764 integer line number (1-based); and
765 the contents of the line as a string.
766 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000767 return _RightHandSideLinesImpl(
768 x for x in self.AffectedFiles(include_deletes=False)
769 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000770
771
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000772class SvnChange(Change):
773 _AFFECTED_FILES = SvnAffectedFile
774
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000775 def __init__(self, *args, **kwargs):
776 Change.__init__(self, *args, **kwargs)
777 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000778 self._changelists = None
779
780 def _GetChangeLists(self):
781 """Get all change lists."""
782 if self._changelists == None:
783 previous_cwd = os.getcwd()
784 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000785 # Need to import here to avoid circular dependency.
786 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000787 self._changelists = gcl.GetModifiedFiles()
788 os.chdir(previous_cwd)
789 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000790
791 def GetAllModifiedFiles(self):
792 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000793 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000794 all_modified_files = []
795 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000796 all_modified_files.extend(
797 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000798 return all_modified_files
799
800 def GetModifiedFiles(self):
801 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000802 changelists = self._GetChangeLists()
803 return [os.path.join(self.RepositoryRoot(), f[1])
804 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000805
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000806
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000807class GitChange(Change):
808 _AFFECTED_FILES = GitAffectedFile
809
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000810 def __init__(self, *args, **kwargs):
811 Change.__init__(self, *args, **kwargs)
812 self.scm = 'git'
813
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000814
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000815def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000816 """Finds all presubmit files that apply to a given set of source files.
817
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000818 If inherit-review-settings-ok is present right under root, looks for
819 PRESUBMIT.py in directories enclosing root.
820
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000821 Args:
822 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000823 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000824
825 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000826 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000828 files = [normpath(os.path.join(root, f)) for f in files]
829
830 # List all the individual directories containing files.
831 directories = set([os.path.dirname(f) for f in files])
832
833 # Ignore root if inherit-review-settings-ok is present.
834 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
835 root = None
836
837 # Collect all unique directories that may contain PRESUBMIT.py.
838 candidates = set()
839 for directory in directories:
840 while True:
841 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000843 candidates.add(directory)
844 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000845 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000846 parent_dir = os.path.dirname(directory)
847 if parent_dir == directory:
848 # We hit the system root directory.
849 break
850 directory = parent_dir
851
852 # Look for PRESUBMIT.py in all candidate directories.
853 results = []
854 for directory in sorted(list(candidates)):
855 p = os.path.join(directory, 'PRESUBMIT.py')
856 if os.path.isfile(p):
857 results.append(p)
858
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000859 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000860 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000861
862
thestig@chromium.orgde243452009-10-06 21:02:56 +0000863class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000864 @staticmethod
865 def ExecPresubmitScript(script_text):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000866 """Executes GetPreferredTrySlaves() from a single presubmit script.
867
868 Args:
869 script_text: The text of the presubmit script.
870
871 Return:
872 A list of try slaves.
873 """
874 context = {}
875 exec script_text in context
876
877 function_name = 'GetPreferredTrySlaves'
878 if function_name in context:
879 result = eval(function_name + '()', context)
880 if not isinstance(result, types.ListType):
881 raise exceptions.RuntimeError(
882 'Presubmit functions must return a list, got a %s instead: %s' %
883 (type(result), str(result)))
884 for item in result:
885 if not isinstance(item, basestring):
886 raise exceptions.RuntimeError('All try slaves names must be strings.')
887 if item != item.strip():
888 raise exceptions.RuntimeError('Try slave names cannot start/end'
889 'with whitespace')
890 else:
891 result = []
892 return result
893
894
895def DoGetTrySlaves(changed_files,
896 repository_root,
897 default_presubmit,
898 verbose,
899 output_stream):
900 """Get the list of try servers from the presubmit scripts.
901
902 Args:
903 changed_files: List of modified files.
904 repository_root: The repository root.
905 default_presubmit: A default presubmit script to execute in any case.
906 verbose: Prints debug info.
907 output_stream: A stream to write debug output to.
908
909 Return:
910 List of try slaves
911 """
912 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
913 if not presubmit_files and verbose:
914 output_stream.write("Warning, no presubmit.py found.\n")
915 results = []
916 executer = GetTrySlavesExecuter()
917 if default_presubmit:
918 if verbose:
919 output_stream.write("Running default presubmit script.\n")
920 results += executer.ExecPresubmitScript(default_presubmit)
921 for filename in presubmit_files:
922 filename = os.path.abspath(filename)
923 if verbose:
924 output_stream.write("Running %s\n" % filename)
925 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000926 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000927 results += executer.ExecPresubmitScript(presubmit_script)
928
929 slaves = list(set(results))
930 if slaves and verbose:
931 output_stream.write(', '.join(slaves))
932 output_stream.write('\n')
933 return slaves
934
935
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000936class PresubmitExecuter(object):
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000937 def __init__(self, change, committing, tbr, host_url):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000938 """
939 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000940 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000942 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
943 host_url: scheme, host, and path of rietveld instance
944 (or None for default)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000945 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000946 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000947 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000948 self.tbr = tbr
949 self.host_url = host_url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000950
951 def ExecPresubmitScript(self, script_text, presubmit_path):
952 """Executes a single presubmit script.
953
954 Args:
955 script_text: The text of the presubmit script.
956 presubmit_path: The path to the presubmit file (this will be reported via
957 input_api.PresubmitLocalPath()).
958
959 Return:
960 A list of result objects, empty if no problems.
961 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000962
963 # Change to the presubmit file's directory to support local imports.
964 main_path = os.getcwd()
965 os.chdir(os.path.dirname(presubmit_path))
966
967 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000968 input_api = InputApi(self.change, presubmit_path, self.committing,
969 self.tbr, self.host_url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000970 context = {}
971 exec script_text in context
972
973 # These function names must change if we make substantial changes to
974 # the presubmit API that are not backwards compatible.
975 if self.committing:
976 function_name = 'CheckChangeOnCommit'
977 else:
978 function_name = 'CheckChangeOnUpload'
979 if function_name in context:
980 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000981 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000982 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000983 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984 if not (isinstance(result, types.TupleType) or
985 isinstance(result, types.ListType)):
986 raise exceptions.RuntimeError(
987 'Presubmit functions must return a tuple or list')
988 for item in result:
989 if not isinstance(item, OutputApi.PresubmitResult):
990 raise exceptions.RuntimeError(
991 'All presubmit results must be of types derived from '
992 'output_api.PresubmitResult')
993 else:
994 result = () # no error since the script doesn't care about current event.
995
chase@chromium.org8e416c82009-10-06 04:30:44 +0000996 # Return the process to the original working directory.
997 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 return result
999
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001000
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001001# TODO(dpranke): make all callers pass in tbr, host_url?
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001002def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001003 committing,
1004 verbose,
1005 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001006 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001007 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001008 may_prompt,
1009 tbr=False,
1010 host_url=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001011 """Runs all presubmit checks that apply to the files in the change.
1012
1013 This finds all PRESUBMIT.py files in directories enclosing the files in the
1014 change (up to the repository root) and calls the relevant entrypoint function
1015 depending on whether the change is being committed or uploaded.
1016
1017 Prints errors, warnings and notifications. Prompts the user for warnings
1018 when needed.
1019
1020 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001021 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001022 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1023 verbose: Prints debug info.
1024 output_stream: A stream to write output from presubmit tests to.
1025 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001026 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001027 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001028 tbr: was --tbr specified to skip any reviewer/owner checks?
1029 host_url: scheme, host, and port of host to use for rietveld-related
1030 checks
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001031
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001032 Warning:
1033 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1034 SHOULD be sys.stdin.
1035
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001037 A PresubmitOutput object. Use output.should_continue() to figure out
1038 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001039 """
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001040 output = PresubmitOutput(input_stream, output_stream)
1041 output.write("Running presubmit hooks...\n")
jam@chromium.org2a891dc2009-08-20 20:33:37 +00001042 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001043 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
1044 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001045 if not presubmit_files and verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001046 output.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047 results = []
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001048 executer = PresubmitExecuter(change, committing, tbr, host_url)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001049 if default_presubmit:
1050 if verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001051 output.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001052 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001053 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001054 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +00001055 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001056 if verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001057 output.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +00001058 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001059 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001060 results += executer.ExecPresubmitScript(presubmit_script, filename)
1061
1062 errors = []
1063 notifications = []
1064 warnings = []
1065 for result in results:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001066 if result.fatal:
1067 errors.append(result)
1068 elif result.should_prompt:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001069 warnings.append(result)
1070 else:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001071 notifications.append(result)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001072
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001073 for name, items in (('Messages', notifications),
1074 ('Warnings', warnings),
1075 ('ERRORS', errors)):
1076 if items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001077 output.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001078 for item in items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001079 item.handle(output)
1080 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001081
1082 total_time = time.time() - start_time
1083 if total_time > 1.0:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001084 output.write("Presubmit checks took %.1fs to calculate.\n" % total_time)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001085
maruel@chromium.org07bbc212009-06-11 02:08:41 +00001086 if not errors and warnings and may_prompt:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001087 output.prompt_yes_no('There were presubmit warnings. '
1088 'Are you sure you wish to continue? (y/N): ')
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001089
1090 global _ASKED_FOR_FEEDBACK
1091 # Ask for feedback one time out of 5.
1092 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001093 output.write("Was the presubmit check useful? Please send feedback "
1094 "& hate mail to maruel@chromium.org!\n")
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001095 _ASKED_FOR_FEEDBACK = True
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001096 return output
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001097
1098
1099def ScanSubDirs(mask, recursive):
1100 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001101 return [x for x in glob.glob(mask) if '.svn' not in x and '.git' not in x]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001102 else:
1103 results = []
1104 for root, dirs, files in os.walk('.'):
1105 if '.svn' in dirs:
1106 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001107 if '.git' in dirs:
1108 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001109 for name in files:
1110 if fnmatch.fnmatch(name, mask):
1111 results.append(os.path.join(root, name))
1112 return results
1113
1114
1115def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001116 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001117 files = []
1118 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001119 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001120 return files
1121
1122
1123def Main(argv):
1124 parser = optparse.OptionParser(usage="%prog [options]",
1125 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001126 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001127 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001128 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1129 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001130 parser.add_option("-r", "--recursive", action="store_true",
1131 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001132 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001133 help="Verbose output")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001134 parser.add_option("--files")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001135 parser.add_option("--name", default='no name')
1136 parser.add_option("--description", default='')
1137 parser.add_option("--issue", type='int', default=0)
1138 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001139 parser.add_option("--root", default=os.getcwd(),
1140 help="Search for PRESUBMIT.py up to this directory. "
1141 "If inherit-review-settings-ok is present in this "
1142 "directory, parent directories up to the root file "
1143 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001144 parser.add_option("--default_presubmit")
1145 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001146 options, args = parser.parse_args(argv[1:])
maruel@chromium.org7444c502011-02-09 14:02:11 +00001147 if options.verbose:
1148 logging.basicConfig(level=logging.DEBUG)
1149 if os.path.isdir(os.path.join(options.root, '.svn')):
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001150 change_class = SvnChange
1151 if not options.files:
1152 if args:
1153 options.files = ParseFiles(args, options.recursive)
1154 else:
1155 # Grab modified files.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001156 options.files = scm.SVN.CaptureStatus([options.root])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001157 else:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001158 is_git = os.path.isdir(os.path.join(options.root, '.git'))
1159 if not is_git:
1160 is_git = (0 == subprocess.call(
1161 ['git', 'rev-parse', '--show-cdup'],
1162 stdout=subprocess.PIPE, cwd=options.root))
1163 if is_git:
1164 # Only look at the subdirectories below cwd.
1165 change_class = GitChange
1166 if not options.files:
1167 if args:
1168 options.files = ParseFiles(args, options.recursive)
1169 else:
1170 # Grab modified files.
1171 options.files = scm.GIT.CaptureStatus([options.root])
1172 else:
1173 logging.info('Doesn\'t seem under source control.')
1174 change_class = Change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001175 if options.verbose:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001176 if not options.files:
1177 print "Found no files."
1178 elif len(options.files) != 1:
chase@chromium.org8e416c82009-10-06 04:30:44 +00001179 print "Found %d files." % len(options.files)
1180 else:
1181 print "Found 1 file."
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001182 results = DoPresubmitChecks(change_class(options.name,
1183 options.description,
1184 options.root,
1185 options.files,
1186 options.issue,
1187 options.patchset),
1188 options.commit,
1189 options.verbose,
1190 sys.stdout,
1191 sys.stdin,
1192 options.default_presubmit,
1193 options.may_prompt)
1194 return not results.should_continue()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001195
1196
1197if __name__ == '__main__':
1198 sys.exit(Main(sys.argv))