blob: fcc6a8555c036a4a469958cb1fa12e5d19f9c7eb [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
maruel@chromium.org2b5ce562011-03-31 16:15:44 +00009__version__ = '1.5'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
15import cPickle # Exposed through the API.
16import cStringIO # Exposed through the API.
17import exceptions
18import fnmatch
19import glob
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000020import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000021import marshal # Exposed through the API.
22import optparse
23import os # Somewhat exposed through the API.
24import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000025import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import re # Exposed through the API.
27import subprocess # Exposed through the API.
28import sys # Parts exposed through API.
29import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000030import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000031import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000033import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000035from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000037try:
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000038 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000039except ImportError:
40 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000041 import json # pylint: disable=F0401
42 except ImportError:
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000043 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000044 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000045 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000046
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000047# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000048import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000049import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000050import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000051import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000052import scm
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
54
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000055# Ask for feedback only once in program lifetime.
56_ASKED_FOR_FEEDBACK = False
57
58
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059class NotImplementedException(Exception):
60 """We're leaving placeholders in a bunch of places to remind us of the
61 design of the API, but we have not implemented all of it yet. Implement as
62 the need arises.
63 """
64 pass
65
66
67def normpath(path):
68 '''Version of os.path.normpath that also changes backward slashes to
69 forward slashes when not running on Windows.
70 '''
71 # This is safe to always do because the Windows version of os.path.normpath
72 # will replace forward slashes with backward slashes.
73 path = path.replace(os.sep, '/')
74 return os.path.normpath(path)
75
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000076
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000077def _RightHandSideLinesImpl(affected_files):
78 """Implements RightHandSideLines for InputApi and GclChange."""
79 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000080 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000081 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000082 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000083
84
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000085class PresubmitOutput(object):
86 def __init__(self, input_stream=None, output_stream=None):
87 self.input_stream = input_stream
88 self.output_stream = output_stream
89 self.reviewers = []
90 self.written_output = []
91 self.error_count = 0
92
93 def prompt_yes_no(self, prompt_string):
94 self.write(prompt_string)
95 if self.input_stream:
96 response = self.input_stream.readline().strip().lower()
97 if response not in ('y', 'yes'):
98 self.fail()
99 else:
100 self.fail()
101
102 def fail(self):
103 self.error_count += 1
104
105 def should_continue(self):
106 return not self.error_count
107
108 def write(self, s):
109 self.written_output.append(s)
110 if self.output_stream:
111 self.output_stream.write(s)
112
113 def getvalue(self):
114 return ''.join(self.written_output)
115
116
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000117class OutputApi(object):
118 """This class (more like a module) gets passed to presubmit scripts so that
119 they can specify various types of results.
120 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000121 class PresubmitResult(object):
122 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000123 fatal = False
124 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000125
126 def __init__(self, message, items=None, long_text=''):
127 """
128 message: A short one-line message to indicate errors.
129 items: A list of short strings to indicate where errors occurred.
130 long_text: multi-line text output, e.g. from another tool
131 """
132 self._message = message
133 self._items = []
134 if items:
135 self._items = items
136 self._long_text = long_text.rstrip()
137
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000138 def handle(self, output):
139 output.write(self._message)
140 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000141 for index, item in enumerate(self._items):
142 output.write(' ')
143 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000144 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000145 if index < len(self._items) - 1:
146 output.write(' \\')
147 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000148 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000149 output.write('\n***************\n')
150 # Write separately in case it's unicode.
151 output.write(self._long_text)
152 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000153 if self.fatal:
154 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000155
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000156 class PresubmitAddReviewers(PresubmitResult):
157 """Add some suggested reviewers to the change."""
158 def __init__(self, reviewers):
159 super(OutputApi.PresubmitAddReviewers, self).__init__('')
160 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000161
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000162 def handle(self, output):
163 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000164
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000165 class PresubmitError(PresubmitResult):
166 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000167 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000168
169 class PresubmitPromptWarning(PresubmitResult):
170 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000171 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000172
173 class PresubmitNotifyResult(PresubmitResult):
174 """Just print something to the screen -- but it's not even a warning."""
175 pass
176
177 class MailTextResult(PresubmitResult):
178 """A warning that should be included in the review request email."""
179 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000180 super(OutputApi.MailTextResult, self).__init__()
181 raise NotImplementedException()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000182
183
184class InputApi(object):
185 """An instance of this object is passed to presubmit scripts so they can
186 know stuff about the change they're looking at.
187 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000188 # Method could be a function
189 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000190
maruel@chromium.org3410d912009-06-09 20:56:16 +0000191 # File extensions that are considered source files from a style guide
192 # perspective. Don't modify this list from a presubmit script!
193 DEFAULT_WHITE_LIST = (
194 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000195 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000196 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000197 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000198 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000199 # No extension at all, note that ALL CAPS files are black listed in
200 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000201 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000202 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000203 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000204 )
205
206 # Path regexp that should be excluded from being considered containing source
207 # files. Don't modify this list from a presubmit script!
208 DEFAULT_BLACK_LIST = (
209 r".*\bexperimental[\\\/].*",
210 r".*\bthird_party[\\\/].*",
211 # Output directories (just in case)
212 r".*\bDebug[\\\/].*",
213 r".*\bRelease[\\\/].*",
214 r".*\bxcodebuild[\\\/].*",
215 r".*\bsconsbuild[\\\/].*",
216 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000217 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000218 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000219 r"(|.*[\\\/])\.git[\\\/].*",
220 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000221 )
222
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000223 # TODO(dpranke): Update callers to pass in tbr, host_url, remove
224 # default arguments.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000225 def __init__(self, change, presubmit_path, is_committing, tbr, host_url=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226 """Builds an InputApi object.
227
228 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000229 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000231 is_committing: True if the change is about to be committed.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000232 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
233 host_url: scheme, host, and path of rietveld instance
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000234 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000235 # Version number of the presubmit_support script.
236 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000237 self.change = change
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000238 self.host_url = host_url
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000239 self.is_committing = is_committing
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000240 self.tbr = tbr
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000241 self.host_url = host_url or 'http://codereview.chromium.org'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000242
243 # We expose various modules and functions as attributes of the input_api
244 # so that presubmit scripts don't have to import them.
245 self.basename = os.path.basename
246 self.cPickle = cPickle
247 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000248 self.json = json
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000249 self.os_listdir = os.listdir
250 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000251 self.os_path = os.path
252 self.pickle = pickle
253 self.marshal = marshal
254 self.re = re
255 self.subprocess = subprocess
256 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000257 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000258 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000259 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000260 self.urllib2 = urllib2
261
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000262 # To easily fork python.
263 self.python_executable = sys.executable
264 self.environ = os.environ
265
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000266 # InputApi.platform is the platform you're currently running on.
267 self.platform = sys.platform
268
269 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000270 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271
272 # We carry the canned checks so presubmit scripts can easily use them.
273 self.canned_checks = presubmit_canned_checks
274
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000275 # TODO(dpranke): figure out a list of all approved owners for a repo
276 # in order to be able to handle wildcard OWNERS files?
277 self.owners_db = owners.Database(change.RepositoryRoot(),
278 fopen=file, os_path=self.os_path)
279
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000280 def PresubmitLocalPath(self):
281 """Returns the local path of the presubmit script currently being run.
282
283 This is useful if you don't want to hard-code absolute paths in the
284 presubmit script. For example, It can be used to find another file
285 relative to the PRESUBMIT.py script, so the whole tree can be branched and
286 the presubmit script still works, without editing its content.
287 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000288 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000290 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000291 """Translate a depot path to a local path (relative to client root).
292
293 Args:
294 Depot path as a string.
295
296 Returns:
297 The local path of the depot path under the user's current client, or None
298 if the file is not mapped.
299
300 Remember to check for the None case and show an appropriate error!
301 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000302 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000303 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000304 return local_path
305
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000306 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000307 """Translate a local path to a depot path.
308
309 Args:
310 Local path (relative to current directory, or absolute) as a string.
311
312 Returns:
313 The depot path (SVN URL) of the file if mapped, otherwise None.
314 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000315 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000316 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000317 return depot_path
318
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000319 def AffectedFiles(self, include_dirs=False, include_deletes=True):
320 """Same as input_api.change.AffectedFiles() except only lists files
321 (and optionally directories) in the same directory as the current presubmit
322 script, or subdirectories thereof.
323 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000324 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000325 if len(dir_with_slash) == 1:
326 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000327 return filter(
328 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
329 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000330
331 def LocalPaths(self, include_dirs=False):
332 """Returns local paths of input_api.AffectedFiles()."""
333 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
334
335 def AbsoluteLocalPaths(self, include_dirs=False):
336 """Returns absolute local paths of input_api.AffectedFiles()."""
337 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
338
339 def ServerPaths(self, include_dirs=False):
340 """Returns server paths of input_api.AffectedFiles()."""
341 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
342
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000343 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344 """Same as input_api.change.AffectedTextFiles() except only lists files
345 in the same directory as the current presubmit script, or subdirectories
346 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000348 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000349 warn("AffectedTextFiles(include_deletes=%s)"
350 " is deprecated and ignored" % str(include_deletes),
351 category=DeprecationWarning,
352 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000353 return filter(lambda x: x.IsTextFile(),
354 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000355
maruel@chromium.org3410d912009-06-09 20:56:16 +0000356 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
357 """Filters out files that aren't considered "source file".
358
359 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
360 and InputApi.DEFAULT_BLACK_LIST is used respectively.
361
362 The lists will be compiled as regular expression and
363 AffectedFile.LocalPath() needs to pass both list.
364
365 Note: Copy-paste this function to suit your needs or use a lambda function.
366 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000367 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000368 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000369 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000370 if self.re.match(item, local_path):
371 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000372 return True
373 return False
374 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
375 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
376
377 def AffectedSourceFiles(self, source_file):
378 """Filter the list of AffectedTextFiles by the function source_file.
379
380 If source_file is None, InputApi.FilterSourceFile() is used.
381 """
382 if not source_file:
383 source_file = self.FilterSourceFile
384 return filter(source_file, self.AffectedTextFiles())
385
386 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000387 """An iterator over all text lines in "new" version of changed files.
388
389 Only lists lines from new or modified text files in the change that are
390 contained by the directory of the currently executing presubmit script.
391
392 This is useful for doing line-by-line regex checks, like checking for
393 trailing whitespace.
394
395 Yields:
396 a 3 tuple:
397 the AffectedFile instance of the current file;
398 integer line number (1-based); and
399 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000400
401 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000403 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000404 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000406 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000407 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000408
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000409 Deny reading anything outside the repository.
410 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000411 if isinstance(file_item, AffectedFile):
412 file_item = file_item.AbsoluteLocalPath()
413 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000414 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000415 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000416
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417
418class AffectedFile(object):
419 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000420 # Method could be a function
421 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000422 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000423 self._path = path
424 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000425 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000426 self._is_directory = None
427 self._properties = {}
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000428 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000429
430 def ServerPath(self):
431 """Returns a path string that identifies the file in the SCM system.
432
433 Returns the empty string if the file does not exist in SCM.
434 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000435 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000436
437 def LocalPath(self):
438 """Returns the path of this file on the local disk relative to client root.
439 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000440 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000441
442 def AbsoluteLocalPath(self):
443 """Returns the absolute path of this file on the local disk.
444 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000445 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000446
447 def IsDirectory(self):
448 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000449 if self._is_directory is None:
450 path = self.AbsoluteLocalPath()
451 self._is_directory = (os.path.exists(path) and
452 os.path.isdir(path))
453 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454
455 def Action(self):
456 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000457 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
458 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000459 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000460
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000461 def Property(self, property_name):
462 """Returns the specified SCM property of this file, or None if no such
463 property.
464 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000465 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000466
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000467 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000468 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000469
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000470 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000471 raise NotImplementedError() # Implement when needed
472
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000473 def NewContents(self):
474 """Returns an iterator over the lines in the new version of file.
475
476 The new version is the file in the user's workspace, i.e. the "right hand
477 side".
478
479 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000480 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000481 """
482 if self.IsDirectory():
483 return []
484 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000485 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
486 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000487
488 def OldContents(self):
489 """Returns an iterator over the lines in the old version of file.
490
491 The old version is the file in depot, i.e. the "left hand side".
492 """
493 raise NotImplementedError() # Implement when needed
494
495 def OldFileTempPath(self):
496 """Returns the path on local disk where the old contents resides.
497
498 The old version is the file in depot, i.e. the "left hand side".
499 This is a read-only cached copy of the old contents. *DO NOT* try to
500 modify this file.
501 """
502 raise NotImplementedError() # Implement if/when needed.
503
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000504 def ChangedContents(self):
505 """Returns a list of tuples (line number, line text) of all new lines.
506
507 This relies on the scm diff output describing each changed code section
508 with a line of the form
509
510 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
511 """
512 new_lines = []
513 line_num = 0
514
515 if self.IsDirectory():
516 return []
517
518 for line in self.GenerateScmDiff().splitlines():
519 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
520 if m:
521 line_num = int(m.groups(1)[0])
522 continue
523 if line.startswith('+') and not line.startswith('++'):
524 new_lines.append((line_num, line[1:]))
525 if not line.startswith('-'):
526 line_num += 1
527 return new_lines
528
maruel@chromium.org5de13972009-06-10 18:16:06 +0000529 def __str__(self):
530 return self.LocalPath()
531
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000532 def GenerateScmDiff(self):
533 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000534
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000535class SvnAffectedFile(AffectedFile):
536 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000537 # Method 'NNN' is abstract in class 'NNN' but is not overridden
538 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000539
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000540 def __init__(self, *args, **kwargs):
541 AffectedFile.__init__(self, *args, **kwargs)
542 self._server_path = None
543 self._is_text_file = None
544
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000545 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000546 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000547 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000548 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000549 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000550
551 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000552 if self._is_directory is None:
553 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000554 if os.path.exists(path):
555 # Retrieve directly from the file system; it is much faster than
556 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000557 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000558 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000559 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000560 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000561 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000562
563 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000564 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000565 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000566 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000567 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000568
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000569 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000570 if self._is_text_file is None:
571 if self.Action() == 'D':
572 # A deleted file is not a text file.
573 self._is_text_file = False
574 elif self.IsDirectory():
575 self._is_text_file = False
576 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000577 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
578 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000579 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
580 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000581
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000582 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000583 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
584
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000585
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000586class GitAffectedFile(AffectedFile):
587 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000588 # Method 'NNN' is abstract in class 'NNN' but is not overridden
589 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000590
591 def __init__(self, *args, **kwargs):
592 AffectedFile.__init__(self, *args, **kwargs)
593 self._server_path = None
594 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000595
596 def ServerPath(self):
597 if self._server_path is None:
598 raise NotImplementedException() # TODO(maruel) Implement.
599 return self._server_path
600
601 def IsDirectory(self):
602 if self._is_directory is None:
603 path = self.AbsoluteLocalPath()
604 if os.path.exists(path):
605 # Retrieve directly from the file system; it is much faster than
606 # querying subversion, especially on Windows.
607 self._is_directory = os.path.isdir(path)
608 else:
609 # raise NotImplementedException() # TODO(maruel) Implement.
610 self._is_directory = False
611 return self._is_directory
612
613 def Property(self, property_name):
614 if not property_name in self._properties:
615 raise NotImplementedException() # TODO(maruel) Implement.
616 return self._properties[property_name]
617
618 def IsTextFile(self):
619 if self._is_text_file is None:
620 if self.Action() == 'D':
621 # A deleted file is not a text file.
622 self._is_text_file = False
623 elif self.IsDirectory():
624 self._is_text_file = False
625 else:
626 # raise NotImplementedException() # TODO(maruel) Implement.
627 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
628 return self._is_text_file
629
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000630 def GenerateScmDiff(self):
631 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000632
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000633class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000634 """Describe a change.
635
636 Used directly by the presubmit scripts to query the current change being
637 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000638
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000639 Instance members:
640 tags: Dictionnary of KEY=VALUE pairs found in the change description.
641 self.KEY: equivalent to tags['KEY']
642 """
643
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000644 _AFFECTED_FILES = AffectedFile
645
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000646 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000647 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000648 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000649
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000650 def __init__(self, name, description, local_root, files, issue, patchset):
651 if files is None:
652 files = []
653 self._name = name
654 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000655 # Convert root into an absolute path.
656 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000657 self.issue = issue
658 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000659 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000660
661 # From the description text, build up a dictionary of key/value pairs
662 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000663 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000664 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000665 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000666 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000667 if m:
668 self.tags[m.group('key')] = m.group('value')
669 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000670 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000671
672 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000673 self._description_without_tags = (
674 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000675
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000676 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000677 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
678 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000679 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000681 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000683 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685 def DescriptionText(self):
686 """Returns the user-entered changelist description, minus tags.
687
688 Any line in the user-provided description starting with e.g. "FOO="
689 (whitespace permitted before and around) is considered a tag line. Such
690 lines are stripped out of the description this function returns.
691 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000692 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693
694 def FullDescriptionText(self):
695 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000696 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697
698 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000699 """Returns the repository (checkout) root directory for this change,
700 as an absolute path.
701 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000702 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703
704 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000705 """Return tags directly as attributes on the object."""
706 if not re.match(r"^[A-Z_]*$", attr):
707 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000708 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709
710 def AffectedFiles(self, include_dirs=False, include_deletes=True):
711 """Returns a list of AffectedFile instances for all files in the change.
712
713 Args:
714 include_deletes: If false, deleted files will be filtered out.
715 include_dirs: True to include directories in the list
716
717 Returns:
718 [AffectedFile(path, action), AffectedFile(path, action)]
719 """
720 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000721 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000722 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000723 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000724
725 if include_deletes:
726 return affected
727 else:
728 return filter(lambda x: x.Action() != 'D', affected)
729
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000730 def AffectedTextFiles(self, include_deletes=None):
731 """Return a list of the existing text files in a change."""
732 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000733 warn("AffectedTextFiles(include_deletes=%s)"
734 " is deprecated and ignored" % str(include_deletes),
735 category=DeprecationWarning,
736 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000737 return filter(lambda x: x.IsTextFile(),
738 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000739
740 def LocalPaths(self, include_dirs=False):
741 """Convenience function."""
742 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
743
744 def AbsoluteLocalPaths(self, include_dirs=False):
745 """Convenience function."""
746 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
747
748 def ServerPaths(self, include_dirs=False):
749 """Convenience function."""
750 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
751
752 def RightHandSideLines(self):
753 """An iterator over all text lines in "new" version of changed files.
754
755 Lists lines from new or modified text files in the change.
756
757 This is useful for doing line-by-line regex checks, like checking for
758 trailing whitespace.
759
760 Yields:
761 a 3 tuple:
762 the AffectedFile instance of the current file;
763 integer line number (1-based); and
764 the contents of the line as a string.
765 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000766 return _RightHandSideLinesImpl(
767 x for x in self.AffectedFiles(include_deletes=False)
768 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000769
770
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000771class SvnChange(Change):
772 _AFFECTED_FILES = SvnAffectedFile
773
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000774 def __init__(self, *args, **kwargs):
775 Change.__init__(self, *args, **kwargs)
776 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000777 self._changelists = None
778
779 def _GetChangeLists(self):
780 """Get all change lists."""
781 if self._changelists == None:
782 previous_cwd = os.getcwd()
783 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000784 # Need to import here to avoid circular dependency.
785 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000786 self._changelists = gcl.GetModifiedFiles()
787 os.chdir(previous_cwd)
788 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000789
790 def GetAllModifiedFiles(self):
791 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000792 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000793 all_modified_files = []
794 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000795 all_modified_files.extend(
796 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000797 return all_modified_files
798
799 def GetModifiedFiles(self):
800 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000801 changelists = self._GetChangeLists()
802 return [os.path.join(self.RepositoryRoot(), f[1])
803 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000804
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000805
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000806class GitChange(Change):
807 _AFFECTED_FILES = GitAffectedFile
808
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000809 def __init__(self, *args, **kwargs):
810 Change.__init__(self, *args, **kwargs)
811 self.scm = 'git'
812
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000813
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000814def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000815 """Finds all presubmit files that apply to a given set of source files.
816
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000817 If inherit-review-settings-ok is present right under root, looks for
818 PRESUBMIT.py in directories enclosing root.
819
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820 Args:
821 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000822 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823
824 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000825 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000826 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000827 files = [normpath(os.path.join(root, f)) for f in files]
828
829 # List all the individual directories containing files.
830 directories = set([os.path.dirname(f) for f in files])
831
832 # Ignore root if inherit-review-settings-ok is present.
833 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
834 root = None
835
836 # Collect all unique directories that may contain PRESUBMIT.py.
837 candidates = set()
838 for directory in directories:
839 while True:
840 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000841 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000842 candidates.add(directory)
843 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000844 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000845 parent_dir = os.path.dirname(directory)
846 if parent_dir == directory:
847 # We hit the system root directory.
848 break
849 directory = parent_dir
850
851 # Look for PRESUBMIT.py in all candidate directories.
852 results = []
853 for directory in sorted(list(candidates)):
854 p = os.path.join(directory, 'PRESUBMIT.py')
855 if os.path.isfile(p):
856 results.append(p)
857
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000858 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000859 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000860
861
thestig@chromium.orgde243452009-10-06 21:02:56 +0000862class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000863 @staticmethod
864 def ExecPresubmitScript(script_text):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000865 """Executes GetPreferredTrySlaves() from a single presubmit script.
866
867 Args:
868 script_text: The text of the presubmit script.
869
870 Return:
871 A list of try slaves.
872 """
873 context = {}
874 exec script_text in context
875
876 function_name = 'GetPreferredTrySlaves'
877 if function_name in context:
878 result = eval(function_name + '()', context)
879 if not isinstance(result, types.ListType):
880 raise exceptions.RuntimeError(
881 'Presubmit functions must return a list, got a %s instead: %s' %
882 (type(result), str(result)))
883 for item in result:
884 if not isinstance(item, basestring):
885 raise exceptions.RuntimeError('All try slaves names must be strings.')
886 if item != item.strip():
887 raise exceptions.RuntimeError('Try slave names cannot start/end'
888 'with whitespace')
889 else:
890 result = []
891 return result
892
893
894def DoGetTrySlaves(changed_files,
895 repository_root,
896 default_presubmit,
897 verbose,
898 output_stream):
899 """Get the list of try servers from the presubmit scripts.
900
901 Args:
902 changed_files: List of modified files.
903 repository_root: The repository root.
904 default_presubmit: A default presubmit script to execute in any case.
905 verbose: Prints debug info.
906 output_stream: A stream to write debug output to.
907
908 Return:
909 List of try slaves
910 """
911 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
912 if not presubmit_files and verbose:
913 output_stream.write("Warning, no presubmit.py found.\n")
914 results = []
915 executer = GetTrySlavesExecuter()
916 if default_presubmit:
917 if verbose:
918 output_stream.write("Running default presubmit script.\n")
919 results += executer.ExecPresubmitScript(default_presubmit)
920 for filename in presubmit_files:
921 filename = os.path.abspath(filename)
922 if verbose:
923 output_stream.write("Running %s\n" % filename)
924 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000925 presubmit_script = gclient_utils.FileRead(filename, 'rU')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000926 results += executer.ExecPresubmitScript(presubmit_script)
927
928 slaves = list(set(results))
929 if slaves and verbose:
930 output_stream.write(', '.join(slaves))
931 output_stream.write('\n')
932 return slaves
933
934
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935class PresubmitExecuter(object):
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000936 def __init__(self, change, committing, tbr, host_url):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000937 """
938 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000939 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000941 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
942 host_url: scheme, host, and path of rietveld instance
943 (or None for default)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000944 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000945 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000946 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000947 self.tbr = tbr
948 self.host_url = host_url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000949
950 def ExecPresubmitScript(self, script_text, presubmit_path):
951 """Executes a single presubmit script.
952
953 Args:
954 script_text: The text of the presubmit script.
955 presubmit_path: The path to the presubmit file (this will be reported via
956 input_api.PresubmitLocalPath()).
957
958 Return:
959 A list of result objects, empty if no problems.
960 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000961
962 # Change to the presubmit file's directory to support local imports.
963 main_path = os.getcwd()
964 os.chdir(os.path.dirname(presubmit_path))
965
966 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000967 input_api = InputApi(self.change, presubmit_path, self.committing,
968 self.tbr, self.host_url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000969 context = {}
970 exec script_text in context
971
972 # These function names must change if we make substantial changes to
973 # the presubmit API that are not backwards compatible.
974 if self.committing:
975 function_name = 'CheckChangeOnCommit'
976 else:
977 function_name = 'CheckChangeOnUpload'
978 if function_name in context:
979 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000980 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000982 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983 if not (isinstance(result, types.TupleType) or
984 isinstance(result, types.ListType)):
985 raise exceptions.RuntimeError(
986 'Presubmit functions must return a tuple or list')
987 for item in result:
988 if not isinstance(item, OutputApi.PresubmitResult):
989 raise exceptions.RuntimeError(
990 'All presubmit results must be of types derived from '
991 'output_api.PresubmitResult')
992 else:
993 result = () # no error since the script doesn't care about current event.
994
chase@chromium.org8e416c82009-10-06 04:30:44 +0000995 # Return the process to the original working directory.
996 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000997 return result
998
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000999
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001000# TODO(dpranke): make all callers pass in tbr, host_url?
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001001def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002 committing,
1003 verbose,
1004 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001005 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001006 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001007 may_prompt,
1008 tbr=False,
1009 host_url=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 """Runs all presubmit checks that apply to the files in the change.
1011
1012 This finds all PRESUBMIT.py files in directories enclosing the files in the
1013 change (up to the repository root) and calls the relevant entrypoint function
1014 depending on whether the change is being committed or uploaded.
1015
1016 Prints errors, warnings and notifications. Prompts the user for warnings
1017 when needed.
1018
1019 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001020 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001021 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1022 verbose: Prints debug info.
1023 output_stream: A stream to write output from presubmit tests to.
1024 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001025 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001026 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001027 tbr: was --tbr specified to skip any reviewer/owner checks?
1028 host_url: scheme, host, and port of host to use for rietveld-related
1029 checks
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001030
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001031 Warning:
1032 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1033 SHOULD be sys.stdin.
1034
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001036 A PresubmitOutput object. Use output.should_continue() to figure out
1037 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 """
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001039 output = PresubmitOutput(input_stream, output_stream)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001040 if committing:
1041 output.write("Running presubmit commit checks ...\n")
1042 else:
1043 output.write("Running presubmit upload checks ...\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
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001075 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001076 for name, items in (('Messages', notifications),
1077 ('Warnings', warnings),
1078 ('ERRORS', errors)):
1079 if items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001080 output.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001081 for item in items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001082 item.handle(output)
1083 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001084
1085 total_time = time.time() - start_time
1086 if total_time > 1.0:
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001087 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001088
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001089 if not errors:
1090 if not warnings:
1091 output.write('Presubmit checks passed.\n')
1092 elif may_prompt:
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001093 output.prompt_yes_no('There were presubmit warnings. '
1094 'Are you sure you wish to continue? (y/N): ')
1095 else:
1096 output.fail()
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001097
1098 global _ASKED_FOR_FEEDBACK
1099 # Ask for feedback one time out of 5.
1100 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001101 output.write("Was the presubmit check useful? Please send feedback "
1102 "& hate mail to maruel@chromium.org!\n")
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001103 _ASKED_FOR_FEEDBACK = True
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001104 return output
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001105
1106
1107def ScanSubDirs(mask, recursive):
1108 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001109 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 +00001110 else:
1111 results = []
1112 for root, dirs, files in os.walk('.'):
1113 if '.svn' in dirs:
1114 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001115 if '.git' in dirs:
1116 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001117 for name in files:
1118 if fnmatch.fnmatch(name, mask):
1119 results.append(os.path.join(root, name))
1120 return results
1121
1122
1123def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001124 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001125 files = []
1126 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001127 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001128 return files
1129
1130
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001131def load_files(options, args):
1132 """Tries to determine the SCM."""
1133 change_scm = scm.determine_scm(options.root)
1134 files = []
1135 if change_scm == 'svn':
1136 change_class = SvnChange
1137 status_fn = scm.SVN.CaptureStatus
1138 elif change_scm == 'git':
1139 change_class = GitChange
1140 status_fn = scm.GIT.CaptureStatus
1141 else:
1142 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1143 if not args:
1144 return None, None
1145 change_class = Change
1146 if args:
1147 files = ParseFiles(args, options.recursive)
1148 else:
1149 # Grab modified files.
1150 files = status_fn([options.root])
1151 return change_class, files
1152
1153
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001154def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001155 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001156 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001157 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001158 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001159 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1160 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161 parser.add_option("-r", "--recursive", action="store_true",
1162 help="Act recursively")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001163 parser.add_option("-v", "--verbose", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001164 help="Verbose output")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001165 parser.add_option("--name", default='no name')
1166 parser.add_option("--description", default='')
1167 parser.add_option("--issue", type='int', default=0)
1168 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001169 parser.add_option("--root", default=os.getcwd(),
1170 help="Search for PRESUBMIT.py up to this directory. "
1171 "If inherit-review-settings-ok is present in this "
1172 "directory, parent directories up to the root file "
1173 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001174 parser.add_option("--default_presubmit")
1175 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001176 options, args = parser.parse_args(argv)
maruel@chromium.org7444c502011-02-09 14:02:11 +00001177 if options.verbose:
1178 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001179 change_class, files = load_files(options, args)
1180 if not change_class:
1181 parser.error('For unversioned directory, <files> is not optional.')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001182 if options.verbose:
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001183 print "Found %d file(s)." % len(files)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001184 results = DoPresubmitChecks(change_class(options.name,
1185 options.description,
1186 options.root,
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001187 files,
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001188 options.issue,
1189 options.patchset),
1190 options.commit,
1191 options.verbose,
1192 sys.stdout,
1193 sys.stdin,
1194 options.default_presubmit,
1195 options.may_prompt)
1196 return not results.should_continue()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001197
1198
1199if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001200 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001201 sys.exit(Main(None))