blob: 6146addd6487a3a4e00eeab2e2695dbdaee68681 [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.orgcab38e92011-04-09 00:30:51 +00009__version__ = '1.6.1'
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
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000218 def __init__(self, change, presubmit_path, is_committing, tbr,
219 rietveld, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000220 """Builds an InputApi object.
221
222 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000223 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000224 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000225 is_committing: True if the change is about to be committed.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000226 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000227 rietveld: rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000229 # Version number of the presubmit_support script.
230 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000231 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000232 self.is_committing = is_committing
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000233 self.tbr = tbr
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000234 self.rietveld = rietveld
235 # TBD
236 self.host_url = 'http://codereview.chromium.org'
237 if self.rietveld:
238 self.host_url = rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000239
240 # We expose various modules and functions as attributes of the input_api
241 # so that presubmit scripts don't have to import them.
242 self.basename = os.path.basename
243 self.cPickle = cPickle
244 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000245 self.json = json
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000246 self.os_listdir = os.listdir
247 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000248 self.os_path = os.path
249 self.pickle = pickle
250 self.marshal = marshal
251 self.re = re
252 self.subprocess = subprocess
253 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000254 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000255 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000256 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000257 self.urllib2 = urllib2
258
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000259 # To easily fork python.
260 self.python_executable = sys.executable
261 self.environ = os.environ
262
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000263 # InputApi.platform is the platform you're currently running on.
264 self.platform = sys.platform
265
266 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000267 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268
269 # We carry the canned checks so presubmit scripts can easily use them.
270 self.canned_checks = presubmit_canned_checks
271
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000272 # TODO(dpranke): figure out a list of all approved owners for a repo
273 # in order to be able to handle wildcard OWNERS files?
274 self.owners_db = owners.Database(change.RepositoryRoot(),
275 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000276 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000277
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000278 def PresubmitLocalPath(self):
279 """Returns the local path of the presubmit script currently being run.
280
281 This is useful if you don't want to hard-code absolute paths in the
282 presubmit script. For example, It can be used to find another file
283 relative to the PRESUBMIT.py script, so the whole tree can be branched and
284 the presubmit script still works, without editing its content.
285 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000286 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000287
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000288 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289 """Translate a depot path to a local path (relative to client root).
290
291 Args:
292 Depot path as a string.
293
294 Returns:
295 The local path of the depot path under the user's current client, or None
296 if the file is not mapped.
297
298 Remember to check for the None case and show an appropriate error!
299 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000300 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000301 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000302 return local_path
303
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000304 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305 """Translate a local path to a depot path.
306
307 Args:
308 Local path (relative to current directory, or absolute) as a string.
309
310 Returns:
311 The depot path (SVN URL) of the file if mapped, otherwise None.
312 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000313 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000314 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000315 return depot_path
316
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000317 def AffectedFiles(self, include_dirs=False, include_deletes=True):
318 """Same as input_api.change.AffectedFiles() except only lists files
319 (and optionally directories) in the same directory as the current presubmit
320 script, or subdirectories thereof.
321 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000322 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000323 if len(dir_with_slash) == 1:
324 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000325 return filter(
326 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
327 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000328
329 def LocalPaths(self, include_dirs=False):
330 """Returns local paths of input_api.AffectedFiles()."""
331 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
332
333 def AbsoluteLocalPaths(self, include_dirs=False):
334 """Returns absolute local paths of input_api.AffectedFiles()."""
335 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
336
337 def ServerPaths(self, include_dirs=False):
338 """Returns server paths of input_api.AffectedFiles()."""
339 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
340
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000341 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000342 """Same as input_api.change.AffectedTextFiles() except only lists files
343 in the same directory as the current presubmit script, or subdirectories
344 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000346 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000347 warn("AffectedTextFiles(include_deletes=%s)"
348 " is deprecated and ignored" % str(include_deletes),
349 category=DeprecationWarning,
350 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000351 return filter(lambda x: x.IsTextFile(),
352 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000353
maruel@chromium.org3410d912009-06-09 20:56:16 +0000354 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
355 """Filters out files that aren't considered "source file".
356
357 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
358 and InputApi.DEFAULT_BLACK_LIST is used respectively.
359
360 The lists will be compiled as regular expression and
361 AffectedFile.LocalPath() needs to pass both list.
362
363 Note: Copy-paste this function to suit your needs or use a lambda function.
364 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000365 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000366 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000367 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000368 if self.re.match(item, local_path):
369 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000370 return True
371 return False
372 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
373 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
374
375 def AffectedSourceFiles(self, source_file):
376 """Filter the list of AffectedTextFiles by the function source_file.
377
378 If source_file is None, InputApi.FilterSourceFile() is used.
379 """
380 if not source_file:
381 source_file = self.FilterSourceFile
382 return filter(source_file, self.AffectedTextFiles())
383
384 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000385 """An iterator over all text lines in "new" version of changed files.
386
387 Only lists lines from new or modified text files in the change that are
388 contained by the directory of the currently executing presubmit script.
389
390 This is useful for doing line-by-line regex checks, like checking for
391 trailing whitespace.
392
393 Yields:
394 a 3 tuple:
395 the AffectedFile instance of the current file;
396 integer line number (1-based); and
397 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000398
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000399 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000400 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000401 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000402 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000403
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000404 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000405 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000406
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000407 Deny reading anything outside the repository.
408 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000409 if isinstance(file_item, AffectedFile):
410 file_item = file_item.AbsoluteLocalPath()
411 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000412 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000413 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000414
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415
416class AffectedFile(object):
417 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000418 # Method could be a function
419 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000420 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000421 self._path = path
422 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000423 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000424 self._is_directory = None
425 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000426 self._cached_changed_contents = None
427 self._cached_new_contents = None
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.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000480 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000481 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000482 if self._cached_new_contents is None:
483 self._cached_new_contents = []
484 if not self.IsDirectory():
485 try:
486 self._cached_new_contents = gclient_utils.FileRead(
487 self.AbsoluteLocalPath(), 'rU').splitlines()
488 except IOError:
489 pass # File not found? That's fine; maybe it was deleted.
490 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000491
492 def OldContents(self):
493 """Returns an iterator over the lines in the old version of file.
494
495 The old version is the file in depot, i.e. the "left hand side".
496 """
497 raise NotImplementedError() # Implement when needed
498
499 def OldFileTempPath(self):
500 """Returns the path on local disk where the old contents resides.
501
502 The old version is the file in depot, i.e. the "left hand side".
503 This is a read-only cached copy of the old contents. *DO NOT* try to
504 modify this file.
505 """
506 raise NotImplementedError() # Implement if/when needed.
507
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000508 def ChangedContents(self):
509 """Returns a list of tuples (line number, line text) of all new lines.
510
511 This relies on the scm diff output describing each changed code section
512 with a line of the form
513
514 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
515 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000516 if self._cached_changed_contents is not None:
517 return self._cached_changed_contents[:]
518 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000519 line_num = 0
520
521 if self.IsDirectory():
522 return []
523
524 for line in self.GenerateScmDiff().splitlines():
525 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
526 if m:
527 line_num = int(m.groups(1)[0])
528 continue
529 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000530 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000531 if not line.startswith('-'):
532 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000533 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000534
maruel@chromium.org5de13972009-06-10 18:16:06 +0000535 def __str__(self):
536 return self.LocalPath()
537
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000538 def GenerateScmDiff(self):
539 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000540
maruel@chromium.org58407af2011-04-12 23:15:57 +0000541
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000542class SvnAffectedFile(AffectedFile):
543 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000544 # Method 'NNN' is abstract in class 'NNN' but is not overridden
545 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000546
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000547 def __init__(self, *args, **kwargs):
548 AffectedFile.__init__(self, *args, **kwargs)
549 self._server_path = None
550 self._is_text_file = None
551
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000552 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000553 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000554 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000555 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000556 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000557
558 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000559 if self._is_directory is None:
560 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000561 if os.path.exists(path):
562 # Retrieve directly from the file system; it is much faster than
563 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000564 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000565 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000566 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000567 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000568 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000569
570 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000571 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000572 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000573 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000574 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000575
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000576 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000577 if self._is_text_file is None:
578 if self.Action() == 'D':
579 # A deleted file is not a text file.
580 self._is_text_file = False
581 elif self.IsDirectory():
582 self._is_text_file = False
583 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000584 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
585 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000586 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
587 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000588
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000589 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000590 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
591
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000592
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000593class GitAffectedFile(AffectedFile):
594 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000595 # Method 'NNN' is abstract in class 'NNN' but is not overridden
596 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000597
598 def __init__(self, *args, **kwargs):
599 AffectedFile.__init__(self, *args, **kwargs)
600 self._server_path = None
601 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000602
603 def ServerPath(self):
604 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000605 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000606 return self._server_path
607
608 def IsDirectory(self):
609 if self._is_directory is None:
610 path = self.AbsoluteLocalPath()
611 if os.path.exists(path):
612 # Retrieve directly from the file system; it is much faster than
613 # querying subversion, especially on Windows.
614 self._is_directory = os.path.isdir(path)
615 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000616 self._is_directory = False
617 return self._is_directory
618
619 def Property(self, property_name):
620 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000621 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000622 return self._properties[property_name]
623
624 def IsTextFile(self):
625 if self._is_text_file is None:
626 if self.Action() == 'D':
627 # A deleted file is not a text file.
628 self._is_text_file = False
629 elif self.IsDirectory():
630 self._is_text_file = False
631 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000632 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
633 return self._is_text_file
634
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000635 def GenerateScmDiff(self):
636 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000637
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000638
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000639class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000640 """Describe a change.
641
642 Used directly by the presubmit scripts to query the current change being
643 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000644
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000645 Instance members:
646 tags: Dictionnary of KEY=VALUE pairs found in the change description.
647 self.KEY: equivalent to tags['KEY']
648 """
649
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000650 _AFFECTED_FILES = AffectedFile
651
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000652 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000653 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000654 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000655 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000656
maruel@chromium.org58407af2011-04-12 23:15:57 +0000657 def __init__(
658 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000659 if files is None:
660 files = []
661 self._name = name
662 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000663 # Convert root into an absolute path.
664 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000665 self.issue = issue
666 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000667 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668
669 # From the description text, build up a dictionary of key/value pairs
670 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000671 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000673 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000674 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000675 if m:
676 self.tags[m.group('key')] = m.group('value')
677 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000678 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679
680 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000681 self._description_without_tags = (
682 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000684 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000685 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
686 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000687 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000689 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000691 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000692
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693 def DescriptionText(self):
694 """Returns the user-entered changelist description, minus tags.
695
696 Any line in the user-provided description starting with e.g. "FOO="
697 (whitespace permitted before and around) is considered a tag line. Such
698 lines are stripped out of the description this function returns.
699 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000700 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000701
702 def FullDescriptionText(self):
703 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000704 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000705
706 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000707 """Returns the repository (checkout) root directory for this change,
708 as an absolute path.
709 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000710 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000711
712 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000713 """Return tags directly as attributes on the object."""
714 if not re.match(r"^[A-Z_]*$", attr):
715 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000716 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717
718 def AffectedFiles(self, include_dirs=False, include_deletes=True):
719 """Returns a list of AffectedFile instances for all files in the change.
720
721 Args:
722 include_deletes: If false, deleted files will be filtered out.
723 include_dirs: True to include directories in the list
724
725 Returns:
726 [AffectedFile(path, action), AffectedFile(path, action)]
727 """
728 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000729 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000730 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000731 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000732
733 if include_deletes:
734 return affected
735 else:
736 return filter(lambda x: x.Action() != 'D', affected)
737
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000738 def AffectedTextFiles(self, include_deletes=None):
739 """Return a list of the existing text files in a change."""
740 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000741 warn("AffectedTextFiles(include_deletes=%s)"
742 " is deprecated and ignored" % str(include_deletes),
743 category=DeprecationWarning,
744 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000745 return filter(lambda x: x.IsTextFile(),
746 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747
748 def LocalPaths(self, include_dirs=False):
749 """Convenience function."""
750 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
751
752 def AbsoluteLocalPaths(self, include_dirs=False):
753 """Convenience function."""
754 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
755
756 def ServerPaths(self, include_dirs=False):
757 """Convenience function."""
758 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
759
760 def RightHandSideLines(self):
761 """An iterator over all text lines in "new" version of changed files.
762
763 Lists lines from new or modified text files in the change.
764
765 This is useful for doing line-by-line regex checks, like checking for
766 trailing whitespace.
767
768 Yields:
769 a 3 tuple:
770 the AffectedFile instance of the current file;
771 integer line number (1-based); and
772 the contents of the line as a string.
773 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000774 return _RightHandSideLinesImpl(
775 x for x in self.AffectedFiles(include_deletes=False)
776 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000777
778
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000779class SvnChange(Change):
780 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000781 scm = 'svn'
782 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000783
784 def _GetChangeLists(self):
785 """Get all change lists."""
786 if self._changelists == None:
787 previous_cwd = os.getcwd()
788 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000789 # Need to import here to avoid circular dependency.
790 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000791 self._changelists = gcl.GetModifiedFiles()
792 os.chdir(previous_cwd)
793 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000794
795 def GetAllModifiedFiles(self):
796 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000797 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000798 all_modified_files = []
799 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000800 all_modified_files.extend(
801 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000802 return all_modified_files
803
804 def GetModifiedFiles(self):
805 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000806 changelists = self._GetChangeLists()
807 return [os.path.join(self.RepositoryRoot(), f[1])
808 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000809
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000810
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000811class GitChange(Change):
812 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000813 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000814
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000815
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000816def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817 """Finds all presubmit files that apply to a given set of source files.
818
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000819 If inherit-review-settings-ok is present right under root, looks for
820 PRESUBMIT.py in directories enclosing root.
821
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000822 Args:
823 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000824 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825
826 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000827 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000828 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000829 files = [normpath(os.path.join(root, f)) for f in files]
830
831 # List all the individual directories containing files.
832 directories = set([os.path.dirname(f) for f in files])
833
834 # Ignore root if inherit-review-settings-ok is present.
835 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
836 root = None
837
838 # Collect all unique directories that may contain PRESUBMIT.py.
839 candidates = set()
840 for directory in directories:
841 while True:
842 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000844 candidates.add(directory)
845 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000846 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000847 parent_dir = os.path.dirname(directory)
848 if parent_dir == directory:
849 # We hit the system root directory.
850 break
851 directory = parent_dir
852
853 # Look for PRESUBMIT.py in all candidate directories.
854 results = []
855 for directory in sorted(list(candidates)):
856 p = os.path.join(directory, 'PRESUBMIT.py')
857 if os.path.isfile(p):
858 results.append(p)
859
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000860 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000861 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000862
863
thestig@chromium.orgde243452009-10-06 21:02:56 +0000864class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000865 @staticmethod
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000866 def ExecPresubmitScript(script_text, presubmit_path):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000867 """Executes GetPreferredTrySlaves() from a single presubmit script.
868
869 Args:
870 script_text: The text of the presubmit script.
871
872 Return:
873 A list of try slaves.
874 """
875 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000876 try:
877 exec script_text in context
878 except Exception, e:
879 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000880
881 function_name = 'GetPreferredTrySlaves'
882 if function_name in context:
883 result = eval(function_name + '()', context)
884 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000885 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000886 'Presubmit functions must return a list, got a %s instead: %s' %
887 (type(result), str(result)))
888 for item in result:
889 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000890 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000891 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000892 raise PresubmitFailure(
893 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000894 else:
895 result = []
896 return result
897
898
899def DoGetTrySlaves(changed_files,
900 repository_root,
901 default_presubmit,
902 verbose,
903 output_stream):
904 """Get the list of try servers from the presubmit scripts.
905
906 Args:
907 changed_files: List of modified files.
908 repository_root: The repository root.
909 default_presubmit: A default presubmit script to execute in any case.
910 verbose: Prints debug info.
911 output_stream: A stream to write debug output to.
912
913 Return:
914 List of try slaves
915 """
916 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
917 if not presubmit_files and verbose:
918 output_stream.write("Warning, no presubmit.py found.\n")
919 results = []
920 executer = GetTrySlavesExecuter()
921 if default_presubmit:
922 if verbose:
923 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000924 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
925 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000926 for filename in presubmit_files:
927 filename = os.path.abspath(filename)
928 if verbose:
929 output_stream.write("Running %s\n" % filename)
930 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000931 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000932 results += executer.ExecPresubmitScript(presubmit_script, filename)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000933
934 slaves = list(set(results))
935 if slaves and verbose:
936 output_stream.write(', '.join(slaves))
937 output_stream.write('\n')
938 return slaves
939
940
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941class PresubmitExecuter(object):
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000942 def __init__(self, change, committing, tbr, rietveld, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000943 """
944 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000945 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000946 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000947 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000948 rietveld: rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000949 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000950 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000951 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000952 self.tbr = tbr
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000953 self.rietveld = rietveld
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000954 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000955
956 def ExecPresubmitScript(self, script_text, presubmit_path):
957 """Executes a single presubmit script.
958
959 Args:
960 script_text: The text of the presubmit script.
961 presubmit_path: The path to the presubmit file (this will be reported via
962 input_api.PresubmitLocalPath()).
963
964 Return:
965 A list of result objects, empty if no problems.
966 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000967
968 # Change to the presubmit file's directory to support local imports.
969 main_path = os.getcwd()
970 os.chdir(os.path.dirname(presubmit_path))
971
972 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000973 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000974 self.tbr, self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000975 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000976 try:
977 exec script_text in context
978 except Exception, e:
979 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980
981 # These function names must change if we make substantial changes to
982 # the presubmit API that are not backwards compatible.
983 if self.committing:
984 function_name = 'CheckChangeOnCommit'
985 else:
986 function_name = 'CheckChangeOnUpload'
987 if function_name in context:
988 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000989 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000990 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000991 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992 if not (isinstance(result, types.TupleType) or
993 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000994 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995 'Presubmit functions must return a tuple or list')
996 for item in result:
997 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000998 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000999 'All presubmit results must be of types derived from '
1000 'output_api.PresubmitResult')
1001 else:
1002 result = () # no error since the script doesn't care about current event.
1003
chase@chromium.org8e416c82009-10-06 04:30:44 +00001004 # Return the process to the original working directory.
1005 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 return result
1007
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001008
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001009def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 committing,
1011 verbose,
1012 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001013 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001014 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001015 may_prompt,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001016 tbr,
1017 rietveld):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018 """Runs all presubmit checks that apply to the files in the change.
1019
1020 This finds all PRESUBMIT.py files in directories enclosing the files in the
1021 change (up to the repository root) and calls the relevant entrypoint function
1022 depending on whether the change is being committed or uploaded.
1023
1024 Prints errors, warnings and notifications. Prompts the user for warnings
1025 when needed.
1026
1027 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001028 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001029 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1030 verbose: Prints debug info.
1031 output_stream: A stream to write output from presubmit tests to.
1032 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001033 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001034 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001035 tbr: was --tbr specified to skip any reviewer/owner checks?
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001036 rietveld: rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001038 Warning:
1039 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1040 SHOULD be sys.stdin.
1041
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001042 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001043 A PresubmitOutput object. Use output.should_continue() to figure out
1044 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001045 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001046 old_environ = os.environ
1047 try:
1048 # Make sure python subprocesses won't generate .pyc files.
1049 os.environ = os.environ.copy()
1050 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001051
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001052 output = PresubmitOutput(input_stream, output_stream)
1053 if committing:
1054 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001055 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001056 output.write("Running presubmit upload checks ...\n")
1057 start_time = time.time()
1058 presubmit_files = ListRelevantPresubmitFiles(
1059 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1060 if not presubmit_files and verbose:
1061 output.write("Warning, no presubmit.py found.\n")
1062 results = []
1063 executer = PresubmitExecuter(change, committing, tbr, rietveld, verbose)
1064 if default_presubmit:
1065 if verbose:
1066 output.write("Running default presubmit script.\n")
1067 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1068 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1069 for filename in presubmit_files:
1070 filename = os.path.abspath(filename)
1071 if verbose:
1072 output.write("Running %s\n" % filename)
1073 # Accept CRLF presubmit script.
1074 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1075 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001076
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001077 errors = []
1078 notifications = []
1079 warnings = []
1080 for result in results:
1081 if result.fatal:
1082 errors.append(result)
1083 elif result.should_prompt:
1084 warnings.append(result)
1085 else:
1086 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001087
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001088 output.write('\n')
1089 for name, items in (('Messages', notifications),
1090 ('Warnings', warnings),
1091 ('ERRORS', errors)):
1092 if items:
1093 output.write('** Presubmit %s **\n' % name)
1094 for item in items:
1095 item.handle(output)
1096 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001097
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001098 total_time = time.time() - start_time
1099 if total_time > 1.0:
1100 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001101
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001102 if not errors:
1103 if not warnings:
1104 output.write('Presubmit checks passed.\n')
1105 elif may_prompt:
1106 output.prompt_yes_no('There were presubmit warnings. '
1107 'Are you sure you wish to continue? (y/N): ')
1108 else:
1109 output.fail()
1110
1111 global _ASKED_FOR_FEEDBACK
1112 # Ask for feedback one time out of 5.
1113 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
1114 output.write("Was the presubmit check useful? Please send feedback "
1115 "& hate mail to maruel@chromium.org!\n")
1116 _ASKED_FOR_FEEDBACK = True
1117 return output
1118 finally:
1119 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001120
1121
1122def ScanSubDirs(mask, recursive):
1123 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001124 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 +00001125 else:
1126 results = []
1127 for root, dirs, files in os.walk('.'):
1128 if '.svn' in dirs:
1129 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001130 if '.git' in dirs:
1131 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001132 for name in files:
1133 if fnmatch.fnmatch(name, mask):
1134 results.append(os.path.join(root, name))
1135 return results
1136
1137
1138def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001139 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001140 files = []
1141 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001142 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001143 return files
1144
1145
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001146def load_files(options, args):
1147 """Tries to determine the SCM."""
1148 change_scm = scm.determine_scm(options.root)
1149 files = []
1150 if change_scm == 'svn':
1151 change_class = SvnChange
1152 status_fn = scm.SVN.CaptureStatus
1153 elif change_scm == 'git':
1154 change_class = GitChange
1155 status_fn = scm.GIT.CaptureStatus
1156 else:
1157 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1158 if not args:
1159 return None, None
1160 change_class = Change
1161 if args:
1162 files = ParseFiles(args, options.recursive)
1163 else:
1164 # Grab modified files.
1165 files = status_fn([options.root])
1166 return change_class, files
1167
1168
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001169def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001170 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001171 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001172 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001173 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001174 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1175 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001176 parser.add_option("-r", "--recursive", action="store_true",
1177 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001178 parser.add_option("-v", "--verbose", action="count", default=0,
1179 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001180 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001181 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001182 parser.add_option("--description", default='')
1183 parser.add_option("--issue", type='int', default=0)
1184 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001185 parser.add_option("--root", default=os.getcwd(),
1186 help="Search for PRESUBMIT.py up to this directory. "
1187 "If inherit-review-settings-ok is present in this "
1188 "directory, parent directories up to the root file "
1189 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001190 parser.add_option("--default_presubmit")
1191 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001192 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001193 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001194 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001195 elif options.verbose:
1196 logging.basicConfig(level=logging.INFO)
1197 else:
1198 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001199 change_class, files = load_files(options, args)
1200 if not change_class:
1201 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001202 logging.info('Found %d file(s).' % len(files))
1203 try:
1204 results = DoPresubmitChecks(
1205 change_class(options.name,
1206 options.description,
1207 options.root,
1208 files,
1209 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001210 options.patchset,
1211 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001212 options.commit,
1213 options.verbose,
1214 sys.stdout,
1215 sys.stdin,
1216 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001217 options.may_prompt,
1218 False,
1219 None)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001220 return not results.should_continue()
1221 except PresubmitFailure, e:
1222 print >> sys.stderr, e
1223 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1224 print >> sys.stderr, 'If all fails, contact maruel@'
1225 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001226
1227
1228if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001229 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001230 sys.exit(Main(None))