blob: 0929bd1c83e8a2a5fe2dd18728fdd81b8efa352a [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.org899e1c12011-04-07 17:03:18 +00009__version__ = '1.6'
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.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000017import fnmatch
18import glob
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000019import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000020import marshal # Exposed through the API.
21import optparse
22import os # Somewhat exposed through the API.
23import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000024import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000025import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import sys # Parts exposed through API.
27import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000028import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000029import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000030import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000031import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000033from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000035try:
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000036 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000037except ImportError:
38 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000039 import json # pylint: disable=F0401
40 except ImportError:
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000041 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000042 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000043 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000044
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000045# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000046import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000047import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000048import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000050import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000051import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052
53
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000054# Ask for feedback only once in program lifetime.
55_ASKED_FOR_FEEDBACK = False
56
57
maruel@chromium.org899e1c12011-04-07 17:03:18 +000058class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059 pass
60
61
62def normpath(path):
63 '''Version of os.path.normpath that also changes backward slashes to
64 forward slashes when not running on Windows.
65 '''
66 # This is safe to always do because the Windows version of os.path.normpath
67 # will replace forward slashes with backward slashes.
68 path = path.replace(os.sep, '/')
69 return os.path.normpath(path)
70
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000071
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000072def _RightHandSideLinesImpl(affected_files):
73 """Implements RightHandSideLines for InputApi and GclChange."""
74 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000075 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000076 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000077 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078
79
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000080class PresubmitOutput(object):
81 def __init__(self, input_stream=None, output_stream=None):
82 self.input_stream = input_stream
83 self.output_stream = output_stream
84 self.reviewers = []
85 self.written_output = []
86 self.error_count = 0
87
88 def prompt_yes_no(self, prompt_string):
89 self.write(prompt_string)
90 if self.input_stream:
91 response = self.input_stream.readline().strip().lower()
92 if response not in ('y', 'yes'):
93 self.fail()
94 else:
95 self.fail()
96
97 def fail(self):
98 self.error_count += 1
99
100 def should_continue(self):
101 return not self.error_count
102
103 def write(self, s):
104 self.written_output.append(s)
105 if self.output_stream:
106 self.output_stream.write(s)
107
108 def getvalue(self):
109 return ''.join(self.written_output)
110
111
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000112class OutputApi(object):
113 """This class (more like a module) gets passed to presubmit scripts so that
114 they can specify various types of results.
115 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000116 class PresubmitResult(object):
117 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000118 fatal = False
119 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000120
121 def __init__(self, message, items=None, long_text=''):
122 """
123 message: A short one-line message to indicate errors.
124 items: A list of short strings to indicate where errors occurred.
125 long_text: multi-line text output, e.g. from another tool
126 """
127 self._message = message
128 self._items = []
129 if items:
130 self._items = items
131 self._long_text = long_text.rstrip()
132
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000133 def handle(self, output):
134 output.write(self._message)
135 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000136 for index, item in enumerate(self._items):
137 output.write(' ')
138 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000139 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000140 if index < len(self._items) - 1:
141 output.write(' \\')
142 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000143 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000144 output.write('\n***************\n')
145 # Write separately in case it's unicode.
146 output.write(self._long_text)
147 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000148 if self.fatal:
149 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000150
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000151 class PresubmitAddReviewers(PresubmitResult):
152 """Add some suggested reviewers to the change."""
153 def __init__(self, reviewers):
154 super(OutputApi.PresubmitAddReviewers, self).__init__('')
155 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000156
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000157 def handle(self, output):
158 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000159
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000160 class PresubmitError(PresubmitResult):
161 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000162 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000163
164 class PresubmitPromptWarning(PresubmitResult):
165 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000166 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000167
168 class PresubmitNotifyResult(PresubmitResult):
169 """Just print something to the screen -- but it's not even a warning."""
170 pass
171
172 class MailTextResult(PresubmitResult):
173 """A warning that should be included in the review request email."""
174 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000175 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000176 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000177
178
179class InputApi(object):
180 """An instance of this object is passed to presubmit scripts so they can
181 know stuff about the change they're looking at.
182 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000183 # Method could be a function
184 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000185
maruel@chromium.org3410d912009-06-09 20:56:16 +0000186 # File extensions that are considered source files from a style guide
187 # perspective. Don't modify this list from a presubmit script!
188 DEFAULT_WHITE_LIST = (
189 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000190 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000191 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000192 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000193 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000194 # No extension at all, note that ALL CAPS files are black listed in
195 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000196 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000197 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000198 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000199 )
200
201 # Path regexp that should be excluded from being considered containing source
202 # files. Don't modify this list from a presubmit script!
203 DEFAULT_BLACK_LIST = (
204 r".*\bexperimental[\\\/].*",
205 r".*\bthird_party[\\\/].*",
206 # Output directories (just in case)
207 r".*\bDebug[\\\/].*",
208 r".*\bRelease[\\\/].*",
209 r".*\bxcodebuild[\\\/].*",
210 r".*\bsconsbuild[\\\/].*",
211 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000212 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000213 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000214 r"(|.*[\\\/])\.git[\\\/].*",
215 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000216 )
217
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000218 # TODO(dpranke): Update callers to pass in tbr, host_url, remove
219 # default arguments.
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000220 def __init__(self, change, presubmit_path, is_committing, tbr, host_url,
221 verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000222 """Builds an InputApi object.
223
224 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000225 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000227 is_committing: True if the change is about to be committed.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000228 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
229 host_url: scheme, host, and path of rietveld instance
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000231 # Version number of the presubmit_support script.
232 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000233 self.change = change
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000234 self.host_url = host_url
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000235 self.is_committing = is_committing
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000236 self.tbr = tbr
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000237 self.host_url = host_url or 'http://codereview.chromium.org'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000238
239 # We expose various modules and functions as attributes of the input_api
240 # so that presubmit scripts don't have to import them.
241 self.basename = os.path.basename
242 self.cPickle = cPickle
243 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000244 self.json = json
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000245 self.os_listdir = os.listdir
246 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000247 self.os_path = os.path
248 self.pickle = pickle
249 self.marshal = marshal
250 self.re = re
251 self.subprocess = subprocess
252 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000253 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000254 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000255 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256 self.urllib2 = urllib2
257
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000258 # To easily fork python.
259 self.python_executable = sys.executable
260 self.environ = os.environ
261
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000262 # InputApi.platform is the platform you're currently running on.
263 self.platform = sys.platform
264
265 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000266 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267
268 # We carry the canned checks so presubmit scripts can easily use them.
269 self.canned_checks = presubmit_canned_checks
270
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000271 # TODO(dpranke): figure out a list of all approved owners for a repo
272 # in order to be able to handle wildcard OWNERS files?
273 self.owners_db = owners.Database(change.RepositoryRoot(),
274 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000275 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000276
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000277 def PresubmitLocalPath(self):
278 """Returns the local path of the presubmit script currently being run.
279
280 This is useful if you don't want to hard-code absolute paths in the
281 presubmit script. For example, It can be used to find another file
282 relative to the PRESUBMIT.py script, so the whole tree can be branched and
283 the presubmit script still works, without editing its content.
284 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000285 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000287 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000288 """Translate a depot path to a local path (relative to client root).
289
290 Args:
291 Depot path as a string.
292
293 Returns:
294 The local path of the depot path under the user's current client, or None
295 if the file is not mapped.
296
297 Remember to check for the None case and show an appropriate error!
298 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000299 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000300 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000301 return local_path
302
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000303 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000304 """Translate a local path to a depot path.
305
306 Args:
307 Local path (relative to current directory, or absolute) as a string.
308
309 Returns:
310 The depot path (SVN URL) of the file if mapped, otherwise None.
311 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000312 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000313 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000314 return depot_path
315
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000316 def AffectedFiles(self, include_dirs=False, include_deletes=True):
317 """Same as input_api.change.AffectedFiles() except only lists files
318 (and optionally directories) in the same directory as the current presubmit
319 script, or subdirectories thereof.
320 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000321 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000322 if len(dir_with_slash) == 1:
323 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000324 return filter(
325 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
326 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000327
328 def LocalPaths(self, include_dirs=False):
329 """Returns local paths of input_api.AffectedFiles()."""
330 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
331
332 def AbsoluteLocalPaths(self, include_dirs=False):
333 """Returns absolute local paths of input_api.AffectedFiles()."""
334 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
335
336 def ServerPaths(self, include_dirs=False):
337 """Returns server paths of input_api.AffectedFiles()."""
338 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
339
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000340 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000341 """Same as input_api.change.AffectedTextFiles() except only lists files
342 in the same directory as the current presubmit script, or subdirectories
343 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000345 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000346 warn("AffectedTextFiles(include_deletes=%s)"
347 " is deprecated and ignored" % str(include_deletes),
348 category=DeprecationWarning,
349 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000350 return filter(lambda x: x.IsTextFile(),
351 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000352
maruel@chromium.org3410d912009-06-09 20:56:16 +0000353 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
354 """Filters out files that aren't considered "source file".
355
356 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
357 and InputApi.DEFAULT_BLACK_LIST is used respectively.
358
359 The lists will be compiled as regular expression and
360 AffectedFile.LocalPath() needs to pass both list.
361
362 Note: Copy-paste this function to suit your needs or use a lambda function.
363 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000364 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000365 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000366 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000367 if self.re.match(item, local_path):
368 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000369 return True
370 return False
371 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
372 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
373
374 def AffectedSourceFiles(self, source_file):
375 """Filter the list of AffectedTextFiles by the function source_file.
376
377 If source_file is None, InputApi.FilterSourceFile() is used.
378 """
379 if not source_file:
380 source_file = self.FilterSourceFile
381 return filter(source_file, self.AffectedTextFiles())
382
383 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000384 """An iterator over all text lines in "new" version of changed files.
385
386 Only lists lines from new or modified text files in the change that are
387 contained by the directory of the currently executing presubmit script.
388
389 This is useful for doing line-by-line regex checks, like checking for
390 trailing whitespace.
391
392 Yields:
393 a 3 tuple:
394 the AffectedFile instance of the current file;
395 integer line number (1-based); and
396 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000397
398 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000399 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000400 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000401 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000403 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000404 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000405
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000406 Deny reading anything outside the repository.
407 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000408 if isinstance(file_item, AffectedFile):
409 file_item = file_item.AbsoluteLocalPath()
410 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000411 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000412 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000413
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000414
415class AffectedFile(object):
416 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000417 # Method could be a function
418 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000419 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000420 self._path = path
421 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000422 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000423 self._is_directory = None
424 self._properties = {}
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000425 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000426
427 def ServerPath(self):
428 """Returns a path string that identifies the file in the SCM system.
429
430 Returns the empty string if the file does not exist in SCM.
431 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000432 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433
434 def LocalPath(self):
435 """Returns the path of this file on the local disk relative to client root.
436 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000437 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438
439 def AbsoluteLocalPath(self):
440 """Returns the absolute path of this file on the local disk.
441 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000442 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000443
444 def IsDirectory(self):
445 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000446 if self._is_directory is None:
447 path = self.AbsoluteLocalPath()
448 self._is_directory = (os.path.exists(path) and
449 os.path.isdir(path))
450 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000451
452 def Action(self):
453 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000454 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
455 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000456 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000457
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000458 def Property(self, property_name):
459 """Returns the specified SCM property of this file, or None if no such
460 property.
461 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000462 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000463
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000464 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000465 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000466
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000467 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000468 raise NotImplementedError() # Implement when needed
469
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000470 def NewContents(self):
471 """Returns an iterator over the lines in the new version of file.
472
473 The new version is the file in the user's workspace, i.e. the "right hand
474 side".
475
476 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000477 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000478 """
479 if self.IsDirectory():
480 return []
481 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000482 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
483 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484
485 def OldContents(self):
486 """Returns an iterator over the lines in the old version of file.
487
488 The old version is the file in depot, i.e. the "left hand side".
489 """
490 raise NotImplementedError() # Implement when needed
491
492 def OldFileTempPath(self):
493 """Returns the path on local disk where the old contents resides.
494
495 The old version is the file in depot, i.e. the "left hand side".
496 This is a read-only cached copy of the old contents. *DO NOT* try to
497 modify this file.
498 """
499 raise NotImplementedError() # Implement if/when needed.
500
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000501 def ChangedContents(self):
502 """Returns a list of tuples (line number, line text) of all new lines.
503
504 This relies on the scm diff output describing each changed code section
505 with a line of the form
506
507 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
508 """
509 new_lines = []
510 line_num = 0
511
512 if self.IsDirectory():
513 return []
514
515 for line in self.GenerateScmDiff().splitlines():
516 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
517 if m:
518 line_num = int(m.groups(1)[0])
519 continue
520 if line.startswith('+') and not line.startswith('++'):
521 new_lines.append((line_num, line[1:]))
522 if not line.startswith('-'):
523 line_num += 1
524 return new_lines
525
maruel@chromium.org5de13972009-06-10 18:16:06 +0000526 def __str__(self):
527 return self.LocalPath()
528
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000529 def GenerateScmDiff(self):
530 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000531
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000532class SvnAffectedFile(AffectedFile):
533 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000534 # Method 'NNN' is abstract in class 'NNN' but is not overridden
535 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000536
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000537 def __init__(self, *args, **kwargs):
538 AffectedFile.__init__(self, *args, **kwargs)
539 self._server_path = None
540 self._is_text_file = None
541
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000542 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000543 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000544 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000545 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000546 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000547
548 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000549 if self._is_directory is None:
550 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000551 if os.path.exists(path):
552 # Retrieve directly from the file system; it is much faster than
553 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000554 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000555 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000556 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000557 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000558 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000559
560 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000561 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000562 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000563 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000564 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000565
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000566 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000567 if self._is_text_file is None:
568 if self.Action() == 'D':
569 # A deleted file is not a text file.
570 self._is_text_file = False
571 elif self.IsDirectory():
572 self._is_text_file = False
573 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000574 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
575 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000576 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
577 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000578
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000579 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000580 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
581
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000582
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000583class GitAffectedFile(AffectedFile):
584 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000585 # Method 'NNN' is abstract in class 'NNN' but is not overridden
586 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000587
588 def __init__(self, *args, **kwargs):
589 AffectedFile.__init__(self, *args, **kwargs)
590 self._server_path = None
591 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000592
593 def ServerPath(self):
594 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000595 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000596 return self._server_path
597
598 def IsDirectory(self):
599 if self._is_directory is None:
600 path = self.AbsoluteLocalPath()
601 if os.path.exists(path):
602 # Retrieve directly from the file system; it is much faster than
603 # querying subversion, especially on Windows.
604 self._is_directory = os.path.isdir(path)
605 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000606 self._is_directory = False
607 return self._is_directory
608
609 def Property(self, property_name):
610 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000611 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000612 return self._properties[property_name]
613
614 def IsTextFile(self):
615 if self._is_text_file is None:
616 if self.Action() == 'D':
617 # A deleted file is not a text file.
618 self._is_text_file = False
619 elif self.IsDirectory():
620 self._is_text_file = False
621 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000622 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
623 return self._is_text_file
624
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000625 def GenerateScmDiff(self):
626 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000627
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000628class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000629 """Describe a change.
630
631 Used directly by the presubmit scripts to query the current change being
632 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000633
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000634 Instance members:
635 tags: Dictionnary of KEY=VALUE pairs found in the change description.
636 self.KEY: equivalent to tags['KEY']
637 """
638
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000639 _AFFECTED_FILES = AffectedFile
640
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000641 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000642 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000643 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000644
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000645 def __init__(self, name, description, local_root, files, issue, patchset):
646 if files is None:
647 files = []
648 self._name = name
649 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000650 # Convert root into an absolute path.
651 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000652 self.issue = issue
653 self.patchset = patchset
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000654 self.scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000655
656 # From the description text, build up a dictionary of key/value pairs
657 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000658 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000659 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000660 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000661 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000662 if m:
663 self.tags[m.group('key')] = m.group('value')
664 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000665 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000666
667 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000668 self._description_without_tags = (
669 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000671 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000672 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
673 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000674 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000675
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000676 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000677 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000678 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680 def DescriptionText(self):
681 """Returns the user-entered changelist description, minus tags.
682
683 Any line in the user-provided description starting with e.g. "FOO="
684 (whitespace permitted before and around) is considered a tag line. Such
685 lines are stripped out of the description this function returns.
686 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000687 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688
689 def FullDescriptionText(self):
690 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000691 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000692
693 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000694 """Returns the repository (checkout) root directory for this change,
695 as an absolute path.
696 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000697 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698
699 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000700 """Return tags directly as attributes on the object."""
701 if not re.match(r"^[A-Z_]*$", attr):
702 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000703 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704
705 def AffectedFiles(self, include_dirs=False, include_deletes=True):
706 """Returns a list of AffectedFile instances for all files in the change.
707
708 Args:
709 include_deletes: If false, deleted files will be filtered out.
710 include_dirs: True to include directories in the list
711
712 Returns:
713 [AffectedFile(path, action), AffectedFile(path, action)]
714 """
715 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000716 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000718 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719
720 if include_deletes:
721 return affected
722 else:
723 return filter(lambda x: x.Action() != 'D', affected)
724
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000725 def AffectedTextFiles(self, include_deletes=None):
726 """Return a list of the existing text files in a change."""
727 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000728 warn("AffectedTextFiles(include_deletes=%s)"
729 " is deprecated and ignored" % str(include_deletes),
730 category=DeprecationWarning,
731 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000732 return filter(lambda x: x.IsTextFile(),
733 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000734
735 def LocalPaths(self, include_dirs=False):
736 """Convenience function."""
737 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
738
739 def AbsoluteLocalPaths(self, include_dirs=False):
740 """Convenience function."""
741 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
742
743 def ServerPaths(self, include_dirs=False):
744 """Convenience function."""
745 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
746
747 def RightHandSideLines(self):
748 """An iterator over all text lines in "new" version of changed files.
749
750 Lists lines from new or modified text files in the change.
751
752 This is useful for doing line-by-line regex checks, like checking for
753 trailing whitespace.
754
755 Yields:
756 a 3 tuple:
757 the AffectedFile instance of the current file;
758 integer line number (1-based); and
759 the contents of the line as a string.
760 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000761 return _RightHandSideLinesImpl(
762 x for x in self.AffectedFiles(include_deletes=False)
763 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000764
765
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000766class SvnChange(Change):
767 _AFFECTED_FILES = SvnAffectedFile
768
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000769 def __init__(self, *args, **kwargs):
770 Change.__init__(self, *args, **kwargs)
771 self.scm = 'svn'
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000772 self._changelists = None
773
774 def _GetChangeLists(self):
775 """Get all change lists."""
776 if self._changelists == None:
777 previous_cwd = os.getcwd()
778 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000779 # Need to import here to avoid circular dependency.
780 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000781 self._changelists = gcl.GetModifiedFiles()
782 os.chdir(previous_cwd)
783 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000784
785 def GetAllModifiedFiles(self):
786 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000787 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000788 all_modified_files = []
789 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000790 all_modified_files.extend(
791 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000792 return all_modified_files
793
794 def GetModifiedFiles(self):
795 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000796 changelists = self._GetChangeLists()
797 return [os.path.join(self.RepositoryRoot(), f[1])
798 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000799
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000800
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000801class GitChange(Change):
802 _AFFECTED_FILES = GitAffectedFile
803
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000804 def __init__(self, *args, **kwargs):
805 Change.__init__(self, *args, **kwargs)
806 self.scm = 'git'
807
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000808
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000809def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000810 """Finds all presubmit files that apply to a given set of source files.
811
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000812 If inherit-review-settings-ok is present right under root, looks for
813 PRESUBMIT.py in directories enclosing root.
814
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000815 Args:
816 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000817 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000818
819 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000820 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000821 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000822 files = [normpath(os.path.join(root, f)) for f in files]
823
824 # List all the individual directories containing files.
825 directories = set([os.path.dirname(f) for f in files])
826
827 # Ignore root if inherit-review-settings-ok is present.
828 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
829 root = None
830
831 # Collect all unique directories that may contain PRESUBMIT.py.
832 candidates = set()
833 for directory in directories:
834 while True:
835 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000837 candidates.add(directory)
838 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000839 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000840 parent_dir = os.path.dirname(directory)
841 if parent_dir == directory:
842 # We hit the system root directory.
843 break
844 directory = parent_dir
845
846 # Look for PRESUBMIT.py in all candidate directories.
847 results = []
848 for directory in sorted(list(candidates)):
849 p = os.path.join(directory, 'PRESUBMIT.py')
850 if os.path.isfile(p):
851 results.append(p)
852
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000853 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000854 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000855
856
thestig@chromium.orgde243452009-10-06 21:02:56 +0000857class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000858 @staticmethod
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000859 def ExecPresubmitScript(script_text, presubmit_path):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000860 """Executes GetPreferredTrySlaves() from a single presubmit script.
861
862 Args:
863 script_text: The text of the presubmit script.
864
865 Return:
866 A list of try slaves.
867 """
868 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000869 try:
870 exec script_text in context
871 except Exception, e:
872 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000873
874 function_name = 'GetPreferredTrySlaves'
875 if function_name in context:
876 result = eval(function_name + '()', context)
877 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000878 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000879 'Presubmit functions must return a list, got a %s instead: %s' %
880 (type(result), str(result)))
881 for item in result:
882 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000883 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000884 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000885 raise PresubmitFailure(
886 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000887 else:
888 result = []
889 return result
890
891
892def DoGetTrySlaves(changed_files,
893 repository_root,
894 default_presubmit,
895 verbose,
896 output_stream):
897 """Get the list of try servers from the presubmit scripts.
898
899 Args:
900 changed_files: List of modified files.
901 repository_root: The repository root.
902 default_presubmit: A default presubmit script to execute in any case.
903 verbose: Prints debug info.
904 output_stream: A stream to write debug output to.
905
906 Return:
907 List of try slaves
908 """
909 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
910 if not presubmit_files and verbose:
911 output_stream.write("Warning, no presubmit.py found.\n")
912 results = []
913 executer = GetTrySlavesExecuter()
914 if default_presubmit:
915 if verbose:
916 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000917 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
918 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000919 for filename in presubmit_files:
920 filename = os.path.abspath(filename)
921 if verbose:
922 output_stream.write("Running %s\n" % filename)
923 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000924 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000925 results += executer.ExecPresubmitScript(presubmit_script, filename)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000926
927 slaves = list(set(results))
928 if slaves and verbose:
929 output_stream.write(', '.join(slaves))
930 output_stream.write('\n')
931 return slaves
932
933
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000934class PresubmitExecuter(object):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000935 def __init__(self, change, committing, tbr, host_url, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000936 """
937 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000938 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000939 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000940 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
941 host_url: scheme, host, and path of rietveld instance
942 (or None for default)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000943 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000944 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000945 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000946 self.tbr = tbr
947 self.host_url = host_url
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000948 self.verbose = verbose
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,
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000968 self.tbr, self.host_url, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000969 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000970 try:
971 exec script_text in context
972 except Exception, e:
973 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000974
975 # These function names must change if we make substantial changes to
976 # the presubmit API that are not backwards compatible.
977 if self.committing:
978 function_name = 'CheckChangeOnCommit'
979 else:
980 function_name = 'CheckChangeOnUpload'
981 if function_name in context:
982 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000983 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000985 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000986 if not (isinstance(result, types.TupleType) or
987 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000988 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000989 'Presubmit functions must return a tuple or list')
990 for item in result:
991 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000992 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000993 'All presubmit results must be of types derived from '
994 'output_api.PresubmitResult')
995 else:
996 result = () # no error since the script doesn't care about current event.
997
chase@chromium.org8e416c82009-10-06 04:30:44 +0000998 # Return the process to the original working directory.
999 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000 return result
1001
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001002
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001003# TODO(dpranke): make all callers pass in tbr, host_url?
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001004def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001005 committing,
1006 verbose,
1007 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001008 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001009 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001010 may_prompt,
1011 tbr=False,
1012 host_url=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013 """Runs all presubmit checks that apply to the files in the change.
1014
1015 This finds all PRESUBMIT.py files in directories enclosing the files in the
1016 change (up to the repository root) and calls the relevant entrypoint function
1017 depending on whether the change is being committed or uploaded.
1018
1019 Prints errors, warnings and notifications. Prompts the user for warnings
1020 when needed.
1021
1022 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001023 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1025 verbose: Prints debug info.
1026 output_stream: A stream to write output from presubmit tests to.
1027 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001028 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001029 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001030 tbr: was --tbr specified to skip any reviewer/owner checks?
1031 host_url: scheme, host, and port of host to use for rietveld-related
1032 checks
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001034 Warning:
1035 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1036 SHOULD be sys.stdin.
1037
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001039 A PresubmitOutput object. Use output.should_continue() to figure out
1040 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001041 """
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001042 output = PresubmitOutput(input_stream, output_stream)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001043 if committing:
1044 output.write("Running presubmit commit checks ...\n")
1045 else:
1046 output.write("Running presubmit upload checks ...\n")
jam@chromium.org2a891dc2009-08-20 20:33:37 +00001047 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001048 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
1049 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001050 if not presubmit_files and verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001051 output.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001052 results = []
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001053 executer = PresubmitExecuter(change, committing, tbr, host_url, verbose)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001054 if default_presubmit:
1055 if verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001056 output.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001057 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001058 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001059 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +00001060 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001061 if verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001062 output.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +00001063 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001064 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001065 results += executer.ExecPresubmitScript(presubmit_script, filename)
1066
1067 errors = []
1068 notifications = []
1069 warnings = []
1070 for result in results:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001071 if result.fatal:
1072 errors.append(result)
1073 elif result.should_prompt:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001074 warnings.append(result)
1075 else:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001076 notifications.append(result)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001077
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001078 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001079 for name, items in (('Messages', notifications),
1080 ('Warnings', warnings),
1081 ('ERRORS', errors)):
1082 if items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001083 output.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001084 for item in items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001085 item.handle(output)
1086 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001087
1088 total_time = time.time() - start_time
1089 if total_time > 1.0:
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001090 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001091
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001092 if not errors:
1093 if not warnings:
1094 output.write('Presubmit checks passed.\n')
1095 elif may_prompt:
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001096 output.prompt_yes_no('There were presubmit warnings. '
1097 'Are you sure you wish to continue? (y/N): ')
1098 else:
1099 output.fail()
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001100
1101 global _ASKED_FOR_FEEDBACK
1102 # Ask for feedback one time out of 5.
1103 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001104 output.write("Was the presubmit check useful? Please send feedback "
1105 "& hate mail to maruel@chromium.org!\n")
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001106 _ASKED_FOR_FEEDBACK = True
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001107 return output
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001108
1109
1110def ScanSubDirs(mask, recursive):
1111 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001112 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 +00001113 else:
1114 results = []
1115 for root, dirs, files in os.walk('.'):
1116 if '.svn' in dirs:
1117 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001118 if '.git' in dirs:
1119 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001120 for name in files:
1121 if fnmatch.fnmatch(name, mask):
1122 results.append(os.path.join(root, name))
1123 return results
1124
1125
1126def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001127 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001128 files = []
1129 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001130 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001131 return files
1132
1133
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001134def load_files(options, args):
1135 """Tries to determine the SCM."""
1136 change_scm = scm.determine_scm(options.root)
1137 files = []
1138 if change_scm == 'svn':
1139 change_class = SvnChange
1140 status_fn = scm.SVN.CaptureStatus
1141 elif change_scm == 'git':
1142 change_class = GitChange
1143 status_fn = scm.GIT.CaptureStatus
1144 else:
1145 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1146 if not args:
1147 return None, None
1148 change_class = Change
1149 if args:
1150 files = ParseFiles(args, options.recursive)
1151 else:
1152 # Grab modified files.
1153 files = status_fn([options.root])
1154 return change_class, files
1155
1156
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001157def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001158 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001159 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001160 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001161 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001162 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1163 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001164 parser.add_option("-r", "--recursive", action="store_true",
1165 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001166 parser.add_option("-v", "--verbose", action="count", default=0,
1167 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001168 parser.add_option("--name", default='no name')
1169 parser.add_option("--description", default='')
1170 parser.add_option("--issue", type='int', default=0)
1171 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001172 parser.add_option("--root", default=os.getcwd(),
1173 help="Search for PRESUBMIT.py up to this directory. "
1174 "If inherit-review-settings-ok is present in this "
1175 "directory, parent directories up to the root file "
1176 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001177 parser.add_option("--default_presubmit")
1178 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001179 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001180 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001181 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001182 elif options.verbose:
1183 logging.basicConfig(level=logging.INFO)
1184 else:
1185 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001186 change_class, files = load_files(options, args)
1187 if not change_class:
1188 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001189 logging.info('Found %d file(s).' % len(files))
1190 try:
1191 results = DoPresubmitChecks(
1192 change_class(options.name,
1193 options.description,
1194 options.root,
1195 files,
1196 options.issue,
1197 options.patchset),
1198 options.commit,
1199 options.verbose,
1200 sys.stdout,
1201 sys.stdin,
1202 options.default_presubmit,
1203 options.may_prompt)
1204 return not results.should_continue()
1205 except PresubmitFailure, e:
1206 print >> sys.stderr, e
1207 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1208 print >> sys.stderr, 'If all fails, contact maruel@'
1209 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001210
1211
1212if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001213 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001214 sys.exit(Main(None))